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.
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:
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.
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.
@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!
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).
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 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.
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.
@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.
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!
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.
@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:
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?
I am having problems with this one, where my .js.rjs template isn't being used, and instead there's an error saying that Rails apparently expects a .js.erb template file instead.
Great screencast, and a wonderful concept. Though, I wonder which is better to do (or more likely safer from an sql injection point of view); using the "updated_at > ?" hide_time or "updated_at #{hide_time}"
all viewpoints would be great to hear from on the different methods of queries.
Thanks,
Master Denzuko
Feed your head free you mind
I ended up changing my 'hide message' store to cookies as was suggested above. I think cookies is the best solution because it doesn't require registered users, and it's more scalable. Here's a link with some code:
I dont know how to do that in SQLite. My best advice is to use google and see if that helps. I should have been more clear in my post. My change is MySQL specific.
I fixed the code to work with SQLite and Rails 2.1, plus added a few improvements like extracting the announcements div into a partial, implementing named_scope, and a feature for a plural hide message if more than one announcement is active.
Excellent as always. I'm using timezones though, and for the longest time I thought there was something with my code. Remember to convert you hide_time to UTC when doing the find!
I just expanded on Ryan's and Jeremy's code... graceful degradation, cookies, and jQuery (jGrowl). See more here:
http://davidwparker.com/2008/09/17/site-wide-announcements-in-rails-using-jquery-jgrowl/
I recently released a gem which simplifies the whole process of creating site-wide announcement messages. It is called announcements, you can check it out at github:
Thanks for another good episode! You've helped me become a better rails dev. Cheers!
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.
Good idea. I'm going to do that in my app that uses this.
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.
@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?
I am having problems with this one, where my .js.rjs template isn't being used, and instead there's an error saying that Rails apparently expects a .js.erb template file instead.
Here's the error I get:
ActionController::MissingTemplate (Missing template javascripts/hide_announcements.js.erb
Here's the line from my routing file:
map.connect ":controller/:action.:format"
And here's my controller action method:
def hide_announcements
session[:announcement_hide_time] = Time.now
end
What could be causing Rails to think that the template file should be an ERB file instead of an RJS file?
Ryan, great screencast, but with the new timezone stuff in rails 2.1, you gotta be careful with UTC.
Instead of using now(), i'm using utc_timestamp on mysql.
with_scope :find => { :conditions => "starts_at <= utc_timestamp AND ends_at >= utc_timestamp" } do
I have to convert central time to UTC. Just a helpful hint, and keep up the great work!
@sam:
utc_timestamp does not work in SQLlite. Any alternatives, or must we input that directly into the code?
I can't get this code to work on my Rails 2.1 app.
Great screencast, and a wonderful concept. Though, I wonder which is better to do (or more likely safer from an sql injection point of view); using the "updated_at > ?" hide_time or "updated_at #{hide_time}"
all viewpoints would be great to hear from on the different methods of queries.
Thanks,
Master Denzuko
Feed your head free you mind
Ryan, great screencast! You Rock!
I ended up changing my 'hide message' store to cookies as was suggested above. I think cookies is the best solution because it doesn't require registered users, and it's more scalable. Here's a link with some code:
<a href="http://geoff.evason.name/2008/05/27/railscasts-does-it-again-site-wide-announcements/">http://geoff.evason.name/2008/05/27/railscasts-does-it-again-site-wide-announcements/</a>
@Jeremy,
I dont know how to do that in SQLite. My best advice is to use google and see if that helps. I should have been more clear in my post. My change is MySQL specific.
Good Luck!
Hi all.
I fixed the code to work with SQLite and Rails 2.1, plus added a few improvements like extracting the announcements div into a partial, implementing named_scope, and a feature for a plural hide message if more than one announcement is active.
See it here:
http://railsforum.com/viewtopic.php?pid=63353
Excellent as always. I'm using timezones though, and for the longest time I thought there was something with my code. Remember to convert you hide_time to UTC when doing the find!
<code>
all(:conditions => ["updated_at >= ?", hide_time.getutc])
</code>
I just expanded on Ryan's and Jeremy's code... graceful degradation, cookies, and jQuery (jGrowl). See more here:
http://davidwparker.com/2008/09/17/site-wide-announcements-in-rails-using-jquery-jgrowl/
Thanks for the great screencast Ryan.
I didn't take a look through the solutions posted from others, but I made some tweaks to allow for multiple announcements and use cookies.
http://pastie.org/277410
Thanks for the great webcast.
To get it to work with Rails 2.1 with REST you need to do some changes. Here is my code:
# in routes.rb
map.resources :javascripts, :collection => { :hide => :get }
# in your controller
def hide
session[:announcment_hide_time]=Time.now()
respond_to do |format|
format.js # hide.js.rjs
end
end
# in the view ( I am using HAML )
= link_to_remote
'hide messages', :update => 'messages', :url => hide_javascripts_path, :method => 'get'
GOOD LUCK!
@houssem: wtf.
Thanks 39. gadi !!!
for anyone using Postgresql, I beleive current_timestamp does not return in utc - I fixed it with the following:
named_scope :current, :conditions => "starts_at <= now() AT TIME ZONE 'UTC' AND ends_at >= now() AT TIME ZONE 'UTC'"
cheers
What is the CSS code used for the announcement div?
I recently released a gem which simplifies the whole process of creating site-wide announcement messages. It is called
announcements
, you can check it out at github:https://github.com/svileng/announcements
I hope you'll find that useful.