adds reviewer vote reminders

completes #86

needs cronjob added on deploy to production
This commit is contained in:
Mark Moser 2017-02-09 16:31:43 -06:00
commit 37f6e49592
19 changed files with 453 additions and 8 deletions

View File

@ -6,4 +6,17 @@ class ReviewerMailer < ApplicationMailer
mail to: recipients, subject: "Skills Assessment Results - #{@candidate.test_hash}" mail to: recipients, subject: "Skills Assessment Results - #{@candidate.test_hash}"
end end
def reminder reminder
@reminder = reminder
mail to: reminder.email, subject: "Review Reminder"
end
def notify_manager candidate_id
@candidate = Candidate.find_by(id: candidate_id)
@manager = @candidate.manager
mail to: @manager.email, subject: "Voting Complete"
end
end end

View File

@ -35,6 +35,13 @@ class Candidate < ApplicationRecord
end end
end end
def manager
manager_votes = votes.joins(:user).where("users.role = 'manager'")
return nil if manager_votes.empty?
manager_votes.first.user
end
def submitted_answers def submitted_answers
answers.where(submitted: true) answers.where(submitted: true)
end end

View File

@ -5,6 +5,8 @@ class ReviewerVote < ApplicationRecord
validates :user_id, uniqueness: { scope: :candidate_id } validates :user_id, uniqueness: { scope: :candidate_id }
after_save :notify_manager
enum vote: { enum vote: {
undecided: 0, undecided: 0,
yea: 1, yea: 1,
@ -15,4 +17,38 @@ class ReviewerVote < ApplicationRecord
approved: 1, approved: 1,
rejected: 2 rejected: 2
} }
private
def notify_manager
ReviewerMailer.notify_manager(candidate_id).deliver_now if all_reviewers_voted?
end
def all_reviewers_voted? # rubocop:disable Metrics/MethodLength
sql = " select distinct rev.candidate_id
, full_votes.voters, full_votes.vote_count
, c.test_hash
from reviewer_votes rev
inner join users u on u.id = rev.user_id
inner join candidates c on c.id = rev.candidate_id
left join (
select candidate_id
from reviewer_votes
where veto > 0 or veto is null
) as vetos on vetos.candidate_id = rev.candidate_id
left join (
select candidate_id
, count(vote) voters
, sum(case when vote != 0 then 1 else 0 end) vote_count
from reviewer_votes
left join users on users.id = reviewer_votes.user_id
where users.role != 'manager'
group by candidate_id
) as full_votes on full_votes.candidate_id = rev.candidate_id
where vetos.candidate_id is null
and full_votes.voters = full_votes.vote_count
and rev.candidate_id = #{candidate_id};"
result = ActiveRecord::Base.connection.exec_query(sql)
result.count == 1
end # rubocop:enable Metrics/MethodLength
end end

View File

@ -19,6 +19,8 @@ class User < ApplicationRecord
end end
# Voting # Voting
# TODO: Refactor this out of User, belongs on ReviewerVote
# ie: cast_yea(candidate, user)
def cast_yea_on candidate def cast_yea_on candidate
vote = votes.find_by(candidate_id: candidate.to_i) vote = votes.find_by(candidate_id: candidate.to_i)
vote.vote = :yea vote.vote = :yea

View File

@ -0,0 +1,10 @@
<row>
<columns class="email-body">
<p>Hello <%= @manager.name %>,</p>
<p>
Everyone has voted and you need to request or decline an interview for
<%= link_to nil, admin_result_url(@candidate.test_hash) %>
</p>
</columns>
</row>

View File

@ -0,0 +1,6 @@
PERFICIENT/digital SKILLS ASSESSMENT RESULTS
Hello <%= @manager.name %>,
Everyone has voted and you need to request or decline an interview for
<%= admin_result_url(@candidate.test_hash) %>

View File

@ -0,0 +1,9 @@
<row>
<columns class="email-body">
<p>Hello <%= @reminder.name %>,</p>
<p>
Please review and vote on the results for <%= link_to nil, admin_result_url(@reminder.test_hash) %>
</p>
</columns>
</row>

View File

@ -0,0 +1,5 @@
PERFICIENT/digital SKILLS ASSESSMENT RESULTS
Hello <%= @reminder.name %>,
Please review and vote on the results for <%= admin_result_url(@reminder.test_hash) %>.

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
class Reminder class CandidateReminder
def initialize def initialize
@collection = reminder_collection @collection = reminder_collection
end end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class ReviewerReminder
def initialize
@collection = reminder_collection
end
def count
@collection.count
end
alias size count
def reminders
@reminders ||= @collection.to_hash.map { |r| OpenStruct.new(r) }
end
def send_all
reminders.each do |reminder|
ReviewerMailer.reminder(reminder).deliver_now
end
end
private
def reminder_collection
sql = "select u.name, u.email, c.test_hash, c.project
from reviewer_votes rev
inner join users u on u.id = rev.user_id
inner join candidates c on c.id = rev.candidate_id
where rev.vote = 0 and rev.veto = 0
and u.role != 'manager' and u.active is not false;"
ActiveRecord::Base.connection.exec_query(sql)
end
end

