Chapter 12. Review Quiz


Review Quiz is the first of three complete example applications in this book, each designed to demonstrate different techniques for building rich Ajax applications with Rails. The purpose of this application is simply to provide shared quizzes for self-studylike flash cards. The quizzes are self-administered and self-judged, as shown in Figure A-1. Typical use cases:

A quiz is created and used by just one person, such as a college student drilling for an exam

A quiz is created by one person and then shared with a group, such as a high school teacher helping students review course material

A general-interest quiz is created for fun and discovered by other users on the site

Figure 12-1. Review Quiz home


To keep things simple, the application has no user accounts or mechanism for logging on or off. It does, however, have session-based authentication. When a user creates a new quiz, her session ID is stored, and changes can only be made with the same session ID. That means the barrier to entry for new users is extremely low; but it also means that a user can't reliably return to a quiz to change it after creating it. For most applications, this trade-off wouldn't be worthwhile, but in this case, an argument can be made that each quiz is sufficiently disposable for this approach. For an example of a user accounts system, see the Intranet Workgroup Collaboration application described in Example C.

To download the source to this application, rails quiz, visit http://www.oreilly.com/catalog/9780596527440. Where files aren't listed (e.g., config/environment.rb), they are the same as the Rails default skeleton. Once the files are in the correct place, you'll need to configure a database by editing config/database.yml. The default configuration as generated by rails quiz expects a MySQL database named quiz_development, accessible at localhost with the username root and no password. To get started, create a database for the application and change database.yml as needed, then run rake db:schema:load to create the application's database structure, as specified by schema.rb.

The database and model for the application is very simple: just two tables and two models, for quizzes and questions.

ActiveRecord::Schema.define(:version => 1) do   create_table "questions", :force => true do |t|     t.column "quiz_id",  :integer     t.column "position", :integer     t.column "question", :text, :default => "", :null => false     t.column "answer",   :text, :default => "", :null => false   end   add_index "questions", ["quiz_id"],      :name => "questions_quiz_id_index"   add_index "questions", ["position"],      :name => "position"   create_table "quizzes", :force => true do |t|     t.column "name", :string,       :default => "New Quiz", :null => false     t.column "session_id", :string,        :limit => 50, :default => "", :null => false     t.column "created_at", :datetime,       :null => false   end   add_index "quizzes", ["created_at"], :name => "created_at" end

The Question model is essentially simple: beyond basic ActiveRecord stuff, it defines a method for returning the next question.

class Question < ActiveRecord::Base   belongs_to   :quiz   acts_as_list :scope => :quiz      # Returns the next question in the quiz after   # this one, excluding those keys passed in +right_keys+   def next right_keys     quiz.questions.find :first,       :conditions => "position > #{position}" +         (right_keys.blank? ? "" : " and id not in (#{right_keys})")   end end

Likewise, the Quiz model is simple. We add a method to the association between a Quiz and its questions, which allows us to easily find all questions that haven't yet been missed.

class Quiz < ActiveRecord::Base   # Methods added to the association, e.g quiz.questions.unmissed   # to retrieve questions that have not been missed   module AssociationExtension     def unmissed right_keys       cond = "id not in (#{right_keys})" unless right_keys.blank?       find :all, :conditions => (cond || nil), :limit => 5     end   end   has_many :questions,      :order     =>'position',      :dependent => :destroy,      :extend    => AssociationExtension   # Finds the last 20 quizzes created   def self.recent     find :all, :limit => 20, :order => "created_at desc"   end end

The application is implemented with just one controller, QuizzesController. The routing map includes the usual Rails default route, one route for the home page, and one resource that defines a collection of named routes for the quizzes controller.

ActionController::Routing::Routes.draw do |map|   map.resources :quizzes,     :member => { :create_q  => :post,                  :destroy_q => :post,                  :reorder   => :post,                  :answer    => :post,                  :reset     => :post }   map.home '', :controller =>'quizzes'   map.connect ':controller/:action/:id' end

