#103 Site-Wide Announcements (revised)
Here I show how to add an announcement message at the top of every page in the application and allow the user to permanently hide it. This is all done test-first and even includes testing the JavaScript behavior.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Really nice solution, thanks. I like these episodes the most, where you have the time to create a basic solution, test it and make it more user friendly (hiding + AJAX in this case). Plus I learned something about explicitly testing JavaScript today :)
good
Thanks Ryan. You seem to have this uncanny ability to put out new episodes that pertain directly to what I'm working on. I'm very new to testing, and I'm doing my utmost to learn it - it's like I can code without fear of breaking anything :)
Ryan, any thoughts about doing the "hide" request at the user level vs the browser level, such as using Redis to store the hidden state per user rather than cookies?
Yeah, I didn't like the use (abuse?) of cookies there either. I would just created a relationship in the DB between users and announcements, (AnnouncementsUser user_id, announcement_id, read:boolean).
Then I'd have a method on the user object (current_user.unseen_announcements) that would return an array of all announcements that the user hasn't already marked as read. Then, the act of hiding the announcement would create a record in the DB that records the fact that that user has hidden the announcement.
Of course this isn't a solution for users without accounts.
Im also slightly dubious about using cookies, if the user plays with their cookies (especially the signed ones), while they are still logged in the announcement code seems to go into a tailspin.
Has anyone else seen this issue ?
Thanks Ryan for this really cool episode. I'm currently working on a side project and I'm committed to full TDD so I'm not allowing myself to add any new code without a failing test first.
The only thing that in my opinion could be improved regarding your episode is the way that the hidden_announcement_ids are passed to the scope; in my opinion, it is not very intuitive. I would probably use a hash with the key {skip: []} or something, but that's just me.
Thanks again!
I wonder why you always seem to use
Time.zone.now
, Ryan.Time.current
achieves the same thing in Rails: It usesTime.zone.now
ifTime.zone
is set and otherwise falls back toTime.now
. See http://apidock.com/rails/Time/current/class.And its recommended in the Rails guides
He might simply have not known about it. ;-) I didn't either, so thanx for the tip.
I found the testing really distracting on this one. I just wanted to see how you handle the announcements and it made the episode much more verbose than usual.
I think showing how to do testing is outside the scope of each episode and could be tackled once if needed.
There's a gem that can help with one-off notifications, too: Paul Revere by Thoughtbot
Thanks Ryan. One thing I hope to see in the future is avoiding the use of that kind of templates hide.js.erb I like pretty much using directly the client side
I would like to know how other people do this.
Yep, in this case I'd have done it exactly like you. In some cases you want to plug in variables server side. For those cases rails templates are useful.
Ryan, I am suprised those tests passed. You should use
should have_no_content
instead ofshould_not have_content
, see Capybara README. Maybe PhantomJS makes all AJAX synchronous? I don't know. Thanks for tons of great episodes!To quote the capybara README you linked: "Capybara's Rspec matchers, however, are smart enough to handle either form." That means that
should have_no_content
andshould_not have_content
are functionally equivalent.But thanks for pointing that out, I didn't even know there was a difference.
Thanks for clearing it! I didn't notice that matchers behavior differs from query methods behavior.
Thanks for another great screencast, Ryan.
Something to keep in mind: the
/announcements/:id/hide
route will fail with a 500 error if visited directly, as there is no request.referer in that case. This should be checked by the controller and redirect to a default url in that case, or maybe render a 404.Also, I would add rel="nofollow" to the link, so that spider bots don't follow it.
I love the fact you did this TDD from the start, and I hope all future episodes have at least some TDD element. As a beginner, this actually made this more approachable for me, I know its working if my tests (your tests really) are passing. This is my first time reading comments on one of your episodes and man people are hard on you! Railscasts is my favorite tutorial site by far.... I always come back here and learn something new every time. Keep up the great work!
So, I have a riddle for the experts out there. Not sure if anyone checks these comments often, but here goes.
I'm using this in a custom spree app, so there is some name spacing going on. But I was able to get almost everything to work, making the required changes for the spree namespace. But the hide.js.erb just isn't firing. It's rendering fine, I can see in the log, the extra strange part is this works in production, just not in development...??? Which means my last test, "page.should_not have_content("Hello World")" fails...
In my case I had to "wrap" in a jQuery function for it to work in development:
no idea why, but it worked and all tests are passing!
Just subscribed because I needed to do this exact task. I'm not disappointed I signed up! Good episode thanks!
I tried to do a little gem about this https://github.com/14113/hello_bar
Perhaps, for someone will be useful.
For Mongoid
lte : less than or equal
gte : greater than or equal
nin : not in
I really love the Mongoid syntax.
How to have only active announcement ids in the cookies.signed[:hidden_announcement_ids]
This is kind of 'amateur hour' code (my specialty), but:
After using site-wide-announcements for several months, the development log was passing several dozen expired ids on each page load:
I got tired of looking at all those ids, so as a test I tried to figure out how to have only active announcement ids in the cookies.signed[:hidden_announcement_ids].
Deleting the cookie was simple, but would re-display previously hidden announcements.
Here's my solution.
An 'active' name scope (id in (?)), as opposed to the 'current' scope (id not in (?))
An after_filter :flush_announcement_cookie, placed on the users' login landing dashboard_controller.
Hope this helps somebody.
Following the ASCIIcast and just noticed a typo: "This test fails when we run it as our app doesn’t have an
Application
model. We’ll generate one now with the fields we use in our test then migrate the database."I believe the
Application
should beAnnouncement
Thanks for reporting that, I've corrected this error in the ASCIIcast.
I also just realized that later in the screencast after creating the announcement controller at
/app/controllers/announcements_controller.rb
the first line hasprotect_from_forgery
which doesn't belong. That is already in the application_controller.rbA bit farther down is a paragraph about: "Before we change current we’ll write some tests for the new functionality." that references
/spec/announcement_spec.rb
instead of/spec/models/announcement_spec.rb
Thanks for reporting those inconsistencies, I have corrected the ASCIIcast.
Thanks for updating. Also hope my comment below about typos isn't rude. I realize that it takes extra work to add the ASCIIcasts and in the final analysis I find them very useful as I don't always want to rewatch a video. Your diligence in maintaining them is appreciated.
You're welcome! I certainly did not perceive your comment about typos as rude, and I can certainly imagine that some of these typos could certainly confuse beginners a great deal. I do not write the ASCIIcasts though ;-)
As a moderator I am happy with anyone who takes the time to report inconsistencies or flaws in the textual content on the site so that they can be corrected by one of the moderators, by doing so you make Railscasts a better place for everyone! I am glad this work is appreciated!
Ok, last comment. I think this screencast should have included a complete Announcement resource with views, controller actions and model validations. It was fairly simple to wire up the controller and views to accomplish this. I assume it wasn't done as this screencast was focusing more on the test side. In my case, I used this as a guide to create an announcement function for a client site where the client needs to be able to easily manage his announcements.
Also was surprised by the number of small, but important typos in the ASCIIcast. Depending on your skill level, small mistakes can really throw you for a loop.