View File

@ -2,7 +2,13 @@
namespace :reminders do namespace :reminders do
desc "send reminders to stagnate quizes" desc "send reminders to stagnate quizes"
task send_all: :environment do task send_all: :environment do
reminders = Reminder.new reminders = CandidateReminder.new
reminders.send_all
end
desc "send reminders to reviewers"
task reviewers: :environment do
reminders = ReviewerReminder.new
reminders.send_all reminders.send_all
end end
end end

View File

@ -696,3 +696,197 @@ wade10:
created_at: <%= DateTime.now() - 36.hours - 40.minutes %> created_at: <%= DateTime.now() - 36.hours - 40.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 20.minutes %> updated_at: <%= DateTime.now() - 36.hours - 20.minutes %>
jorge1:
candidate: jorge
question: Cras justo odio, dapibus ac facilisis in, egestas eget quam. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit.
answer: option 3
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 22.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 22.minutes %>
jorge2:
candidate: jorge
question: fed2
answer: 'indexOf()'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 24.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 4.minutes %>
jorge3:
candidate: jorge
question: fed3
answer: {html: '<h1>Salmon</h1>', css: 'h1 {color: salmon;}', js: '', text: 'Gotta lotta GOOD things on sale, strangah.'}
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 26.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 6.minutes %>
jorge4:
candidate: jorge
question: fed4
answer: Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 28.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 28.minutes %>
jorge5:
candidate: jorge
question: fed5
answer: 'Dynamic listeners'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 30.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 30.minutes %>
jorge6:
candidate: jorge
question: fed6
answer: Integer posuere erat a ante venenatis dapibus posuere velit aliquet.
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 32.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 12.minutes %>
jorge7:
candidate: jorge
question: fed7
answer: {html: '<p>This means <strong>jQuery</strong> needs to be available in live-coder!</p>', css: "strong {font-size: 1.6em;}\n.green {color: green;}", js: '$("strong").addClass("green");'}
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 34.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 14.minutes %>
jorge8:
candidate: jorge
question: fed8
answer:
other: Some generic user input
options:
- other
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 36.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 16.minutes %>
jorge9:
candidate: jorge
question: fed9
answer:
other: Brunch
options:
- Neither
- other
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 38.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 18.minutes %>
jorge10:
candidate: jorge
question: fed10
answer: ["Live long and prosper", "Who you calling Scruffy?"]
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 40.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 20.minutes %>
elsie1:
candidate: elsie
question: Cras justo odio, dapibus ac facilisis in, egestas eget quam. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit.
answer: option 3
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 22.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 22.minutes %>
elsie2:
candidate: elsie
question: fed2
answer: 'indexOf()'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 24.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 4.minutes %>
elsie3:
candidate: elsie
question: fed3
answer: {html: '<h1>Salmon</h1>', css: 'h1 {color: salmon;}', js: '', text: 'Gotta lotta GOOD things on sale, strangah.'}
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 26.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 6.minutes %>
elsie4:
candidate: elsie
question: fed4
answer: Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 28.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 28.minutes %>
elsie5:
candidate: elsie
question: fed5
answer: 'Dynamic listeners'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 30.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 30.minutes %>
elsie6:
candidate: elsie
question: fed6
answer: Integer posuere erat a ante venenatis dapibus posuere velit aliquet.
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 32.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 12.minutes %>
elsie7:
candidate: elsie
question: fed7
answer: {html: '<p>This means <strong>jQuery</strong> needs to be available in live-coder!</p>', css: "strong {font-size: 1.6em;}\n.green {color: green;}", js: '$("strong").addClass("green");'}
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 34.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 14.minutes %>
elsie8:
candidate: elsie
question: fed8
answer:
other: Some generic user input
options:
- other
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 36.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 16.minutes %>
elsie9:
candidate: elsie
question: fed9
answer:
other: Brunch
options:
- Neither
- other
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 38.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 18.minutes %>
elsie10:
candidate: elsie
question: fed10
answer: ["Live long and prosper", "Who you calling Scruffy?"]
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 40.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 20.minutes %>

View File

@ -116,6 +116,30 @@ wade: # Wade has completed AND submitted the test
reminded: false reminded: false
test_hash: BkSkpapJnkz2N test_hash: BkSkpapJnkz2N
jorge: # Jorge has completed AND submitted the test
name: Jorge Holmes
email: <%= CryptSerializer.dump 'jorge.holmes@mailinator.com' %>
experience: 0-3
project: Client/Project
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current %>
reminded: false
test_hash: iC5FdWJxcyySBmpOpU
elsie: # Elsie has completed AND submitted the test
name: Elsie Lowe
email: <%= CryptSerializer.dump 'elsie.lowe@mailinator.com' %>
experience: 0-3
project: Client/Project
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current %>
reminded: false
test_hash: rLSoizA3ATMNSCx
gustov: # Gustov is NOT for FED gustov: # Gustov is NOT for FED
name: Gustov name: Gustov
email: <%= CryptSerializer.dump 'gustov@mailinator.com' %> email: <%= CryptSerializer.dump 'gustov@mailinator.com' %>

View File

