diff options
author | 2011-03-12 21:13:18 +0200 | |
---|---|---|
committer | 2011-03-12 21:13:18 +0200 | |
commit | c38161f8d317aee036f197a9b670f96dcd4c9ec3 (patch) | |
tree | 3ba7b4cd90a3bb5ea4a351c3c3221fd8de8603fd | |
parent | Rename QuestionCategory to Category (diff) | |
download | recruiting-webapp-c38161f8d317aee036f197a9b670f96dcd4c9ec3.tar.gz recruiting-webapp-c38161f8d317aee036f197a9b670f96dcd4c9ec3.tar.bz2 recruiting-webapp-c38161f8d317aee036f197a9b670f96dcd4c9ec3.zip |
Questions can belong to many categories
While starting to input quiz questions for the arch tester quizzes we
found out that it would be best if questions could belong to many
categories. Now the relationship between questions and categories is
many to many. Bug #356179.
-rw-r--r-- | app/models/answer.rb | 2 | ||||
-rw-r--r-- | app/models/category.rb | 3 | ||||
-rw-r--r-- | app/models/question.rb | 18 | ||||
-rw-r--r-- | app/models/question_category.rb | 27 | ||||
-rw-r--r-- | app/models/question_group.rb | 4 | ||||
-rw-r--r-- | app/models/user.rb | 7 | ||||
-rw-r--r-- | app/models/user_mailer.rb | 3 | ||||
-rw-r--r-- | app/views/user_mailer/new_question.erb | 2 | ||||
-rw-r--r-- | db/migrate/20110312181715_add_question_category_pivot.rb | 21 | ||||
-rw-r--r-- | db/schema.rb | 11 | ||||
-rw-r--r-- | features/clean_ui.feature | 2 | ||||
-rw-r--r-- | features/step_definitions/questions_steps.rb | 3 | ||||
-rw-r--r-- | spec/factories.rb | 9 | ||||
-rw-r--r-- | spec/models/answer_spec.rb | 9 | ||||
-rw-r--r-- | spec/models/question_group_spec.rb | 8 | ||||
-rw-r--r-- | spec/models/question_spec.rb | 17 | ||||
-rw-r--r-- | spec/models/user_category_spec.rb | 4 | ||||
-rw-r--r-- | spec/models/user_mailer_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 4 | ||||
-rw-r--r-- | spec/support/factory_orders.rb | 28 |
20 files changed, 129 insertions, 55 deletions
diff --git a/app/models/answer.rb b/app/models/answer.rb index b214ebb..26a273f 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -45,7 +45,7 @@ class Answer < ActiveRecord::Base :joins => :owner, :conditions => { 'users.mentor_id', mentor } } } named_scope :in_category, lambda { |category| { - :joins => :question, :conditions => { 'questions.category_id', category} } } + :joins => {:question => :question_categories}, :conditions => { 'question_categories.category_id', category} } } named_scope :with_feedback, lambda { |opt| { :conditions => { :feedback => opt } } } diff --git a/app/models/category.rb b/app/models/category.rb index a52a50d..146921b 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -25,7 +25,8 @@ class Category < ActiveRecord::Base validates_presence_of :name - has_many :questions + has_many :question_categories + has_many :questions, :through => :question_categories has_many :user_categories has_many :users, :through => :user_categories, :accessible => true diff --git a/app/models/question.rb b/app/models/question.rb index a660de6..fc9b176 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -39,7 +39,8 @@ class Question < ActiveRecord::Base #maybe add a page for not complete questions belongs_to :user, :creator => true - belongs_to :category + has_many :question_categories + has_many :categories, :through => :question_categories belongs_to :question_group has_many :answers has_one :reference_answer, :class_name => "Answer", :conditions => ["answers.reference = ?", true] @@ -58,7 +59,7 @@ class Question < ActiveRecord::Base return true if new_record? # when it's not new record allow changing only some properties - return only_changed?(:title, :content, :documentation, :category) + return only_changed?(:title, :content, :documentation) end false @@ -89,7 +90,7 @@ class Question < ActiveRecord::Base end named_scope :unanswered_ungrouped, lambda { |uid|{ - :joins => {:category => :user_categories}, + :joins => {:question_categories => {:category => :user_categories}}, :conditions => [ 'user_categories.user_id = ? AND questions.question_group_id IS NULL ' + 'AND NOT EXISTS (' + 'SELECT * FROM answers WHERE answers.owner_id = ? AND answers.question_id = questions.id)', @@ -109,7 +110,7 @@ class Question < ActiveRecord::Base :conditions => ['questions.id != ?', id]}} named_scope :ungrouped_questions_of_user, lambda { |user_id|{ - :joins => {:category => :user_categories}, + :joins => {:question_categories => {:category => :user_categories}}, :conditions => ['user_categories.user_id = ? AND questions.question_group_id IS NULL', user_id]}} named_scope :grouped_questions_of_user, lambda { |user_id|{ @@ -120,7 +121,7 @@ class Question < ActiveRecord::Base :conditions => { :user_id => user_id, :approved => false }}} named_scope :unanswered, lambda { |uid|{ - :joins => {:category => {:user_categories => :user}}, + :joins => {:question_categories => {:category => {:user_categories => :user}}}, :conditions => [ 'users.id = ? AND NOT EXISTS ( ' + 'SELECT * FROM answers WHERE answers.owner_id = ? AND answers.question_id = questions.id)', uid, uid]}} @@ -183,9 +184,8 @@ class Question < ActiveRecord::Base protected # Sends notification about new question (TODO: check for group). def notify_new_question - # If category isn't assigned don't try to access it - if category && approved - for user in category.users + if approved + for user in categories.*.users.flatten UserMailer.delay.deliver_new_question(user, self) end end @@ -193,7 +193,7 @@ class Question < ActiveRecord::Base # Sends notification about new question (TODO: check for group). def notify_approved_question - if category && !approved_was && approved + if !approved_was && approved notify_new_question end end diff --git a/app/models/question_category.rb b/app/models/question_category.rb new file mode 100644 index 0000000..c67ce9b --- /dev/null +++ b/app/models/question_category.rb @@ -0,0 +1,27 @@ +# Gentoo Recruiters Web App - to help Gentoo recruiters do their job better +# Copyright (C) 2011 Petteri Räty +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 of the License +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Associates questions to categories +class QuestionCategory < ActiveRecord::Base + + hobo_model # Don't put anything above this + + fields do + end + + belongs_to :question, :null => false, :index => false + belongs_to :category, :null => false, :index => false + index [:question_id, :category_id], :unique => true +end diff --git a/app/models/question_group.rb b/app/models/question_group.rb index 4ecd037..fa06cc9 100644 --- a/app/models/question_group.rb +++ b/app/models/question_group.rb @@ -32,12 +32,12 @@ class QuestionGroup < ActiveRecord::Base include Permissions::AnyoneCanViewAdminCanChange named_scope :in_category, lambda { |cid| { - :joins => :questions, :conditions => ['questions.category_id = ?', cid], + :joins => {:questions => :question_categories}, :conditions => ['question_categories.category_id = ?', cid], :group => 'question_groups.id, question_groups.name, question_groups.description, question_groups.created_at, question_groups.updated_at'}} named_scope :unassociated_in_category, lambda { |uid, cid| { - :joins => :questions, :conditions => ['questions.category_id = ? AND NOT EXISTS + :joins => {:questions => :question_categories}, :conditions => ['question_categories.category_id = ? AND NOT EXISTS (SELECT user_question_groups.* FROM user_question_groups INNER JOIN questions ON questions.id = user_question_groups.question_id WHERE (user_question_groups.user_id = ? AND questions.question_group_id = question_groups.id))', cid, uid], diff --git a/app/models/user.rb b/app/models/user.rb index ca7af6c..dff9295 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -49,9 +49,10 @@ class User < ActiveRecord::Base named_scope :mentorless_recruits, :conditions => { :role => 'recruit', :mentor_id => nil} named_scope :recruits_answered_all, :conditions => "role = 'recruit' AND NOT EXISTS (SELECT questions.id FROM questions - INNER JOIN categories cat ON questions.category_id = cat.id INNER JOIN - user_categories ON user_categories.category_id = cat.id WHERE - user_categories.user_id = users.id AND questions.question_group_id IS NULL AND NOT EXISTS ( + INNER JOIN question_categories ON question_categories.question_id = questions.id + INNER JOIN categories cat ON question_categories.category_id = cat.id + INNER JOIN user_categories ON user_categories.category_id = cat.id + WHERE user_categories.user_id = users.id AND questions.question_group_id IS NULL AND NOT EXISTS ( SELECT answers.id FROM answers WHERE answers.question_id = questions.id AND answers.owner_id = users.id)) AND NOT EXISTS (SELECT questions.id FROM questions INNER JOIN user_question_groups ON questions.id = user_question_groups.question_id diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb index 4acaff2..71c03e2 100644 --- a/app/models/user_mailer.rb +++ b/app/models/user_mailer.rb @@ -32,8 +32,7 @@ class UserMailer < ActionMailer::Base def new_question(user, question) common(user, "New question") - @body = { :title=> question.title, :category => question.category, - :id => question.id} + @body = { :title=> question.title, :id => question.id} end def new_answer(user, answer) diff --git a/app/views/user_mailer/new_question.erb b/app/views/user_mailer/new_question.erb index 4005ec7..d97737a 100644 --- a/app/views/user_mailer/new_question.erb +++ b/app/views/user_mailer/new_question.erb @@ -1,3 +1,3 @@ -There is a new question "<%=@title%>" in category "<%=@category%>" you are assigned to. +There is a new question "<%=@title%>" in one of the categories you are assigned to. <%= question_url(@id) %> diff --git a/db/migrate/20110312181715_add_question_category_pivot.rb b/db/migrate/20110312181715_add_question_category_pivot.rb new file mode 100644 index 0000000..ef1a810 --- /dev/null +++ b/db/migrate/20110312181715_add_question_category_pivot.rb @@ -0,0 +1,21 @@ +class AddQuestionCategoryPivot < ActiveRecord::Migration + def self.up + create_table :question_categories do |t| + t.integer :question_id, :null => false + t.integer :category_id, :null => false + end + add_index :question_categories, [:question_id, :category_id], :unique => true + + remove_column :questions, :category_id + + remove_index :questions, :name => :index_questions_on_category_id rescue ActiveRecord::StatementInvalid + end + + def self.down + add_column :questions, :category_id, :integer + + drop_table :question_categories + + add_index :questions, [:category_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index cbdde12..140e762 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -9,7 +9,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20110312173240) do +ActiveRecord::Schema.define(:version => 20110312181715) do create_table "answers", :force => true do |t| t.text "content", :default => "", :null => false @@ -78,6 +78,13 @@ ActiveRecord::Schema.define(:version => 20110312173240) do add_index "project_acceptances", ["user_id"], :name => "index_project_acceptances_on_user_id" + create_table "question_categories", :force => true do |t| + t.integer "question_id", :null => false + t.integer "category_id", :null => false + end + + add_index "question_categories", ["question_id", "category_id"], :name => "index_question_categories_on_question_id_and_category_id", :unique => true + create_table "question_content_emails", :force => true do |t| t.text "requirements", :default => "", :null => false t.text "description" @@ -120,11 +127,9 @@ ActiveRecord::Schema.define(:version => 20110312173240) do t.datetime "created_at" t.datetime "updated_at" t.integer "user_id" - t.integer "category_id" t.integer "question_group_id" end - add_index "questions", ["category_id"], :name => "index_questions_on_category_id" add_index "questions", ["question_group_id"], :name => "index_questions_on_question_group_id" add_index "questions", ["user_id"], :name => "index_questions_on_user_id" diff --git a/features/clean_ui.feature b/features/clean_ui.feature index 237136e..f475abf 100644 --- a/features/clean_ui.feature +++ b/features/clean_ui.feature @@ -63,7 +63,7 @@ Feature: Clean UI Given following questions: |question 1|category| - |question 3|category| + |question 2|category| |question 3|category| Given email question content for "question 1" Given text content "something" for question "question 2" diff --git a/features/step_definitions/questions_steps.rb b/features/step_definitions/questions_steps.rb index b0cd6fd..7f96e30 100644 --- a/features/step_definitions/questions_steps.rb +++ b/features/step_definitions/questions_steps.rb @@ -12,8 +12,7 @@ end Given /^a question "([^\"]*)" in category "([^\"]*)"$/ do |title, category| Given "a question \"#{title}\"" Given "a category \"#{category}\"" - @question.category = @category - @question.save! + QuestionCategory.create!(:category => @category, :question => @question) end Given /^a question "([^\"]*)" in group "([^\"]*)"$/ do |title, group| diff --git a/spec/factories.rb b/spec/factories.rb index 2c663af..3ead1e1 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -51,6 +51,11 @@ q.name { Factory.next(:category) } end + Factory.define :question_category do |qc| + qc.association :question + qc.association :category + end + Factory.sequence :question do |n| "question-#{n}" end @@ -58,7 +63,9 @@ # it'll belong to new category by default Factory.define :question do |q| q.title { Factory.next(:question) } - q.category { Factory(:category)} + q.after_build { |q| + q.categories = [Factory.build :category] + } end Factory.sequence :answer do |n| diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index e300537..61e23a8 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -250,7 +250,6 @@ describe Answer do (!produced_ans.attributes[i[0]]).should be_true # it can be nil or false end end - end it "should produce proper updated answer from params" do @@ -270,10 +269,10 @@ describe Answer do it "should properly return wrong answers of recruit" do recruit = Factory(:recruit) cat = Factory(:category) - q1 = Factory(:question, :category => cat) - q2 = Factory(:question, :category => cat) - q3 = Factory(:question, :category => cat) - q4 = Factory(:question, :category => cat) + q1 = Factory(:question_category, :category => cat).question + q2 = Factory(:question_category, :category => cat).question + q3 = Factory(:question_category, :category => cat).question + q4 = Factory(:question_category, :category => cat).question Factory(:question_content_text, :question => q4) diff --git a/spec/models/question_group_spec.rb b/spec/models/question_group_spec.rb index b490f89..dc62f83 100644 --- a/spec/models/question_group_spec.rb +++ b/spec/models/question_group_spec.rb @@ -23,7 +23,9 @@ describe QuestionGroup do for n in 1..5 groups_in_cat.push Factory(:question_group) for i in 1..n - Factory(:question, :category => category, :question_group => groups_in_cat.last) + Factory(:question_category, + :category => category, + :question => Factory(:question, :question_group => groups_in_cat.last)) end end @@ -44,7 +46,9 @@ describe QuestionGroup do for n in 1..5 groups_in_cat.push Factory(:question_group) for i in 1..n - Factory(:question, :category => category, :question_group => groups_in_cat.last) + Factory(:question_category, + :category => category, + :question => Factory(:question, :question_group => groups_in_cat.last)) end end diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb index 5dddd70..99a286f 100644 --- a/spec/models/question_spec.rb +++ b/spec/models/question_spec.rb @@ -68,8 +68,8 @@ describe Question do it "should send email notifications to watching recruits when created by recruiter" do category = Factory(:category) recruit = Factory(:recruit, :categories => [category]) - question = Question.new(:title => "new question", - :category => category) + question = Factory.build(:question) + question.categories << category UserMailer.should_receive_delayed(:deliver_new_question, recruit, question) @@ -79,19 +79,19 @@ describe Question do it "should send email notifications to watching recruits when approved" do category = Factory(:category) recruit = Factory(:recruit, :categories => [category]) - question = Factory(:question, :title => "new question", - :category => category, :user => Factory(:recruit)) + question = Factory(:question, :user => Factory(:recruit)) + question.categories << category UserMailer.should_receive_delayed(:deliver_new_question, recruit, question) question.approved = true question.save! end - it "should not send email notifications to watching recruits when approved is changed" do + it "should not send email notifications to watching recruits when approved is not changed" do category = Factory(:category) recruit = Factory(:recruit, :categories => [category]) - question = Factory(:question, :title => "new question", - :category => category, :user => Factory(:recruit), :approved => true) + question = Factory(:question, :user => Factory(:recruit), :approved => true) + question.categories << category UserMailer.should_not_receive(:deliver_new_question).with(recruit, question) @@ -146,7 +146,6 @@ describe Question do question.should be_editable_by(recruit) question.should be_editable_by(recruit, :title) question.should be_editable_by(recruit, :documentation) - question.should be_editable_by(recruit, :category) question.should_not be_editable_by(recruit, :user) question.should_not be_editable_by(recruit, :approved) @@ -159,7 +158,6 @@ describe Question do question.should be_editable_by(recruit) question.should be_editable_by(recruit, :title) question.should be_editable_by(recruit, :documentation) - question.should be_editable_by(recruit, :category) question.should_not be_editable_by(recruit, :user) question.should_not be_editable_by(recruit, :approved) @@ -172,7 +170,6 @@ describe Question do question.should be_editable_by(admin) question.should be_editable_by(admin, :title) question.should be_editable_by(admin, :documentation) - question.should be_editable_by(admin, :category) question.should be_editable_by(admin, :approved) question.should_not be_editable_by(admin, :user) diff --git a/spec/models/user_category_spec.rb b/spec/models/user_category_spec.rb index 99efdea..130a359 100644 --- a/spec/models/user_category_spec.rb +++ b/spec/models/user_category_spec.rb @@ -70,7 +70,9 @@ describe UserCategory do for n in 1..5 groups_in_cat.push Factory(:question_group) for i in 1..n - Factory(:question, :category => category, :question_group => groups_in_cat.last) + Factory(:question_category, + :category => category, + :question => Factory(:question, :question_group => groups_in_cat.last)) end end diff --git a/spec/models/user_mailer_spec.rb b/spec/models/user_mailer_spec.rb index 39a45e2..d681e9e 100644 --- a/spec/models/user_mailer_spec.rb +++ b/spec/models/user_mailer_spec.rb @@ -10,7 +10,7 @@ describe UserMailer do notification.should deliver_to(recruit.email_address) notification.should deliver_from("no-reply@localhost") notification.should have_text(/There is a new question "#{question.title}"/) - notification.should have_text(/in category "#{question.category.name}" you are assigned to./) + notification.should have_text(/one of the categories you are assigned to./) notification.should have_text(/http:\/\/localhost:3000\/questions\/#{question.id}/) notification.should have_subject('New question') end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ee256c4..b4a3dbf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -310,7 +310,7 @@ describe User do q1 = Factory(:question) Factory(:question_content_multiple_choice, :question => q1) Factory(:user_category, - :category => q1.category, + :category => q1.categories.first, :user => recruit) recruit.answered_all_multi_choice_questions?.should be_false @@ -337,7 +337,7 @@ describe User do q1 = Factory(:question) Factory(:user_category, :user => recruit, - :category => q1.category) + :category => q1.categories.first) recruit.progress.should == "Answered 0 of 1 questions." Factory(:answer, :owner => recruit, :question => q1) diff --git a/spec/support/factory_orders.rb b/spec/support/factory_orders.rb index d02249a..9548330 100644 --- a/spec/support/factory_orders.rb +++ b/spec/support/factory_orders.rb @@ -12,19 +12,27 @@ def recruit_with_answered_and_unanswered_questions(n=5) for i in 1..n # answered and unanswered ungrouped questions category = Factory(:category) - r.answered.push Factory(:question, :category => category) - r.unanswered.push Factory(:question, :category => category) + r.answered.push Factory(:question_category, :category => category).question + r.unanswered.push Factory(:question_category, :category => category).question Factory(:answer, :owner => r.recruit, :question => r.answered.last) # and answered and unanswered question in one group group = Factory(:question_group) - r.answered.push Factory(:question, :category => category, :question_group => group) + r.answered.push Factory(:question_category, + :category => category, + :question => Factory(:question, :question_group => group) + ).question Factory(:user_question_group, :user => r.recruit, :question => r.answered.last) # This question isn't unanswered! This is question user can't answer - Factory(:question, :category => category, :question_group => group) + Factory(:question_category, + :category => category, + :question => Factory(:question, :question_group => group)) # add a unanswered grouped question - r.unanswered.push Factory(:question, :category => category, :question_group => Factory(:question_group)) + r.unanswered.push Factory(:question_category, + :category => category, + :question => Factory(:question, :question_group => Factory(:question_group)) + ).question Factory(:user_question_group, :user => r.recruit, :question => r.unanswered.last) Factory(:answer, :owner => r.recruit, :question => r.answered.last) @@ -51,14 +59,18 @@ def recruit_with_answers_in_categories(mentor = nil, n_categories = 5, n_ans_in_ r.categories.push Factory(:category) r.answers_in_cat.push [] for i in 1..n_ans_in_cat - question = Factory(:question, :category => r.categories.last) + question = Factory(:question_category, :category => r.categories.last).question r.all_answers.push Factory(:answer, :owner => r.recruit, :question => question) r.answers_in_cat.last.push r.all_answers.last # group of two questions, answered group = Factory(:question_group) - question = Factory(:question, :category => r.categories.last, :question_group => group) - Factory(:question, :category => r.categories.last, :question_group => group) + question = Factory(:question_category, + :category => r.categories.last, + :question => Factory(:question, :question_group => group)).question + Factory(:question_category, + :category => r.categories.last, + :question => Factory(:question, :question_group => group)) Factory(:user_question_group, :user => r.recruit, :question => question) r.all_answers.push Factory(:answer, :owner => r.recruit, :question => question) r.answers_in_cat.last.push r.all_answers.last |