Imagine you have two views with code like the following:
controller_a/a.html.erb
<%= content_tag(:div) do %>
<%= I18n.t "some.key" %>
<% end %>
controller_b/b.html.erb
<%= content_tag(:div) do %>
<%= I18n.t "some.key" %>
<% end %>
<%= content_tag(:div) do %>
<%= I18n.t "some.other_key" %>
<% end %>
So, a.html.erb is on controller_a#a, while b.html.erb is on controller_b#b. Both actions are cached by caches_action. How can I make sure that when I change the some.key translation key, both views are invalidated? How could I build a generic mechanism?
Say, in your ApplicationController create the following class-method (or in a lib and extend by it):
def self.i18n_digest(*scopes)
Digest::MD5.hexdigest I18n.t(scopes).to_s
end
Then you can use :cache_path option in your caches_action this way:
caches_action :some_action, cache_path: { some_key: i18n_digest('some', 'foo') }
Just make sure that you set the locale in a before_filter before this statement.
Docs on cache_path.
Note: I'm using the scope of translation ('some') to get all its nested messages as a hash.
Related
Rails Newbie. Be gentle. If I need to show more stuff I'll do it.
Trying to insert a newsletter signup block above my footer on a project but didn't make it a partial in the layouts set up.
I have the yield outputting an index from a blog.
Right now it's just saying "false" on my local host.
Is it possible to have multiple yields to different indexes?
Is it possible to insert another page into a layout page?
application.html.erb
<div id="blog">
<%= yield %>
</div>
<div>
<%= content_for?(:newsletter) ? yield(:newsletter) : yield %>
</div>
<div>
<%= render 'layouts/footer' %>
</div>
newsletter.html.erb
<% content_for :newsletter do %>
<h1>Get My Awesome News Letter</h1>
<p>Give me your email and keep up to date on my cat's thoughts.</p>
<%= form_tag('/emailapi/subscribe', method: "post", id: "subscribe", remote: "true") do -%>
<%= email_field(:email, :address, {id: "email", placeholder: "email address"}) %>
<%= submit_tag("Sign me up!") %>
<% end %>
emailapi_controller.rb
class EmailapiController < ApplicationController
def newsletter
render params[:newsletter]
end
def subscribe
gb = Gibbon::Request.new
gb.lists.subscribe({
:id => ENV["MAILCHIMP_LIST_ID"],
:email => {:email => params[:email][:address]}
})
end
end
routes.rb
root to: 'posts#index'
get "/:newsletter" => 'emailapi#newsletter'
post 'emailapi/subscribe' => 'emailapi#subscribe'
You shouldn't need this conditional test:
content_for?(:newsletter) ? yield(:newsletter) : yield
try just:
<%= content_for :newsletter %>
Here's the doc on content_for:
http://apidock.com/rails/v4.2.1/ActionView/Helpers/CaptureHelper/content_for
Ie only show the newsletter if newsletter is present.
The extra yield (if newsletter-content is not present) is repeated from the blog-section above.
You probably shouldn't have duplicate plain yields just the one... everything else should have a name (eg :newsletter)
Also - you seem to be missing an <% end %> in newsletter.html.erb
You should be able to just use another render block. I'm not sure where your newsletter.html.erb lives, but if, for example it lived in a folder such as includes/ you could do something like:
<%= render 'includes/newsletter' %>
I have a partial that needs to have some controller logic run before it can render without issue. Is there some way to associate the partial with some controller logic that is run whenever it is rendered?
For example, this is what my current code looks like:
MyDataController:
class MyDataController < ApplicationController
def view
#obj = MyData.find(params[:id])
run_logic_for_partial
end
def some_method_i_dont_know_about
#obj = MyData.find(params[:id])
# Doesn't call run_logic_for_partial
end
def run_logic_for_partial
#important_hash = {}
for item in #obj.internal_array
#important_hash[item] = "Important value"
end
end
end
view.html.erb:
Name: <%= #obj.name %>
Date: <%= #obj.date %>
<%= render :partial => "my_partial" %>
some_method_i_dont_know_about.html.erb:
Name: <%= #obj.name %>
User: <%= #obj.user %>
<%# This will fail because #important_hash isn't initialized %>
<%= render :partial => "my_partial" %>
_my_partial.html.erb:
<% for item in #obj.internal_array %>
<%= item.to_s %>: <%= #important_hash[item] %>
<% end %>
How can I make sure that run_logic_for_partial is called whenever _my_partial.html.erb is rendered, even if the method isn't explicitly called from the controller? If I can't, are there any common patterns used in Rails to deal with these kinds of situations?
You should be using a views helper for this sort of logic. If you generated your resource using rails generate, a helper file for your resource should already be in your app/helpers directory. Otherwise, you can create it yourself:
# app/helpers/my_data.rb
module MyDataHelper
def run_logic_for_partial(obj)
important_hash = {}
for item in obj.internal_array
important_hash[item] = "Important value" // you'll need to modify this keying to suit your purposes
end
important_hash
end
end
Then, in your partial, pass the object you want to operate on to your helper:
# _my_partial.html.erb
<% important_hash = run_logic_for_partial(#obj) %>
<% for item in important_hash %>
<%= item.to_s %>: <%= important_hash[item] %>
<% end %>
Or:
# app/helpers/my_data.rb
module MyDataHelper
def run_logic_for_partial(item)
# Do your logic
"Important value"
end
end
# _my_partial.html.erb
<% for item in #obj.internal_array %>
<%= item.to_s %>: <%= run_logic_for_partial(item) %>
<% end %>
EDIT:
As commented Ian Kennedy points out, this logic can also reasonably be abstracted into a convenience method in your model:
# app/models/obj.rb
def important_hash
hash = {}
for item in internal_array
important_hash[item] = "Important value"
end
hash
end
Then, you'd access the important_hash attribute in the following manner in your partial:
# _my_partial.html.erb
<% for item in #obj.important_hash %>
<%= item.to_s %>: <%= item %>
<% end %>
What you're trying to do runs against the grain of how Rails controllers/views are designed to be used. It would be better to structure things a bit differently. Why not put run_logic_for_partial into a helper, and make it take an argument (rather than implicitly working on #obj)?
To see an example of a view "helper", look here: http://guides.rubyonrails.org/getting_started.html#view-helpers
In this case which pattern will be faster?
Obviously Pattern1 with helper looks much more sophisticated and looks clean.
But it send SQL every time when user_link method is called.
Here it calls up to 100times at one page loading.
Which way would be better for benchmark performance?
Pattern1. With helper
application_helper
def user_link(username)
link_to User.find_by_username(username).user_profile.nickname, show_user_path(username)
end
view
<% #topics.order("updated_at DESC").limit(100).each do |topic| %>
<%= user_link(topic.comment_threads.order("id").last.user.username) if topic.comment_threads.present? %>
<% end %>
Pattern2. Without helper. Just only view
<% #topics.order("updated_at DESC").limit(100).each do |topic| %>
<%= link_to(topic.comment_threads.order("id").last.user.nickname, show_user_path(topic.comment_threads.order("id").last.user.username) ) if topic.comment_threads.present? %>
<% end %>
try
# Topics model
#via scope
scope :get_topic_list, lambda do
order("updated_at DESC").joins(:comment_threads => :user).limit(100)
end
#via method
def self.get_topic_list
Topic.order("updated_at DESC").joins(:comment_threads => :user).limit(100)
end
# in your controller or move to model itself (recommened)
#topics = Topic.get_topic_list
# in you view
<% #topics.each do |topic| %>
<%= link_to(topic.comment_threads.order("id").last.user.nickname, show_user_path(topic.comment_threads.order("id").last.user.username) ) if topic.comment_threads.present? %>
<% end %>
I've been away from Rails for a while now, so maybe I'm missing something simple.
How can you accomplish this:
<%= yield_or :sidebar do %>
some default content
<% end %>
Or even:
<%= yield_or_render :sidebar, 'path/to/default/sidebar' %>
In the first case, I'm trying:
def yield_or(content, &block)
content_for?(content) ? yield(content) : yield
end
But that throws a 'no block given' error.
In the second case:
def yield_or_render(content, template)
content_for?(content) ? yield(content) : render(template)
end
This works when there's no content defined, but as soon as I use content_for to override the default content, it throws the same error.
I used this as a starting point, but it seems it only works when used directly in the view.
Thanks!
How about something like this?
<% if content_for?(:whatever) %>
<div><%= yield(:whatever) %></div>
<% else %>
<div>default_content_here</div>
<% end %>
Inspiration from this SO question
Try this:
# app/helpers/application_helper.rb
def yield_or(name, content = nil, &block)
if content_for?(name)
content_for(name)
else
block_given? ? capture(&block) : content
end
end
so you could do
<%= yield_or :something, 'default content' %>
or
<%= yield_or :something do %>
block of default content
<% end %>
where the default can be overridden using
<%= content_for :something do %>
overriding content
<% end %>
I didn't know you could use content_for(:content_tag) without a block and it will return the same content as if using yield(:content_tag).
So:
def yield_or_render(content, template)
content_for?(content) ? content_for(content) : render(template)
end
I want to edit multiple items of my model photo in one form. I am unsure of how to correctly present and POST this with a form, as well as how to gather the items in the update action in the controller.
This is what I want:
<form>
<input name="photos[1][title]" value="Photo with id 1" />
<input name="photos[2][title]" value="Photo with id 2" />
<input name="photos[3][title]" value="Custom title" />
</form>
The parameters are just an example, like I stated above: I am not sure of the best way to POST these values in this form.
In the controller I want to something like this:
#photos = Photo.find( params[photos] )
#photos.each do |photo|
photo.update_attributes!(params[:photos][photo] )
end
In Rails 4, just this
<%= form_tag photos_update_path do %>
<% #photos.each do |photo| %>
<%= fields_for "photos[]", photo do |pf| %>
<%= pf.text_field :caption %>
... other photo fields
UPDATE: This answer applies to Rails 2, or if you have special constraints that require custom logic. The easy cases are well addressed using fields_for as discussed elsewhere.
Rails isn't going to help you out a lot to do this. It goes against the standard view conventions, so you'll have to do workarounds in the view, the controller, even the routes. That's no fun.
The key resources on dealing with multi-model forms the Rails way are Stephen Chu's params-foo series, or if you're on Rails 2.3, check out Nested Object Forms
It becomes much easier if you define some kind of singular resource that you are editing, like a Photoset. A Photoset could be a real, ActiveRecord type of model or it can just be a facade that accepts data and throws errors as if it were an ActiveRecord model.
Now you can write a view form somewhat like this:
<%= form_for :photoset do |f|%>
<% f.object.photos.each do |photo| %>
<%= f.fields_for photo do |photo_form| %>
<%= photo_form.text_field :caption %>
<%= photo_form.label :caption %>
<%= photo_form.file_field :attached %>
<% end %>
<% end %>
<% end %>
Your model should validate each child Photo that comes in and aggregate their errors. You may want to check out a good article on how to include Validations in any class. It could look something like this:
class Photoset
include ActiveRecord::Validations
attr_accessor :photos
validate :all_photos_okay
def all_photos_okay
photos.each do |photo|
errors.add photo.errors unless photo.valid?
end
end
def save
photos.all?(&:save)
end
def photos=(incoming_data)
incoming_data.each do |incoming|
if incoming.respond_to? :attributes
#photos << incoming unless #photos.include? incoming
else
if incoming[:id]
target = #photos.select { |t| t.id == incoming[:id] }
end
if target
target.attributes = incoming
else
#photos << Photo.new incoming
end
end
end
end
def photos
# your photo-find logic here
#photos || Photo.find :all
end
end
By using a facade model for the Photoset, you can keep your controller and view logic simple and straightforward, reserving the most complex code for a dedicated model. This code probably won't run out of the box, but hopefully it will give you some ideas and point you in the right direction to resolve your question.
Rails does have a way to do this - I don't know when it was introduced, but it's basically described here: http://guides.rubyonrails.org/form_helpers.html#using-form-helpers
It took a bit of fiddling to alter the configuration properly for the case where there's no parent object, but this seems to be correct (it's basically the same as gamov's answer, but cleaner and doesn't allow for "new" records mixed in with the "update" records):
<%= form_tag photos_update_path do %>
<% #photos.each do |photo| %>
<%= fields_for "photos[#{photo.id}]", photo do |pf| %>
<%= pf.text_field :caption %>
... [other fields]
<% end %>
<% end %>
<% end %>
In your controller, you'll end up with a hash in params[:photos], where the keys are photo IDs, and the values are attribute hashes.
You can use "model name[]" syntax to represent multiple objects.
In view, use "photo[]" as a model name.
<% form_for "photo[]", :url => photos_update_path do |f| %>
<% for #photo in #photos %>
<%= render :partial => "photo_form", :locals => {f => f} %>
<%= submit_tag "Save"%>
<% end %>
<% end %>
This will populate input fields just like you described.
In your controller, you can do bulk updates.
def update
Photo.update(params[:photo].keys, params[:photo].values)
...
end
Indeed, as Turadg mentioned, Rack (Rails 3.0.5) fails if you mix new & existing records in Glen's answer.
You can work around this by making fields_for work manually:
<%= form_tag photos_update_path do %>
<% #photos.each_with_index do |photo,i| %>
<%= fields_for 'photos[#{i}]', photo do |pf| %>
<%= pf.hidden_field :id %>
... [other photo fields]
<% end %>
<% end %>
This is pretty ugly if you ask me, but it's the only way I found to edit multiple records while mixing new and existing records.
The trick here is that instead of having an array of records, the params hash gets a array of hashes (numbered with i, 0,1,2, etc) AND the id in the record hash. Rails will update the existing records accordingly and create the new ones.
One more note: You still need to process the new and existing records in the controller separately (check if :id.present?)