Categories: archive |code

Site wide announcements in Rails using jQuery (jGrowl)

I like it when a site tells me that they’re going to do some maintenance, so I wanted to implement similar features for a web application that I’m working on. I based my announcements on Ryan Bates’ Railscasts episode 103 and updated code based on avarhirion’s post on the Rails Forum. The major changes are the added changes for cookie support, graceful degradation, and jQuery (jGrowl).

If you’d rather, check out all the files on Github.

announcement.rb:

class Announcement < ActiveRecord::Base
  named_scope :active, lambda { { :conditions => ['starts_at <= ? AND ends_at >= ?', Time.now.utc, Time.now.utc] } }
  named_scope :since, lambda { |hide_time| { :conditions => (hide_time ? ['updated_at > ? OR starts_at > ?', hide_time.utc, hide_time.utc] : nil) } }
  def self.current_announcements(hide_time)
    active.since(hide_time)
  end
end

application.html.haml:

- unless current_announcements.empty?
  #announcements_box
    -for announcement in current_announcements
      .announcement{:id => 'announcement_' + announcement.id.to_s}
        =h announcement.message
        = link_to "Hide Announcements", hide_announcements_path, :id => 'hideAnn'

routes.rb:

map.hide_announcements '/hide_announcements', :controller => 'javascripts', :action => 'hide_announcements'

Here’s the first of the important ones: javascripts_controller.rb. Note that I’m setting the session and a cookie. Also see my TODO. I’d like to set the expiration date of the cookie to the oldest expiring active announcement.

class JavascriptsController < ApplicationController
  def hide_announcements
    time = Time.now.utc
    set_session time
    set_cookies time
    respond_to do |format|
      format.html { redirect_to(root_path) }
      format.js { redirect_to(root_path) }
    end
  end

  private
    def set_session(time)
      session[:announcement_hide_time] = time
    end
    #TODO change expiration time to be the expiration
    #date from the list in current_announcements
    def set_cookies(time)
      cookies[:announcement_hide_time] = {
        :value => time.to_datetime.to_s,
        :expires => time.next_week
      }
    end
end

In the application_helper.rb, we check the session first and set the announcements, otherwise, we check the cookies.

def current_announcements
  unless session[:announcement_hide_time].nil?
    time = session[:announcement_hide_time]
  else
    time = cookies[:announcement_hide_time].to_datetime unless cookies[:announcement_hide_time].nil?
  end
  @current_announcements ||= Announcement.current_announcements(time)
end

Finally, to make it with the cool-kids, we need to add some jQuery. So I decided to use the jGrowl plugin because it’s awesome. This code goes in your application.js file (somewhere in $(function){ //here })

//jgrowl announcements. ajax GET to hide ann/store cookie
$.jGrowl.defaults.closer = false;
$("#announcements_box").css("display","none");
$("#announcements_box .announcement").each(function(){
  $.jGrowl(this.textContent,{ sticky:true, close:function(e,m,o){hide_announcements();} });
});

function hide_announcements(){
  $.get(
    '/hide_announcements'
  );

  $("#announcements_box").fadeOut();
  return false;
}

Obviously, you’ll need to include jGrowl js and css files.

I like this solution because it provides a great looking way to do site-wide announcements. The solution degrades gracefully if the user does not have javascript enabled. Finally, by saving to a cookie, users can leave, close their browsers, and come back as the please and not see the same announcements over and over again.

I’d like to leave you with a couple of specs that I wrote. I attempted to do TDD/BDD and do the specs first, but it was tough. I didn’t get too far, as I was having some cookie/session problems. They’re in some serious need of help, so if you know cool stuff, help me out in the comments, thanks.

application_helper_spec.rb:

#TODO these are weak/inaccurate specs.  These are more-or-less stubs
describe "should find the current announcements" do
  it "when the session['announcement_hide_time'] is not nil" do
    session['announcement_hide_time'] = Time.now.utc
    current_announcements.should == Announcement.current_announcements(session['announcement_hide_time'])
  end

  it "when the session['announcement_hide_time'] is nil and cookies['announcement_hide_time'] is not nil" do
    session['announcement_hide_time'] = nil
    time = Time.now.utc
    request.cookies['announcement_hide_time'] = {:value   => time.to_datetime.to_s,
                                                  :expires => time.next_week}
  end

  it "when the session['announcement_hide_time'] and cookies['announcement_hide_time'] are not nil" do
    session['announcement_hide_time'] = nil
    cookies['announcement_hide_time'] = nil
    current_announcements.should == Announcement.current_announcements(nil)
  end
end

announcement_spec.rb:

#TODO these are weak/inaccurate specs.  These are more-or-less stubs
describe "the current_announcements" do
  it "should return announcements that are active" do
    time = Time.now.utc
    Announcement.active.should == Announcement.find(:all, :conditions => ['starts_at <= ? AND ends_at >= ?', time, time])
  end

  it "should return announcements since a time provided" do
    time = Time.now.utc
    Announcement.since.should == Announcement.find(
          :all,
          :conditions => (time ? ['updated_at > ? OR starts_at > ?', time.utc, time.utc] : nil))
  end

  it "should return announcements since a time provided (nil)" do
    time = nil
    Announcement.since.should == Announcement.find(
          :all,
          :conditions => (time ? ['updated_at > ? OR starts_at > ?', time.utc, time.utc] : nil))
  end
end

Any feedback is appreciated.