diff options
author | Joachim Filip Ignacy Bartosik <jbartosik@gmail.com> | 2010-07-12 18:47:01 +0200 |
---|---|---|
committer | Joachim Filip Ignacy Bartosik <jbartosik@gmail.com> | 2010-07-29 19:49:13 +0200 |
commit | 84c29dd7f3fc64ea491f0342476f7bc31a20171f (patch) | |
tree | 6bd86e4dac5bc216de46c5e18569de8deeaf012d | |
parent | Allow project leads to add Project Acceptances easily (diff) | |
download | recruiting-webapp-84c29dd7f3fc64ea491f0342476f7bc31a20171f.tar.gz recruiting-webapp-84c29dd7f3fc64ea491f0342476f7bc31a20171f.tar.bz2 recruiting-webapp-84c29dd7f3fc64ea491f0342476f7bc31a20171f.zip |
Email questions
Also make sure users can answer questions with multiple and text
content and can't answer email questions within application. Added
"Gentoo-dev-announce posting" question to seed
-rw-r--r-- | app/controllers/question_content_emails_controller.rb | 12 | ||||
-rw-r--r-- | app/models/email_answer.rb | 55 | ||||
-rw-r--r-- | app/models/question.rb | 5 | ||||
-rw-r--r-- | app/models/question_content_email.rb | 72 | ||||
-rw-r--r-- | app/models/user.rb | 18 | ||||
-rw-r--r-- | app/models/user_mailer.rb | 6 | ||||
-rw-r--r-- | app/views/question_content_emails/new_for_question.dryml | 4 | ||||
-rw-r--r-- | app/views/questions/show.dryml | 3 | ||||
-rw-r--r-- | app/views/taglibs/detailed.dryml | 20 | ||||
-rw-r--r-- | app/views/taglibs/forms.dryml | 12 | ||||
-rw-r--r-- | app/views/taglibs/views.dryml | 22 | ||||
-rw-r--r-- | db/fixtures/questions-email.yml | 14 | ||||
-rw-r--r-- | db/schema.rb | 14 | ||||
-rw-r--r-- | db/seeds.rb | 5 | ||||
-rw-r--r-- | doc/config/config.yml | 3 | ||||
-rw-r--r-- | features/email_answers.feature | 40 | ||||
-rw-r--r-- | features/step_definitions/email_answer_steps.rb | 75 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 6 |
18 files changed, 383 insertions, 3 deletions
diff --git a/app/controllers/question_content_emails_controller.rb b/app/controllers/question_content_emails_controller.rb new file mode 100644 index 0000000..345f9c3 --- /dev/null +++ b/app/controllers/question_content_emails_controller.rb @@ -0,0 +1,12 @@ +class QuestionContentEmailsController < ApplicationController + + hobo_model_controller + + auto_actions :create, :update + auto_actions_for :question, :new + + def new_for_question + hobo_new QuestionContentEmail.new(:question_id => params[:question_id]) + end + +end diff --git a/app/models/email_answer.rb b/app/models/email_answer.rb new file mode 100644 index 0000000..730f483 --- /dev/null +++ b/app/models/email_answer.rb @@ -0,0 +1,55 @@ +class EmailAnswer < Answer + fields do + correct :boolean + end + + # Users can't change Email answers - app does it internally + multi_permission(:create, :update, :destroy, :edit){ false } + + # Creates new answer/ updates old answer + # expects user to send email from address [s]he has given to app + # and title in format "#{question.id}-#{user.token}" + def self.answer_from_email(email) + user = User.find_by_email_address(email.from) + return if user.nil? + + subject = /^([0-9]+)-(\w+)$/.match(email.subject) + return if subject.nil? + return unless user.token == subject.captures[1] + + question = Question.first :conditions => { :id => subject.captures[0] } + return if question.nil? + return unless question.content.is_a? QuestionContentEmail + + # Fetch existing answer, if it doesn't exist create a new one + # them mark it as incorrect (if it passes all tests it will be correct) + answer = question.answer_of(user) + answer = EmailAnswer.new(:question => question, :owner => user) if answer.nil? + answer.correct = false + answer.save! + + for condition in question.content.req_array + val = email.send(condition[0].downcase.sub('-', '_')) # Value we're testing + cond = /#{condition[1]}/i # Regexp value should match + passed = false # Was test passed + + val = [''] if val.is_a? NilClass + val = [val] if val.is_a? String + + for v in val + if v.match(cond) + # Test passed + passed = true + break + end + end + + return unless passed + end + + # passed all test - mark it and return answer + answer.correct = true + answer.save! + answer + end +end diff --git a/app/models/question.rb b/app/models/question.rb index f5ad800..b126708 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -23,6 +23,7 @@ class Question < ActiveRecord::Base has_many :user_question_groups has_one :question_content_text has_one :question_content_multiple_choice + has_one :question_content_email multi_permission :create, :update, :destroy do # Allow changes if user is administrator @@ -121,7 +122,9 @@ class Question < ActiveRecord::Base end def content - question_content_text || question_content_multiple_choice + question_content_text || + question_content_multiple_choice || + question_content_email end before_create{ |question| diff --git a/app/models/question_content_email.rb b/app/models/question_content_email.rb new file mode 100644 index 0000000..37c1197 --- /dev/null +++ b/app/models/question_content_email.rb @@ -0,0 +1,72 @@ +require 'permissions/inherit.rb' +class QuestionContentEmail < ActiveRecord::Base + + hobo_model # Don't put anything above this + + fields do + requirements :text, :nil => false, :default => "" + description HoboFields::MarkdownString + timestamps + end + + belongs_to :question + attr_readonly :question + never_show :requirements + + inherit_permissions(:question) + + def req_text_view_permitted? + return true if acting_user.try.role.try.is_recruiter? + return true if question.owner_is?(acting_user) && !question.approved + end + + # Returns array. + # Each item of array is array [field, expected value] + def req_array + if requirements.nil? || requirements.empty? + [] + else + ActiveSupport::JSON.decode requirements + end + end + + # Returns easy-human-readable string + # Each line is in format + # field : value + def req_text + res = req_array.inject(String.new) do |res, cur| + # escape colons + cur[0].sub!(':', '\:') + cur[1].sub!(':', '\:') + + res += "#{cur[0]} : #{cur[1]}\n" + end + HoboFields::Text.new(res) + end + + # req_text escaped to display properly as HTML + def req_html + h(req_text).sub("\n", "<br/>\n") + end + + # Converts easy-human-readable string to JSON and saves in requirements + # Ignore improperly formatted lines ( i.e. lines that + def req_text=(str) + # Split to lines + # Split every line at /\s:/, unescape colons, strip white space + # Ignore lines that don't have exactly one /\s:/ + res = str.split(/\n/).inject(Array.new) do |result, line| + + item = line.split(/\s:/).inject(Array.new) do |r,c| + c.sub!('\:', ':') + c.strip! + r.push c + end + + result.push(item) if item.count == 2 + result + end + + self.requirements = res.to_json + end +end diff --git a/app/models/user.rb b/app/models/user.rb index e652e37..a974abb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,6 +10,7 @@ class User < ActiveRecord::Base nick :string contributions HoboFields::MarkdownString project_lead :boolean, :default => false + token :string timestamps end @@ -61,6 +62,23 @@ class User < ActiveRecord::Base validates_uniqueness_of :nick, :if => :nick never_show :project_lead + + # Token + never_show :token + + # Generate new token + def token=(more_salt) + # Time.now.to_f.to_s gives enough precision to be considered random + token = Digest::SHA1.hexdigest("#{Time.now.to_f.to_s}#{@salt}#{more_salt}") + write_attribute("token", token) + token + end + + # Give user token on creation + before_create do |u| + u.token = '' + end + # --- Permissions --- # def create_permitted? diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb index db60b86..1c9521e 100644 --- a/app/models/user_mailer.rb +++ b/app/models/user_mailer.rb @@ -38,4 +38,10 @@ class UserMailer < ActionMailer::Base common(user, "New comment") @body = { :question_title=> question_title(comment.answer), :id => comment.answer.id } end + + def receive(email) + # For now email answers for questions are only emails app receives + # so try use any received email as answer. + EmailAnswer.answer_from_email(email) + end end diff --git a/app/views/question_content_emails/new_for_question.dryml b/app/views/question_content_emails/new_for_question.dryml new file mode 100644 index 0000000..180f65e --- /dev/null +++ b/app/views/question_content_emails/new_for_question.dryml @@ -0,0 +1,4 @@ +<% + # For unknown reason Hobo does not render a proper new_for page without this file +%> +<new-page/> diff --git a/app/views/questions/show.dryml b/app/views/questions/show.dryml index 0c3fc71..179fbe1 100644 --- a/app/views/questions/show.dryml +++ b/app/views/questions/show.dryml @@ -8,12 +8,13 @@ <ul> <li><a href="&new_question_content_text_for_question_path(this.id)">Add text content</a></li> <li><a href="&new_question_content_multiple_choice_for_question_path(this.id)">Add multiple choice content</a></li> + <li><a href="&new_question_content_email_for_question_path(this.id)">Add email content</a></li> </ul> </if> </if> <else> <if with="&this.answer_of(current_user)"> - <a>View you answer</a>' + <a href="&answer_path(this)">View you answer</a>' </if> <else> <if test="& current_user.signed_up? && this.content.try.new_answer_of(current_user)"> diff --git a/app/views/taglibs/detailed.dryml b/app/views/taglibs/detailed.dryml index f7df48e..ceb8dc9 100644 --- a/app/views/taglibs/detailed.dryml +++ b/app/views/taglibs/detailed.dryml @@ -50,3 +50,23 @@ </answer:> </detailed> </def> + +<def tag="detailed" for="EmailAnswer"> + <h2> + Answer of + <with:owner><name/></with> + for question + "<with:question><name/></with> + <a action="edit" if="&can_edit?">(Edit)</a> + </h2> + <h5>Question:</h5> + <with:question><view:content/></with> + + <h5>Answer:</h5> + <if:correct> + You sent proper email. + </if> + <else> + Email you sent didn't match requirements. + </else> +</def> diff --git a/app/views/taglibs/forms.dryml b/app/views/taglibs/forms.dryml index eddfc0f..013f84c 100644 --- a/app/views/taglibs/forms.dryml +++ b/app/views/taglibs/forms.dryml @@ -37,3 +37,15 @@ </div> </form> </def> + +<def tag="form" for="QuestionContentEmail"> + <form merge param="default"> + <error-messages param/> + <field-list fields="description, req_text" param> + <req_text-label:>Requirements</req_text-label:> + </field-list> + <div param="actions"> + <submit label="#{ht 'question_content_emails.actions.save', :default=>['Save']}" param/><or-cancel param="cancel"/> + </div> + </form> +</def> diff --git a/app/views/taglibs/views.dryml b/app/views/taglibs/views.dryml index fcbec6d..79bd41d 100644 --- a/app/views/taglibs/views.dryml +++ b/app/views/taglibs/views.dryml @@ -19,3 +19,25 @@ <%raise HoboError, "view of non-viewable field '#{this_field}' of #{this_parent.typed_id rescue this_parent}" unless can_view?%> <input disabled/> </def> + +<def tag="view" for="QuestionContentEmail"> + <%= + if this.viewable_by?(current_user, :req_text) + "#{h(this.req_text).sub("\n", "<br/>\n")}<br/>" + else + raise HoboError, "view of non-viewable field '#{this_field}' of #{this_parent.typed_id rescue this_parent}" + end + %> + + <%= + if this.viewable_by?(current_user, :description) + "#{this.description.to_html}" + else + raise HoboError, "view of non-viewable field '#{this_field}' of #{this_parent.typed_id rescue this_parent}" + end + %> + + <if test="¤t_user.signed_up?"> + Your answer should have subject (without quotes) "<%= "#{this.question.id}-#{current_user.try.token}" %>". + </if> +</def> diff --git a/db/fixtures/questions-email.yml b/db/fixtures/questions-email.yml new file mode 100644 index 0000000..6ad2d95 --- /dev/null +++ b/db/fixtures/questions-email.yml @@ -0,0 +1,14 @@ +email_q1: + title: Gentoo-dev-announce posting + documentation: + question_category: ebuild + content: "Email a major eclass change announcement. Replace all + @gentoo.org addresses with @localhost addresses. + The from field should match email you set in the application. + \n\nTo configure postfix use for example recruiting: + \n\n gentoo-dev-announce: | \"curl -F 'email=<-' http://localhost:3000/users/receive_email\" + \n gentoo-dev: /dev/null + \n\nin /etc/mail/aliases." + req_text: 'To : gentoo-dev-announce@localhost + To : gentoo-dev@localhost + Reply-to : gentoo-dev@localhost' diff --git a/db/schema.rb b/db/schema.rb index 15b0b5a..c9db40b 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 => 20100728172058) do +ActiveRecord::Schema.define(:version => 20100729170624) do create_table "answers", :force => true do |t| t.text "content" @@ -21,6 +21,7 @@ ActiveRecord::Schema.define(:version => 20100728172058) do t.integer "owner_id" t.string "type" t.string "feedback", :default => "" + t.boolean "correct" end add_index "answers", ["owner_id"], :name => "index_answers_on_owner_id" @@ -64,6 +65,16 @@ ActiveRecord::Schema.define(:version => 20100728172058) do t.datetime "updated_at" end + create_table "question_content_emails", :force => true do |t| + t.text "requirements", :default => "" + t.text "description" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "question_id" + end + + add_index "question_content_emails", ["question_id"], :name => "index_question_content_emails_on_question_id" + create_table "question_content_multiple_choices", :force => true do |t| t.text "content", :null => false t.datetime "created_at" @@ -141,6 +152,7 @@ ActiveRecord::Schema.define(:version => 20100728172058) do t.string "state", :default => "active" t.datetime "key_timestamp" t.boolean "project_lead", :default => false + t.string "token" end add_index "users", ["mentor_id"], :name => "index_users_on_mentor_id" diff --git a/db/seeds.rb b/db/seeds.rb index a2b4daf..afed2eb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -79,6 +79,11 @@ seeder.read_yaml('db/fixtures/questions-multichoice.yml', Question, ['question_c end end +# Questions with email content - load from YAML file +seeder.read_yaml('db/fixtures/questions-email.yml', Question, ['question_category', 'question_group']) do |name, hash, objects, klass| + objects[name] = klass.create!(hash - {'content' => nil, 'req_text' => nil}) + objects["#{name}-content"] = QuestionContentEmail.create! :question => objects[name], :description=> hash['content'], :req_text => hash['req_text'] +end # Users - load from YAML file seeder.read_yaml 'db/fixtures/users.yml', User, 'mentor' diff --git a/doc/config/config.yml b/doc/config/config.yml index 8cf3b34..679a60e 100644 --- a/doc/config/config.yml +++ b/doc/config/config.yml @@ -5,6 +5,9 @@ defaults: &defaults min_months_mentor_is_dev: 6 seed: users_domain: example.com + answers_address: + local_part: answers + domain: localhost development: <<: *defaults production: diff --git a/features/email_answers.feature b/features/email_answers.feature new file mode 100644 index 0000000..19ef7fe --- /dev/null +++ b/features/email_answers.feature @@ -0,0 +1,40 @@ +Feature: gentoo-dev-announce + As a recruiting lead + I want recruits to send actual emails instead of writing + So that they learn in practice + + Scenario: Testing gentoo-dev-announce-posting + Given recruit that should answer gentoo-dev-announce posting question + And I am logged in as "recruit" + When I send wrong email announcement + And I am on show "gentoo-dev-announce posting" question page + Then I should see "Your answer should have subject (without quotes)" + Then I should see subject for email "recruit" should send to answer "gentoo-dev-announce posting" + + When I follow "View you answer" + Then I should see "Email you sent didn't match requirements" + + When I send proper email announcement + And I am on show "gentoo-dev-announce posting" question page + Then I should see "Remember to replace @gentoo.org with localhost" + + When I follow "View you answer" + Then I should see "You sent proper email" + + Scenario: Don't see answer it link on email question page + Given recruit that should answer gentoo-dev-announce posting question + And I am logged in as "recruit" + And I am on show "gentoo-dev-announce posting" question page + Then I should not see "Answer it" + + Scenario: Protect from forged responses + Given recruit that should answer gentoo-dev-announce posting question + And someone sends forged answer + And I am logged in as "recruit" + And I am on show "gentoo-dev-announce posting" question page + Then I should not see "View you answer" + + Scenario: Don't show answering subject to guest + Given recruit that should answer gentoo-dev-announce posting question + When I am on show "gentoo-dev-announce posting" question page + Then I should not see "Your answer should have subject (without quotes)" diff --git a/features/step_definitions/email_answer_steps.rb b/features/step_definitions/email_answer_steps.rb new file mode 100644 index 0000000..24a2555 --- /dev/null +++ b/features/step_definitions/email_answer_steps.rb @@ -0,0 +1,75 @@ +Given /^email question content for "([^\"]*)"$/ do |question| + Given "a question \"#{question}\"" + @content = @question.content + unless @content.is_a? QuestionContentEmail + @content.try.destroy + @content = QuestionContentEmail.create!(:question => @question, :description => "Remember to replace @gentoo.org with localhost") + @question.reload + end +end + +Given /^email question content for "([^\"]*)" with following requirements:$/ do |question, table| + Given "email question content for \"#{question}\"" + res = table.raw.inject(String.new) do |res, cur| + cur[0].sub!(':', '\:') + cur[1].sub!(':', '\:') + res += "#{cur[0]} : #{cur[1]}\n" + res + end + @content.req_text = res + @content.save! +end + +Given /^recruit that should answer gentoo-dev-announce posting question$/ do + Given 'user "recruit" has category "questions"' + Given 'a question "gentoo-dev-announce posting" in category "questions"' + Given 'email question content for "gentoo-dev-announce posting" with following requirements:', table(%{ + |To|gentoo-dev-announce@localhost| + |To|gentoo-dev@localhost| + |Reply-To|gentoo-dev@localhost| + }) +end + +When /^I send wrong email announcement$/ do + Given 'user "recruit"' + Given 'a question "gentoo-dev-announce posting"' + + mail = TMail::Mail.new + mail.subject = "#{@question.id}-#{@user.token}" + mail.from = @user.email_address + mail.to = ['gentoo-dev-announce@localhost'] + + UserMailer.receive(mail.to_s) +end + +When /^I send proper email announcement$/ do + Given 'user "recruit"' + + mail = TMail::Mail.new + mail.from = @user.email_address + mail.subject = "#{@question.id}-#{@user.token}" + mail.to = ['gentoo-dev-announce@localhost', 'gentoo-dev@localhost'] + mail.reply_to = 'gentoo-dev@localhost' + + UserMailer.receive(mail.to_s) +end + +When /^someone sends forged answer$/ do + Given 'user "recruit"' + + mail = TMail::Mail.new + mail.from = @user.email_address + mail.to = ['gentoo-dev-announce@localhost', 'gentoo-dev@localhost'] + mail.reply_to = 'gentoo-dev@localhost' + + Given 'user "forger"' + mail.subject = "#{@question.id}-#{@user.token}" + + UserMailer.receive(mail.to_s) +end + +Then /^I should see subject for email "([^"]+)" should send to answer "([^"]+)"$/ do |user, question| + Given "user \"#{user}\"" + Given "a question \"#{question}\"" + Then "I should see \"#{@question.id}-#{@user.token}\"" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f0cc8af..8589bcc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -200,4 +200,10 @@ describe User do user.should_not be_editable_by(Factory(:recruit), :project_acceptances) user.should_not be_editable_by(Guest.new, :project_acceptances) end + + it "should have token right after creation" do + for u in fabricate_all_roles + u.token.should_not be_nil + end + end end |