Assignment 4: Models, Controllers, and Views
Errata
- (14-Nov-2009) The AddTags migration is missing one tag and had the wrong name for another. Without this tweak, clicking on a couple of publications will raise an exception. So, in AddTags, change the lines that add the tags to this:
%W[ java ruby ruby1.9 rails projects crud models rest activerecord ].each do |tag_name| Tag.create!(:name => tag_name, :user => john) end
- (9-Nov-2009) At the request of Ron, the download version 1.0.004 wraps up a number of small changes from these errata, namely: (a) the use of :uniq on Tag.index_publications; (b) a similar addition on Publication.index_tags; (c) removal of .uniq in views/publications/_publication.html.erb; (d) fix for doubled occurrence of the word “for” in views/index_entries/new.html.erb; (e) a check that there’s a current_user before displaying the link to add an index entry in views/publications/show.html.erb, and, in this same template, another check for a current_user before displaying annotations, as well as the use of the “for_user” association extension on Publication.annotations; (f) from Keith, functional tests of the PublicationsController and SessionsController. That should be it.
- (8-Nov-2009) Thanks to a mistake on my part, I forgot to constrain the indexed publications for a tag so that the results are unique. Ron found the bug and suggested the fix, which works. In app/models/tag.rb, add “:uniq => true” to the indexed_publications association:
has_many :indexed_publications, :source => :publication, :through => :index_entries, :uniq => true
“uniq” is described on p. 371 (print), p. 375 (PDF).
- (5-Nov-2009) For item 1, the CreateIndexEntries migration: The self.down method is already done for you — disregard the comment in the code that says you need to do something. I accidentally left in the drop_table. :-)
- (4-Nov-2009) As I was revising some slides on Rails filtering, a stream of invective emerged from my mouth as I discovered that I had not “turned on” the filtering completely in the code download for assignment 4. This tweak does not affect your ability to get the assignment done, but I recommend it nonetheless. For all controllers except ApplicationController, RequiresAuthenticationController, and SessionController, make sure that the controllers subclass RequiresAuthenticationController. Example:
class TagsController < RequiresAuthenticationController
This tweak is reflected in e168-assignments4and5-jgn-1.0.003.zip on the downloads page. Again, this goof doesn’t really affect your ability to complete the assignment. The sympton is that if you log out and then go to a page such as /tags/new, you won’t properly be kicked over to authentication.
- In app/models/publication.rb, there may be a “for_tag” association extension on on the wrong association. The has_many associations are as follows. This is the only difference between e168-assignments4and5-jgn-1.0.000.zip, and e168-assignments4and5-jgn-1.0.002.zip (001 was missing the update). You can either download e168-assignments4and5-jgn-1.0.002.zip from the downloads page, or put this code below in place of the associations in the Publication model. The reason I wanted you to have this is because I’ll be discussion association extensions Thursday (at Keith’s request) and these additional methods should make it easier to avoid putting “find” code into your views.
belongs_to :user has_many :annotations, :dependent => :destroy do # Students: This is an "association extension": # See AWDR, p. 372 (print), p. 376 (PDF) def for_user(user) find(:first, :conditions => ['annotations.user_id = ?', user.id]) end end has_many :publication_tags, :dependent => :destroy has_many :tags, :through => :publication_tags has_many :index_entries, :dependent => :destroy do # Another "association extension"; see references above. def for_tag(tag) find(:all, :conditions => { :tag_id => tag.id }) end end has_many :index_tags, :source => :tag, :through => :index_entriesThis is fixed in the freshest download: e168-assignments4and5-jgn-1.0.002.zip on the downloads page.
Both assignments 4 and 5 are due 23-Nov.; automatic extension to 25-Nov. If you complete assignment 4 early, submit it, and we’ll see if we can get comments back to you early.
In this assignment you will implement parts of a full-blown web application: migrations, models, views, and controllers. These components are the foundation of any Rails application. If you can do this well, it should be clear sailing to the end of the course, because most of the other aspects of Rails go deeper or decorate the application with plugin or gem functionality.
You can find a package for the assignment on the downloads page.
The application is essentially an online bibliography/index of publications. (We actually wanted to use the word “resource,” but that has a technical meaning in Rails, so we’re going with something a bit more ordinary.) Here’s the home page of the reference implementation (which you can try out at http://publications.plugh.org):
For the most part, the application just gives a list of books and links to web sites. Each publication may have tags and annotations. There are also links to Amazon and Safari when appropriate.
[NOTE regard "Safari" links: For the "reference implementation" and your own app, you can log in as john@7fff.com to see the Safari links; or, in your own implementation, you can set user.safari = true [and save that user] to turn on the Safari links — I decided to leave user management out of this assignment, because it gets complicated fast.]
If a user logs in, there are buttons on the page that allow the user to see the detail for a publication, edit a publication (if the user added it in the first place), and add an annotation (if the user has never added an annotation for the publication). Additionally, in the “detail” view, there are some additional buttons for adding index entries. You will need to play around with the application to get a feel for what’s there. Here’s what the home page looks after a user logs in:
Here’s the schema (click to enlarge):
The central table is publications. As you can see, a Publication has many annotations, publication tags, and index entries. A publication has tags through both index entries and through publication tags. For example, the book Agile Web Development with Rails might be tagged as “rails.” That means there would be a row in the publication_tags table with publication_id set to the id of AWDR in the publications table, and tag_id set to the id for the “rails” tag. There might also be an index entry for the tag “models.” This would be represented by a row in index_entries. The publication_id would be set to the id for AWDR, and the tag_id would be set to the id for the “models” tag.
The annotations table has brief comments on publications. A publication has many of these. Note that a user is only allowed one annotation per publication.
Finally, notice that all of the tables except users have a foreign key user_id pointing back to the users table. By this means, we know who created every row in the application. In general, we only want the user who created a row to be able to edit or delete it. I believe this is implemented completely in the reference implementation, but I might have forgotten a case!
Here’s what you need to do. We have left a number of parts of the application unimplemented. You need to implement them so that they behave in the same manner as the reference implemention (link above). Here’s the list:
- You must implement the CreateIndexEntries migration. The full set of migrations won’t complete without it. The down method is already done (disregard the code comment that says there’s something for you to do — there isn’t).
- You must complete the Annotation model.
- You must validate the uniqueness of the publication_id for a particular value of the user_id. In other words, the combination of publication_id and user_id must be unique. Hint: Study the validations for PublicationTag.
- The presence of the ‘brief’ attribute is required.
- Add a before validation callback that converts all whitespace in the ‘brief’ and ‘full’ attributes to one space.
- On the Publication model, add a custom validation that ensures that there is a value for either the ‘title’ or ‘url’ attributes, or both.
- Add helpers link_to_amazon and link_to_safari in ApplicationHelper that format links to Amazon and Safari. For the formats, hover over the links in the reference implementation. Hint: Implement these by calling the link_to helper with the appropriate values. See the “Note regarding Safari links” above, regarding how to see them in the application.
- In AnnotationController, implement the new, edit, create, update, and destroy actions. It is critical that it be impossible for a user to edit or delete another user’s annotation. For example, the edit URL (in the reference implemenation) is http://localhost:3000/annotations/5/edit. If the current user didn’t create annotation 5, the action should fail (it doesn’t have to fail gracefully; it just has to not work). Hint: See the other controllers. It would also help to compare how the other controllers are implemented, vs. what script/generate scaffold gives you.
- In PublicationController, change create and update so that they can handle the keywords. This is by far the hardest part of the assignment because of the management of the checkboxes. To get some ideas for handling the free-form tag box, take a look at IndexEntriesController. The “Child-Care Coop” application also provides some guidance for checkbox handling.
- Implement views/annotations/_annotation.html.erb
- Implement views/shared/_indexed_publication.html.erb
- Implement the missing parts of views/publications/show.html.erb
That’s it!



That's what I thought!
@Karin Berger
Yep. Oops for me. I added a note at the top of the page and a comment in the "todo" list itself.
Why does it not work for me to type this into script/console ?
link_to("Example", "http://example.org")
I get this error:
NoMethodError: undefined method `link_to' for #<Object:0x0dc73c @controller=#>
@Ron Newman
Great question.
link_to is a helper, and is, basically, a module that is included for controllers and views -- it's not a global method. However, there is a way to fake this out, which I will show in lecture. Can you wait until tonight?
Sure!
I was trying to see how link_to works, because in your reference implementation, your Amazon link seems to be not just an <a href="..."> link, but also some JavaScript (whose purpose I don't understand)
@Ron Newman
You should be able to figure out what's going on. As you can imagine, this assignment is all about forcing us to read the docs.
1. Observe the behavior of the Amazon link carefully. Compare it to some of the other links, such as the "details" link.
2. Now study the link_to options: http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#M001597
Aha?
I think I see my answer now. (But this is why I wanted to play around with the various parameters in script/console. I can wait a day.)
@Ron Newman
Ron, you are a lifesaver! Not the first time either... thank you.
In the download, I see that some of the views .erb files have names that start with underscore characters, and others don't. What is the significance of this?
@Ron Newman
Those are for "partials," which are subsections of pages for shared content. I will be talking about those tonight. In AWDR, see index under Templates, Partials (I think).
Thanks. I find I'm struggling with "what code is located where" and "what code is executed when" ...
app/views/publications/index.html.erb contains this line:
<%= render(publication) %>
The documentation for render() , at http://api.rubyonrails.org/classes/ActionController/Base.html#M000658 , doesn't explain what it does when it is given a single argument (without keywords) and that argument is an ActiveRecord::Base model.
@Ron Newman
I think you want to read here: http://api.rubyonrails.org/classes/ActionView/Partials.html
Thanks -- are these two different and unrelated render() methods belonging to different classes?
@Ron Newman
I am not 100% certain as to whether they use the same module to accomplish rendering, but they are spiritually similar. I will look into it, but let's leave the details for next week's lecture, or the week after that.
@Ron Newman
I learned that emacs and rinari really ease your life in this concern. With rinari you have shortcuts that take you from say a controller to its views, helpers, etc. you don't have to worry anymore where the stuff is located, emacs takes care of this for you :)
I didn't find any note in AWDR on how to set cookies for functional tests. This might come in handy:
http://www.pluitsolutions.com/2006/08/02/rails-functional-test-with-cookie/
Why does the application store the currently logged in user ID in cookies[:user_id] instead of session[:user_id] ? Seems like this causes the userID to travel back and forth in plaintext with each request and reply, whereas it would always stay on the server side if you put it in the session.
The authentication scheme in assn4 is "the simplest thing that could possibly work," and in any real application, you would want to swap it out and use Clearance or AuthLogic or some other plugin/gem (hence, the screencast). Also, because the auth system in assn4 is so simple, you can actually understand it (I hope).
Sure, I know that it's simple, I'm just curious if there's an advantage to using cookies[:user_id] instead of session[:user_id] for this purpose.
Is there any difference between putting a method in FooController and calling helper_method on it, vs. putting it in FooHelper?
(ApplicationController has two helper methods defined, but there's also an ApplicationHelper class with other methods.)
I'm trying to call the various controller methods from script/console, but having no luck. Not sure how I'd write tests for them either if I can't do this:
>> pc = PublicationsController.new
=> #
>> pc.index
NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.parameters
from /Users/ronnewman/.gem/ruby/1.9.1/gems/actionpack-2.3.3/lib/action_controller/mime_responds.rb:118:in `initialize'
from /Users/ronnewman/.gem/ruby/1.9.1/gems/actionpack-2.3.3/lib/action_controller/mime_responds.rb:105:in `new'
from /Users/ronnewman/.gem/ruby/1.9.1/gems/actionpack-2.3.3/lib/action_controller/mime_responds.rb:105:in `respond_to'
from /Users/ronnewman/cs168/projects/4/app/controllers/publications_controller.rb:7:in `index'
from (irb):2
from /Users/ronnewman/.ruby_versions/ruby-1.9.1-p243/bin/irb:12:in `'
=> #
>> ac.index
NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.parameters
from /Users/ronnewman/.gem/ruby/1.9.1/gems/actionpack-2.3.3/lib/action_controller/mime_responds.rb:118:in `initialize'
from /Users/ronnewman/.gem/ruby/1.9.1/gems/actionpack-2.3.3/lib/action_controller/mime_responds.rb:105:in `new'
from /Users/ronnewman/.gem/ruby/1.9.1/gems/actionpack-2.3.3/lib/action_controller/mime_responds.rb:105:in `respond_to'
from /Users/ronnewman/cs168/projects/4/app/controllers/annotations_controller.rb:7:in `index'
from (irb):4
from /Users/ronnewman/.ruby_versions/ruby-1.9.1-p243/bin/irb:12:in `'
The issue here is that you don't have an HTTP header to pass to the controller, so respond_to is failing (it is looking for an HTTP request). In a test process, you have all that stuff set up for you. John is going to review some code I sent him that demonstrates how to authenticate a user for functional tests.
If a publication has two index entries for the same tag (doesn't matter whether they were added by the same user), and you click on the tag, those index entries will display twice. At the moment, see http://publications.plugh.org/tags/20 .
Both the reference implementation and the incomplete assignment behave this way. There's a "uniq" or "distinct" missing somewhere, either in a "has_many through" association or in the code that uses it.
@Ron Newman
I'll fix that. I think I may be starting the query on the wrong model.
I think the Tag model just needs to have ;uniq on the has_many :indexed_publications association. Fix will be needed in both the reference implementation and the downloaded assignment.
@Ron Newman
That will do it. See note above.
I am not quite able to figure out the purpose of having both the publication_tags model and the index_entries model, especially when the fields contained in the former are a subset of the fields contained in the latter.
There are two things you can tag:
(1) Publications
(2) Index entries
They share the same tags.
So, for example, you might tag Agile Web Development with Rails with the "rails" tag; but it might not have even one index entry with that tag. (Indeed, it would probably have index entries with tags such as "model" "association" and the like.)
So, by this means, you can get a list of:
-- All publications with a certain tag
-- All index entries across all publications with a certain tag
The goal here is to make it possible for a student to say:
-- On what pages of what books can I find discussions of java?
If we only had tags on publications, we wouldn't be able to ask that question.
Is it OK to add more validations? Seems to me that an index_entry should require presence of a first page and a non-blank summary, that the first page and last page should be integers, and maybe even that the last page (if present) be greater than the first page.
@Ron Newman
The product manager has made no such requirements.
Why should page references be integers? One has observed in books page numbers such as i, ii, iii, etc.? Suppose the page reference was to the colophon (typically unpaged, so you would want to write "colophon")? On what rule would you decide the ordering principle between such a page and a numeric one? Maybe you would say that if both page numbers are numeric, then the 2nd one should be greater than the first one. What would the messaging be?
Suppose we allowed users to edit index entries. If we did that, then an obvious use case would be to allow someone to create a new index entry with only partial information as a kind of "foothold." Then that user might come back later and write a summary. If you required a summary up front, you might stymie a worthy contribution.
A product manager might say that it's more prudent to observe user behavior. This is essentially an alpha product -- you might suggest to the product manager that more research be conducted, but I'm not sure I see the payoff in extra validations. Extra validations mean saying "no" to user behavior. But since we don't know much about user behavior yet, we might have to live with some instability for now.
Fair enough, I won't add any more validations. BUT, the data model for index_entry specifies page_first and page_last as integer. If the user enters anything else such as "vii" or "colophon", it's going to be automatically converted to a 0.
@Ron Newman
Do you think page_first and page_last should be changed :string ?
I think they should either be changed to string, or the inputs should be validated as integer -- one or the other.
@Ron Newman
Let's leave it as is for #4, but for #5, when you implement voting -- you can enhance it more thoroughly according to what fits your mental model of how the app should work.
If I go to http://localhost:3000/publications/3 while NOT logged in, I die with a "Called id for nil" RuntimeError at this line of app/views/publications/show.html.erb:
because current_user is nil. However, if I go to http://publications.plugh.org/publications/3 while not logged in, the page displays fine. Is there a difference between the reference implementation and the code that we downloaded for assignment 4? I can easily fix this by checking for logged_in? , but this wasn't mentioned in the assignment.
Are you talking about the code below [h3]Annotations[/h3] ?
The stub code is a bit different from the page that the stub overwrites. [The "stubs" are the code with comments where students need to implement stuff.]
The best thing would be to replace @publication.annotations.find(:first, :conditions => ['annotations.user_id = ?', current_user.id]) with code that leverages the for_user association extension.
Yep, that exact code. I did not realize that the reference implementation and the assignment download might differ in places other than where you removed code so that we could implement it.
I can rewrite the code in the way that you suggest, and probably will, but I'll still need to check that there is a current_user before calling the for_user association extension on it.
@Ron Newman
It's an accident/oversight on my part -- I didn't copy a change over to the stubs.
Another difference I see, now that I've fixed the above: When you're logged out, the reference implementation suppresses the "add index entry" button, while the download displays it.
OK -- I'll look into copying those over into the stubs.
Thanks. This would even allow me to safely put the "details" buttons on the logged-out version of the home page -- but that's probably more functionality change than you want me to do for assignment 4.
In errata #3 it states that e168-assignments4and5-jgn-1.0.003.zip has the corrections applied to the appropriate controllers. It appears the fix wasn't applied to users_controller.rb.
Don't do as I did....in errata #1....when implementing Ron's fix, be sure to add a comma to the previous line :through => :index_entries,...otherwise the migration will fail. I know its obvious, but it got me.
@Mike Byrne
That's true about the UsersController, but you probably noticed that it doesn't do anything . . .
regarding the hint for the update method in PublicationsController:
This is simple, and allows DRY sharing of code between create and update, but it has one disadvantage. After you do it, all of the new PublicationTag records will belong to the current user, wiping out any previous association between the old PublicationTags and other users.
On second thought, I guess this is not really a problem, because the current application design doesn't allow anyone but the creator of the Publication record to add or remove tags on it.
@Ron Newman
In theory, only the owner of the publication can mess with those tags. I think.
the new download version 1.0.004 does NOT contain (b) a similar addition on Publication.index_tags (i.e, adding :uniq to the association)
in the reference solution, it allows us to enter "empty" index entries. I just did it at
http://publications.plugh.org/publications/15
should we be validating if the user entered the necessary info?
when there are no index entries, and you attempt to add an empty one, it even incorrectly informs the user that it was successful, but you don't see anything visually listed. When you next go an add an entry for real, you see a blank entry listed with the valid one you just created. don't know if we need to fix this for the assignment.