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 %>
-

+ +
+
+ Test ID: <%= @candidate.test_hash %>
+ Years of Experience: <%= @candidate.experience %>
+ Recruiter Email: <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %>
+
+ +
<%= render partial: 'voting' %>
+
<% @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 %> +
+
Quiz Review List
+

+ Quizzes this user should be reviewing the results of.
+ Admins and Recruiters should not have any checked, unless they are expected + to participate in the technical review for that quiz. +

+ <%= 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