Merge branch 'feature/interview-voting' into develop

This commit is contained in:
Mark Moser 2016-11-20 13:10:33 -06:00
commit 85bb4d4368
32 changed files with 912 additions and 23 deletions

@ -75,7 +75,7 @@ guard :shell, all_on_start: true do
# TODO: Annoyingly, all files are linted twice on start/full runs. Why?
watch %r{app/assets/javascripts/*/.*} do |file|
system %(echo "ESLint:\033[32m #{file[0]}\033[0m")
system %(eslint #{file[0]})
system %(./node_modules/eslint/bin/eslint.js #{file[0]})
end
end

@ -77,10 +77,14 @@ There are some convenience scripts included to make starting the container and r
* `./start-server.sh`
- starts up just rails server for viewing application
## Deploying a new version
* ssh into server
* cd into app root
* run deploy.sh
## TODOs and notes
* Question attachment path: http://dev.perficientxd.com/skill_assets/
* Question attachment path: https://dev.perficientdigital.com/skills-app-images/
* clean code
* [Confident Ruby](http://www.confidentruby.com/)
* [POODR](http://www.poodr.com/)

@ -1,7 +1,8 @@
function handleAjaxResponse($el) {
function handleAjaxResponse($el, callback) {
var $header = $('header');
$el.on("ajax:success", function(e, data){
$header.after('<div class="success">' + data.message + '</div>');
callback(data);
}).on("ajax:error", function(e, xhr) {
if (xhr.status === 400){
$header.after('<div class="error">' + xhr.responseJSON.join('<br>') + '</div>');
@ -11,6 +12,25 @@ function handleAjaxResponse($el) {
});
}
function updateVotes(data){
$("[data-id=up-votes]").html(data.upCount);
$("[data-id=down-votes]").html(data.downCount);
$("[data-id=my-vote]").html(data.myVote);
}
function updateVeto(data){
$("[data-id=interview-request]").html(data.requestCopy);
$("[data-id=interview-decline]").html(data.declineCopy);
}
$(document).ready(function() {
$('[data-id=ajax-action]').each(function(){ handleAjaxResponse($(this)); });
});
$(document).ready(function() {
$('[data-id=vote-count]').each(function(){ handleAjaxResponse($(this), updateVotes); });
});
$(document).ready(function() {
$('[data-id=veto-status]').each(function(){ handleAjaxResponse($(this), updateVeto); });
});

@ -0,0 +1,12 @@
.review_meta {
@media screen and (min-width: 768px) {
display: flex;
& > div { flex: 1 1 auto; }
}
.review_meta__votes,
.review_meta__vetos {
a { padding: 5px; }
}
}

@ -0,0 +1,58 @@
# frozen_string_literal: true
module Admin
class VoteController < AdminController
def up
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.cast_yea_on(@candidate)
results = {
message: "Vote Counted",
upCount: @candidate.votes.yea.count,
downCount: @candidate.votes.nay.count,
myVote: "yea"
}
render json: results.to_json
end
def down
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.cast_nay_on(@candidate)
results = {
message: "Vote Counted",
upCount: @candidate.votes.yea.count,
downCount: @candidate.votes.nay.count,
myVote: "nay"
}
render json: results.to_json
end
def approve
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.approve_candidate(@candidate)
results = {
message: "Interview requested!",
requestCopy: "Requested",
declineCopy: "Decline Interview"
}
render json: results.to_json
end
def decline
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.decline_candidate(@candidate)
results = {
message: "Interview declined.",
requestCopy: "Request Interview",
declineCopy: "Declined"
}
render json: results.to_json
end
end
end

@ -36,6 +36,7 @@ class QuizController < ApplicationController
def complete_and_email
if current_candidate.update_attributes(completed: true)
current_candidate.build_reviews
CandidateMailer.submitted(current_candidate).deliver_later
RecruiterMailer.candidate_submitted(current_candidate).deliver_later
ReviewerMailer.candidate_submission(current_candidate).deliver_later

@ -4,6 +4,8 @@ class Candidate < ApplicationRecord
has_many :questions, -> { order("sort") }, through: :quiz
has_many :answers
belongs_to :recruiter, class_name: "User"
has_many :votes, class_name: "ReviewerVote"
has_many :reviewers, through: :quiz
serialize :email, CryptSerializer
@ -15,6 +17,18 @@ class Candidate < ApplicationRecord
validates :email, uniqueness: true, presence: true, email_format: true
validates :test_hash, uniqueness: true, presence: true
enum review_status: {
pending: 0,
approved: 1,
declined: 2
}
def build_reviews
reviewers.each do |reviewer|
votes.find_or_create_by(user_id: reviewer.id)
end
end
def submitted_answers
answers.where(submitted: true)
end

@ -0,0 +1,18 @@
# frozen_string_literal: true
class ReviewerVote < ApplicationRecord
belongs_to :candidate
belongs_to :user
validates :user_id, uniqueness: { scope: :candidate_id }
enum vote: {
undecided: 0,
yea: 1,
nay: 2
}
enum veto: {
approved: 1,
rejected: 2
}
end

@ -4,6 +4,9 @@ class User < ApplicationRecord
has_many :candidates, foreign_key: :recruiter_id
has_many :reviewer_to_quizzes
has_many :quizzes, through: :reviewer_to_quizzes
has_many :votes, class_name: 'ReviewerVote'
has_many :reviewees, through: :quizzes, source: :candidates
validates :email, presence: true, uniqueness: true
validates :name, presence: true
@ -15,6 +18,42 @@ class User < ApplicationRecord
save
end
# Voting
def cast_yea_on candidate
vote = votes.find_by(candidate_id: candidate.to_i)
vote.vote = :yea
vote.save
end
def cast_nay_on candidate
vote = votes.find_by(candidate_id: candidate.to_i)
vote.vote = :nay
vote.save
end
def approve_candidate candidate
candidate = Candidate.find(candidate.to_i)
vote = votes.find_by(candidate_id: candidate.to_i)
vote.veto = :approved
candidate.update_attribute(:review_status, :approved) if vote.save
end
def decline_candidate candidate
candidate = Candidate.find(candidate.to_i)
vote = votes.find_by(candidate_id: candidate.to_i)
vote.veto = :rejected
candidate.update_attribute(:review_status, :declined) if vote.save
end
def my_vote candidate
candidate = Candidate.find(candidate.to_i)
my_vote = votes.find_by(candidate_id: candidate.id)
my_vote.vote unless my_vote.nil?
end
# Roles
def admin?
'admin' == role
@ -45,7 +84,7 @@ class User < ApplicationRecord
end
def acts_as_reviewer?
%w(admin reviewer).include? role
%w(admin manager reviewer).include? role
end
private

@ -0,0 +1,45 @@
# frozen_string_literal: true
class ReviewerVotePolicy < ApplicationPolicy
# Voting Policy
#
# Only Reviewers, Managers, and Admins, can cast a vote on a quiz result
#
# Reviewers can vote any quiz they are linked to
# Only Managers, and Admins, can veto a quiz result
def up?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_reviewer?
end
def down?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_reviewer?
end
def approve?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_manager?
end
def decline?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_manager?
end
class Scope < Scope
def resolve
return ReviewerVote.none if user.recruiter?
if user.reviewer?
scope.where(user_id: user.id)
else
scope
end
end
end
end

@ -15,6 +15,7 @@
<th>Progress</th>
<th>Completed</th>
<th>Reminded</th>
<th>Review Status</th>
</tr>
<% @candidates.each do |candidate| %>
@ -28,8 +29,9 @@
</td>
<td><%= candidate.experience %> years</td>
<td><%= candidate.status %></td>
<td><%= candidate.completed ? "Submitted" : "" %></td>
<td><%= candidate.completed ? link_to("Submitted", admin_result_path(candidate.test_hash)) : "" %></td>
<td><%= candidate.reminded ? "Yes" : "" %></td>
<td><%= candidate.review_status %></td>
</tr>
<% end %>
</table>

@ -0,0 +1,34 @@
<% # TODO: This needs to be extracted into a decorator, or something. It is only a quick hack solution. %>
<% if current_user.acts_as_reviewer? %>
<div class="review_meta__votes" data-id="vote-count">
<strong>Votes: </strong>
<%= link_to admin_up_vote_path(test_hash: @candidate.test_hash), remote: true do %>
Yea (<span data-id="up-votes"><%= @candidate.votes.yea.count %></span>)
<% end %>
<%= link_to admin_down_vote_path(test_hash: @candidate.test_hash), remote: true do %>
Nay (<span data-id="down-votes"><%= @candidate.votes.nay.count %></span>)
<% end %>
<small>(Your vote: <span data-id="my-vote"><%= current_user.my_vote(@candidate) %></span>)</small>
</div>
<% end %>
<% if current_user.acts_as_manager? %>
<div class="review_meta__vetos" data-id="veto-status">
<strong>Manager Vetos: </strong>
<%= link_to admin_approve_vote_path(test_hash: @candidate.test_hash), remote: true do %>
<span data-id="interview-request">
<%= @candidate.approved? ? "Requested" : "Request Interview" %>
</span>
<% end %>
<%= link_to admin_decline_vote_path(test_hash: @candidate.test_hash), remote: true do %>
<span data-id="interview-decline">
<%= @candidate.declined? ? "Declined" : "Decline Interview" %>
</span>
<% end %>
</div>
<% else %>
<strong>Candidate Interview Status: </strong><%= @candidate.review_status %>
<% end %>

@ -1,10 +1,15 @@
<main class="summary_tpl">
<h2 class="prft-heading">Quiz Review</h2>
<p>
<strong>Test ID:</strong> <%= @candidate.test_hash %><br />
<strong>Years of Experience:</strong> <%= @candidate.experience %><br />
<strong>Recruiter Email:</strong> <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %><br />
</p>
<div class="review_meta">
<div>
<strong>Test ID:</strong> <%= @candidate.test_hash %><br />
<strong>Years of Experience:</strong> <%= @candidate.experience %><br />
<strong>Recruiter Email:</strong> <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %><br />
</div>
<div><%= render partial: 'voting' %></div>
</div>
<% @quiz.each do |question| %>
<%= form_for(:answer, url: '#never-post', html:{id: 'summary-form'}) do |form| %>

@ -15,12 +15,20 @@
<%= form.select :role, admin_role_options(user.role), include_blank: false %>
</div>
<%= form.collection_check_boxes(:quiz_ids, Quiz.all, :id, :name, {}, {class: 'checkbox'}) do | quiz | %>
<div class="form-group-multiples">
<%= quiz.check_box( checked: user.quizzes.include?(quiz.object)) %>
<%= quiz.label %>
</div>
<% end %>
<div class="form-group">
<div><strong>Quiz Review List</strong></div>
<p>
Quizzes this user should be reviewing the results of.<br />
Admins and Recruiters should not have any checked, unless they are expected
to participate in the technical review for that quiz.
</p>
<%= form.collection_check_boxes(:quiz_ids, Quiz.all, :id, :name, {}, {class: 'checkbox'}) do | quiz | %>
<div class="form-group-multiples">
<%= quiz.check_box( checked: user.quizzes.include?(quiz.object)) %>
<%= quiz.label %>
</div>
<% end %>
</div>
<%= form.submit %>
<% end %>

@ -50,6 +50,11 @@ Rails.application.routes.draw do
get "/admin/results", to: "admin/result#index", as: :admin_results
get "/admin/result/:test_hash", to: "admin/result#view", as: :admin_result
get "admin/vote/:test_hash/up", to: "admin/vote#up", as: :admin_up_vote
get "admin/vote/:test_hash/down", to: "admin/vote#down", as: :admin_down_vote
get "admin/vote/:test_hash/approve", to: "admin/vote#approve", as: :admin_approve_vote
get "admin/vote/:test_hash/decline", to: "admin/vote#decline", as: :admin_decline_vote
get "/admin", to: "admin/dashboard#show", as: :admin
#########################################################################################

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CandidateReviewSystem < ActiveRecord::Migration[5.0]
def change
create_table :reviewer_votes do |t|
t.integer :candidate_id
t.integer :user_id
t.integer :vote, default: 0, null: false
t.integer :veto, default: 0, null: false
t.datetime :last_reminded
t.boolean :locked, default: false, null: false
t.timestamps
end
add_index :reviewer_votes, [:candidate_id, :user_id], unique: true
add_column :candidates, :review_status, :integer, default: 0, null: false
end
end

@ -0,0 +1,6 @@
# frozen_string_literal: true
class InitReviewerVotes < ActiveRecord::Migration[5.0]
def up
Candidate.where(completed: true).each(&:build_reviews)
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160915164450) do
ActiveRecord::Schema.define(version: 20161120175737) do
create_table "answers", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "candidate_id"
@ -33,9 +33,10 @@ ActiveRecord::Schema.define(version: 20160915164450) do
t.integer "recruiter_id"
t.boolean "completed"
t.boolean "reminded"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "quiz_id"
t.integer "review_status", default: 0, null: false
t.index ["quiz_id"], name: "index_candidates_on_quiz_id", using: :btree
t.index ["recruiter_id"], name: "index_candidates_on_recruiter_id", using: :btree
t.index ["test_hash"], name: "index_candidates_on_test_hash", unique: true, using: :btree
@ -73,6 +74,18 @@ ActiveRecord::Schema.define(version: 20160915164450) do
t.index ["quiz_id"], name: "index_reviewer_to_quizzes_on_quiz_id", using: :btree
end
create_table "reviewer_votes", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "candidate_id"
t.integer "user_id"
t.integer "vote", default: 0, null: false
t.integer "veto", default: 0, null: false
t.datetime "last_reminded"
t.boolean "locked", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["candidate_id", "user_id"], name: "index_reviewer_votes_on_candidate_id_and_user_id", unique: true, using: :btree
end
create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "name"
t.string "email"

BIN
erd.pdf

Binary file not shown.

@ -19,5 +19,19 @@ module Admin
assert assigns(:quiz), "@quiz not present"
assert assigns(:status), "@status not present"
end
test "reviewer can view result for henry" do
auth_reviewer
get admin_result_url(candidates(:henry).test_hash)
assert_response :success
end
test "recruiter can view result for henry" do
auth_user users(:recruiter)
get admin_result_url(candidates(:henry).test_hash)
assert_response :success
end
end
end

@ -1,4 +1,4 @@
# frozen_string_literal: true
# frozen_string_literal: true()
require 'test_helper'
module Admin

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'test_helper'
module Admin
class VoteControllerTest < ActionDispatch::IntegrationTest
test "reviewer can up vote henry" do
auth_user users(:reviewer)
henry = candidates(:henry)
assert_difference("Candidate.find(#{henry.id}).votes.yea.count", 1) do
get admin_up_vote_url(henry.test_hash)
end
assert_response :success
end
test "reviewer can down vote henry" do
auth_user users(:reviewer)
henry = candidates(:henry)
assert_difference("Candidate.find(#{henry.id}).votes.nay.count", 1) do
get admin_down_vote_url(henry.test_hash)
end
assert_response :success
end
test "reviewer can change vote on henry" do
auth_user users(:reviewer)
henry = candidates(:henry)
get admin_up_vote_url(henry.test_hash)
assert_difference("Candidate.find(#{henry.id}).votes.yea.count", -1) do
assert_difference("Candidate.find(#{henry.id}).votes.nay.count", 1) do
get admin_down_vote_url(henry.test_hash)
end
end
assert_response :success
end
test "manager can approve henry" do
auth_user users(:manager)
henry = candidates(:henry)
get admin_approve_vote_url(henry.test_hash)
assert_equal 1, henry.votes.approved.count
assert_equal 'approved', Candidate.find(henry.to_i).review_status
assert_response :success
end
test "manager can decline henry" do
auth_user users(:manager)
henry = candidates(:henry)
get admin_decline_vote_url(henry.test_hash)
assert_equal 1, henry.votes.rejected.count
assert_equal 'declined', Candidate.find(henry.to_i).review_status
assert_response :success
end
end
end

@ -404,3 +404,295 @@ juan10:
created_at: <%= DateTime.now() - 38.hours - 40.minutes %>
updated_at: <%= DateTime.now() - 38.hours - 20.minutes %>
stacy1:
candidate: stacy
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 %>
stacy2:
candidate: stacy
question: fed2
answer: 'indexOf()'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 24.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 4.minutes %>
stacy3:
candidate: stacy
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 %>
stacy4:
candidate: stacy
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 %>
stacy5:
candidate: stacy
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 %>
stacy6:
candidate: stacy
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 %>
stacy7:
candidate: stacy
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 %>
stacy8:
candidate: stacy
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 %>
stacy9:
candidate: stacy
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 %>
stacy10:
candidate: stacy
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 %>
henry1:
candidate: henry
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 %>
henry2:
candidate: henry
question: fed2
answer: 'indexOf()'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 24.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 4.minutes %>
henry3:
candidate: henry
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 %>
henry4:
candidate: henry
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 %>
henry5:
candidate: henry
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 %>
henry6:
candidate: henry
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 %>
henry7:
candidate: henry
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 %>
henry8:
candidate: henry
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 %>
henry9:
candidate: henry
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 %>
henry10:
candidate: henry
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 %>
wade1:
candidate: wade
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 %>
wade2:
candidate: wade
question: fed2
answer: 'indexOf()'
saved: 0
submitted: true
created_at: <%= DateTime.now() - 36.hours - 24.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 4.minutes %>
wade3:
candidate: wade
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 %>
wade4:
candidate: wade
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 %>
wade5:
candidate: wade
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 %>
wade6:
candidate: wade
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 %>
wade7:
candidate: wade
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 %>
wade8:
candidate: wade
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 %>
wade9:
candidate: wade
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 %>
wade10:
candidate: wade
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 %>

@ -59,6 +59,7 @@ richard: # Richard has completed AND submitted the test
completed: true
reminded: false
test_hash: 6NjnourLE6Y
review_status: 1
juan: # Juan has chosen "finish later" for live coders
name: Juan Campbell
@ -69,3 +70,45 @@ juan: # Juan has chosen "finish later" for live coders
completed: false
reminded: true
test_hash: qKQo0l4dyol
stacy: # Stacy has completed AND submitted the test
name: Stacy Scott
email: <%= CryptSerializer.dump 'stacy.scott@mailinator.com' %>
experience: 7-9
recruiter: recruiter
quiz: fed
completed: true
reminded: false
test_hash: s6oFExZliYYFx
review_status: 2
henry: # Henry has completed AND submitted the test
name: Henry Butler
email: <%= CryptSerializer.dump 'henry.butler@mailinator.com' %>
experience: 4-6
recruiter: recruiter
quiz: fed
completed: true
reminded: false
test_hash: egPomAuVDeCEp
wade: # Wade has completed AND submitted the test
name: Wade Armstrong
email: <%= CryptSerializer.dump 'wade.armstrong@mailinator.com' %>
experience: 0-3
recruiter: recruiter
quiz: fed
completed: true
reminded: false
test_hash: BkSkpapJnkz2N
gustov: # Gustov is NOT for FED
name: Gustov
email: <%= CryptSerializer.dump 'gustov@mailinator.com' %>
experience: 0-3
recruiter: recruiter
quiz: admin
completed: false
reminded: false
test_hash: kp6tfghjyapJnkz2N

@ -55,7 +55,7 @@ fed5:
fed6:
quiz: fed
question: Comment on how realistic the following image is.
attachment: "http://dev.perficientxd.com/skill_assets/commets_css.jpg"
attachment: "https://dev.perficientdigital.com/skills-app-images/commets_css.jpg"
category: CSS
input_type: text
input_options:

@ -7,3 +7,7 @@ one:
two:
user: reviewer2
quiz: fed
three:
user: manager
quiz: fed

68
test/fixtures/reviewer_votes.yml vendored Normal file

@ -0,0 +1,68 @@
gustov:
candidate: gustov
user_id: 12341234
manager_richard:
candidate: richard
user: manager
vote: 1
reviewer_richard:
candidate: richard
user: reviewer
vote: 1
reviewer2_richard:
candidate: richard
user: reviewer2
vote: 1
manager_stacy:
candidate: stacy
user: manager
vote: 2
reviewer_stacy:
candidate: stacy
user: reviewer
vote: 2
reviewer2_stacy:
candidate: stacy
user: reviewer2
vote: 2
manager_henry:
candidate: henry
user: manager
vote: 0
veto: 2
reviewer_henry:
candidate: henry
user: reviewer
reviewer2_henry:
candidate: henry
user: reviewer2
manager_wade:
candidate: wade
user: manager
vote: 2
veto: 2
reviewer_wade:
candidate: wade
user: reviewer
vote: 2
reviewer2_wade:
candidate: wade
user: reviewer2

@ -24,4 +24,11 @@ class CandidateTest < ActiveSupport::TestCase
refute_equal email, enc_email
end
test "can build reviewer records" do
candidate = candidates(:dawn)
candidate.build_reviews
assert_equal 3, candidate.votes.count
end
end

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'test_helper'
class ReviewerVoteTest < ActiveSupport::TestCase
test "the truth" do
assert ReviewerVoteTest
end
test "richard has 3 votes" do
richard = candidates(:richard)
assert_equal 3, richard.votes.size
end
test "manager has 4 votes" do
manager = users(:manager)
assert_equal 4, manager.votes.size
end
test "richard has been approved" do
richard = candidates(:richard)
assert richard.approved?
refute richard.declined?
end
test "stacy has been declined" do
stacy = candidates(:stacy)
assert stacy.declined?
refute stacy.approved?
end
end

@ -20,14 +20,14 @@ class UserTest < ActiveSupport::TestCase
refute user.reviewer?
end
test 'manager should act as manager' do
test 'manager should act as manager and reviewer' do
user = users(:manager)
assert user.acts_as_manager?
assert user.acts_as_reviewer?
refute user.acts_as_admin?
refute user.acts_as_recruiter?
refute user.acts_as_reviewer?
end
test 'manager should only be manager' do

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'test_helper'
class ReviewerVotePolicyTest < PolicyAssertions::Test
test 'should require current_user' do
assert_raise Pundit::NotAuthorizedError do
ReviewerVotePolicy.new(nil, ReviewerVote.first).view?
end
end
test 'should allow admin to scope' do
scope = ReviewerVotePolicy::Scope.new(users(:admin), ReviewerVote).resolve
assert_equal ReviewerVote.count, scope.count
end
test 'should allow manager to scope' do
scope = ReviewerVotePolicy::Scope.new(users(:manager), ReviewerVote).resolve
assert_equal ReviewerVote.count, scope.count
end
test 'should allow reviewer to scope' do
scope = ReviewerVotePolicy::Scope.new(users(:reviewer), ReviewerVote).resolve
assert_equal users(:reviewer).votes.count, scope.count
end
test 'should NOT allow recruiter to scope' do
scope = ReviewerVotePolicy::Scope.new(users(:recruiter), ReviewerVote).resolve
assert_equal 0, scope.count
end
def test_up
assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:reviewer), reviewer_votes(:reviewer_richard)
assert_permit users(:admin), reviewer_votes(:manager_henry)
refute_permit users(:recruiter), reviewer_votes(:manager_henry)
refute_permit users(:reviewer), reviewer_votes(:gustov)
refute_permit users(:manager), reviewer_votes(:gustov)
end
def test_down
assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:reviewer), reviewer_votes(:reviewer_richard)
assert_permit users(:admin), reviewer_votes(:manager_henry)
refute_permit users(:recruiter), reviewer_votes(:manager_henry)
refute_permit users(:reviewer), reviewer_votes(:gustov)
refute_permit users(:manager), reviewer_votes(:gustov)
end
def approve
assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:admin), reviewer_votes(:manager_henry)
refute_permit users(:recruiter), reviewer_votes(:manager_henry)
refute_permit users(:reviewer), reviewer_votes(:reviewer_richard)
end
def decline
assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:admin), reviewer_votes(:manager_henry)
refute_permit users(:recruiter), reviewer_votes(:manager_henry)
refute_permit users(:reviewer), reviewer_votes(:reviewer_richard)
end
end