Assignment 5: New feature for the “publications” app
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.
The publications application is useful, but there is no way to tell what people really like.
Your assignment is to implement some means to gauge the popularity or value of a publication. You must also write up how you decided on your strategy, and how you implemented it.
You must modify the application to
- Accept ratings, votes, or “likes” for individual publications (more on these terms below), or figure out some other way to determine popularity (for example, count clicks on links).
- Show a count or average of the ratings, votes, or “likes” on each publication.
- Sort the publications by your metric.
Now, in deciding how to determine popularity or value, you’re going to have to put on your design / usability / product features hat. We want your evaluation of popularity/value to stand up. Here are some considerations:
- Voting means that you can vote “up” or “down.” Most likely, a user or visitor should only get one vote. I would think that a user or visitor should be able to change his or her vote (perhaps the user made a mistake, or changed his or her mind).
- Another way to think about it is a rating from 0 to 5 stars. Again, users should be able to change their ratings.
- A “like” (as used on Facebook) it like voting, but there’s no negative vote. I don’t think you can “unlike” something on Facebook, but you would probably want to provide that if you go this route.
- To express an assessment of a publication, must the user be logged in? If not, you open the ratings system up to abuse, and it would be harder for a user to change a vote after leaving the site, and coming back. You’ll have to decide.
- Another way to do this is to track clicks. The way this works is you point the Amazon and Safari links to an action which writes a row into a clicks table. The clicks table would need to have a foreign key pointing to the publication; and you’d want to know whether the link was an Amazon or Safari link. After the click has been recorded, then you redirect the user to the place he or she originally wanted to go. Again, there is the question as to whether a click should be counted multiple times per user, or only once.
Do not worry about visual appearance! For voting, you can provide ordinary text links or buttons that say “Vote Up” or “Vote Down.” Similarly, for a rating, you can have links to text for “1″ “2″ “3″ “4″ and “5.” In other words, assume that for a real feature a designer would help you out.
Nice-to-haves . . . While there is no extra credit, the very best submissions may . . .
- Implement the voting / rating / like system with Ajax;
- OR, provide an administrative controller that provides a more analytic view of the collected data.
I guess we are not allowed to use acts_as_rateable or acts_as_voteable?
I'm trying to log the ip address of the user. I'm working on my dev environment and localhost:3000 What is the difference between
1) request.env['REMOTE_HOST']
2) request.remote_ip
for 1) I get back 'localhost' and 2) ::1
It seems I want 1) as the result, but the Ruby ways seems to imply 2)
help??
My above question is not required if we can use the fact that non-logged in users should *not* be considered in the clicks metric. Can we assume so?
In class you mentioned something about links and if they should be GET or POST e.g. what happens when a user clicks on a link to mark a rating. You said there is a way to do link_to as a POST or something along that line. Can you please explain?
@Lateral Punk
oh it's like this:
link_to 'clickme', 'www.clickme.com', :method => :post
awesome
If I keep a table called 'clickers' that keeps a list of IP addresses, it will get *big* very soon. Is that a bad design decision?
I want to unit test users clicking on link and going around the website. What's the best way to do this? I remember John demoing mechanize, but I think there is a simpler way right? Or can you recommend the best way to test Assignment 5?
@Gabriel Hase
No acts_as_rateable, etc. No pain, no gain.
For some reason I am not getting comment notifications, so you may seem some tardiness from me -- I'll try and fix it.
@Lateral Punk
A few answers:
(1) request.remote_ip will work in production.
(2) I wouldn't worry about the size of your clickers table.
I like to validate_uniqueness_of across multiple columns. Is this the right syntax:
validates_uniqueness_of :x, :scope => [:y, :z]
@Lateral Punk
Yep:
It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, making sure that a teacher can only be on the schedule once per semester for a particular class.
http://ar.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#M000086
I have a model which basically acts as a look-up table. There are like 5 entries in it. I want to references to this entry just once cause I know it aint going to change. So what I was thinking was putting either a class variable (@@var) or a class member variable for that model. e.g.
@@var = TheTable.find(:first,...)
Is this bad practice,idea? I would think that the model is only created once in the rails environment so I should be safe. Or should it go in a controller or something? but in the controller??
thanks
I decided to use class instance variables to help me make a lookup table easier to access. Also railscast #1 was useful for caching the instance variable. Also, did some Ruby meta-programming so that I can I dynamically create class methods for the different named rows in my look-up table. It helped to keep things DRY.
@Lateral Punk
@Lateral Punk
This is a perennial problem for Rails developers -- how to model "constant" data from the database. You might ask: Should this really be modeled as a database table? Your solution is reasonable, but you will probably find that for different projects you will try different things. Some people just have an Array with their constant column values. Or they'll make a little "Enum" class that has the human-readable name, the constant value for the column, etc., and do it in such a way that they can populate a collection_select form helper properly.
Could you share what you did re: "did some Ruby meta-programming so that I can I dynamically create class methods for the different named rows in my look-up table"?
@john
I've sent you an email regarding what I did. If you think it's appropriate, please let me know and I will post it here.
what's the correct way for Controller A to send a message to Controller B for further processing of object C? is it using redirect_to? is it instantiating Controller B in Controller A?
@Lateral Punk
Redirect; the target controller should get its "orders" from the normal params Hash set up from routing. You could also use a session variable.
If you're doing a lot of redirection, you should reconsider your application architecture.
Well I'm doing several redirects yeah, and it's a pain. It's the only way for A to talk to B. Unless, if I create a class method on B which A can directly call. Is that a better option? I have some logic which is independent of any instance of B, so I don't see any issue in just doing B.foo? that avoids a redirect and doesn't necessarily break encapsulation. What do you think? If you agree, then how can I make the class method foo not also be an action (e.g. private action?)
@Lateral Punk
If A and B should be separate controllers, then you should use a redirect.
If there is a method foo that might be called by BOTH A and B, then you could create a superclass, which A and B would both extend.
Finally, you can mark any method in your controller as private.
Also, see the beginning of 22.1 in AWDR: "By default, any public method in a controller may be invoked as an action method. You can prevent particular methods from being accessible as actions by making them protected or private. If for some reason you must make a method in a controller public but don’t want it to be accessible as an action, hide it using hide_action . . ."
A and B are both distinct controllers, so I can't use inheritance. I don't understand why i can't just make a class method on B? You've noted before that too many redirects are bad, so I thought another way for distinct controllers to talk to each other is via class methods? what's wrong with that design?
thanks for that private/protected info...will use that
I see myself doing this a lot:
rater = Rater.find_by_name(rater_name)
rater = Rater.create!(:name=>rater_name) rescue nil if rater.nil?
is there a short cut to do a create if it doesn't exist and find in one shot?
Rater.find_or_create_by_name
@Lateral Punk
Of course you can use inheritance.
class Parent < ApplicationController
def your_method
end
end
class A < Parent
end
class B < Parent
end
Also: What does the class method do? If it manipulates data, it belongs on a model, not on a controller.
@Ron - Thanks man!
@John - No I meant I don't want to use inheritance since A & B aren't related. Anyways, I learned something very important from Jonathan today in our tutorial. It's just what you said in your last statement above "model not a controller". Jonathan advised to "keep controllers thin & models fat". That's an awesome piece of advise IMHO! I did just that did that to solve my above problem, and voila good overall solution!
thanks!!!
@Lateral Punk
You say they're not related, but if you want to call a method on one controller from another, isn't that telling you something?
This is the classic statement on "skinny controller, fat model," which I think has been mentioned here or in lecture before . . .
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
A is a publication and B is a rating. So they don't necessarily share the same inheritance hierarchy but it's more that A uses B e.g. a publication says to rate it.
I must have missed such an important statement in class, but hearing it now and reading that article makes a whole difference to the way I've developed MVC apps (mostly for iPhone) before. I now know I could have made huge changes to my previous designs and they would have been much better. Thanks for shedding some light on this fact..
@Lateral Punk
Well, you would have discovered it yourself as you add HTML and XML interfaces simultaneously -- as you find duplicated code generating responses for each, the natural place to move it is to the model.
From what I read it doesn't seem necessary that a rating has to be tied down to a user. But if we choose not to, will we lose points? In my implementation I would like to associate a rating to an IP address with some throttling: 1 vote per day, hour, etc. Just a thought because I don't enjoy "logging in."
Espn, Pro-bowl, Allstar voting uses similar voting systems which do not require any log in. I think voting up annotations would be useful, aside from publications. If I saw a good one that helped me out during a quick google search I'd be tempted to +1 it without registering and logging in. Of course, I don't mind following the rules to a T. Just a thought!
@Donnie
Without login, you will need to address in your writeup and implementation:
-- How you handle people attempting to vote more than once. (You might set a cookie for them, so that you can detect if they are trying to vote twice on the same thing; you might make it LOOK like they're getting another vote, while silently ignoring a 2nd vote.) You would have to acknowledge that a user could probably get around any defense you'd establish. Or maybe you'd use a Captcha.
For voting on annotations:
-- How will you reorganize the display so that it is sorted to show the user the best annotations? Does that make sense? Why or why not? Suppose there is an annotation everyone votes for, but it's so tied to the specific book, that it's uninteresting to anyone not interested in that book?
A great submission will take all of these issues into account in the writeup/requirements, and will try to address some of the problems in the implementation.
Without login, there's no way for a user to change his/her vote -- which is a stated requirement here.
If there's voting on annotations, it would make sense to sort the annotations per book, not globally.
@Ron Newman
If you set a persistent cookie, a visitor can return and change their vote. To be sure, such a user might delete his or her cookie, but you can most certainly write code that will allow the user to have some context even without a full-blown user/login system. I am not saying that this is a good thing. But plenty of sites do it.
I've done it via method 5). So it's all implicit e.g. creating annotations, indexes, clicking safari, etc. I assume that i *don't* have to allow for changing a vote because a user really isn't ever explicitly voting for something (hence it isn't a requirement for 5) right?
I take care of users & multiple votes by allowing both logged in users & doing a simple IP check. I'm not putting emphasis on cookies or firewalls and stuff, I think the point of this assignment was to come up with a voting scheme that works well. Not too concerned about edge cases.
@Ron Newman
Um, I don't think we require that a visitor be able to change a vote. It really depends on your assessment model. We've left it pretty open. You might have visitors. . .
Vote (yea or nay)
Rate (0 to 5)
Express "liking" something. In the Facebook model, you can only "like" something, not dislike something; and it seems to be irreversable
All of these have tradeoffs. For example, being able to "like" something sets the bar very low; you probably don't even have to think about it. (A good thing?) Whereas when you have to rate something you might start to wonder what a "4" (out of 5) really means, and you might give up -- it might just be too hard.
I interpreted "user or visitor should be able to change his or her vote" to mean that I could vote today on my home computer, then change my vote next week from another computer in Widener. I guess that's too strict an interpretation.
in Facebook you can Like, and then later you can come back and Unlike (i.e., wipe out your previous LIke).
@Ron Newman
That is all carefully hedged: "Most likely . . ."; "I would think . . ." You need to explain why -- or why not -- changing your vote makes any sense. This is a required part of the writeup ("You must also write up how you decided on your strategy"). To be sure, the best implementations will likely do this, but obviously you may have an idea of "voting" that doesn't including taking a vote back or changing it (as in a Federal election). If you do votes without comments, maybe it should be "hard" to vote so that people take it seriously. Or, maybe with the assumption that people don't take it seriously, they should be able to change their votes. Maybe your design puts the vote for/vote against buttons, checkboxes, or links too close together, and so it's easy to vote incorrectly.
Point taken about Facebook. But you still can't "dislike" something, which is really too bad; but, again, someone thought long and hard about what the Facebook experience is supposed to be. Maybe it is inappropriate for a "friendly" app primary directed to students to permit disliking publications on Ruby and Rails (?).
I was planning on keeping it simple with a throttle table. Where an IP+datetime+object-type+object-id pairing would determine who voted and when. I would like it to be as easy as possible for an end user to vote. If they're going to ghost their IP, they could probably register a thousand e-mail addresses etc. Methinks I'll take the cookie cutter approach to avoid all this philosophy talk and 'ithinks.'
It would make sense for me to see the best annotations (not just pubs) -- the best bits of ruby from all publications. Typically, there are 'bits' of books that I like BUT I may dislike the whole book. I was also thinking that Amazon already has a stronghold on book ratings and I could just import that "rating."
But yes I really need to stop diverging from the requirements. In the real world, the customer (school) gave me requirements so I'll follow those to a T. Thanks for entertaining the thought though!
@Donnie Demuth
A throttle isn't a bad idea. IP's, though, can look the same behind a router, especially at a school. A common strategy is to unique the user agent in the unique identifier for a user. And you can't beat cookies.
Ah, excellent point.
Is there a specific sort you would like us to do? I have 5 stars, and I am assuming that you would want it sorted on the highest average. Could we look at most votes or highest total of all points in votes? Should I give 3 sort links that lets the user choose?
@Jeff Ancel
I think you probably want to sort it one way.
If a publication gets 3 ratings of 5 points each, should it sort higher or lower than another one with 100 ratings of 4.5 points each? Hard to say.
Suppose my view hypothetically wanted to display a list of all publications sorted by "number of index entries descending"". Is there a more idiomatic (or more efficient) way to do it than this?
<% @publications.sort_by {|p| p.index_entries.count}.reverse_each do |publication| %>Seems like there must be some way to take that sort_by/reverse_each logic and make it into a method of the @publications array, but I'm not sure how to do that.
And when I execute that statement, I find this in the log:
SQL (0.2ms) SELECT count(*) AS count_all FROM "annotations" WHERE ("annotations".publication_id = 1) SQL (0.1ms) SELECT count(*) AS count_all FROM "annotations" WHERE ("annotations".publication_id = 2) SQL (0.2ms) SELECT count(*) AS count_all FROM "annotations" WHERE ("annotations".publication_id = 3) SQL (0.1ms) SELECT count(*) AS count_all FROM "annotations" WHERE ("annotations".publication_id = 4) ... etc ...which seems highly sub-optimal. How do I get Rails to generate a single SQL statement that uses GROUP BY publication_id, instead of N separate statements?
(I changed my code between those last two posts from "index_entries.count" to "annotations.count", but my basic questions still stand.)
AHH, i'm going crazy here with the rake command. I'm ready to handin my assignment, so I'm running rake to package things. Well, it goes and does all the tests first. That's where my problem is. Some of the tests have pre-created data inside of them e.g. User has several users by default. Well, for some reason my test.sqlite3 DB is only creating the structure of the scheme and not populating it with the pre-created user data. Hence all my test cases are failing. I also notice that the test.sqlite3 DB file is always smaller than it's equivalent develoopment.sqlite3. I'm so confused about how Rails works with the test DB. I've tried:
rake db:migrate RAILS_ENV=test
rake db:test:clone
manually copying the development DB to the test DB
and a whole bunch of other crazy things
what does this mean also:
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
adapter: sqlite3
database: db/test.sqlite3
pool: 5
timeout: 5000
Can you please help me clarify this matter. I've spent over 4 hours trying to package my assignment!
@Lateral Punk
When you run rake package, all tests are run.
All of the tests that come with the assigment are set to pass -- there is no actual testing. Did you write some tests? Or did you implement Clearance (which comes with tests)? Or use Keith's tests?
Let me know the answers to those questions. Basically I'm going to recommend that you delete the tests, but I need to know a bit more about how you've diverged from the original code.
@Ron Newman
First, everyone: To get this assignment done, do not be afraid of inefficient queries and/or using regular collection iterators. The good news is that if you can't figure out a way to get ActiveRecord to do an efficient query, you can probably get what you want with Ruby code. (And the bad news is that you may have some inefficient queries -- and then you ask me. :-)
There are a number of ways to get what you want.
The "Rails" way to do this is to use a counter_cache (AWDR section 19.9). Notice that in this section there is an important caution regarding how you add children to the parent. You want to do it via the parent. E.g., parent.children.new or .create or .build. This is great, because you are precomputing the rank. No computation is done when you order by the counter.
Having said that, you also seem to be interested in how Rails does aggregation. This doesn't help much for the view, but it does help for getting counts.
Take a look at AWDR, p.p 339-340 (PDF). This is for the sections "Getting Column Statistics" and "Counting."
Find the example that says:
So let's say we did want to know how many index entries there are per publication. The code above is very similar to what you want to do, however, you actually want to group by publication id, and instead of finding the maximum for a column, you want to count something. But what?
Now, if you know SQL, you might try this in the Firefox Sqlite Manager:
That where clause is defining an inner join. This join will give you one row for each index entry, along with its publication data. Then we group by the publication id, and the count is the number of rows for that publication id, which is to say, the number of index entries for that publication id.
So that's pretty good. The result set looks like
2 1
8 3
All the other publications have 0 index entries. If you can get ActiveRecord to do this in one query, you're in a pretty good situation. So the real trick is how to get an inner join. If you look at http://api.rubyonrails.org/classes/ActiveRecord/Base.html under :joins, it says: "named associations in the same form used for the :include option, which will perform an INNER JOIN on the associated table(s)." So . . .
This returns a Hash, { 2 => 1, 8 => 3 }.
It is actually hard to get Rails to generate a left outer join with an aggregation. The raw SQL would look like this:
I don't know a natural way to get ActiveRecord to provide a result set like that.
For those of you who've read this far, remember: If you want a summary statistic for children, use counter_cache.
If you want an *average* of values for children, you need to work harder. The most obvious way to do this is: When you add a child row, calculate the average for the parent, and save it in the parent row.
More questions?
@john
Yes, forgot to mention that. I did write my own tests (unit).
@Lateral Punk
I would suggest that to get the assignment in, you make a copy of your project, remove the new tests from the project, and package and submit that.
If you like, send me a ZIP of the original project: User the regular ZIP app, not rake package, and I'll take a look at it.
Ok I will do that. I really want to learn how to run tests with a test db properly. A screencast on this would be great since none of the assignments really emphasized how to do it with a real Rails application.
thanks