diff --git a/Guardfile b/Guardfile
index 7a847c9..1027353 100644
--- a/Guardfile
+++ b/Guardfile
@@ -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
diff --git a/README.md b/README.md
index 76b03dc..8d2f5e1 100644
--- a/README.md
+++ b/README.md
@@ -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/)
diff --git a/app/assets/javascripts/ajax-links.js b/app/assets/javascripts/ajax-links.js
index 1dd5fac..55ee365 100644
--- a/app/assets/javascripts/ajax-links.js
+++ b/app/assets/javascripts/ajax-links.js
@@ -1,7 +1,8 @@
-function handleAjaxResponse($el) {
+function handleAjaxResponse($el, callback) {
var $header = $('header');
$el.on("ajax:success", function(e, data){
$header.after('
' + data.message + '
');
+ callback(data);
}).on("ajax:error", function(e, xhr) {
if (xhr.status === 400){
$header.after('' + xhr.responseJSON.join('
') + '
');
@@ -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); });
+});
diff --git a/app/assets/stylesheets/molecules/_review_meta.scss b/app/assets/stylesheets/molecules/_review_meta.scss
new file mode 100644
index 0000000..31b60ee
--- /dev/null
+++ b/app/assets/stylesheets/molecules/_review_meta.scss
@@ -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; }
+ }
+}
diff --git a/app/controllers/admin/vote_controller.rb b/app/controllers/admin/vote_controller.rb
new file mode 100644
index 0000000..3a39bb2
--- /dev/null
+++ b/app/controllers/admin/vote_controller.rb
@@ -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
diff --git a/app/controllers/quiz_controller.rb b/app/controllers/quiz_controller.rb
index 4523f10..4f4e35d 100644
--- a/app/controllers/quiz_controller.rb
+++ b/app/controllers/quiz_controller.rb
@@ -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
diff --git a/app/models/candidate.rb b/app/models/candidate.rb
index a216c37..f75a738 100644
--- a/app/models/candidate.rb
+++ b/app/models/candidate.rb
@@ -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
diff --git a/app/models/reviewer_vote.rb b/app/models/reviewer_vote.rb
new file mode 100644
index 0000000..6ed8fce
--- /dev/null
+++ b/app/models/reviewer_vote.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 35ed228..b7dfbdd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/policies/reviewer_vote_policy.rb b/app/policies/reviewer_vote_policy.rb
new file mode 100644
index 0000000..7e9f1da
--- /dev/null
+++ b/app/policies/reviewer_vote_policy.rb
@@ -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
diff --git a/app/views/admin/candidate/index.html.erb b/app/views/admin/candidate/index.html.erb
index 130c99e..d4ababd 100644
--- a/app/views/admin/candidate/index.html.erb
+++ b/app/views/admin/candidate/index.html.erb
@@ -15,6 +15,7 @@
Progress |
Completed |
Reminded |
+ Review Status |
<% @candidates.each do |candidate| %>
@@ -28,8 +29,9 @@
<%= candidate.experience %> years |
<%= candidate.status %> |
- <%= candidate.completed ? "Submitted" : "" %> |
+ <%= candidate.completed ? link_to("Submitted", admin_result_path(candidate.test_hash)) : "" %> |
<%= candidate.reminded ? "Yes" : "" %> |
+ <%= candidate.review_status %> |
<% end %>
diff --git a/app/views/admin/result/_voting.html.erb b/app/views/admin/result/_voting.html.erb
new file mode 100644
index 0000000..bc04725
--- /dev/null
+++ b/app/views/admin/result/_voting.html.erb
@@ -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? %>
+
+ Votes:
+ <%= link_to admin_up_vote_path(test_hash: @candidate.test_hash), remote: true do %>
+ Yea (<%= @candidate.votes.yea.count %>)
+ <% end %>
+ <%= link_to admin_down_vote_path(test_hash: @candidate.test_hash), remote: true do %>
+ Nay (<%= @candidate.votes.nay.count %>)
+ <% end %>
+ (Your vote: <%= current_user.my_vote(@candidate) %>)
+
+<% end %>
+
+<% if current_user.acts_as_manager? %>
+
+ Manager Vetos:
+ <%= link_to admin_approve_vote_path(test_hash: @candidate.test_hash), remote: true do %>
+
+ <%= @candidate.approved? ? "Requested" : "Request Interview" %>
+
+ <% end %>
+ <%= link_to admin_decline_vote_path(test_hash: @candidate.test_hash), remote: true do %>
+
+ <%= @candidate.declined? ? "Declined" : "Decline Interview" %>
+
+ <% end %>
+
+
+<% else %>
+
+ Candidate Interview Status: <%= @candidate.review_status %>
+<% end %>
diff --git a/app/views/admin/result/view.html.erb b/app/views/admin/result/view.html.erb
index 73a0f72..23dfa7b 100644
--- a/app/views/admin/result/view.html.erb
+++ b/app/views/admin/result/view.html.erb
@@ -1,10 +1,15 @@
Quiz Review
-
- Test ID: <%= @candidate.test_hash %>
- Years of Experience: <%= @candidate.experience %>
- Recruiter Email: <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %>
-
+
+
<% @quiz.each do |question| %>
<%= form_for(:answer, url: '#never-post', html:{id: 'summary-form'}) do |form| %>
diff --git a/app/views/admin/user/_form.html.erb b/app/views/admin/user/_form.html.erb
index c3b5266..de44d21 100644
--- a/app/views/admin/user/_form.html.erb
+++ b/app/views/admin/user/_form.html.erb
@@ -15,12 +15,20 @@
<%= form.select :role, admin_role_options(user.role), include_blank: false %>
- <%= form.collection_check_boxes(:quiz_ids, Quiz.all, :id, :name, {}, {class: 'checkbox'}) do | quiz | %>
-
- <%= quiz.check_box( checked: user.quizzes.include?(quiz.object)) %>
- <%= quiz.label %>
-
- <% end %>
+
<%= form.submit %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index c5356dd..691f4c5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
#########################################################################################
diff --git a/db/migrate/20161118023249_candidate_review_system.rb b/db/migrate/20161118023249_candidate_review_system.rb
new file mode 100644
index 0000000..0c4c8b0
--- /dev/null
+++ b/db/migrate/20161118023249_candidate_review_system.rb
@@ -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
diff --git a/db/migrate/20161120175737_init_reviewer_votes.rb b/db/migrate/20161120175737_init_reviewer_votes.rb
new file mode 100644
index 0000000..706c8af
--- /dev/null
+++ b/db/migrate/20161120175737_init_reviewer_votes.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 41a294d..2126b30 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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"
diff --git a/erd.pdf b/erd.pdf
index 1d050f2..35d725d 100644
Binary files a/erd.pdf and b/erd.pdf differ
diff --git a/test/controllers/admin/result_controller_test.rb b/test/controllers/admin/result_controller_test.rb
index 699517d..eb9ac57 100644
--- a/test/controllers/admin/result_controller_test.rb
+++ b/test/controllers/admin/result_controller_test.rb
@@ -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
diff --git a/test/controllers/admin/user_controller_test.rb b/test/controllers/admin/user_controller_test.rb
index 8498ffc..be8c9a9 100644
--- a/test/controllers/admin/user_controller_test.rb
+++ b/test/controllers/admin/user_controller_test.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: true
+# frozen_string_literal: true()
require 'test_helper'
module Admin
diff --git a/test/controllers/admin/vote_controller_test.rb b/test/controllers/admin/vote_controller_test.rb
new file mode 100644
index 0000000..f05e4e2
--- /dev/null
+++ b/test/controllers/admin/vote_controller_test.rb
@@ -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
diff --git a/test/fixtures/answers.yml b/test/fixtures/answers.yml
index 9c3455a..de70e0c 100644
--- a/test/fixtures/answers.yml
+++ b/test/fixtures/answers.yml
@@ -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: 'Salmon
', 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: 'This means jQuery needs to be available in live-coder!
', 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: 'Salmon
', 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: 'This means jQuery needs to be available in live-coder!
', 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: 'Salmon
', 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: 'This means jQuery needs to be available in live-coder!
', 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 %>
+
diff --git a/test/fixtures/candidates.yml b/test/fixtures/candidates.yml
index a3c07f7..4def21b 100644
--- a/test/fixtures/candidates.yml
+++ b/test/fixtures/candidates.yml
@@ -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
+
diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml
index 97c7d00..4380750 100644
--- a/test/fixtures/questions.yml
+++ b/test/fixtures/questions.yml
@@ -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:
diff --git a/test/fixtures/reviewer_to_quizzes.yml b/test/fixtures/reviewer_to_quizzes.yml
index 4e8812f..cb91027 100644
--- a/test/fixtures/reviewer_to_quizzes.yml
+++ b/test/fixtures/reviewer_to_quizzes.yml
@@ -7,3 +7,7 @@ one:
two:
user: reviewer2
quiz: fed
+
+three:
+ user: manager
+ quiz: fed
diff --git a/test/fixtures/reviewer_votes.yml b/test/fixtures/reviewer_votes.yml
new file mode 100644
index 0000000..2669604
--- /dev/null
+++ b/test/fixtures/reviewer_votes.yml
@@ -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
+
diff --git a/test/models/candidate_test.rb b/test/models/candidate_test.rb
index 7cb0f5d..393c7e8 100644
--- a/test/models/candidate_test.rb
+++ b/test/models/candidate_test.rb
@@ -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
diff --git a/test/models/reviewer_vote_test.rb b/test/models/reviewer_vote_test.rb
new file mode 100644
index 0000000..48db0fe
--- /dev/null
+++ b/test/models/reviewer_vote_test.rb
@@ -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
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index bc372e4..d7d9327 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -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
diff --git a/test/policies/reviewer_vote_policy_test.rb b/test/policies/reviewer_vote_policy_test.rb
new file mode 100644
index 0000000..1824293
--- /dev/null
+++ b/test/policies/reviewer_vote_policy_test.rb
@@ -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
diff --git a/test/test_helpers/README.txt b/test/test_helpers/README.md
similarity index 100%
rename from test/test_helpers/README.txt
rename to test/test_helpers/README.md