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/_accordions.scss b/app/assets/stylesheets/molecules/_accordions.scss new file mode 100644 index 0000000..728203d --- /dev/null +++ b/app/assets/stylesheets/molecules/_accordions.scss @@ -0,0 +1,57 @@ +.accordion { + margin-bottom: 0.75em; + + [type="checkbox"]:checked + label, + [type="checkbox"]:checked ~ label:after, + [type="checkbox"]:not(:checked) + label, + [type="checkbox"]:not(:checked) ~ label:after, + [type="radio"]:checked + label, + [type="radio"]:checked ~ label:after, + [type="radio"]:not(:checked) + label, + [type="radio"]:not(:checked) ~ label:after { + content: ""; + border: 0; + background-color: transparent; + } + + [type="checkbox"]:hover:not(:disabled) + label:before, + [type="radio"]:hover:not(:disabled) + label:before, + [type="radio"]:not(:checked) ~ label:before, + [type="checkbox"]:not(:checked) ~ label:before, + [type="radio"]:checked ~ label:before, + [type="checkbox"]:checked ~ label:before { + background-color: transparent; + border: 0; + content: "+"; + font-size: 1.3em; + line-height: 1; + } + + [type="checkbox"]:hover:checked + label:before, + [type="radio"]:hover:checked + label:before, + [type="radio"]:checked ~ label:before, + [type="checkbox"]:checked ~ label:before { + background-color: transparent; + content: "-"; + } + + [type="radio"]:not(:checked) + label, + [type="checkbox"]:not(:checked) + label, + [type="radio"]:checked + label, + [type="checkbox"]:checked + label { + padding-left: 20px; + } + + .accordion__copy { + display: none; + margin-top: 0.75em; + } + + .accordion__toggle:checked ~ .accordion__copy { + background-color: $gray-lighter; + display: block; + padding: 2.5em 1.25em 1em 1.75em; + margin: -2.25em 0 2em -0.5em; + } + +} diff --git a/app/assets/stylesheets/molecules/_admin_review.scss b/app/assets/stylesheets/molecules/_admin_review.scss new file mode 100644 index 0000000..586367d --- /dev/null +++ b/app/assets/stylesheets/molecules/_admin_review.scss @@ -0,0 +1,31 @@ +.admin-review { + counter-reset: question; + + form { + margin-left: 2.3em; + position: relative; + + &::before { + content: counter(question) ") "; + counter-increment: question; + font-size: 1.25em; + left: -1.8em; + position: absolute; + top: 0.4em; + } + } + +} + +.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/assets/stylesheets/molecules/_livecoder.scss b/app/assets/stylesheets/molecules/_livecoder.scss index 5ca90d2..0a110f1 100644 --- a/app/assets/stylesheets/molecules/_livecoder.scss +++ b/app/assets/stylesheets/molecules/_livecoder.scss @@ -22,6 +22,10 @@ display: block; } +.code-results { + margin-bottom: -0.65em; +} + .results { border: 1px solid $secondary-color; clear: both; @@ -31,8 +35,15 @@ background-color: #fff; } -fieldset:disabled .results { - border-color: #bbb; +fieldset:disabled { + .results { + border-color: #bbb; + } + + .code-results, + .code-input label { + display: block; + } } iframe { diff --git a/app/controllers/admin/question_controller.rb b/app/controllers/admin/question_controller.rb index 1dfbb84..d68160b 100644 --- a/app/controllers/admin/question_controller.rb +++ b/app/controllers/admin/question_controller.rb @@ -62,7 +62,7 @@ module Admin def question_params params.require(:question).permit( - :quiz_id, :question, :category, :input_type, :sort, :active, :input_options, + :quiz_id, :question, :category, :attachment, :input_type, :sort, :active, :input_options, multi_choice: [], live_code: [:later, :html, :css, :js, :text] ) end 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/mailers/reviewer_mailer.rb b/app/mailers/reviewer_mailer.rb index 0f06024..9d57d89 100644 --- a/app/mailers/reviewer_mailer.rb +++ b/app/mailers/reviewer_mailer.rb @@ -4,6 +4,6 @@ class ReviewerMailer < ApplicationMailer @candidate = candidate recipients = candidate.quiz.reviewers.map(&:email) - mail to: recipients, subject: "Skills Assessment Results" + mail to: recipients, subject: "Skills Assessment Results - #{@candidate.test_hash}" end end 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/auth/login.html.erb b/app/views/admin/auth/login.html.erb index e7bde46..62f0e5a 100644 --- a/app/views/admin/auth/login.html.erb +++ b/app/views/admin/auth/login.html.erb @@ -1,11 +1,11 @@ <% content_for :main_class, "intro_tpl" + content_for :title, "Skills Assessment Admin" %>

Admin Login

<%= form_for :auth, url: admin_login_path do |form| %> - <% if flash[:error].present? %>
Need a <%= link_to "password reset", admin_reset_request_path %>? diff --git a/app/views/admin/auth/reset.html.erb b/app/views/admin/auth/reset.html.erb index 2b1aee8..ddc3e80 100644 --- a/app/views/admin/auth/reset.html.erb +++ b/app/views/admin/auth/reset.html.erb @@ -1,5 +1,6 @@ <% content_for :main_class, "intro_tpl" + content_for :title, "Skills Assessment Admin" %>

Password Reset

diff --git a/app/views/admin/auth/reset_request.html.erb b/app/views/admin/auth/reset_request.html.erb index bb23fb8..6618a19 100644 --- a/app/views/admin/auth/reset_request.html.erb +++ b/app/views/admin/auth/reset_request.html.erb @@ -1,5 +1,6 @@ <% content_for :main_class, "intro_tpl" + content_for :title, "Skills Assessment Admin" %>

Password Reset

diff --git a/app/views/admin/candidate/edit.html.erb b/app/views/admin/candidate/edit.html.erb index 5de8973..10899c3 100644 --- a/app/views/admin/candidate/edit.html.erb +++ b/app/views/admin/candidate/edit.html.erb @@ -1,3 +1,7 @@ +<% + content_for :title, "Edit Candidate - Skills Assessment Admin" +%> +

Edit: <%= @candidate.name %>

Test ID: <%= @candidate.test_hash %>

diff --git a/app/views/admin/candidate/index.html.erb b/app/views/admin/candidate/index.html.erb index 130c99e..67f464b 100644 --- a/app/views/admin/candidate/index.html.erb +++ b/app/views/admin/candidate/index.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Candidates" + content_for :title, "Candidates - Skills Assessment Admin" %>
<%= link_to(admin_new_candidate_path, { class: 'secondary-btn' }) do %> @@ -15,6 +16,7 @@ Progress Completed Reminded + Interview Request <% @candidates.each do |candidate| %> @@ -28,8 +30,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 unless candidate.pending? %> <% end %> diff --git a/app/views/admin/candidate/new.html.erb b/app/views/admin/candidate/new.html.erb index c529467..db1ebc3 100644 --- a/app/views/admin/candidate/new.html.erb +++ b/app/views/admin/candidate/new.html.erb @@ -1,3 +1,7 @@ +<% + content_for :title, "New Candidate - Skills Assessment Admin" +%> +

New Candidate

diff --git a/app/views/admin/profile/edit.html.erb b/app/views/admin/profile/edit.html.erb index d1014d2..04eac6b 100644 --- a/app/views/admin/profile/edit.html.erb +++ b/app/views/admin/profile/edit.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Edit: #{@user.name}" + content_for :title, "Profile - Skills Assessment Admin" %> <%= render partial: 'shared/form_model_errors', locals: {obj: @user} %> diff --git a/app/views/admin/profile/view.html.erb b/app/views/admin/profile/view.html.erb index 1c1af5f..6cc0db1 100644 --- a/app/views/admin/profile/view.html.erb +++ b/app/views/admin/profile/view.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Profile" + content_for :title, "Profile - Skills Assessment Admin" %>

Name: <%= current_user.name %>

diff --git a/app/views/admin/question/edit.html.erb b/app/views/admin/question/edit.html.erb index 6428daa..67ca722 100644 --- a/app/views/admin/question/edit.html.erb +++ b/app/views/admin/question/edit.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Questions" + content_for :title, "Edit Question - Skills Assessment Admin" %>

<%= @question.quiz.name %>

diff --git a/app/views/admin/question/index.html.erb b/app/views/admin/question/index.html.erb index f6f17b0..4f78c39 100644 --- a/app/views/admin/question/index.html.erb +++ b/app/views/admin/question/index.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Questions" + content_for :title, "Questions - Skills Assessment Admin" %> <% quizzes = @questions.group_by{ |q| q.quiz.name } %> diff --git a/app/views/admin/question/new.html.erb b/app/views/admin/question/new.html.erb index 351b388..cfda31c 100644 --- a/app/views/admin/question/new.html.erb +++ b/app/views/admin/question/new.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "New Question" + content_for :title, "New Question - Skills Assessment Admin" %> <%= render partial: 'form', locals: {question: @question, action: admin_create_question_path } %> diff --git a/app/views/admin/question/view.html.erb b/app/views/admin/question/view.html.erb index fc72e6c..913e737 100644 --- a/app/views/admin/question/view.html.erb +++ b/app/views/admin/question/view.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Question for #{@question.quiz.name}" + content_for :title, "Question - Skills Assessment Admin" %> diff --git a/app/views/admin/quiz/index.html.erb b/app/views/admin/quiz/index.html.erb index 1bce4e8..068759b 100644 --- a/app/views/admin/quiz/index.html.erb +++ b/app/views/admin/quiz/index.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Quizzes" + content_for :title, "Quizzes - Skills Assessment Admin" %> <%= render partial: 'admin/quiz/table_list', locals: { quizzes: @quizzes } %> diff --git a/app/views/admin/quiz/new.html.erb b/app/views/admin/quiz/new.html.erb index 2bb5726..15b084a 100644 --- a/app/views/admin/quiz/new.html.erb +++ b/app/views/admin/quiz/new.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "New Quiz" + content_for :title, "New Quiz - Skills Assessment Admin" %> <%= render partial: 'form', locals: { quiz: @quiz, action: admin_create_quiz_path } %> diff --git a/app/views/admin/quiz/view.html.erb b/app/views/admin/quiz/view.html.erb index f37b865..ad00425 100644 --- a/app/views/admin/quiz/view.html.erb +++ b/app/views/admin/quiz/view.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "#{@quiz.name}" + content_for :title, "Quiz - Skills Assessment Admin" %>

<%= @quiz.name %>

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/index.html.erb b/app/views/admin/result/index.html.erb index 3c1373c..79ed6de 100644 --- a/app/views/admin/result/index.html.erb +++ b/app/views/admin/result/index.html.erb @@ -1,5 +1,6 @@ <% content_for :section_title, "Completed Tests" + content_for :title, "Quiz Results - Skills Assessment Admin" %>
@@ -7,6 +8,7 @@ + <% @candidates.each do |candidate| %> @@ -14,6 +16,7 @@ + <% end %>
Test ID Experience RecruiterInterview Request
<%= link_to candidate.test_hash, admin_result_path(candidate.test_hash) %> <%= candidate.experience %> years <%= mail_to(candidate.recruiter.email) %><%= candidate.review_status unless candidate.pending? %>
diff --git a/app/views/admin/result/view.html.erb b/app/views/admin/result/view.html.erb index 73a0f72..61863fa 100644 --- a/app/views/admin/result/view.html.erb +++ b/app/views/admin/result/view.html.erb @@ -1,10 +1,19 @@ -
+<% + content_for :title, "Quiz Review - Skills Assessment Admin" +%> + +

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/app/views/candidate/oops.html.erb b/app/views/candidate/oops.html.erb index 81eda59..e17a4cc 100644 --- a/app/views/candidate/oops.html.erb +++ b/app/views/candidate/oops.html.erb @@ -1,3 +1,5 @@ +<% content_for :title, "Skills Assessment" %> +

Oops!

diff --git a/app/views/candidate/saved.html.erb b/app/views/candidate/saved.html.erb index bd7dda0..e7f09eb 100644 --- a/app/views/candidate/saved.html.erb +++ b/app/views/candidate/saved.html.erb @@ -1,3 +1,4 @@ +<% content_for :title, "Saved! - Skills Assessment" %>

Your test results have been saved. You can visit again later with your Test ID to complete diff --git a/app/views/candidate/thankyou.html.erb b/app/views/candidate/thankyou.html.erb index b796e52..e2df7e2 100644 --- a/app/views/candidate/thankyou.html.erb +++ b/app/views/candidate/thankyou.html.erb @@ -1,3 +1,4 @@ +<% content_for :title, "Thank You - Skills Assessment" %>

Thank you!

diff --git a/app/views/quiz/_live_code.html.erb b/app/views/quiz/_live_code.html.erb index 91814dd..cce5495 100644 --- a/app/views/quiz/_live_code.html.erb +++ b/app/views/quiz/_live_code.html.erb @@ -45,6 +45,17 @@ +

+ +
@@ -71,6 +83,7 @@ <% # removes the no-js message %> document.getElementById("nojs<%= question.question_id %>").style.display = "none"; document.getElementById("answer<%= question.question_id %>").style.display = ""; + document.getElementById("accordion<%= question.question_id %>").style.display = ""; <% # we want the coders disabled until JS is confirmed, so form post is easier to validate %> var coders = document.querySelectorAll("[data-id=live-coder-answer] textarea"); diff --git a/app/views/quiz/summary.html.erb b/app/views/quiz/summary.html.erb index cf0bab9..1491bd9 100644 --- a/app/views/quiz/summary.html.erb +++ b/app/views/quiz/summary.html.erb @@ -1,5 +1,5 @@ <% - content_for :title, "Skills Assessment" + content_for :title, "Summary - Skills Assessment" content_for :footer_title, "Skills Assessment" content_for :progress, @status.progress.to_s content_for_javascript_once 'summary-edit' do 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/question_controller_test.rb b/test/controllers/admin/question_controller_test.rb index 767eada..71212b0 100644 --- a/test/controllers/admin/question_controller_test.rb +++ b/test/controllers/admin/question_controller_test.rb @@ -67,6 +67,18 @@ module Admin assert_select 'p', 'foo bar baz' end + test "should post attachment" do + question = questions(:fed1) + post admin_update_question_url(question.to_i), params: { question: + { quiz_id: quizzes(:fed).to_i, attachment: 'https://dev.perficientdigital.com/logo.png' } } + assert_redirected_to admin_question_path(question.to_i) + + get admin_question_path question.to_i + assert_select 'img' do + assert_select "[src=?]", "https://dev.perficientdigital.com/logo.png" + end + end + test "should fail to update question" do question = questions(:fed9) post admin_update_question_url(question.to_i), params: { question: { question: nil } } 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 c55d18f..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 @@ -68,4 +69,46 @@ juan: # Juan has chosen "finish later" for live coders quiz: fed completed: false reminded: true - test_hash: <%= CryptSerializer.dump 'qKQo0l4dyol + 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/mailers/reviewer_mailer_test.rb b/test/mailers/reviewer_mailer_test.rb index 6241752..35673eb 100644 --- a/test/mailers/reviewer_mailer_test.rb +++ b/test/mailers/reviewer_mailer_test.rb @@ -6,6 +6,7 @@ class ReviewerMailerTest < ActionMailer::TestCase candidate = candidates :dawn mail = ReviewerMailer.candidate_submission candidate assert_match "Results", mail.subject + assert_match candidate.test_hash, mail.subject assert_equal candidate.quiz.reviewers.map(&:email), mail.to assert_equal [ENV["default_mail_from"]], mail.from assert_match candidate.test_hash, mail.body.encoded 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