#103
Apr 28
Site Wide Announcements
Sometimes you need to display an administrative announcement to every page on the site and give the users the ability to hide the announcement. See how in this episode.
Update: as Simon pointed out in the comments, there’s a problem with the announcement find conditions when passing a hide_time. The code below has been updated with the fix.
script/generate scaffold announcement message:text starts_at:datetime ends_at:datetime script/generate controller javascripts
<!-- layouts/application.html.erb --> <% unless current_announcements.empty? %> <div id="announcement"> <% for announcement in current_announcements %> <p><%=h announcement.message %></p> <% end %> <p><%= link_to_remote "Hide this message", :url => "/javascripts/hide_announcement.js" %></p> </div> <% end %>
# models/announcement.rb def self.current_announcements(hide_time) with_scope :find => { :conditions => "starts_at <= now() AND ends_at >= now()" } do if hide_time.nil? find(:all) else find(:all, :conditions => ["updated_at > ? OR starts_at > ?", hide_time, hide_time]) end end end # application_helper.rb def current_announcements @current_announcements ||= Announcement.current_announcements(session[:announcement_hide_time]) end # javascripts_controller.rb def hide_announcement session[:announcement_hide_time] = Time.now end # hide_announcement.js.rjs page[:announcement].hide # routes.rb map.connect ":controller/:action.:format"




Thanks for another good episode! You've helped me become a better rails dev. Cheers!
I just love it! Thanks!
Nice tip with using a controller by the name 'javascripts'.
Nice cast as always, thanks.
For completeness, you may want to change find conditions to take into account announcements that start after the user chose to hide a previous announcement:
<pre>
["updated_at > ? OR starts_at > ?", hide_time, hide_time]
</pre>
Otherwise if you had, for example an announcement set for today and a different one set for tomorrow, a user hiding today's wouldn't see the later announcement unless it had been edited in the mean time.
Hi Ryan,
nice episode again, but i think there is one litte problem unsolved.
Just think about a user which signs in to your webside (if there is authentication) and hides the message after reading.
After he closes his browser (and cleans his session variable) he will have to hide the same message again for every time he views your side until the event has ended for every message in your database.
So maybe you or someone else has a really efficient solution for this ;) and shares it with us.
Then this would be a really helpful feature.
lg
If you are really worried about that, create a join table.
@QuBiT, Zach Inglis
Or simply stores that value into the database along side with any other user preferences. For performace reason though, these user preference are usually stored, or at least cached in a client side cookie.
@Simon, you'r right! I missed that edge case. I'll update the code in the show notes. Thanks.
@QuBiT, to avoid complications I didn't want to go into that for this episode. However you could store the hide time in some place that is more persistant. Whether that be the database or in a cookie with a longer expiration date.
Thanks, Ryan.
I solved this problem (a variation of it) just by creating boolean column in the database, updating it through an Ajax Updater, and then checking whether the column is true or false.
Your solution is much simpler and I appreciate seeing a different solution!
I don't mind the length, as it's really worth my time... Great work!
Ryan, now() is not a SQL ANSII standard, but you can replace now() by current_timestamp, which is a standard :D
Great screencast. I've had to do this many times and it is a really useful tool. Also, I like the idea of the javascripts controller for these odd little bits that don't quite fit in.
One thing that I might also suggest for those using newer versions of Rails is the is the use of named_scope in the model instead of a function. This would give you all of the benefits of active record collections (like counting when querying for size instead of doing a whole find, etc.). Also, and while admittedly nitpicky, you can split the function a little so that you aren't combining current_announcements with the announcements to display (which would most likely come into play while delving into the management of the announcements).
So, in the model I'd have:
named_scope :current, :conditions => {'starts_at <= current_timestamp() AND ends_at >= current_timestamp()'}
named_scope :since, lambda {|hide_time|
{ :conditions => (hide_time ? ['updated_at > ? or starts_at > ? )', hide_time, hide_time] : nil) }
}
def self.to_display(hide_time)
current.since(hide_time)
end
And since I changed the function name, I'd have to change the helper to:
@current_announcements ||= Announcement.to_display(session[:announcement_hide_time])
Refactoring the model to include a since attribute might be overkill (though I find it comes in handy for other reasons more often than not), but this is an excellent place for the use of named_scope.
You twittered wondering if people like the longer screencasts. I say the longer the better. They are more engaging and in the end, more helpful. Thanks so much!
I personally like the "show_announcement" boolean column better.
I have been watching railscasts for months learning so much about rails. This episode is one of the best yet, chock-full of not only useful techniques but also programming theory and application. I loved it! Thanks for a good show. I am a better programmer because of it.
Dear Ryan
The Railscasts site is awesome, and you totally rock, but has anybody ever told you that your voice is a little reminiscent of Kenneth the NBC page from the TV show 30 Rock ? I find it somewhat disturbing when I am trying to absorb the pearls of Rails wisdom that you are trying to impart that I have a mental image of them coming from the mouth of Kenneth. Could you perhaps consider adding a photograph of yourself on the site to fix this problem for me ?
Your sincerely
Matthew
PS. I realise this isn't exactly a flattering comparison. Sorry about that.
Railscasts are terrific. Thanks so much for creating them.
How about using a partial and then doing: render :partial => "announcements" unless announcements.empty?
This would significantly clean up the view and eliminate the the if / end clause.
Is it a ok to access directly model methods in views? I mean mixing presentation and persistence level together.
@Matthew - haha... I actually find Ryan's voice okay. He's concise and focused in the way he presents. With all due respect, I actually prefer his voice than to the guy from Peepcode.
@Simon2, I find Ryan's presentation skills to be excellent as well, but there's just that unfortunate resemblance to this character that creates a disturbing mental image for me while I'm listening.
@Matthew, http://workingwithrails.com/person/6491-ryan-bates
That what you looking for?
@Kieran, thanks, that's much better than <a href="http://amysrobot.com/files/30rock_kenneth.JPG">this</a>
For something like this, would y'all put an index on the announcement date fields?
There's going to be a query against them for every page view and the index won't be updated very often. I always struggle with what to index and what not to index!
@Wes
Just to prove that my contributions here aren't totally frivolous, adding an index is usually only of any value if the tables are large, which seems unlikely in this example. Remember that accessing a table through an index requires at least two buffer gets (and potentially physical I/O's) - one to the index and one to the data, so on very small tables it can actually be worse than just a table scan. Most databases with cost based optimizers will not even consider using an index for tables less than a couple of thousand rows.
Find conditions are so much easier to contstruct in datamapper. Also you really need to just start teaching the viewers a little javascript. The built in helpers are not DRY at all.
not work for me :(
@Kino: It didn't work for me either when using SQLlite, perhaps because of the "now()" error. Replace "now()" with "current_timestamp" and it worked for me.
Now a general question/request.
So say you have two site-wide announcements at once. Instead of the text that says "Hide this message", I want the system to detect that there are two or more messages and then revise the link text to a plural form if applicable.
I wrote a helper method called announcements_hide_link that deals with the if...then logic but unfortuately I couldn't get count to work except in the model. So to check the count I call:
Announcement.count_all_current_announcements(session[:announcement_hide_time])
which coresponds to this code in the model:
def self.count_all_current_announcements(hide_time)
with_scope :find => { :conditions => "starts_at <= current_timestamp AND ends_at >= current_timestamp" } do
if hide_time.nil?
count
else
count(:conditions => ["updated_at > ? OR starts_at > ?", hide_time, hide_time])
end
end
end
Notice how this all looks a lot alike the code in the current_announcements method?
How can I get rid of this duplicate code?