aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoachim Filip Ignacy Bartosik <jbartosik@gmail.com>2010-07-12 18:47:01 +0200
committerJoachim Filip Ignacy Bartosik <jbartosik@gmail.com>2010-07-29 19:49:13 +0200
commit84c29dd7f3fc64ea491f0342476f7bc31a20171f (patch)
tree6bd86e4dac5bc216de46c5e18569de8deeaf012d
parentAllow project leads to add Project Acceptances easily (diff)
downloadrecruiting-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.rb12
-rw-r--r--app/models/email_answer.rb55
-rw-r--r--app/models/question.rb5
-rw-r--r--app/models/question_content_email.rb72
-rw-r--r--app/models/user.rb18
-rw-r--r--app/models/user_mailer.rb6
-rw-r--r--app/views/question_content_emails/new_for_question.dryml4
-rw-r--r--app/views/questions/show.dryml3
-rw-r--r--app/views/taglibs/detailed.dryml20
-rw-r--r--app/views/taglibs/forms.dryml12
-rw-r--r--app/views/taglibs/views.dryml22
-rw-r--r--db/fixtures/questions-email.yml14
-rw-r--r--db/schema.rb14
-rw-r--r--db/seeds.rb5
-rw-r--r--doc/config/config.yml3
-rw-r--r--features/email_answers.feature40
-rw-r--r--features/step_definitions/email_answer_steps.rb75
-rw-r--r--spec/models/user_spec.rb6
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="&current_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