The layout view, application.rhtml, is the quiz's top-level layout, as shown in Figure A-2. It contains a simple Ajax form for adding a new quiz, and a DIV where other parts of the application can display the questions. The yield at the end of the template allows the edit.rhtml template to insert a form for adding questions to the quiz.

Figure 12-2. Editing a quiz


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">   <head>     <title>Review Quiz</title>     <%= stylesheet_link_tag "application" %>     <%= javascript_include_tag :defaults %>   </head>   <body>     <h1><%= link_to "Review Quiz", home_url %></h1>     <% form_for :new_quiz, Quiz.new, :url => quizzes_url,            :html => { :id => "new_quiz" } do |f| %>       <label for="new_quiz_name">Create a quiz:</label>        <%= f.text_field :name %> <%= submit_tag "Create" %>     <% end %>     <div ></div>     <%= yield %>   </body> </html>

The Quizzes controller starts with a couple of before_filters to make sure there's a current quiz (if one is needed) and, if the action requires permission, makes sure that the user is allowed to edit the quiz.

The first few actions are simple, but things gets a little more complex with create_q, which lets the user add a new question. It uses respond_to to handle an Ajax form submission or a traditional submission, all in one action. The RJS template create_q.rjs handles the Ajax side.

Drag-and-drop reordering is, of course, handled through Ajax. And it's simple: it's just a matter of assigning the new positions to each question and saving the quiz. Other ways of manipulating the quiz (deleting a question, showing whether the user's answer was right or wrong) are also handled with Ajax. But the controller has little to do with manipulating the page itself: it just manages the data, and renders (if a render is needed). That's how we want it!

class QuizzesController < ApplicationController      before_filter :find_quiz, :except => [ :index, :create ]   before_filter :check_permissions,      :only => [ :edit, :reorder, :questions, :destroy_question ]   # Lists recent quizzes   def index     @quizzes = Quiz.recent   end      # Creates a new quiz and saves the user's session id in it   def create     quiz = Quiz.new params[:new_quiz]     quiz.session_id = session.session_id     quiz.save     redirect_to edit_quiz_url(:id => quiz)   end      # Presents a view to edit quiz   def edit   end   # Creates a new question, via either Ajax or traditional form   def create_q     @question = @quiz.questions.create params[:question]     respond_to do |format|       format.html { redirect_to edit_quiz_url }       format.js     end   end   # Handles drag-and-drop reordering questions via Ajax   def reorder     params[:quiz].each_with_index do |id, position|       q = @quiz.questions.find id       q.position = position + 1       q.save     end     render :nothing => true   end   # Handles deleting a question via Ajax   def destroy_q     question = @quiz.questions.find params[:question_id]     question.destroy     render :nothing => true   end   # Shows the first five questions that have not been missed   def show     @questions = @quiz.questions.unmissed right_keys   end      # Returns a response to a question via Ajax   def answer     score @quiz.id, params[:question_id], params[:right]=='true'     last = @quiz.questions.find params[:last]     @next = last.next right_keys   end   # Resets the user's scoreboard for the quiz   def reset     reset_scoreboard params[:id]     redirect_to quiz_url   end      private        # Before filter to lookup a quiz by id     def find_quiz(  ) @quiz = Quiz.find params[:id] end          # Before filter to ensure only a quiz's creator can edit it     def check_permissions       redirect_to home_url and return false unless mine?     end          # Whether @quiz was created by the user     def mine?       @quiz.session_id == session.session_id     end     helper_method :mine?     # Wraps session to track user's quiz results     def scoreboard id=nil       return (session[:quizzes] ||= {}) unless id       return (scoreboard[id.to_i] ||= {})     end     # Wipes the user's scoreboard for a given quiz     def reset_scoreboard id       scoreboard[id.to_i] = {}     end     # A response (+right+) for question +q+ of quiz +quiz+     def score id, q, right       scoreboard(id)[q.to_i] = right     end     # An array of hashes representing right answers for quiz +id+     def right(id) scoreboard(id).reject{ |q, v| !v } end     helper_method :right     # An array of hashes representing wrong answers for quiz +id+     def wrong(id) scoreboard(id).reject{ |q, v| v } end     helper_method :wrong          # A comma-delimited string of ids to the right responses      # for the current quiz.     def right_keys       questions = right(@quiz.id)       questions.keys.join ','     end end

Editing a quiz is fairly simple: you can add questions and you can delete questions. This partial displays a question and its answer and provides a link that lets you delete it.

<li >   <%=h question.question %> <em>(<%=h question.answer %>)</em>    <%= link_to_function "x", remote_function(         :url => destroy_q_quiz_url(:question_id => question),         :complete => "$('question_#{question.id}').hide(  )") %> </li>

This partial displays a question, along with its answer and "Got It"/"Missed It" links (hidden by default, thanks to the "display: none"). Both links defer to the JavaScript function Quiz.answer( ), which is defined in application.js.

<div  >   <div  >     <%=h question.question %>     <%= link_to_function "Reveal",           "Quiz.reveal(#{question.id})",           :class => "yellow" %>   </div>   <div   style="display: none">     <%=h question.answer %>     <%= link_to_function "Got It",           "Quiz.answer('#{question.quiz_id}', #{question.id}, true)",           :class => "green" %>     <%= link_to_function "Missed It",           "Quiz.answer('#{question.quiz_id}', #{question.id}, false)",           :class => "red" %>   </div> </div>

The scoreboard partial just tallies the right and wrong answers.

<div >   <div >     <%= pluralize @quiz.questions_count,'question' %>   </div>   <div >     <span ><%= right(@quiz.id).size %> right</span> /      <span ><%= wrong(@quiz.id).size %> wrong</span>   </div>   <div >     <%= @quiz.questions_count             right(@quiz.id).size             wrong(@quiz.id).size %> remaining   </div> </div>

This RJS template starts by rendering the question partial, loading it with the next question, and inserting the result at the bottom of the page. It also updates the page's scoreboard.

if @next   content = render :partial => "question",      :locals => { :question => @next }   page.insert_html :bottom, :questions, content end page[:scoreboard].reload

This RJS template appends a just-created question to the bottom of the page, fires a visual effect to alert the user that the page has changed, and resets the form fields to empty strings. The last line calls Quiz.update_hints( ) as defined in application.js.

page.insert_html :bottom, :quiz,    render(:partial => "edit_question",      :locals => { :question => @question }) page["question_#{@question.id}"].visual_effect :highlight page.sortable :quiz, :url => reorder_quiz_url page[:question_question].value = '' page[:question_answer].value = '' page[:question_question].focus page.quiz.update_hints

The edit template displays the quiz and allows the user to add new questions, delete existing ones, and reorder questions via drag and drop, as shown in Figure A-3. The most important part of this template is the remote_form_for, which allows the user to add a new question.

Figure 12-3. Reordering questions with drag and drop


<h2>Edit: <%= @quiz.name %></h2> <ul >   <li  <% unless @quiz.questions.any? %>     style="display: none"<% end %>>     <%= link_to "Take the quiz", quiz_url %>   </li> </ul> <ul >   <% @quiz.questions.each do |question| %>     <%= render :partial => "edit_question",            :locals => { :question => question } %>   <% end %> </ul> <%= sortable_element :quiz, :url => reorder_quiz_url %> <% remote_form_for :question, Question.new,       :url => create_q_quiz_url,      :html => { :id => "new_question",      :onKeyPress  => "return Quiz.captureKeypress(event);" } do |f| %>   <div  <% if @quiz.questions.any? %>     style="display: none"<% end %>>     <strong>Add the first question</strong> to your new quiz.   </div>   <h3>Add a Question</h3>   <label for="question_question">Question</label>   <%= f.text_area :question %>   <label for="question_answer">Answer</label>   <%= f.text_area :answer %>   <%= submit_tag "Save" %>   <%= javascript_tag "$('question_question').focus(  )" %> <% end %>

This template provides a list of links to the recently created quizzes. It's displayed when the application first starts up, as was shown in Figure A-1.

<h2>Recent Quizzes</h2> <% if @quizzes.any? %>   <ul>     <% @quizzes.each do |quiz| %>       <li><%= link_to h(quiz.name), quiz_url(:id => quiz) %></li>     <% end %>   </ul> <% else %>   <p><em>There are no quizzes yet.</em></p> <% end %>

The show template is responsible for rendering a given quiz, including the scoreboard and the list of questions for the user to answer. Figure A-4 shows a quiz in progress.

Figure 12-4. Taking a quiz


<%= render :partial =>'scoreboard' %> <h2><%= h(@quiz.name) %></h2> <ul >   <% if mine? %>     <li><%= link_to "Edit this quiz", edit_quiz_url %></li>   <% end %>   <li style="display: none" >     <%= link_to "Start Over", reset_quiz_url, :method => :post %>   </li> </ul> <div >   <%= render :partial => "question", :collection => @questions %> </div> <div  style="display: none">     <strong>You're done!</strong> Now you can      <%= link_to "start over", reset_quiz_url, :method => :post %>,      or just <%= link_to "review what you missed.", quiz_url %> </div>

The application-specific JavaScript is defined in application.js.

var Quiz = {   /* Handles returns within the create-question form */   captureKeypress: function(evt) {     var keyCode = evt.keyCode ? evt.keyCode :       evt.charCode ? evt.charCode : evt.which;     if (keyCode == Event.KEY_RETURN) {       if(Event.element(evt).id=='question_question')         $('question_answer').focus(  );       if(Event.element(evt).id=='question_answer')         $('new_question').onsubmit(  );       return false;     }     return true;   },      /* Hides and shows help messages while editing a quiz */   updateHints: function(  ) {     $('quiz').cleanWhitespace(  );     if($A($('quiz').childNodes).any(  )) {       $('done').show(  );       $('starting').hide(  );     }   },   /* Reveals the answer node for a question */   reveal: function(questionId) {     $(questionId+'_a').visualEffect('blind_down', {duration:0.25})   },   /* Handles submitting an answer */   answer: function(quizId, questionId, right) {     var url = '/quizzes/' + quizId + ';answer';     var params ='question_id=' + questionId +                  '&right='      + (right ?'true' : false) +                  '&last='       + this.questions().last(  ).id;     new Ajax.Request(url, {parameters:params});     $(questionId.toString(  )).visualEffect('fade_up', {duration:0.5});     if(this.showingQuestions() && !$('finished').visible(  ))       $('finished').visualEffect('appear_down');     $('startover').show(  );   },   /* Returns all question DOM nodes */   questions: function(  ) {     var questions = $('questions');     questions.cleanWhitespace(  );     return $A(questions.childNodes);   },      /* Returns whether there are any showing question nodes */   showingQuestions: function(  ) {     return this.questions(  ).select(function(e){       return e.visible(  );     }).length==1;   } } // Custom effect combining BlindUp and Fade Effect.FadeUp = function(element) {   element = $(element);   element.makeClipping(  );   return new Effect.Parallel(     [ new Effect.Opacity(element, {from:1,to:0}),       new Effect.Scale(element, 0,          {scaleX:false,scaleContent:false,restoreAfterFinish: true}) ],     Object.extend({       to: 1.0,       from: 0.0,       transition: Effect.Transitions.linear,       afterFinishInternal: function(effect) {          effect.effects[0].element.hide(  );         effect.effects[0].element.undoClipping(  );       }}, arguments[1] || {})   ); } // Custom effect combining BlindDown and Appear Effect.AppearDown = function(element) {   element = $(element);   var elementDimensions = element.getDimensions(  );   return new Effect.Parallel(     [ new Effect.Opacity(element, {from:0,to:1}),       new Effect.Scale(element, 100,          {from:0,to:1,scaleX:false,          scaleContent:false,restoreAfterFinish:true,          scaleMode:{originalHeight:elementDimensions.height,            originalWidth:elementDimensions.width}}) ],     Object.extend({       transition: Effect.Transitions.linear,       afterSetup: function(effect) {         effect.effects[0].element.makeClipping(  );         effect.effects[0].element.setStyle({height:'0px'});         effect.effects[0].element.show(  );        },         afterFinishInternal: function(effect) {          effect.effects[0].element.undoClipping(  );       }}, arguments[1] || {})   ); }

There's nothing really significant in the application's stylesheet. It's here for completeness and to show that we aren't playing any tricks in it.

/* Basics */ /* ------------------------- */ html {     background-color: #ddd;     padding: 20px;     border-top: 8px solid #494;     height: 100%; } body {     width: 80%;     margin: 0 auto 0 auto;     padding: 0 20px 0 20px;     border-top: 1px solid #bbb;     border-right: 1px solid #999;     border-bottom: 1px solid #999;     border-left: 1px solid #bbb;     background-color: #fff;     font-family: helvetica, arial, sans-serif;     min-height: 100%; } h1 {     float: left; } h2 a {     font-size: 0.5em; } .clear {     clear: both; } #links {     margin-top: -1.7em;     padding-left: 15px;     list-style-type: square;     font-size: 0.7em; } /* Links */ /* ------------------------- */ a {     color: #a44;     text-decoration: none; } a:hover {     text-decoration: underline;     color: #464; } a.green, a.red, a.yellow {     text-transform: uppercase;     font-size: 0.7em;     padding: 1px 2px; } a.green {     color: #363;     background-color: #cfc;     border: 1px solid #696; } a.red {     color: #633;     background-color: #fcc;     border: 1px solid #966; } a.yellow {     color: #663;     background-color: #ffc;     border: 1px solid #996; } /* Create Quiz */ /* ------------------------- */ #new_quiz {     font-size: .7em;     text-transform: uppercase;     float: right;     margin-top: 20px;     background-color: #bdb;     padding: 5px 10px;     border: 1px solid #9b9; } #new_quiz input[type='text'] {     width: 100px;     font-weight: bold;     background-color: #cfc; } /* Edit Quiz */ /* ------------------------- */ #new_question {     clear: right;     background-color: #bdb;     padding: 5px 10px;     border: 1px solid #9b9;     width: 55%;     padding-right: 80px;     padding-top: 10px;     margin-top: 50px; } #new_question h3 {     margin-top: 0;     margin-bottom: 8px;     font-size: 0.7em;     letter-spacing: 0.1em;     text-transform: uppercase;     font-weight: bold; } #new_question label {     font-size: 0.7em;     text-transform: uppercase;     font-weight: normal;     float: left;     width: 65px;     margin-top: 5px; } #new_question textarea {     width: 100%;     display: block;     height: 40px;     vertical-align: top;     margin-bottom: 10px; } #new_question input {     margin-left: 65px; } #starting {     color: #331;     background-color: #ffc;     border: 1px solid #cca;     padding: 5px;     margin-bottom: 10px; } #finished {     color: #331;     background-color: #ffc;     border: 1px solid #cca;     padding: 10px;     width: 270px; } /* Take Quiz */ /* ------------------------- */ #questions {     padding-top: 20px; } .question .q {     margin-bottom: 10px; } .question .a {     margin-bottom: 30px;     margin-left: 30px; } /* Scoreboard */ /* ------------------------- */ #scoreboard {     padding: 6px;     float: right;     width: 150px;     color: #331;     background-color: #ffc;     border: 1px solid #cca;     text-align: center;     margin-left: 20px;     margin-bottom: 10px; } #scoreboard #total, #scoreboard #remaining {     text-transform: uppercase;     font-size: 0.7em;     color: #888;     letter-spacing: 0.1em; } #scoreboard #score {     font-weight: bold;     margin: 2px 0 3px 0; } #scoreboard #right {     color: #090; } #scoreboard #wrong {     color: #900; } 




Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net