@ -66,3 +66,30 @@ reviewer2_wade:
candidate: wade candidate: wade
user: reviewer2 user: reviewer2
manager_jorge:
candidate: jorge
user: manager
reviewer_jorge:
candidate: jorge
user: reviewer
vote: 1
reviewer2_jorge:
candidate: jorge
user: reviewer2
manager_elsie:
candidate: elsie
user: manager
reviewer_elsie:
candidate: elsie
user: reviewer
reviewer2_elsie:
candidate: elsie
user: reviewer2

View File

@ -4,4 +4,14 @@ class ReviewerMailerPreview < ActionMailer::Preview
def candidate_submission def candidate_submission
ReviewerMailer.candidate_submission Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn ReviewerMailer.candidate_submission Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn
end end
def reminder
reminders = ReviewerReminder.new
reminder = reminders.reminders.first
ReviewerMailer.reminder reminder
end
def notify_manager
ReviewerMailer.notify_manager Candidate.find_by(test_hash: 'OvP0ZqGKwJ0').id # Dawn
end
end end

View File

@ -11,4 +11,23 @@ class ReviewerMailerTest < ActionMailer::TestCase
assert_equal [ENV["default_mail_from"]], mail.from assert_equal [ENV["default_mail_from"]], mail.from
assert_match candidate.test_hash, mail.body.encoded assert_match candidate.test_hash, mail.body.encoded
end end
test "reminder" do
reminders = ReviewerReminder.new
reminder = reminders.reminders.first
mail = ReviewerMailer.reminder reminder
assert_match "Review Reminder", mail.subject
assert_equal [reminder.email], mail.to
assert_equal [ENV["default_mail_from"]], mail.from
assert_match reminder.test_hash, mail.body.encoded
end
test "notify_manager" do
candidate = candidates(:richard)
mail = ReviewerMailer.notify_manager candidate.id
assert_match "Voting Complete", mail.subject
assert_equal [candidate.manager.email], mail.to
assert_equal [ENV["default_mail_from"]], mail.from
assert_match candidate.test_hash, mail.body.encoded
end
end end

View File

@ -12,10 +12,11 @@ class ReviewerVoteTest < ActiveSupport::TestCase
assert_equal 3, richard.votes.size assert_equal 3, richard.votes.size
end end
test "manager has 4 votes" do test "manager has a vote for every completed quiz" do
manager = users(:manager) manager = users(:manager)
completed_count = Candidate.where(completed: true).count
assert_equal 4, manager.votes.size assert_equal completed_count, manager.votes.size
end end
test "richard has been approved" do test "richard has been approved" do
@ -31,4 +32,22 @@ class ReviewerVoteTest < ActiveSupport::TestCase
assert stacy.declined? assert stacy.declined?
refute stacy.approved? refute stacy.approved?
end end
test "mailer is queued on last vote" do
reviewer = users(:reviewer2)
candidate = candidates(:jorge)
assert_difference("ActionMailer::Base.deliveries.size", 1) do
reviewer.cast_yea_on(candidate)
end
end
test "mailer is NOT queued on first vote" do
reviewer = users(:reviewer2)
candidate = candidates(:elsie)
assert_difference("ActionMailer::Base.deliveries.size", 0) do
reviewer.cast_yea_on(candidate)
end
end
end end

View File

@ -1,14 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ReminderTest < ActiveSupport::TestCase class CandidateReminderTest < ActiveSupport::TestCase
test "collection is created with one result" do test "collection is created with one result" do
reminders = Reminder.new reminders = CandidateReminder.new
assert_equal 1, reminders.size assert_equal 1, reminders.size
end end
test "each candidate has needed attributes" do test "each candidate has needed attributes" do
reminders = Reminder.new reminders = CandidateReminder.new
assert_instance_of String, reminders.candidates.first.name assert_instance_of String, reminders.candidates.first.name
assert_instance_of String, reminders.candidates.first.test_hash assert_instance_of String, reminders.candidates.first.test_hash
@ -16,7 +16,7 @@ class ReminderTest < ActiveSupport::TestCase
end end
test "send reminders sends email, and flags reminded" do test "send reminders sends email, and flags reminded" do
reminders = Reminder.new reminders = CandidateReminder.new
pre_reminded = Candidate.find(reminders.candidates.first.id).reminded pre_reminded = Candidate.find(reminders.candidates.first.id).reminded
assert_difference("ActionMailer::Base.deliveries.size", reminders.count) do assert_difference("ActionMailer::Base.deliveries.size", reminders.count) do

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'test_helper'
class ReviewerReminderTest < ActiveSupport::TestCase
test "collection is created with results" do
reminders = ReviewerReminder.new
assert_equal 6, reminders.size
end
test "each reminder has needed attributes" do
collection = ReviewerReminder.new
assert_instance_of String, collection.reminders.first.name
assert_instance_of String, collection.reminders.first.email
assert_instance_of String, collection.reminders.first.test_hash
end
test "send_all sends emails for each reviewer and test" do
collection = ReviewerReminder.new
assert_difference("ActionMailer::Base.deliveries.size", collection.count) do
collection.send_all
end
end
end