#103
Apr 28, 2008

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.
Download (21.5 MB, 13:30)
alternative download for iPod & Apple TV (16.2 MB, 13:30)

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"

RSS Feed for Episode Comments 47 comments

1. Thomas Apr 28, 2008 at 00:06

Thanks for another good episode! You've helped me become a better rails dev. Cheers!


2. Falk Apr 28, 2008 at 00:17

I just love it! Thanks!


3. Dougal Apr 28, 2008 at 02:52

Nice tip with using a controller by the name 'javascripts'.


4. Simon Apr 28, 2008 at 03:18

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.


5. QuBiT Apr 28, 2008 at 03:21

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


6. Zach Inglis Apr 28, 2008 at 04:22

If you are really worried about that, create a join table.


7. Godfrey Apr 28, 2008 at 04:47

@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.


8. Ryan Bates Apr 28, 2008 at 08:26

@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.


9. Lake Denman Apr 28, 2008 at 08:39

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!


10. Carlos Júnior Apr 28, 2008 at 09:30

Ryan, now() is not a SQL ANSII standard, but you can replace now() by current_timestamp, which is a standard :D


11. Aaron H. Apr 28, 2008 at 10:34

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.


12. Mig Apr 28, 2008 at 10:58

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!


13. Justin Apr 28, 2008 at 12:02

I personally like the "show_announcement" boolean column better.


14. Jeremy (Rails Newb) Apr 28, 2008 at 12:42

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.


15. Matthew Apr 28, 2008 at 17:53

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.


16. John Honovich Apr 28, 2008 at 21:20

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.


17. Valery Apr 30, 2008 at 08:50

Is it a ok to access directly model methods in views? I mean mixing presentation and persistence level together.


18. Simon2 Apr 30, 2008 at 10:08

@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.


19. Matthew Apr 30, 2008 at 17:05

@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.


20. Kieran Apr 30, 2008 at 20:26

@Matthew, http://workingwithrails.com/person/6491-ryan-bates

That what you looking for?


21. Matthew Apr 30, 2008 at 22:12

@Kieran, thanks, that's much better than <a href="http://amysrobot.com/files/30rock_kenneth.JPG">this</a>


22. Wes May 01, 2008 at 07:38

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!


23. Matthew May 01, 2008 at 20:18

@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.


24. Brian May 02, 2008 at 03:52

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.


25. kino May 03, 2008 at 07:22

not work for me :(


26. Jeremy May 09, 2008 at 00:05

@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?

 


27. John May 17, 2008 at 06:32

Greetings! You wish to earn? On this page Russian magic purses which return with percent will be placed!
I will tell to you as it to make!
Be registered in system webmoney.ru
Exchange you can on any other currency, for example E-gold
The most simple and checked up way which I use are MAGIC PURSES webmoney! Yes! There is in a network such system. You translate on a purse the certain sum, completely not big, and through pair hours this Magic purse returns it TWICE MORE!!!
For example, I in day earn on 20-25$ (webmoney) - not bad, the truth?
Follow my councils!!!
1). Do not translate on the Magic purse webmoney the sum more than 60$ and less than 1,5$
2). Send webmoney no more an once a day from one ip addresses...
Here, actually, and everything, than I can help you! Sincerely I wish good luck!
I give you numbers of these purses not simply so. The matter is that it is favourable to owners WebMoney to involve more people,
For this money turns in any business, and percent depend on quantity of the people. So try!
These purses really work!
Z331185425897
Z874315665209


28. Alderete May 19, 2008 at 01:31

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?


29. Sam Granieri May 23, 2008 at 13:38

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!


30. Jeremy May 24, 2008 at 10:56

@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.


31. Dwight Spencer May 25, 2008 at 04:46

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


32. Geoff May 27, 2008 at 05:17

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>


33. Sam Granieri May 27, 2008 at 12:29

@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!


34. Jeremy May 28, 2008 at 00:23

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


35. Ramon Tayag May 29, 2008 at 05:38

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>


36. aredayenglis Jun 02, 2008 at 03:47

things. I never were about the boys took by year. Now, reaction his damage Forest. rewarding


37. David Parker Sep 17, 2008 at 20:21

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/


38. Jeff Judge Sep 22, 2008 at 15:32

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


39. gadi Oct 03, 2008 at 06:06

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!


40. Alfia Oct 06, 2008 at 04:59

My name is Alfia, I am 29 years old, and I’m single. I m from Russia, the Ural region, the city of Perm
I address to all with the request for the help and understanding
It is very inconvenient me, but I do not have other choice.
I do not have not enough very few money for buying an apartment. I am so tired to rent the apartment for many years, but I have almost all sum on habitation, and me does not suffice absolutely slightly
I have simply thought that if everyone will send me though on 5 dollars on other end of a planet one person becomes happier.
Yours faithfully, Alfia
My e-mail address is alfik23@rambler.ru


41. houssem Oct 17, 2008 at 01:56

please i'm student in high school i play volleyball but i don't have the right shooes so any one who could help me to get an asics volleyball sooes please help me


42. Austin Schneider Oct 17, 2008 at 08:16

@houssem: wtf.


43. Zaphod Beeblebrox Oct 21, 2008 at 07:56

Thanks 39. gadi !!!


44. wotlk cd key Nov 13, 2008 at 01:30

it works, thanks, ryan!


45. requiem gold Nov 20, 2008 at 18:17

Thanks Ryan,I think this is one of the most wonderful sites. I have great admiration for you.


46. habbo gold Nov 22, 2008 at 00:11

I have great admiration for you.


47. lily Nov 27, 2008 at 19:19

Thank you Ryan, your screencast is good. Please look at our URL, if necessary we can learn from each other.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player