Lecture 1 My task is to help you avoid pitfalls along the way. 0 is true in Ruby don't copy, don't donate, don't copy and paste from google don't copy code that doesn't work. Book - get book. schedule on the page The language: http://www.ruby-doc.org/docs/ProgrammingRuby/ <- what I used. http://en.wikipedia.org/wiki/Why%27s_(poignant)_Guide_to_Ruby http://ruby-doc.org/docs/ProgrammingRuby/html/builtins.html http://hackety-hack.com/ Chapter 4 of the text. Editor problem solved. git. 1. default rails expects it - installs .gitignore files 2. heroku distribution process relies on git. 3. my project turnin scheme relies on git (gitolite install on my server) - git init - git clone gitolite@scriptroute.cs.umd.edu: - git add . - git commit -m "message of incredible utility" -> local only - git commit -am "message of incredible utility" -> local only - git push - git pull Project milestones and grading breakdown. Tue 4 - have provided ssh key and project repository name via https://scriptroute.cs.umd.edu/389r2011w/ - 4% for a key (per user) and repository (per group) ssh-keygen -t dsa Thu 6 - can push to gitolite@scriptroute.cs.umd.edu:(your-repo) - 4% for something there. Mon 10 - has models, controllers, associations (has_one, etc). Tests on a clean git checkout do not fail. Standard tests are present. - 15% for reasonable organization. - 5% for test success. Thu 13 - has validations, routes, working intro page. Tests do not fail. - 15% for reasonable validations (typically, every element should have at least one validation). - 5% for route goodness. - 2% for removed public/index.html Tue 18 - has user accounts. Tests still do not fail. - 20% user and admin accounts Thu 20 - works, possibly with demonstration. - 5% for no broken links - 5% for at most one broken link (i.e., 10% for perfection, 5% for near perfection) - 10% for unauthenticated users can't delete stuff they shouldn't, even if they synthesize the urls. - 10% for having data to play with (I don't want to add records by hand.) (use db/seeds, fixtures, or a script to be run with #!/usr/bin/env (path)/script/rails runner) - 10% for coolness, creativity, uniqueness, polish, ambition, technique, plugin use, etc. -- History who wrote Perl? Larry Wall. who wrote Python? Guido Rossum. who wrote C? Dennis Ritchie (Kernighan and Ritchie wrote the book) Ruby? Matz. Rails? DHH. David Heinemeir Hansson Ruby language attributes Object Oriented Good for text processing - native regular expressions Dynamic typing do not declare variables to have a type includes function parameters. Duck typing if it quacks, it's a duck. NoMethodFound exception method_missing is called Single inheritance, with mixins/modules Metaprogramming class Foo attr_reader :a attr_writer :a def initialize @a = 1 @b = 2 end end f = Foo.new puts f.a use irb to figure out how things work. Rails Lets you write web sites quickly with lots of nifty features. Philosophy DRY - Don't Repeat Yourself Convention over configuration - if you follow the true path, you won't have to write as much code. Model / View / Controller type of modular organization of an application. data is represented by a model model takes care of enforcing business logic. simple calculations like compute a GPA. constraints go here. view is what you see. might want many views - one set of views for web browsers, a different one for iphones anything presentation specific goes into the view. controller lies between as much of the "action" work as possible. probably also ensures that the actions are authorized for the logged in user. __PROJECT__ Don't get sued: no law violations. no theft of data or IP. don't use or republish data you aren't supposed to, avoid screen-scraping. (conversely, if you can find a data feed, there may not be too many sites taking advantage of it.) Don't steal ideas: no outright clones of existing services or software. (applying old ideas to new domains is okay) Don't create yet another amazon, another blog, another facebook. Don't give away your ideas. Rails makes it possible to deploy software services with no staff, no distribution channels, no online software updates, and generally very few things traditional businesses need. No projects for work. This is your time; I have to be able to see and evaluate all code, you have to be able to demo your code for fellow students, and nobody will sign your NDA (for free). You can prototype ideas that you'd later take to work. Avoid projects that require security (finance, medical) or that hold login credentials (passwords) to other sites. (i.e., I'm not giving you my facebook or UM password to test your service.) -- Suggestions: Calendaring. Fetch UM information and integrate. Schedule time with profs and ta's. University events. Commute help. Monitor traffic feeds, sunset times, weather, history of travel times, nextbus information, etc. to determine when to leave or what route to take. Time-tracking. There are various sites for consultants to aid in billing. If students could convince me that they spent 20 hours a week working on 412 or 417 projects, I might take pity on them. (note that checking in and checking out 20 hours later wouldn't do it; consistent git checkins might.) Undergraduate (or graduate) trophies (badgeville, bigdoor) earn trophies for turning in code early, posting on course forums, award badges to your project partners, perfect assignment trophies, etc. May need to understand FERPA to decide how/what student performance can be publicised. My svn/git project repository. student github? (provide an svn that takes course projects and provides your own svn tracking and sharing with project group members.) We should have space, and student code cannot go on "free" git sites because it would be public. Integration with metro dataset / api - win their prize. http://wmata.com/rider_tools/developer_resources.cfm http://www.facebook.com/group.php?gid=116742961708067 generate on-time statistics? observe tricky transfers? understand uncertainty? tell umd transportation to stop letting shuttles leave as trains pull into college park? generate reliable alerts based on fine-grained disruptions? integration with mobile - win that prize. too late if you haven't been participating in their process. presentation of census.gov data. or other educational, factual presentation of data. facebook app. revised submit server? that uses ruby::unit tests? that better handles long-running code? revised forum? that allows following threads by email that requires login to access, not just to write. best X voting site using different voting methods (condorcet? instant runoff?) using different double-voting protection schemes washington post moderated comments site (their comments engine sucks; there should be a reasonable way to evaluate rebuttals to flawed editorials without wading through crap). course notes annotation and side-channel IRC: imagine building course notes collaboratively with your peers or getting validation that your question is interesting before raising your hand. better raffle system for on-campus job events. (e.g., scan your id to make your resume available to recruiters.) student-to-student textbook resale matchmaking. (already done somewhere?) collect textbook data for classes. find reasonable new book prices. make money selling referrals to amazon? run a spot market auction. (bidders bid, sellers ask, determine a price point s.t. no seller accepts less than asked, no bidder pays more than bid, and as many get matched up as possible.) auction is run in plenty of time to get amazon instead. Lecture 2 plan SSH. keys. where they go. how to figure out what's busted. git should work. if you want to use the same keys on other machines... (replace linuxlab with whatever else) scp ~/.ssh/id_dsa* linuxlab.csic.umd.edu:.ssh if you want to use the passwordless login as well... scp ~/.ssh/id_dsa.pub linuxlab.csic.umd.edu:.ssh/authorized_keys (authorized keys is a list, but I'm assuming that it is empty. if it is not empty, then you'd have to concatenate... ssh linuxlab.csic.umd.edu "cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys" git clone gitolite@scriptroute.cs.umd.edu:(reponame) will create the directory reponame in the current directory cd reponame echo "my name is neil" > README git add . git diff git commit -m "useless" git push --- Commands from the walkthrough rails new ex cd ex ls ls config/database.yml cat config/database.yml ls git init git add . git commit -m "initial" ls git diff history rails generate scaffold --help rails generate scaffold assignment name:string deadline:datetime published:boolean rails --help rails --version rake db:migrate rake db:drop # consider not doing this unless you want to see it fail (like I did) rails server rake test rails generate model question assignment:references body:string points:integer number:integer solution:string cat db/migrate/20110104192932_create_questions.rb rake db:migrate less log/development.log less -R log/development.log cat app/models/question.rb cat app/models/assignment.rb vi app/models/assignment.rb cat app/models/assignment.rb Lecture 3 git clone (gitolite thing...) touch a file git add file git commit -m something git push origin master -- rails new foo cd foo git init git add . git commit -m "meh" git push (gitolite thing...) --- nested routes: resources :assignments do resources :questions end to support urls of the form: assignments/1/questions/(50) assignments/:assignment_id/questions/:id ( corrected 1/16 - should be plurals for the resources routes ) 1 - representing the assignment id. -> params[:assignment_id] 50 - representing the question id. - question_id is -> params[:id] UNIQUE. totally unique, not just unique per assignment. if you wanted the "50" id to be interpreted as a question number within the assignment class QuestionController ... before_filter :load_assignment private def load_assignment @assignment = Assignment.find(params[:assignment_id]) end def show @question = Question.find(params[:id]) ## @question = Question.find(:conditions => [ "assignment_id = ?", @assignment.id ]).offset(params[:id]) @question = @assignment.questions[params[:id].to_id - 1] @question = @assignment.questions.offset(params[:id]).limit(1) # maybe end end relations: has_many belongs_to has_one has_and_belongs_to_many create a join table: skills_users which has a column: skill_id, and a column: user_id rails generate migration create_skills_users_table skills:references users:references When we want to dereference: u = User.find(1) u.skills select skills.* from skills_users, skills where skills_users.skill_id = skills.id and skills_users.user_id = 1 in the model, we're going to expect to see: class MyModel < .. belongs_to :agent validates_associated :agent validates_uniqueness_of :name ... methods ... end HTML basics, .erb, partials. <% <%= partials in the scaffold: <% render 'form' %> partial that represents a table row - need to explicitly provide parameters if they are not from the class (don't have an @) for assignment in @assignments do <% render 'tablerow', :assignment => assignment %> end
"partial" operates by generating the html of the partial, and then substituting. its not like a #include. it doesn't substitute the source. ActiveRecord wraps sql. Warnings: 0 is true undeclared locals are nil ----- Lecture 4 Milestone of the day: Mon 10 - has models, controllers, associations (has_one, etc). Tests on a clean git checkout do not fail. Standard tests are present. - 15% for reasonable organization. - 5% for test success. Also, homework. In the unlikely event campus closes tomorrow (tuesday), please review the following railscasts during lecture time. You could review them anyway: stuff we've done: http://railscasts.com/episodes/209-introducing-devise http://railscasts.com/episodes/211-validations-in-rails-3 more detail on stuff we do: http://railscasts.com/episodes/219-active-model http://railscasts.com/episodes/202-active-record-queries-in-rails-3 features you could have: http://railscasts.com/episodes/155-beginning-with-cucumber http://railscasts.com/episodes/244-gravatar http://railscasts.com/episodes/237-dynamic-attr-accessible http://railscasts.com/episodes/228-sortable-table-columns Don't confuse validations with assertions. Step 1 for today. add deadline enforcement to the exercises application. git clone gitolite@scriptroute.cs.umd.edu:exercises bundle install Step 2 for today. add authentication to a simple app. Step 1: we made the following changes to the repository, though not necessarily in this order: diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 273d68d..60e1457 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -1,3 +1,7 @@ class Assignment < ActiveRecord::Base has_many :questions, :dependent => :destroy + + def overdue? + deadline < Time.now + end end diff --git a/app/models/response.rb b/app/models/response.rb index def3cfa..8752f64 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -1,6 +1,18 @@ class Response < ActiveRecord::Base validates_associated :user validates_associated :question + + validates_presence_of :user + validates_presence_of :question + + validates_each :question do |model, attr, value| + if value == nil then + model.errors.add(attr, "NOOOO!.") + elsif value.assignment.deadline < Time.now then + model.errors.add(attr, "It's too late.") + end + end + belongs_to :user belongs_to :question has_one :grade diff --git a/app/views/assignments/show.html.erb b/app/views/assignments/show.html.erb index 47dbbbe..5a3e672 100644 --- a/app/views/assignments/show.html.erb +++ b/app/views/assignments/show.html.erb @@ -10,6 +10,15 @@ Submit as frequently as you like, in case your browser crashes or loses your work.
    +<% if @assignment.overdue? then %> + <% for q in @assignment.questions %> +
  1. + (<%= q.points %> pts) + <%= q.body %>
    + <%= q.response_for_user(current_user).body %> +
  2. + <% end %> +<% else %> <%= form_for @assignment, :url => { :action => :turnin } do |assignment_form| %> <% for q in @assignment.questions %> <% response = q.response_for_user(current_user) @@ -25,6 +34,7 @@ Submit as frequently as you like, in case your browser crashes or loses your wor <% end %> <%= assignment_form.submit "Done (at least for now)" %> <% end %> +<% end %>
diff --git a/test/fixtures/assignments.yml b/test/fixtures/assignments.yml index ccbe46f..b33bef3 100644 --- a/test/fixtures/assignments.yml +++ b/test/fixtures/assignments.yml @@ -1,5 +1,15 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +future: + name: future assignment + deadline: <%= 10.days.from_now.to_s(:db) %> + published_at: <%= 1.days.from_now.to_s(:db) %> + +overdue: + name: overdue assignment + deadline: <%= 10.days.ago.to_s(:db) %> + published_at: <%= 14.days.from_now.to_s(:db) %> + one: name: MyString deadline: 2010-12-31 17:38:38 diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml index b562e74..cfe3345 100644 --- a/test/fixtures/questions.yml +++ b/test/fixtures/questions.yml @@ -1,5 +1,18 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +future_q1: + assignment: future + body: what is 1+1 + solution: 2 + points: 1 + +overdue_q1: + assignment: overdue + body: what is 1+1 + solution: 2 + points: 1 + + one: assignment: one body: MyText diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 65ebd94..e6a2c3e 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -4,9 +4,25 @@ # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # +teacher: + email: c@example.com + student_idnum: 100000003 + directory_login: steve + is_teacher: true +student: + email: d@example.com + student_idnum: 100000004 + directory_login: lauren + is_teacher: false + one: email: a@example.com + student_idnum: 100000000 + directory_login: alfred is_teacher: true + two: email: b@example.com + student_idnum: 100000001 + directory_login: robert is_teacher: false diff --git a/test/functional/assignments_controller_test.rb b/test/functional/assignments_controller_test.rb index da79d50..37c0415 100644 --- a/test/functional/assignments_controller_test.rb +++ b/test/functional/assignments_controller_test.rb @@ -3,9 +3,75 @@ require 'test_helper' class AssignmentsControllerTest < ActionController::TestCase setup do @assignment = assignments(:one) - sign_in users(:one) + @future = assignments(:future) + @overdue = assignments(:overdue) + sign_in users(:teacher) end + test "should show assignment for a student" do + sign_out users(:teacher) + sign_in users(:student) + + get :show, :id => @assignment.to_param + assert_response :success + end + + test "should show not yet due assignment for a student as a form" do + sign_out users(:teacher) + sign_in users(:student) + + get :show, :id => @future.to_param + assert_response :success + + assert_select "form" + end + test "should show overdue assignment for a student without a form" do + sign_out users(:teacher) + sign_in users(:student) + + get :show, :id => @future.to_param + assert_response :success + + assert_select "form", false, "students shouldn't see an assignment as a form if past deadline" + end + + test "should not be able to turnin late" do + sign_out users(:teacher) + u= users(:student) + sign_in u + + orig_response_count = Response.count + answers = Hash.new + @overdue.questions.each { |q| + answers[q.id] = Response.new( { :question_id => q.id, :user_id => u.id, :body => "eep" } ) + } + + post :turnin, :id => @overdue.to_param, :assignment => { :response => answers } + assert_redirected_to assignments_path + # assert_response :success # would fail + assert_equal orig_response_count, Response.count + + end + test "should be able to turnin early" do + sign_out users(:teacher) + u= users(:student) + sign_in u + + orig_response_count = Response.count + answers = Hash.new + @future.questions.each { |q| + answers[q.id] = Response.new( { :question_id => q.id, :user_id => u.id, :body => "eep" } ) + } + + post :turnin, :id => @future.to_param, :assignment => { :response => answers } + assert_redirected_to assignments_path + + assert_equal orig_response_count + @future.questions.count, Response.count + end + + + + test "should get index" do get :index assert_response :success diff --git a/test/unit/response_test.rb b/test/unit/response_test.rb index 7e0c77f..74d956f 100644 --- a/test/unit/response_test.rb +++ b/test/unit/response_test.rb @@ -5,4 +5,34 @@ class ResponseTest < ActiveSupport::TestCase test "the truth" do assert true end + + test "should not create responses after the deadline" do + question = questions(:overdue_q1) + assert question.valid?, "overdue question wasn't valid." + + user = users(:one) + assert user.valid?, user.errors.full_messages.join("\n") + + r = Response.new( { + :question_id => question.id, + :user_id => user.id, + :body => "42" }) + + assert !r.valid?, "created a 'valid' response even though we shouldn't have" + end + + test "should be able to create responses before the deadline" do + question = questions(:future_q1) + assert question.valid?, "future question wasn't valid." + + user = users(:one) + assert user.valid?, "user wasn't valid." + + r = Response.new( { + :question => question, + :user => user, + :body => "42" }) + + assert r.valid?, r.errors.full_messages.join("\n") + end end Step 2: we quickly ran this set of commands, extracted from my shell history. 101 14:32 rails generate scaffold items name:string price:decimal 102 14:32 vi Gemfile to add gem 'devise' 103 14:33 rails generate devise:install gives lots of instructions 104 14:33 ls config/environments/development.rb including to add a line to this file. 105 14:33 cat config/environments/development.rb 106 14:34 rails generate devise User is_a_god:boolean and to generate a user mode. 107 14:35 less config/routes.rb and to add root#to route 108 14:35 rails server let's see.. 109 14:35 rm public/index.html forgot to remove the index.html file. 110 14:35 vi config/routes.rb forgot to add the root#to route. 111 14:36 rails server and now we run again. 112 14:36 bg put the server into the background so we can run more commands.... 113 14:36 rake db:setup forgot to init the database... 114 14:36 rake db:migrate eh, setup didn't work, since there are no seeds... run the migrate rule instead. 115 14:36 vi app/controllers/items_controller.rb add before_filter to protect methods in the controller for only logged-in users. Lecture 5: a) finding files. b) habtm - partials c) users with devise - postponed again. find . -name "*.rb" | xargs grep class find . -name "*.rb" | xargs cat find . -name "*.rb" | xargs echo find . -name "*.rb" | xargs grep ActiveRecord find . | xargs grep ActiveRecord grep -r ActiveRecord * find /usr/lib/ruby | xargs grep ActiveRecord # various ways of searching for key phrases... which may be helpful # if tracking down an error. rails generate scaffold student name:string idnumber:integer less config/routes.rb # we observe the resource route. rails generate scaffold course name:string enrollment_limit:integer meeting_time:string vi app/models/student.rb # we add habtm to both student and course models. rails generate migration CoursesStudentsJoin cat db/migrate/20110111182701_courses_students_join.rb vi db/migrate/20110111182701_courses_students_join.rb # we added t.references :course and t.references :student rake db:migrate rake db:setup rails console # we used the console to inspect the students and courses created in the seeds file. less log/development.log less -R log/development.log # we looked at the log to observe behavior of the queries. rails console # we used the con rails console cat db/seeds.rb cat db/schema.rb rails console # we used the console to create a file that would include all student names. cat students.lst vi app/views/students/show.html.erb git diff | less git add . git diff | less git commit -am "all" git log git diff 5344adc65188ad1e5c8feb0e3aa3583d8ad1749b git push gitolite@scriptroute.cs.umd.edu:habtm master # and now you may git clone that repository. Lecture 6 -- project milestone due tonight. homework due tonight. (homework sort of hints about exam questions, though I use homework to trick you into learning about related stuff that won't be on the exam.) devise walkthrough. NOTE: this demo was extra complicated by having: a) a previously existing user type (student) b) a user type not named user (student... though not too much more complicated) NOTE: true documentation is at: https://github.com/plataformatec/devise git clone habtm@scriptroute.cs.umd.edu (note that I've committed the new version.) # note that I've committed a new version. if you want to follow along now, then # revert to the initial version by using: # git checkout d2125a0720ca4b815c3b4bfdf0f07ffd7a0b89c7 # after the clone and "cd habtm". it seems to work despite warnings. cd habtm rails g devise:install # follow directions. vi app/views/layouts/application.html.erb # adding notice... which isn't necessarily necessary. rake db:setup # looked like it worked, did not. rails console vi db/seeds.rb # adding the extra bits, raising exception if not valid vi app/controllers/students_controller.rb # add before filters. # test, add more before_filters, test again. rails g devise:views # have devise export the default forms for editing. vi app/views/devise/registrations/new.html.erb # the form for sign up. we added name. cat app/views/devise/sessions/new.html.erb # the form for login. rake test # oh noes, the tests fail # until class ActionController::TestCase include Devise::TestHelpers end # is added to test/test_helper.rb vi test/functional/students_controller_test.rb # and we sign in a student. # and get rid of testing methods we no longer have. vi ./test/fixtures/students.yml # and add unique email addresses for students. # to see the diffs: git clone habtm@scriptroute.cs.umd.edu git diff d2125a0720ca4b815c3b4bfdf0f07ffd7a0b89c7 --- Lecture 7 Demo - Please sign up on the forum thread. Favorable spots to those who sign up early. (?) Today's milestone: - has user accounts. Tests still do not fail. - 20% user and admin accounts. (same model is fine) I'll probably just look through to see if those models are present and that there's a before_filter in at least one controller. Thursday's homework... Maybe give yourself some time to complete it. (not that I think it's any harder than prior assignments) Today's class work, generally stuff you don't have to know for the final exam, but which learning is likely to help you as a sort of active review of the components of a rails app. 1. pagination using the will_paginate plugin - slightly more challenging than a basic pagination over a scaffold action, since we'll do that with the forms in the exercises app. 2. sorting using a dab of javascript. - reorder the questions in an assignment by drag and drop. 3. cute cleanup of the url and views (very short) define to_param in the model. 4. performance fixing of a grading page using has_many :through and .includes(). - didn't really do this well... has_many :responses, :through => :questions lets rails know about indirect relations. NOTE: it only goes one level. The coolness that it applies to queries becomes quite complicated when it goes further, apparently. pagination log using will_paginate 2 13:10 git clone gitolite@scriptroute.cs.umd.edu:exercises paginate_me 3 13:10 cd paginate_me/ 5 13:11 bundle install 6 13:13 rake db:migrate 7 13:13 rake db:setup (I think I messed up how many times I loaded the seeds...) 12 13:14 rails console (I used rails console to be able to set and clear the is_teacher bit on my user account) 3 13:15 rails server (nice to have running.) 14 13:20 vi Gemfile gem 'will_paginate', '>= 3.0.pre' have to restart the server. if not, errors will result. the diff from my preparation is below. key is adding "paginate" in the controller to fetch the questions, and to add <%= will_paginate @questions %> in the view. The paginate method over the collection of questions figures out which set of questions will appear on the page, and sets up the @questions "Array" so that it can tell which page you're on. Modifying the turnin action, with the hidden field in the form, was a bit of a bonus... not typical of modifying a scaffold method. The idea in this example was to show how to figure out what's going on. diff --git a/Gemfile b/Gemfile index 61a380c..cbbb46f 100644 --- a/Gemfile +++ b/Gemfile @@ -31,3 +31,5 @@ gem 'sqlite3-ruby', :require => 'sqlite3' # group :development, :test do # gem 'webrat' # end + +gem 'will_paginate', '>= 3.0.pre' diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 18ec664..f84a5f4 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -2,12 +2,15 @@ class AssignmentsController < ApplicationController before_filter :authenticate_user!, :except => [ :index ] before_filter :must_be_teacher, :except => [ :index, :show, :turnin ] + @@questions_per_page = 3 + def must_be_teacher redirect_to :back, :notice => 'not a teacher.' unless current_user.is_teacher? end def turnin logger.info params + @assignment = Assignment.find(params[:id]) params[:assignment][:response].each { |question_id, answers| existing = Response.find(:first, :conditions => { :question_id => question_id, :user_id => current_user }) if existing then @@ -20,7 +23,13 @@ class AssignmentsController < ApplicationController end end } - redirect_to assignments_path + current_page = params[:page].to_i + current_page = 1 if current_page < 1 + if current_page < @assignment.questions.count.to_f / @@questions_per_page.to_f then + redirect_to :action => 'show', :id => @assignment, :page => current_page + 1 + else + redirect_to assignments_path + end end # GET /assignments @@ -38,6 +47,7 @@ class AssignmentsController < ApplicationController # GET /assignments/1.xml def show @assignment = Assignment.find(params[:id]) + @questions = @assignment.questions.paginate :page => params[:page], :per_page => @@questions_per_page respond_to do |format| format.html # show.html.erb diff --git a/app/views/assignments/_question_form.html.erb b/app/views/assignments/_question_form.html.erb new file mode 100644 index 0000000..a640bc7 --- /dev/null +++ b/app/views/assignments/_question_form.html.erb @@ -0,0 +1,10 @@ + <% response = q.response_for_user(current_user) + raise "argh" unless response %> + + <%= assignment_form.fields_for response, :index => q.id do |qf| %> +
  • + (<%= q.points %> pts) + <%= q.body %>
    + <%= qf.text_area :body, :cols => 60, :rows => 10 %> +
  • + <% end %> diff --git a/app/views/assignments/_question_graded.html.erb b/app/views/assignments/_question_graded.html.erb new file mode 100644 index 0000000..c98a517 --- /dev/null +++ b/app/views/assignments/_question_graded.html.erb @@ -0,0 +1,16 @@ + + <% response = q.response_for_user(current_user) + raise "argh" unless response %> + +
  • + (<%= q.points %> pts) + <%= q.body %>
    + <%= response.body %> + <% if response.grade then %> +
    + <%= response.grade.points %> points. + <% if response.grade.comment and !response.grade.comment.empty? then %> + Comment: <%= response.grade.comment %> + <% end %> + <% end %> +
  • diff --git a/app/views/assignments/show.html.erb b/app/views/assignments/show.html.erb index c9c6552..6c785c4 100644 --- a/app/views/assignments/show.html.erb +++ b/app/views/assignments/show.html.erb @@ -16,42 +16,18 @@ Submit as frequently as you like, in case your browser crashes or loses your wor
      <% if !@assignment.overdue? %> -<%= form_for @assignment, :url => { :action => :turnin } do |assignment_form| %> - <% for q in @assignment.questions %> - <% response = q.response_for_user(current_user) - raise "argh" unless response %> - - <%= assignment_form.fields_for response, :index => q.id do |qf| %> -
    1. - (<%= q.points %> pts) - <%= q.body %>
      - <%= qf.text_area :body, :cols => 60, :rows => 10 %> -
    2. - <% end %> + <%= form_for @assignment, :url => { :action => :turnin } do |assignment_form| %> + <%= hidden_field_tag :page, params[:page] %> + <%= render :partial => 'question_form', :collection => @questions, :as => :q, :locals => { :assignment_form => assignment_form } %> + <%= assignment_form.submit @lastpage ? "Done" : "Done with this page" %> <% end %> - <%= assignment_form.submit "Done (at least for now)" %> -<% end %> <% else %> - <% for q in @assignment.questions %> - <% response = q.response_for_user(current_user) - raise "argh" unless response %> - -
    3. - (<%= q.points %> pts) - <%= q.body %>
      - <%= response.body %> - <% if response.grade then %> -
      - <%= response.grade.points %> points. - <% if response.grade.comment and !response.grade.comment.empty? then %> - Comment: <%= response.grade.comment %> - <% end %> - <% end %> -
    4. - <% end %> + <%= render :partial => 'question_graded', :collection => @questions, :as => :q %> <% end %>
    +<%= will_paginate @questions %> + <% if current_user and current_user.is_teacher? then %> <%= link_to 'Edit', edit_assignment_path(@assignment) %> | The next task is to make questions be sortable. using sortable 18 14:06 less db/schema.rb our question doesn't already have a position in an order. 19 14:07 rails generate migration add_position_to_questions position:integer we need a position, so add it. 20 14:07 less db/migrate/20110118190732_add_position_to_questions.rb and check that it worked. 21 14:07 rake db:migrate always migrate. 22 14:08 ls public/javascripts/ 23 14:08 ls public/javascripts/dragdrop.js 24 14:08 less public/javascripts/dragdrop.js this is where some of the code is. 25 14:10 vi app/views/questions/index.html.erb below are the edits. We had to add some identification to the table rows and the table body (the tbody tag... who knew it existed?) so that the sortable_element helper would help us out. Much of this is described in railsapi under sortable_element. I built the sort method a little bit at a time. once we had the route, then the index page would render (otherwise it wouldn't know about sort_assignment_questions_path). Dragging to reorder wouldn't work, since the sort method wasn't there. With a sort method, we could look at the log to figure out what the parameters were, and how rails complained that there was no view for show()... even though it's just a method for the javascript to invoke without rendering anything. We fixed that, then added order(:position) when we use the questions method to make sure that the questions are rendered in the desired order. It should be possible to add an option to the has_many line so that :order => 'position asc' is assumed. Finally, a little css changes the cursor to appear as a set of up/down/left/right arrows, signaling that its possible to move. One could do that more cleverly with a "handle" for each element, so that only the handle appears draggable. diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index f84a5f4..d0c7e80 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -47,7 +47,7 @@ class AssignmentsController < ApplicationController # GET /assignments/1.xml def show @assignment = Assignment.find(params[:id]) - @questions = @assignment.questions.paginate :page => params[:page], :per_page => @@questions_per_page + @questions = @assignment.questions.order(:position).paginate :page => params[:page], :per_page => @@questions_per_page respond_to do |format| format.html # show.html.erb diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 6389dfe..297d724 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -14,6 +14,20 @@ class QuestionsController < ApplicationController redirect_to :back, :notice => 'not a teacher.' unless current_user.is_teacher? end + def sort + # I don't know what my parameters are going to be, so lets log them? + logger.info params + # params[:questions_list] has the desired order. + params[:questions_list].each_with_index { |question_id, index| + q = Question.find(question_id) + raise "missing question" unless q + raise "consistency error" unless @assignment == q.assignment # be defensive. + q.position = index + 1 + q.save + } + render :nothing => true + end + def fetch_grades @questions = @assignment.questions @students = User.where("responses.question_id in (?)", @questions).joins("inner join responses on responses.user_id = users.id").uniq @@ -64,7 +78,7 @@ class QuestionsController < ApplicationController def index @assignment = Assignment.find(params[:assignment_id]) raise "bad assignment" unless @assignment - @questions = @assignment.questions + @questions = @assignment.questions.order(:position) if current_user and current_user.is_teacher? then @students = User.where("responses.question_id in (?)", @questions).joins("inner join responses on responses.user_id = users.id").uniq end diff --git a/app/views/questions/index.html.erb b/app/views/questions/index.html.erb index f40158e..3214f05 100644 --- a/app/views/questions/index.html.erb +++ b/app/views/questions/index.html.erb @@ -1,22 +1,27 @@

    Listing questions for <%= @assignment.name %>

    + + + <% @questions.each do |question| %> - + <% end %> +
    Pts Body
    <%= link_to question.points, assignment_question_path(@assignment, question) %> <%= question.body %> <%= link_to 'Edit', edit_assignment_question_path(@assignment, question) %> <%= link_to 'Destroy', assignment_question_path(@assignment, question), :confirm => 'Are you sure?', :method => :delete %>
    +<%= sortable_element "questions_list", :tag => "tr", :url => sort_assignment_questions_path %>
    diff --git a/config/routes.rb b/config/routes.rb index 86ed03a..5497957 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,11 @@ Exercises2::Application.routes.draw do # nested resource awesomeness? resources :assignments do - resources :questions + resources :questions do + collection do + post 'sort' + end + end end match 'assignments/turnin/:id' => 'assignments#turnin' diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 297d724..ad31d6f 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -79,8 +79,11 @@ class QuestionsController < ApplicationController @assignment = Assignment.find(params[:assignment_id]) raise "bad assignment" unless @assignment @questions = @assignment.questions.order(:position) + if current_user and current_user.is_teacher? then - @students = User.where("responses.question_id in (?)", @questions).joins("inner join responses on responses.user_id = users.id").uniq + @responses = @assignment.responses.order(:user_id).includes(:user, :question) + # @students = User.where("responses.question_id in (?)", @questions).joins("inner join responses on responses.user_id = users.id").includes(:responses) + # @students = @assignment.users # User.where("responses.question_id in (?)", @questions).joins("inner join responses on responses.user_id = users.id").includes(:responses) end # @questions.delete_if { |q| q.assignment == nil } diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 5922f4c..553b590 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -1,5 +1,7 @@ class Assignment < ActiveRecord::Base has_many :questions, :dependent => :destroy + has_many :responses, :through => :questions + def overdue? Time.now > deadline end diff --git a/app/views/questions/index.html.erb b/app/views/questions/index.html.erb index 3214f05..ddc6001 100644 --- a/app/views/questions/index.html.erb +++ b/app/views/questions/index.html.erb @@ -3,6 +3,8 @@ + + @@ -11,23 +13,51 @@ -<% @questions.each do |question| %> +<% @questions.each_with_index do |question,idx| %> - + + + + <% if @assignment.overdue? %> + + <% else %> + <% end %> <% end %>
    NumSort Pts Body
    <%= link_to question.points, assignment_question_path(@assignment, question) %><%= idx + 1 %> = <%= link_to question.points, assignment_question_path(@assignment, question) %> <%= question.body %><%= link_to 'Grade', assignment_question_path(@assignment, question) %><%= link_to 'Edit', edit_assignment_question_path(@assignment, question) %> <%= link_to 'Destroy', assignment_question_path(@assignment, question), :confirm => 'Are you sure?', :method => :delete %>
    -<%= sortable_element "questions_list", :tag => "tr", :url => sort_assignment_questions_path %> +Reload this page to update question numbers and solutions below after reordering questions. +<%= sortable_element "questions_list", + :tag => "tr", + :url => sort_assignment_questions_path, + :constraint => :vertical + %>
    <% if current_user and current_user.is_teacher? then %> <%= link_to 'New Question', new_assignment_question_path(@assignment) %> + +<% last_student = nil %> +<% @responses.each do |r| + if r.user != last_student then %> +
    + + + <% last_student = r.user + end %> + + + + +<% end %> +
    <%= r.user.email %>
    <%= r.question.position %><%= r.body %>
    + +<% if false then %> <% @students.each do |s| %> @@ -47,5 +77,6 @@ <% end %>
    <% end %> +<% end %> <% end %> diff --git a/public/stylesheets/neil.css b/public/stylesheets/neil.css index 656a5be..2a0c117 100644 --- a/public/stylesheets/neil.css +++ b/public/stylesheets/neil.css @@ -16,3 +16,8 @@ #user_nav a { color: #ffffff; } + +.movable { + align: middle; + cursor: move; +}