A very fine release.

This commit is contained in:
Mark Moser 2017-03-08 09:00:04 -06:00
commit 12a1502037
96 changed files with 1816 additions and 284 deletions

View File

@ -5,6 +5,9 @@ AllCops:
- bin/**/*
- vendor/assets/**/*
Lint/Debugger:
AutoCorrect: False
Style/AndOr:
Enabled: false

52
.sass-lint.yml Normal file
View File

@ -0,0 +1,52 @@
files:
include: site/**/*.scss
ignore:
- site/assets/scss/*bootstrap*
options:
formatter: stylish
merge-default-rules: true
# https://github.com/sasstools/sass-lint/tree/master/docs/rules
rules:
class-name-format:
- 1
- convention: 'hyphenatedbem'
force-pseudo-nesting: 0
id-name-format: 0
leading-zero:
- 1
- include: true
nesting-depth:
- 1
- max-depth: 4
no-css-comments: 0
no-color-literals:
- 1
-
allow-rgba: true
no-duplicate-properties: 1
no-qualifying-elements:
- 1
- allow-element-with-attribute: true # input[type='email'] but not div.class-name
no-vendor-prefixes: 1
property-sort-order:
- 1
-
# https://github.com/sasstools/sass-lint/blob/develop/lib/config/property-sort-orders/concentric.yml
order: concentric
# https://github.com/sasstools/sass-lint/blob/develop/lib/config/property-sort-orders/smacss.yml
# order: smacss
quotes: 0

View File

@ -4,11 +4,12 @@ source 'https://rubygems.org'
gem 'figaro', '~> 1.1.1'
gem 'bcrypt', '~> 3.1.7'
gem 'mysql2', '>= 0.3.18', '< 0.5'
gem 'rails', '~> 5.0', '>= 5.0.0.1'
gem 'rails', '~> 5.0', '>= 5.0.1'
gem 'jbuilder', '~> 2.6'
gem 'jquery-rails'
gem 'json', '~> 2.0.2'
gem 'kaminari'
gem 'mailjet', '~> 1.3.8'
gem 'puma', '~> 3.0'
gem 'pundit'
@ -53,6 +54,7 @@ group :development, :test do
gem 'byebug', platform: :mri
gem 'pry-byebug'
gem 'pry-rails'
gem 'faker'
gem 'brakeman'
gem 'rubocop', '~> 0.42.0'

View File

@ -1,46 +1,47 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.0.0.1)
actionpack (= 5.0.0.1)
actioncable (5.0.1)
actionpack (= 5.0.1)
nio4r (~> 1.2)
websocket-driver (~> 0.6.1)
actionmailer (5.0.0.1)
actionpack (= 5.0.0.1)
actionview (= 5.0.0.1)
activejob (= 5.0.0.1)
actionmailer (5.0.1)
actionpack (= 5.0.1)
actionview (= 5.0.1)
activejob (= 5.0.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.0.0.1)
actionview (= 5.0.0.1)
activesupport (= 5.0.0.1)
actionpack (5.0.1)
actionview (= 5.0.1)
activesupport (= 5.0.1)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.0.0.1)
activesupport (= 5.0.0.1)
actionview (5.0.1)
activesupport (= 5.0.1)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
activejob (5.0.0.1)
activesupport (= 5.0.0.1)
activejob (5.0.1)
activesupport (= 5.0.1)
globalid (>= 0.3.6)
activemodel (5.0.0.1)
activesupport (= 5.0.0.1)
activerecord (5.0.0.1)
activemodel (= 5.0.0.1)
activesupport (= 5.0.0.1)
activemodel (5.0.1)
activesupport (= 5.0.1)
activerecord (5.0.1)
activemodel (= 5.0.1)
activesupport (= 5.0.1)
arel (~> 7.0)
activesupport (5.0.0.1)
activesupport (5.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.4.0)
addressable (2.5.0)
public_suffix (~> 2.0, >= 2.0.2)
ansi (1.5.0)
arel (7.1.1)
arel (7.1.4)
ast (2.3.0)
awesome_print (1.7.0)
bcrypt (3.1.11)
@ -53,29 +54,31 @@ GEM
bourbon (4.2.7)
sass (~> 3.4)
thor (~> 0.19)
brakeman (3.4.0)
brakeman (3.4.1)
builder (3.2.2)
byebug (9.0.5)
byebug (9.0.6)
choice (0.2.0)
coderay (1.1.1)
concurrent-ruby (1.0.2)
css_parser (1.4.5)
concurrent-ruby (1.0.4)
css_parser (1.4.7)
addressable
debug_inspector (0.0.2)
docile (1.1.5)
domain_name (0.5.20160615)
domain_name (0.5.20161129)
unf (>= 0.0.5, < 1.0.0)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
erubis (2.7.0)
eventmachine (1.2.0.1)
eventmachine (1.2.1)
execjs (2.7.0)
faker (1.7.3)
i18n (~> 0.5)
ffi (1.9.14)
figaro (1.1.1)
thor (~> 0.14)
formatador (0.2.5)
foundation_emails (2.2.0.0)
foundation_emails (2.2.1.0)
globalid (0.3.7)
activesupport (>= 4.1.0)
guard (2.14.0)
@ -106,27 +109,40 @@ GEM
guard (>= 2.0.0)
guard-compat (~> 1.0)
htmlentities (4.3.4)
http-cookie (1.0.2)
http-cookie (1.0.3)
domain_name (~> 0.5)
http_parser.rb (0.6.0)
i18n (0.7.0)
inky-rb (1.3.6.1)
inky-rb (1.3.7.2)
foundation_emails (~> 2)
jbuilder (2.6.0)
nokogiri
jbuilder (2.6.1)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jquery-rails (4.2.1)
jquery-rails (4.2.2)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.0.2)
json (2.0.3)
kaminari (1.0.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loofah (2.0.3)
nokogiri (>= 1.5.9)
lumberjack (1.0.10)
lumberjack (1.0.11)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mailjet (1.3.8)
@ -138,29 +154,27 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
minitest (5.9.0)
minitest-reporters (1.1.11)
minitest (5.10.1)
minitest-reporters (1.1.13)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
multi_json (1.12.1)
mysql2 (0.4.4)
mysql2 (0.4.5)
neat (1.8.0)
sass (>= 3.3)
thor (~> 0.19)
nenv (0.3.0)
netrc (0.11.0)
nio4r (1.2.1)
nokogiri (1.6.8)
nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
parser (2.3.1.2)
parser (2.3.3.1)
ast (~> 2.2)
pkg-config (1.1.7)
policy-assertions (0.0.3)
activesupport (>= 3.0.0)
pundit (>= 1.0.0)
@ -168,19 +182,20 @@ GEM
premailer (1.8.7)
css_parser (>= 1.4.5)
htmlentities (>= 4.0.0)
premailer-rails (1.9.4)
premailer-rails (1.9.5)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-byebug (3.4.0)
pry-byebug (3.4.2)
byebug (~> 9.0)
pry (~> 0.10)
pry-rails (0.3.4)
pry (>= 0.9.10)
puma (3.6.0)
public_suffix (2.0.5)
puma (3.6.2)
pundit (1.1.0)
activesupport (>= 3.0.0)
rack (2.0.1)
@ -188,25 +203,25 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
rails (5.0.0.1)
actioncable (= 5.0.0.1)
actionmailer (= 5.0.0.1)
actionpack (= 5.0.0.1)
actionview (= 5.0.0.1)
activejob (= 5.0.0.1)
activemodel (= 5.0.0.1)
activerecord (= 5.0.0.1)
activesupport (= 5.0.0.1)
rails (5.0.1)
actioncable (= 5.0.1)
actionmailer (= 5.0.1)
actionpack (= 5.0.1)
actionview (= 5.0.1)
activejob (= 5.0.1)
activemodel (= 5.0.1)
activerecord (= 5.0.1)
activesupport (= 5.0.1)
bundler (>= 1.3.0, < 2.0)
railties (= 5.0.0.1)
railties (= 5.0.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.1)
actionpack (~> 5.x)
actionview (~> 5.x)
activesupport (~> 5.x)
rails-dom-testing (2.0.1)
rails-dom-testing (2.0.2)
activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6.0)
nokogiri (~> 1.6)
rails-erd (1.5.0)
activerecord (>= 3.2)
activesupport (>= 3.2)
@ -214,15 +229,15 @@ GEM
ruby-graphviz (~> 1.2)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
railties (5.0.0.1)
actionpack (= 5.0.0.1)
activesupport (= 5.0.0.1)
railties (5.0.1)
actionpack (= 5.0.1)
activesupport (= 5.0.1)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
rake (11.2.2)
rb-fsevent (0.9.7)
rainbow (2.2.1)
rake (12.0.0)
rb-fsevent (0.9.8)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
rest-client (2.0.0)
@ -237,8 +252,8 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-graphviz (1.2.2)
ruby-progressbar (1.8.1)
ruby_dep (1.4.0)
sass (3.4.22)
ruby_dep (1.5.0)
sass (3.4.23)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
@ -253,18 +268,19 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slop (3.6.0)
spring (1.7.2)
spring-watcher-listen (2.0.0)
spring (2.0.0)
activesupport (>= 4.2)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
spring (~> 1.2)
sprockets (3.7.0)
spring (>= 1.2, < 3.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.1.1)
sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
thor (0.19.1)
thor (0.19.4)
thread_safe (0.3.5)
tilt (2.0.5)
turbolinks (5.0.1)
@ -272,13 +288,13 @@ GEM
turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (3.0.2)
uglifier (3.0.4)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.1)
web-console (3.3.1)
unicode-display_width (1.1.2)
web-console (3.4.0)
actionview (>= 5.0)
activemodel (>= 5.0)
debug_inspector
@ -298,6 +314,7 @@ DEPENDENCIES
bourbon
brakeman
byebug
faker
figaro (~> 1.1.1)
guard
guard-brakeman
@ -309,6 +326,7 @@ DEPENDENCIES
jbuilder (~> 2.6)
jquery-rails
json (~> 2.0.2)
kaminari
listen
mailjet (~> 1.3.8)
minitest-reporters
@ -321,7 +339,7 @@ DEPENDENCIES
puma (~> 3.0)
pundit
rack-livereload
rails (~> 5.0, >= 5.0.0.1)
rails (~> 5.0, >= 5.0.1)
rails-controller-testing
rails-erd
rubocop (~> 0.42.0)
@ -336,4 +354,4 @@ DEPENDENCIES
web-console
BUNDLED WITH
1.12.5
1.13.3

View File

@ -79,7 +79,7 @@ guard :shell, all_on_start: true do
end
end
guard :rubocop, cli: %w(-D -S) do
guard :rubocop, cli: %w(-D -S -a) do
watch(/.rubocop.yml/)
watch(/.+\.rb$/)
watch(/Rakefile/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

View File

@ -18,11 +18,6 @@ function updateVotes(data){
$("[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)); });
});
@ -30,7 +25,3 @@ $(document).ready(function() {
$(document).ready(function() {
$('[data-id=vote-count]').each(function(){ handleAjaxResponse($(this), updateVotes); });
});
$(document).ready(function() {
$('[data-id=veto-status]').each(function(){ handleAjaxResponse($(this), updateVeto); });
});

View File

@ -1,12 +1,14 @@
.admin-review {
counter-reset: question;
float: left;
width: 66%;
form {
margin-left: 2.3em;
position: relative;
&::before {
content: counter(question) ") ";
content: counter(question) ') ';
counter-increment: question;
font-size: 1.25em;
left: -1.8em;
@ -17,6 +19,67 @@
}
.review-comments {
float: right;
padding-left: 30px;
width: 33%;
> div {
margin-bottom: 30px;
}
form {
margin-bottom: 30px;
}
.comment-message {
margin-right: 5px;
}
.comment-author {
font-size: 0.85em;
font-weight: 700;
margin-bottom: 30px;
text-align: right;
&::before {
content: '- ';
}
}
.comment-edit-stamp {
color: rgba($gray-dark, 0.65);
font-size: 0.75em;
text-align: right;
}
.comment-edit-btn {
cursor: pointer;
display: inline-block;
font-size: 0.85em;
font-weight: bold;
padding: 2px 5px;
&:hover {
background-color: $gray-base;
color: $gray-lighter;
}
}
.comment-edit-form {
display: none;
margin: 30px 0;
padding: 10px 0;
}
[type="checkbox"] {
&:checked + .comment-edit-form {
display: block;
}
}
}
.review_meta {
@media screen and (min-width: 768px) {
@ -24,8 +87,32 @@
& > div { flex: 1 1 auto; }
}
.review_meta__votes,
.review_meta__vetos {
.review_meta__votes {
margin-bottom: 15px;
a { padding: 5px; }
}
.review_meta__vetos {
label {
display: inline-block;
margin-left: 15px;
}
.review-status-comments {
opacity: 0;
padding: 15px 0;
height: 0;
max-height: 0;
overflow: hidden;
transition: all 0.500s;
}
input:checked ~ .review-status-comments {
opacity: 1;
height: auto;
max-height: 9999px;
overflow: auto;
transition: all 0.7500s;
}
}
}

View File

@ -9,6 +9,50 @@ th {
font-weight: 600;
padding: $small-spacing 0;
text-align: left;
a {
display: inline-block;
margin-right: 18px;
padding-right: 5px;
position: relative;
text-decoration: none;
&::after {
background-image: asset_data_url("ic_sort_black_24dp_2x.png");
background-repeat: no-repeat;
background-size: contain;
content: "";
display: block;
height: 18px;
left: 100%;
opacity: 0.5;
position: absolute;
top: 4px;
width: 18px;
}
&.asc {
&::after {
background-image: asset_data_url("ic_arrow_drop_up_black_24dp_2x.png");
height: 25px;
left: calc(100% - 5px);
opacity: 1;
top: 1px;
width: 25px;
}
}
&.desc {
&::after {
background-image: asset_data_url("ic_arrow_drop_down_black_24dp_2x.png");
height: 25px;
left: calc(100% - 5px);
opacity: 1;
top: 1px;
width: 25px;
}
}
}
}
td {

View File

@ -15,7 +15,7 @@ module Admin
if user && user.authenticate(auth_params[:password])
session[:user] = user.to_i
redirect_to admin_path
redirect_to session[:request] || admin_path
else
redirect_to admin_login_path,
flash: { error: "Sorry, incorrect email or password. Please try again." }

View File

@ -4,7 +4,8 @@ module Admin
before_action :collect_quizzes, except: [:login, :auth]
def index
@candidates = policy_scope Candidate.order(:name)
@candidates = policy_scope Candidate.order("#{sort_column} #{sort_direction}")
.page(params[:page])
end
def new
@ -49,13 +50,14 @@ module Admin
authorize Candidate
candidate = Candidate.find_by(id: params[:id])
CandidateMailer.welcome(candidate).deliver_later
render json: { message: "Email queued!" }.to_json
end
private
def candidate_params
params.require(:candidate).permit(:name, :email, :experience, :quiz_id)
params.require(:candidate).permit(
:name, :email, :experience, :quiz_id, :project, :position, :skill_needs
)
end
def collect_quizzes
@ -66,5 +68,9 @@ module Admin
CandidateMailer.welcome(candidate).deliver_later
RecruiterMailer.candidate_created(candidate).deliver_later
end
def sort_column
Candidate.column_names.include?(params[:sort]) ? params[:sort] : 'name'
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Admin
class CommentController < AdminController
def update
comment = QuizComment.find_by(id: params[:id], test_hash: params[:test_hash])
authorize comment
comment.update(comment_params)
flash_message = if comment.save
{ success: "Sucessfully updated comment" }
else
{ error: "Failed to update comment" }
end
redirect_to admin_result_path(params[:test_hash]), flash: flash_message
end
def create
comment = QuizComment.new(comment_params.merge(user_id: current_user.id, test_hash: params[:test_hash]))
authorize comment
flash_message = if comment.save
{ success: "Sucessfully created comment" }
else
{ error: "Failed to save comment" }
end
ReviewerMailer.new_comment(comment).deliver_later if comment.persisted?
redirect_to admin_result_path(params[:test_hash]), flash: flash_message
end
private
def comment_params
params.require(:quiz_comment).permit(:message)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
module Admin
class ResultController < AdminController
#
# TODO: change context from Candidate to Quiz
# bypass pundit lockdowns until completed
after_action :skip_policy_scope
@ -10,13 +9,30 @@ module Admin
# TODO: Limit results to the quizzes current_user has access to
def index
@candidates = Candidate.where(completed: true).includes(:recruiter)
sort_case = "(case when review_status = 0 then '' else name end)"
sort_with_case = sort_column == 'name' ? sort_case : sort_column
@candidates = Candidate.where(completed: true)
.includes(:recruiter)
.order("#{sort_with_case} #{sort_direction}")
.page(params[:page])
end
def view
@candidate = Candidate.find_by(test_hash: params[:test_hash])
@quiz = @candidate.my_quiz
@status = QuizStatus.new(@candidate)
@comments = QuizComment.includes(:user).where(test_hash: @candidate.test_hash).order(:created_at)
@comment = QuizComment.new
end
private
def sort_column
@sort_col ||= Candidate.column_names.include?(params[:sort]) ? params[:sort] : 'completed_at'
end
def sort_direction
%w(asc desc).include?(params[:direction]) ? params[:direction] : 'desc'
end
end
end

View File

@ -2,7 +2,8 @@
module Admin
class UserController < AdminController
def index
@users = policy_scope User.order(:name)
@users = policy_scope User.order("#{sort_column} #{sort_direction}")
.page(params[:page])
end
def new
@ -52,5 +53,9 @@ module Admin
def user_params
params.require(:user).permit(policy(User).permitted_attributes)
end
def sort_column
User.column_names.include?(params[:sort]) ? params[:sort] : 'name'
end
end
end

View File

@ -5,54 +5,41 @@ module Admin
@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
def interview_request
@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
if interview_params[:review_comments].blank?
refuse_interview_request
else
send_interview_request
end
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)
private
results = {
message: "Interview declined.",
requestCopy: "Request Interview",
declineCopy: "Declined"
}
render json: results.to_json
end
def refuse_interview_request
redirect_to admin_result_path(@candidate.test_hash),
flash: { error: "Must provide a comment" }
end
def send_interview_request
current_user.review_candidate(@candidate, interview_params)
RecruiterMailer.candidate_reviewed(@candidate).deliver_later
redirect_to admin_result_path(@candidate.test_hash),
flash: { notice: "Quiz #{interview_params[:review_status]}" }
end
def interview_params
params.permit(:review_status, :review_comments)
end
end
end

View File

@ -9,6 +9,9 @@ class AdminController < ApplicationController
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
helper_method :sort_direction
helper_method :sort_column
def current_user
@current_user ||= User.find_by(id: session[:user]) if session[:user]
end
@ -16,7 +19,16 @@ class AdminController < ApplicationController
private
def sort_column
:completed_at
end
def sort_direction
%w(asc desc).include?(params[:direction]) ? params[:direction] : 'asc'
end
def authorize_user
session[:request] = request.fullpath
redirect_to admin_login_path unless current_user
end

View File

@ -35,7 +35,7 @@ class QuizController < ApplicationController
private
def complete_and_email
if current_candidate.update_attributes(completed: true)
if current_candidate.update_attributes(completed: true, completed_at: DateTime.current)
current_candidate.build_reviews
CandidateMailer.submitted(current_candidate).deliver_later
RecruiterMailer.candidate_submitted(current_candidate).deliver_later

View File

@ -44,4 +44,11 @@ module ApplicationHelper
@js_blocks << code_label
content_for :custom_javascipt, &block
end
def sortable(column, title = nil)
title ||= column.titleize
css_class = column == sort_column ? sort_direction.to_s : nil
direction = column == sort_column && sort_direction == "desc" ? "asc" : "desc"
link_to title, { sort: column, direction: direction }, class: css_class
end
end

View File

@ -1,14 +1,23 @@
# frozen_string_literal: true
class RecruiterMailer < ApplicationMailer
def candidate_created candidate
@candidate = candidate
@candidate = Candidate.find_by(id: candidate.to_i)
mail to: @candidate.recruiter.email, subject: "Skills Assessment Test - #{candidate.name}"
mail to: @candidate.recruiter.email,
subject: "Skills Assessment Test - #{candidate.name}"
end
def candidate_submitted candidate
@candidate = candidate
@candidate = Candidate.find_by(id: candidate.to_i)
mail to: @candidate.recruiter.email, subject: "Skills Assessment Test - #{candidate.name}"
mail to: @candidate.recruiter.email,
subject: "Skills Assessment Test - #{candidate.name}"
end
def candidate_reviewed candidate
@candidate = Candidate.find_by(id: candidate.to_i)
mail to: @candidate.recruiter.email,
subject: "Skills Assesment - Review Complete for #{candidate.name}"
end
end

View File

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

View File

@ -6,6 +6,7 @@ class Candidate < ApplicationRecord
belongs_to :recruiter, class_name: "User"
has_many :votes, class_name: "ReviewerVote"
has_many :reviewers, through: :quiz
has_many :quiz_comments, foreign_key: :test_hash, primary_key: :test_hash
serialize :email, CryptSerializer
@ -14,6 +15,8 @@ class Candidate < ApplicationRecord
validates :recruiter_id, presence: true
validates :name, presence: true
validates :experience, presence: true
validates :project, presence: true
validates :position, presence: true
validates :email, uniqueness: true, presence: true, email_format: true
validates :test_hash, uniqueness: true, presence: true
@ -23,12 +26,24 @@ class Candidate < ApplicationRecord
declined: 2
}
def interview?
return 'yes' if approved?
'no' if declined?
end
def build_reviews
reviewers.each do |reviewer|
votes.find_or_create_by(user_id: reviewer.id)
end
end
def manager
manager_votes = votes.joins(:user).where("users.role = 'manager'")
return User.new(name: "No Manager") if manager_votes.empty?
manager_votes.first.user
end
def submitted_answers
answers.where(submitted: true)
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class QuizComment < ApplicationRecord
belongs_to :user
belongs_to :candidate, foreign_key: :test_hash, primary_key: :test_hash
validates :message, presence: true
def edits?
updated_at > (created_at + 5.seconds)
end
end

View File

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

View File

@ -5,6 +5,7 @@ class User < ApplicationRecord
has_many :reviewer_to_quizzes
has_many :quizzes, through: :reviewer_to_quizzes
has_many :votes, class_name: 'ReviewerVote'
has_many :quiz_comments
has_many :reviewees, through: :quizzes, source: :candidates
@ -18,7 +19,13 @@ class User < ApplicationRecord
save
end
def commented_on? test_hash
quiz_comments.where(test_hash: test_hash).count.positive?
end
# Voting
# TODO: Refactor this out of User, belongs on ReviewerVote
# ie: cast_yea(candidate, user)
def cast_yea_on candidate
vote = votes.find_by(candidate_id: candidate.to_i)
vote.vote = :yea
@ -31,20 +38,16 @@ class User < ApplicationRecord
vote.save
end
def approve_candidate candidate
def review_candidate candidate, parms_hash
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
vote.veto = parms_hash[:review_status]
if vote.save
# skipping validations on candidate because that's not the managers responsibility
candidate.review_comments = parms_hash[:review_comments]
candidate.update_attribute(:review_status, parms_hash[:review_status])
end
end
def my_vote candidate

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class QuizCommentPolicy < ApplicationPolicy
# Quiz Comment Policy
#
# Anyone who can vote on results, can comment
# Only comment owner can edit her comment
def new?
user.acts_as_reviewer?
end
def create?
return true if user.acts_as_admin?
user.acts_as_reviewer? &&
record.candidate.reviewers.where(id: user.id).count.positive?
end
def update?
user.acts_as_reviewer? && user.id == record.user_id
end
end

View File

@ -2,30 +2,26 @@
class ReviewerVotePolicy < ApplicationPolicy
# Voting Policy
#
# Only Reviewers, Managers, and Admins, can cast a vote on a quiz result
# Only Reviewers and Managers 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 user.commented_on?(record.candidate.test_hash)
return false unless record.candidate.reviewers.include? user
return false if user.admin?
user.acts_as_reviewer?
end
def down?
return true if user.acts_as_admin?
return false unless user.commented_on?(record.candidate.test_hash)
return false unless record.candidate.reviewers.include? user
return false if user.admin?
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?
def interview_request?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_manager?

View File

@ -16,6 +16,24 @@
<%= form.select :experience, experience_options(candidate.experience), include_blank: false %>
</div>
<div class="form-group">
<%= form.label :project, "Client or project" %>
<%= form.text_field :project %>
</div>
<div class="form-group">
<%= form.radio_button :position, 'full-time' %>
<%= form.label "position_full-time", "Full-time" %>
<%= form.radio_button :position, 'contract' %>
<%= form.label :position_contract, "Contract" %>
</div>
<div class="form-group">
<%= form.label :skill_needs, "Specific skill needs" %>
<%= form.text_field :skill_needs %>
</div>
<div class="form-group">
<%= form.label :quiz_id, "Quiz" %>
<%= form.select :quiz_id, quiz_options(quizzes, candidate.quiz_id), include_blank: (quizzes.size > 1) %>

View File

@ -9,14 +9,14 @@
<table cellspacing="0" cellpadding="0">
<tr>
<th>Candidate</th>
<th>Test ID</th>
<th>Email</th>
<th>Experience</th>
<th><%= sortable "name", "Candidate" %></th>
<th><%= sortable "test_hash", "Test ID" %></th>
<th><%= sortable "email" %></th>
<th><%= sortable "experience" %></th>
<th>Progress</th>
<th>Completed</th>
<th>Reminded</th>
<th>Interview Request</th>
<th><%= sortable "completed_at", "Completed" %></th>
<th><%= sortable "reminded" %></th>
<th>Interview?</th>
</tr>
<% @candidates.each do |candidate| %>
@ -32,8 +32,9 @@
<td><%= candidate.status %></td>
<td><%= candidate.completed ? link_to("Submitted", admin_result_path(candidate.test_hash)) : "" %></td>
<td><%= candidate.reminded ? "Yes" : "" %></td>
<td><%= candidate.review_status unless candidate.pending? %></td>
<td><%= candidate.interview? %></td>
</tr>
<% end %>
</table>
<%= paginate @candidates %>
</main>

View File

@ -0,0 +1 @@
{ "message" : "Email queued!" }

View File

@ -0,0 +1,23 @@
<div class="comment-message">
<%= comment.message %>
<% if policy(comment).update? %>
<label class="comment-edit-btn" for="comment-<%= comment.id %>">edit</label>
<% end %>
<% if comment.edits? %>
<div class="comment-edit-stamp">Updated <%= time_ago_in_words(comment.updated_at) %> ago</div>
<% else %>
<div class="comment-edit-stamp"><%= time_ago_in_words(comment.created_at) %> ago</div>
<% end %>
</div>
<div class="comment-author"><%= comment.user.name %></div>
<% if policy(comment).update? %>
<input type="checkbox" id="comment-<%= comment.id %>">
<div class="comment-edit-form">
<%= render partial: 'comment_form', locals: {comment: comment, test_hash: comment.test_hash } %>
</div>
<% end %>

View File

@ -0,0 +1,23 @@
<% if comment.id.nil? %>
<%= form_for comment, url: admin_create_comment_path(test_hash: test_hash), method: :post do |form| %>
<div class="form-group">
<%= form.label :message, "New Comment" %>
<%= form.text_area :message %>
</div>
<%= submit_tag "Save Comment" %>
<% end %>
<% else %>
<%= form_for comment, url: admin_update_comment_path(test_hash: test_hash, id: comment.id), method: :post do |form| %>
<div class="form-group">
<%= form.label :message, "Update Comment" %>
<%= form.text_area :message %>
</div>
<%= submit_tag "Update" %>
<% end %>
<% end %>

View File

@ -1,34 +1,54 @@
<% # TODO: This needs to be extracted into a decorator, or something. It is only a quick hack solution. %>
<% # TODO: This should be extracted into a decorator, or something. It is only a quick hack solution. %>
<% if current_user.acts_as_reviewer? %>
<div class="review_meta__votes" data-id="vote-count">
<strong>Votes: </strong>
<%= link_to admin_up_vote_path(test_hash: @candidate.test_hash), remote: true do %>
Yea (<span data-id="up-votes"><%= @candidate.votes.yea.count %></span>)
<% end %>
<%= link_to admin_down_vote_path(test_hash: @candidate.test_hash), remote: true do %>
Nay (<span data-id="down-votes"><%= @candidate.votes.nay.count %></span>)
<% if @candidate.pending? && current_user.commented_on?(@candidate.test_hash) %>
<%= link_to admin_up_vote_path(test_hash: @candidate.test_hash), remote: true do %>
Yea (<span data-id="up-votes"><%= @candidate.votes.yea.count %></span>)
<% end %>
<%= link_to admin_down_vote_path(test_hash: @candidate.test_hash), remote: true do %>
Nay (<span data-id="down-votes"><%= @candidate.votes.nay.count %></span>)
<% end %>
<% elsif @candidate.pending? %>
<div>You must comment before you can vote</div>
<span>Yea (<span data-id="up-votes"><%= @candidate.votes.yea.count %></span>)</span>
<span>Nay (<span data-id="down-votes"><%= @candidate.votes.nay.count %></span>)</span>
<% else %>
Voting closed -
Yea (<%= @candidate.votes.yea.count %>) -
Nay (<%= @candidate.votes.nay.count %>)
<% end %>
<small>(Your vote: <span data-id="my-vote"><%= current_user.my_vote(@candidate) %></span>)</small>
</div>
<% end %>
<% if current_user.acts_as_manager? %>
<div class="review_meta__vetos" data-id="veto-status">
<strong>Manager Vetos: </strong>
<%= link_to admin_approve_vote_path(test_hash: @candidate.test_hash), remote: true do %>
<span data-id="interview-request">
<%= @candidate.approved? ? "Requested" : "Request Interview" %>
</span>
<% end %>
<%= link_to admin_decline_vote_path(test_hash: @candidate.test_hash), remote: true do %>
<span data-id="interview-decline">
<%= @candidate.declined? ? "Declined" : "Decline Interview" %>
</span>
<div class="review_meta__vetos">
<%= form_tag admin_interview_path(test_hash: @candidate.test_hash) do %>
<strong>Interview: </strong>
<%= radio_button_tag :review_status, :approved, checked = @candidate.approved? %>
<%= label_tag :review_status_approved, "Yes" %>
<%= radio_button_tag :review_status, :declined, checked = @candidate.declined? %>
<%= label_tag :review_status_declined, "No" %>
<div class="review-status-comments">
<span>Review comments for recruiter</span>
<%= text_area_tag :review_comments, @candidate.review_comments %>
<%= submit_tag 'Send Request' %>
</div>
<% end %>
</div>
<% else %>
<strong>Candidate Interview Status: </strong><%= @candidate.review_status %>
<% unless @candidate.review_status.blank? %>
<div>Review Status Comments:</div>
<div><%= @candidate.review_comments %></div>
<% end %>
<% end %>

View File

@ -5,19 +5,24 @@
<main class="summary_tpl">
<table cellspacing="0" cellpadding="0">
<tr>
<th>Test ID</th>
<th>Experience</th>
<th><%= sortable "test_hash", "Test ID" %></th>
<th><%= sortable "name" %></th>
<th><%= sortable "project", "Client/Project" %></th>
<th>Recruiter</th>
<th>Interview Request</th>
<th><%= sortable "completed_at", "Submitted on" %></th>
<th>Interview?</th>
</tr>
<% @candidates.each do |candidate| %>
<tr>
<td><%= link_to candidate.test_hash, admin_result_path(candidate.test_hash) %></td>
<td><%= candidate.experience %> years</td>
<td><%= candidate.name if !candidate.pending? %></td>
<td><%= candidate.project %></td>
<td><%= mail_to(candidate.recruiter.email) %></td>
<td><%= candidate.review_status unless candidate.pending? %></td>
<td><%= candidate.completed_at.strftime('%D') unless candidate.completed_at.nil? %></td>
<td><%= candidate.interview? %></td>
</tr>
<% end %>
</table>
<%= paginate @candidates %>
</main>

View File

@ -2,41 +2,65 @@
content_for :title, "Quiz Review - Skills Assessment Admin"
%>
<main class="summary_tpl admin-review">
<h2 class="prft-heading">Quiz Review</h2>
<div class="summary_tpl">
<div class="admin-review">
<h2 class="prft-heading">Quiz Review</h2>
<div class="review_meta">
<div>
<strong>Test ID:</strong> <%= @candidate.test_hash %><br />
<strong>Years of Experience:</strong> <%= @candidate.experience %><br />
<strong>Recruiter Email:</strong> <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %><br />
<div class="review_meta">
<div>
<% unless @candidate.pending? %>
<strong>Name:</strong> <%= @candidate.name %><br />
<% end %>
<strong>Test ID:</strong> <%= @candidate.test_hash %><br />
<strong>Years of Experience:</strong> <%= @candidate.experience %><br />
<strong>Client/Project:</strong> <%= @candidate.project %><br />
<strong>Position Type:</strong> <%= @candidate.position %><br />
<strong>Skill Needs:</strong> <%= @candidate.skill_needs %><br />
<strong>Recruiter Email:</strong> <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %><br />
</div>
</div>
<div><%= render partial: 'voting' %></div>
<% @quiz.each do |question| %>
<%= form_for(:answer, url: '#never-post', html:{id: 'summary-form'}) do |form| %>
<article class="answer-sec <%= question.input_type %>-type" data-qid="<%= question.question_id %>">
<div class="question-heading">
<div class="question-title">
<h3><%= question.question %></h3>
</div>
</div>
<div class="answer-container">
<% if question.attachment.present? %>
<%= image_tag question.attachment %>
<% end %>
<fieldset disabled class="answer-block">
<%= render partial: "quiz/#{question.input_type}", locals: {question: question, answer: question.answer, form: form} %>
</fieldset>
</div>
</article>
<% end #form_tag %>
<% end #questions loop %>
<%= link_to(admin_results_path, { class: 'secondary-btn' }) do %>
<button>Back to list</button>
<% end %>
</div>
<% @quiz.each do |question| %>
<%= form_for(:answer, url: '#never-post', html:{id: 'summary-form'}) do |form| %>
<article class="answer-sec <%= question.input_type %>-type" data-qid="<%= question.question_id %>">
<div class="question-heading">
<div class="question-title">
<h3><%= question.question %></h3>
</div>
</div>
<div class="review-comments">
<div>
<h2 class="prft-heading">Voting</h2>
<div class="review_meta">
<div><%= render partial: 'voting' %></div>
</div>
</div>
<div class="answer-container">
<% if question.attachment.present? %>
<%= image_tag question.attachment %>
<% end %>
<fieldset disabled class="answer-block">
<%= render partial: "quiz/#{question.input_type}", locals: {question: question, answer: question.answer, form: form} %>
</fieldset>
</div>
</article>
<% end #form_tag %>
<% end #questions loop %>
<%= link_to(admin_results_path, { class: 'secondary-btn' }) do %>
<button>Back to list</button>
<% end %>
</main>
<div>
<h2 id="comment-header" class="prft-heading">Comments</h2>
<% if policy(QuizComment).new? %>
<%= render partial: 'comment_form', locals: {comment: @comment, test_hash: @candidate.test_hash } %>
<% end %>
<%= render partial: 'comment', collection: @comments, locals: { test_hash: @candidate.test_hash } %>
<a href="#comment-header">Back to top</a>
</div>
</div>
</div>

View File

@ -1,8 +1,8 @@
<table cellspacing="0" cellpadding="0">
<tr>
<th>User</th>
<th>Email</th>
<th>Role</th>
<th><%= sortable "name", "User" %></th>
<th><%= sortable "email" %></th>
<th><%= sortable "role" %></th>
<th></th>
</tr>

View File

@ -0,0 +1,6 @@
{
"message" : "Vote Counted",
"upCount" : <%= @candidate.votes.yea.count %>,
"downCount" : <%= @candidate.votes.nay.count %>,
"myVote" : "nay"
}

View File

@ -0,0 +1,6 @@
{
"message" : "Vote Counted",
"upCount" : <%= @candidate.votes.yea.count %>,
"downCount" : <%= @candidate.votes.nay.count %>,
"myVote" : "yea"
}

View File

@ -23,7 +23,7 @@
<table>
<tr>
<td class="email-copyright">
&copy;2016 All Rights Reserved - Perficient Digital
&copy;2016-<%= Time.now.year %> All Rights Reserved - Perficient Digital
</td>
<td class="email-logo">
<%= image_tag(attachments["perficientdigital-logo.jpg"].url, alt:"Perficient Digital") %>

View File

@ -7,8 +7,9 @@
<strong>Candidate email:</strong> <%= @candidate.email %><br />
<strong>Candidate ID:</strong> <%= @candidate.test_hash %><br />
<strong>Years of experience:</strong> <%= @candidate.experience %> Years<br />
<strong>Client/Project:</strong> <%= @candidate.project %><br />
</p>
<p>You will be notified when the candidate has finished taking the test.</p>
</columns>
</row>
</row>

View File

@ -6,5 +6,6 @@ Candidate name: <%= @candidate.name %>
Candidate email: <%= @candidate.email %>
Candidate ID: <%= @candidate.test_hash %>
Years of experience: <%= @candidate.experience %> Years
Client/Project: <%= @candidate.project %>
You will be notified when the candidate has finished taking the test.

View File

@ -0,0 +1,9 @@
<row>
<columns class="email-body">
<p>The team has <%= @candidate.review_status %> an interview with <strong><%= @candidate.name %></strong> with the following comments:</p>
<p><%= @candidate.review_comments %></p>
<p>Thank you</p>
</columns>
</row>

View File

@ -0,0 +1,7 @@
PERFICIENT/digital - Skills Assessment Test
The team has <%= @candidate.review_status %> an interview with <%= @candidate.name %> with the following comments:
<%= @candidate.review_comments %>
Thank you.

View File

@ -1,6 +1,6 @@
<row>
<columns class="email-body">
<p><strong><%= @candidate.name %></strong> has completed the Skills Assessment Test.</p>
<p><strong>Martin Ridgway</strong> will let you know if we would like to interview this candidate.</p>
<p><strong><%= @candidate.manager.name %></strong> will let you know if we would like to interview this candidate.</p>
</columns>
</row>
</row>

View File

@ -1,4 +1,4 @@
PERFICIENT/digital - Skills Assessment Test
<%= @candidate.name %> has completed the Skills Assessment Test.
Martin Ridgway will let you know if we would like to interview this candidate.
<%= @candidate.manager.name %> will let you know if we would like to interview this candidate.

View File

@ -1,6 +1,9 @@
<row>
<columns class="email-body">
<p>Candidate <strong><%= @candidate.test_hash %></strong> has completed the Skills Assessment Test.</p>
<p>
Candidate <strong><%= @candidate.test_hash %></strong> has completed the
Skills Assessment Test for client/project <%= @candidate.project %>.
</p>
<p>You can view the results here: <%= link_to nil, admin_result_url(@candidate.test_hash) %>.</p>
</columns>
</row>

View File

@ -1,5 +1,5 @@
PERFICIENT/digital SKILLS ASSESSMENT RESULTS
Candidate <%= @candidate.test_hash %> has completed the Skills Assessment Test.
Candidate <%= @candidate.test_hash %> has completed the Skills Assessment Test for client/project <%= @candidate.project %>.
You can view the results here: <%= admin_result_url(@candidate.test_hash) %>.

View File

@ -0,0 +1,13 @@
<row>
<columns class="email-body">
<p>
<%= @comment.user.name %> wrote a comment for quiz <%= @comment.test_hash %>
</p>
<p style="border-top: 1px solid; border-bottom: 1px solid;">
<%= @comment.message %>
</p>
<p>You can view and reply here: <%= link_to nil, admin_result_url(@comment.test_hash) %>.</p>
</columns>
</row>

View File

@ -0,0 +1,9 @@
SKILLS ASSESSMENT RESULT COMMENT
<%= @comment.user.name %> wrote a comment for quiz <%= @comment.test_hash %>
--- --- --- ---
<%= @comment.message %>
--- --- --- ---
You can view and reply here: <%= admin_result_url(@comment.test_hash) %>.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,5 +30,8 @@ module SkillAssessmentApp
g.helper false
g.routes false
end
# let's use lib/utils for dev only utility classes & helpers
config.autoload_paths << "#{Rails.root}/lib/utils"
end
end

View File

@ -1,10 +1,81 @@
{
"ignored_warnings": [
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "6f3216446dca0fa79e96267eb0323d50cc59e7bc1e1529fd160cd5beb185e2f2",
"message": "Possible SQL injection",
"file": "app/controllers/admin/candidate_controller.rb",
"line": 7,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Candidate.order(\"#{sort_column} #{sort_direction}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Admin::CandidateController",
"method": "index"
},
"user_input": "sort_column",
"confidence": "Medium",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "9e802ac2067c1ee551f97c1f37816b585451abc42b6a2c7903c6d97aa042da61",
"message": "Possible SQL injection",
"file": "app/controllers/admin/user_controller.rb",
"line": 5,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "User.order(\"#{sort_column} #{sort_direction}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Admin::UserController",
"method": "index"
},
"user_input": "sort_column",
"confidence": "Medium",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "a65b53d63f1d43cb831947d0693d2d1b8819e21aec5ee18bf09b577ade02a0ee",
"message": "Possible SQL injection",
"file": "app/controllers/admin/result_controller.rb",
"line": 16,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Candidate.where(:completed => true).includes(:recruiter).order(\"#{(\"(case when review_status = 0 then '' else name end)\" or sort_column)} #{sort_direction}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Admin::ResultController",
"method": "index"
},
"user_input": "sort_column",
"confidence": "Medium",
"note": ""
},
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
"fingerprint": "da17225c940987e6239cc4ecfe27bcb1e5da2db1134435dc3e1025d97927e0ba",
"message": "Render path contains parameter value",
"file": "app/views/admin/question/options.html.erb",
"line": 3,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(partial => \"admin/question/#{params[:input_type]}\", { :locals => ({ :question => ((Question.find(params[:question_id]) or Question.new)) }) })",
"render_path": [{"type":"controller","class":"Admin::QuestionController","method":"options","line":58,"file":"app/controllers/admin/question_controller.rb"}],
"location": {
"type": "template",
"template": "admin/question/options"
},
"user_input": "params[:input_type]",
"confidence": "Medium",
"note": "false positive"
}
],
"updated": "2016-09-19 09:06:25 -0500",
"brakeman_version": "3.4.0"
"updated": "2017-03-06 12:16:23 -0600",
"brakeman_version": "3.4.1"
}

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
Kaminari.configure do |config|
config.default_per_page = 10
# config.max_per_page = nil
# config.window = 4
# config.outer_window = 0
# config.left = 0
# config.right = 0
# config.page_method_name = :page
# config.param_name = :page
# config.params_on_first_page = false
end

View File

@ -1,4 +1,6 @@
# frozen_string_literal: true
# Neat routing visualizer, in irb:
# File.open('routing.html', 'w+'){|f| f.write Rails.application.routes.router.visualizer }
Rails.application.routes.draw do
get "/styleguide", to: "application#styleguide", as: :styleguide
get "/admin/styleguide", to: "application#styleguide"
@ -45,15 +47,17 @@ Rails.application.routes.draw do
post "/admin/candidate/new", to: "admin/candidate#create", as: :admin_create_candidate
get "/admin/candidate/:id", to: "admin/candidate#edit", as: :admin_edit_candidate
post "/admin/candidate/:id", to: "admin/candidate#update", as: :admin_update_candidate
get "/admin/candidate/:id/resend", to: "admin/candidate#resend_welcome", as: :admin_resend_welcome
get "/admin/candidate/:id/resend", to: "admin/candidate#resend_welcome", as: :admin_resend_welcome, defaults: { format: 'json' }
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
post "/admin/comment/:test_hash/:id", to: "admin/comment#update", as: :admin_update_comment
post "/admin/comment/:test_hash", to: "admin/comment#create", as: :admin_create_comment
get "admin/vote/:test_hash/up", to: "admin/vote#up", as: :admin_up_vote, defaults: { format: 'json' }
get "admin/vote/:test_hash/down", to: "admin/vote#down", as: :admin_down_vote, defaults: { format: 'json' }
post "admin/vote/:test_hash", to: "admin/vote#interview_request", as: :admin_interview
get "/admin", to: "admin/dashboard#show", as: :admin

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class RemoveVoteLocking < ActiveRecord::Migration[5.0]
def change
remove_column :reviewer_votes, :locked, :boolean, default: false, null: false
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class AddCompletedAt < ActiveRecord::Migration[5.0]
def change
add_column :candidates, :completed_at, :datetime, after: :completed
Candidate.reset_column_information
Candidate.where(completed: true).each do |candidate|
candidate.completed_at = candidate.updated_at
candidate.save
end
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class AddProjectToClient < ActiveRecord::Migration[5.0]
def change
add_column :candidates, :project, :string, after: :experience
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateQuizComments < ActiveRecord::Migration[5.0]
def change
create_table :quiz_comments do |t|
t.integer :user_id
t.string :test_hash
t.text :message
t.timestamps
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAttributesToCandidate < ActiveRecord::Migration[5.0]
def change
add_column :candidates, :skill_needs, :string, after: :project
add_column :candidates, :position, :string, after: :project
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class AddCommentsToCandidate < ActiveRecord::Migration[5.0]
def change
add_column :candidates, :review_comments, :text, after: :review_status
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class RemoveLastRemindedFromReviewerVotes < ActiveRecord::Migration[5.0]
def change
remove_column :reviewer_votes, :last_reminded, :datetime
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161120175737) do
ActiveRecord::Schema.define(version: 20170228161729) do
create_table "answers", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "candidate_id"
@ -30,13 +30,18 @@ ActiveRecord::Schema.define(version: 20161120175737) do
t.string "name"
t.string "email"
t.string "experience"
t.string "project"
t.string "position"
t.string "skill_needs"
t.integer "recruiter_id"
t.boolean "completed"
t.datetime "completed_at"
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.integer "review_status", default: 0, null: false
t.text "review_comments", limit: 65535
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
@ -58,6 +63,14 @@ ActiveRecord::Schema.define(version: 20161120175737) do
t.index ["sort"], name: "index_questions_on_sort", using: :btree
end
create_table "quiz_comments", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "user_id"
t.string "test_hash"
t.text "message", limit: 65535
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "quizzes", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "unit"
t.string "dept"
@ -77,12 +90,10 @@ ActiveRecord::Schema.define(version: 20161120175737) do
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.integer "vote", default: 0, null: false
t.integer "veto", default: 0, 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

View File

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

69
lib/utils/fake_quiz.rb Normal file
View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
class FakeQuiz
def create_completed_quizzes num = 10
num.times do
candidate = create_candidate Faker::Name.name
answer_questions candidate
candidate.update_attributes(completed: true, completed_at: Time.zone.now - rand(0..112).days)
candidate.build_reviews
end
end
def create_candidate name
Candidate.create(name: name,
email: "#{Faker::Internet.user_name(name)}@mailinator.com",
experience: rando_experience,
project: Faker::Company.name,
recruiter_id: recruiter_id,
quiz_id: fed_quiz_id)
end
def answer_questions candidate
candidate.quiz.questions.each do |question|
candidate.answers.create(question_id: question.id,
answer: generate_answer(question),
submitted: true)
end
end
private
def fed_quiz_id
Quiz.find_by(dept: 'fed').id
end
def recruiter_id
User.find_by(name: 'Sam Recruiter').id
end
def rando_experience
%w(0-3 4-6 7-9 10-14 15+)[rand(0..4)]
end
def generate_answer question # rubocop:disable Metrics/MethodLength
case question.input_type
when "checkbox"
question.input_options
when "checkbox_other"
{
other: Faker::TwinPeaks.quote,
options: question.input_options
}
when "radio"
question.input_options.sample
when "radio_other"
{
other: Faker::TwinPeaks.quote,
options: question.input_options.sample
}
when "live_code"
{
html: "<p>#{Faker::TwinPeaks.quote}</p>",
css: "body {color: #{Faker::Color.hex_color}}",
text: Faker::TwinPeaks.quote
}
else
Faker::TwinPeaks.quote
end
end # rubocop:enable Metrics/MethodLength
end

View File

@ -26,6 +26,18 @@ module Admin
assert_redirected_to admin_url
end
test "should auth to original request" do
elsie = candidates(:elsie)
get admin_result_url(test_hash: elsie.test_hash)
assert_redirected_to admin_login_url
post admin_auth_url, params: { auth:
{ email: 'fed.reviewer@mailinator.com', password: 'password' } }
assert_redirected_to admin_result_url(test_hash: elsie.test_hash)
end
test "should FAIL auth" do
post admin_auth_url, params: { auth:
{ email: 'alan.admin@mailinator.com', password: 'b@d9a$$werD' } }

View File

@ -23,8 +23,14 @@ module Admin
assert_enqueued_jobs 2 do
assert_difference("Candidate.count") do
post admin_create_candidate_path, params: { candidate:
{ name: 'new name', email: 'test@mailinator.com', experience: '0-3', quiz_id: quizzes(:fed).id } }
post admin_create_candidate_path, params: { candidate: {
name: 'new name',
email: 'test@mailinator.com',
experience: '0-3',
position: 'full-time',
project: 'client project',
quiz_id: quizzes(:fed).id
} }
end
end
assert_redirected_to admin_candidates_path

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
require 'test_helper'
module Admin
class CommentControllerTest < ActionDispatch::IntegrationTest
include ActiveJob::TestHelper
test "should post update" do
auth_manager
comment = quiz_comments(:com5)
post admin_update_comment_url(test_hash: comment.test_hash, id: comment.id),
params: { quiz_comment: { message: 'updated comment' } }
assert_redirected_to admin_result_url(test_hash: comment.test_hash)
assert flash[:success]
refute_equal comment.message, QuizComment.find_by(id: comment.id).message
end
test "should require message to update" do
auth_manager
comment = quiz_comments(:com5)
post admin_update_comment_url(test_hash: comment.test_hash, id: comment.id),
params: { quiz_comment: { message: '' } }
assert_redirected_to admin_result_url(test_hash: comment.test_hash)
assert flash[:error]
assert_equal comment.message, QuizComment.find_by(id: comment.id).message
end
test "should post create" do
auth_reviewer
candidate = candidates(:stacy)
assert_difference("QuizComment.count") do
post admin_create_comment_url(test_hash: candidate.test_hash),
params: { quiz_comment: { message: 'this is a test comment' } }
end
assert_redirected_to admin_result_url(test_hash: candidate.test_hash)
assert flash[:success]
end
test "admin can post comment" do
auth_admin
candidate = candidates(:stacy)
assert_difference("QuizComment.count") do
post admin_create_comment_url(test_hash: candidate.test_hash),
params: { quiz_comment: { message: 'this is an admin comment' } }
end
assert_redirected_to admin_result_url(test_hash: candidate.test_hash)
assert flash[:success]
end
test "should queue emails on create" do
auth_reviewer
candidate = candidates(:stacy)
assert_enqueued_jobs 1 do
assert_difference("QuizComment.count", 1) do
post admin_create_comment_url(test_hash: candidate.test_hash),
params: { quiz_comment: { message: 'this is a test comment' } }
end
end
end
test "should require comment to create" do
auth_reviewer
candidate = candidates(:stacy)
assert_difference("QuizComment.count", 0) do
post admin_create_comment_url(test_hash: candidate.test_hash),
params: { quiz_comment: { message: '' } }
end
assert_redirected_to admin_result_url(test_hash: candidate.test_hash)
assert flash[:error]
end
test "should not edit others comments" do
auth_reviewer
comment = quiz_comments(:com5)
post admin_update_comment_url(test_hash: comment.test_hash, id: comment.id),
params: { quiz_comment: { message: 'updated comment' } }
assert_redirected_to admin_login_url
assert_equal comment.message, QuizComment.find_by(id: comment.id).message
end
test "can not comment on Gustov" do
auth_reviewer
candidate = candidates(:gustov)
assert_difference("QuizComment.count", 0) do
post admin_create_comment_url(test_hash: candidate.test_hash),
params: { quiz_comment: { message: 'this is a test comment' } }
end
assert_redirected_to admin_login_url
end
end
end

View File

@ -3,10 +3,30 @@ require 'test_helper'
module Admin
class VoteControllerTest < ActionDispatch::IntegrationTest
test "reviewer can up vote henry" do
include ActiveJob::TestHelper
test "reviewer can only vote after commenting" do
auth_user users(:reviewer)
henry = candidates(:henry)
assert_difference("Candidate.find(#{henry.id}).votes.yea.count", 0) do
get admin_up_vote_url(henry.test_hash)
end
post admin_create_comment_url(test_hash: henry.test_hash),
params: { quiz_comment: { message: 'this is a comment to vote' } }
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 up vote henry" do
auth_user users(:reviewer2)
henry = candidates(:henry)
assert_difference("Candidate.find(#{henry.id}).votes.yea.count", 1) do
get admin_up_vote_url(henry.test_hash)
end
@ -14,7 +34,7 @@ module Admin
end
test "reviewer can down vote henry" do
auth_user users(:reviewer)
auth_user users(:reviewer2)
henry = candidates(:henry)
assert_difference("Candidate.find(#{henry.id}).votes.nay.count", 1) do
@ -24,7 +44,7 @@ module Admin
end
test "reviewer can change vote on henry" do
auth_user users(:reviewer)
auth_user users(:reviewer2)
henry = candidates(:henry)
get admin_up_vote_url(henry.test_hash)
@ -36,24 +56,30 @@ module Admin
assert_response :success
end
test "manager can approve henry" do
test "manager can approve henry and notify recruiter" do
auth_user users(:manager)
henry = candidates(:henry)
get admin_approve_vote_url(henry.test_hash)
assert_equal 1, henry.votes.approved.count
assert_enqueued_jobs 1 do
post admin_interview_url(henry.test_hash), params: {
review_status: 'approved',
review_comments: 'ipsum'
}
end
assert_equal 'approved', Candidate.find(henry.to_i).review_status
assert_response :success
assert_equal 'ipsum', Candidate.find(henry.to_i).review_comments
assert_redirected_to admin_result_url(henry.test_hash)
end
test "manager can decline henry" do
test "approve fails without comment" 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
assert_enqueued_jobs 0 do
post admin_interview_url(henry.test_hash), params: { review_status: 'approved' }
end
assert_match 'comment', flash[:error]
assert_redirected_to admin_result_url(henry.test_hash)
end
end
end

View File

@ -9,4 +9,11 @@ class AdminControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to admin_login_path
assert_match 'not authorized', flash[:error]
end
test 'sort_column present' do
# a stupid coverage report thing.
admin_controller = AdminController.new
assert_equal :completed_at, admin_controller.send(:sort_column)
end
end

View File

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

View File

@ -4,6 +4,9 @@ roy: # Roy should have started, and is ready for a reminder
name: Roy Cruz
email: <%= CryptSerializer.dump 'roy.cruz@mailinator.com' %>
experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: false
@ -14,6 +17,9 @@ gillian: # Gillian has not begun the test
name: Gillian Anderson
email: <%= CryptSerializer.dump 'gillian.anderson@mailinator.com' %>
experience: 4-6
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: false
@ -24,6 +30,9 @@ martha: # Martha has not begun the test
name: Martha Watts
email: <%= CryptSerializer.dump 'martha.watts@mailinator.com' %>
experience: 4-6
project: Client/Project
position: 'contract'
skill_needs: 'angular'
recruiter: recruiter
quiz: fed
completed: false
@ -34,6 +43,9 @@ dawn: # Dawn has completed, and been reminded, but not submitted the test
name: Dawn Hopkins
email: <%= CryptSerializer.dump 'dawn.hopkins@mailinator.com' %>
experience: 0-2
project: Client/Project
position: 'contract'
skill_needs: 'javascript'
recruiter: recruiter
quiz: fed
completed: false
@ -44,6 +56,9 @@ peggy: # Peggy has completed, and been reminded, but not submitted the test
name: Peggy Blisters
email: <%= CryptSerializer.dump 'peggy.blisters@mailinator.com' %>
experience: 0-2
project: Client/Project
position: 'contract'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: false
@ -54,17 +69,25 @@ richard: # Richard has completed AND submitted the test
name: Richard Burns
email: <%= CryptSerializer.dump 'richard.burns@mailinator.com' %>
experience: 15+
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current - 20.days %>
reminded: false
test_hash: 6NjnourLE6Y
review_status: 1
review_comments: "Some reasons why or why not, but here they are."
juan: # Juan has chosen "finish later" for live coders
name: Juan Campbell
email: <%= CryptSerializer.dump 'juan.campbell@mailinator.com' %>
experience: 15+
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: false
@ -75,20 +98,29 @@ stacy: # Stacy has completed AND submitted the test
name: Stacy Scott
email: <%= CryptSerializer.dump 'stacy.scott@mailinator.com' %>
experience: 7-9
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current - 13.hours %>
reminded: false
test_hash: s6oFExZliYYFx
review_status: 2
review_comments: "Some reasons why or why not, but here they are."
henry: # Henry has completed AND submitted the test
name: Henry Butler
email: <%= CryptSerializer.dump 'henry.butler@mailinator.com' %>
experience: 4-6
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current - 3.days %>
reminded: false
test_hash: egPomAuVDeCEp
@ -96,16 +128,51 @@ wade: # Wade has completed AND submitted the test
name: Wade Armstrong
email: <%= CryptSerializer.dump 'wade.armstrong@mailinator.com' %>
experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current - 8.days %>
reminded: false
test_hash: BkSkpapJnkz2N
jorge: # Jorge has completed AND submitted the test
name: Jorge Holmes
email: <%= CryptSerializer.dump 'jorge.holmes@mailinator.com' %>
experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current - 12.days + 3.hours %>
reminded: false
test_hash: iC5FdWJxcyySBmpOpU
elsie: # Elsie has completed AND submitted the test
name: Elsie Lowe
email: <%= CryptSerializer.dump 'elsie.lowe@mailinator.com' %>
experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter
quiz: fed
completed: true
completed_at: <%= DateTime.current - 45.days + 6.hours %>
reminded: false
test_hash: rLSoizA3ATMNSCx
gustov: # Gustov is NOT for FED
name: Gustov
email: <%= CryptSerializer.dump 'gustov@mailinator.com' %>
experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'C#, SQL'
recruiter: recruiter
quiz: admin
completed: false

87
test/fixtures/quiz_comments.yml vendored Normal file
View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
com1:
test_hash: BkSkpapJnkz2N #wade
user: reviewer
message: Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Etiam porta sem malesuada magna mollis euismod. Aenean lacinia bibendum nulla sed consectetur. Maecenas faucibus mollis interdum.
com2:
test_hash: BkSkpapJnkz2N #wade
user: reviewer
message: Donec sed odio dui. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Curabitur blandit tempus porttitor. Nullam quis risus eget urna mollis ornare vel eu leo. Nullam id dolor id nibh ultricies vehicula ut id elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
com3:
test_hash: BkSkpapJnkz2N #wade
user: reviewer2
message: Cras mattis consectetur purus sit amet fermentum. Donec sed odio dui. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.
com4:
test_hash: iC5FdWJxcyySBmpOpU #jorge
user: manager
message: Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed posuere consectetur est at lobortis.
com5:
test_hash: egPomAuVDeCEp #henry
user: manager
message: no.
com6:
test_hash: egPomAuVDeCEp #henry
user: reviewer2
message: fine.
com7:
test_hash: iC5FdWJxcyySBmpOpU #jorge
user: reviewer
message: Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
com8:
test_hash: egPomAuVDeCEp #henry
user: manager
message: no.
com9:
test_hash: rLSoizA3ATMNSCx #elsie
user: reviewer
message: Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec ullamcorper nulla non metus auctor fringilla. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Etiam porta sem malesuada magna mollis euismod. Vestibulum id ligula porta felis euismod semper. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Maecenas faucibus mollis interdum.
com10:
test_hash: rLSoizA3ATMNSCx #elsie
user: reviewer2
message: Ornare Tellus Nullam Mattis
com11:
test_hash: rLSoizA3ATMNSCx #elsie
user: reviewer2
message: Nibh Ultricies Purus
com12:
test_hash: rLSoizA3ATMNSCx #elsie
user: reviewer
message: Donec id elit non mi porta gravida at eget metus.
com13:
test_hash: rLSoizA3ATMNSCx #elsie
user: manager
message: Donec id elit non mi porta gravida at eget metus.
com14:
test_hash: rLSoizA3ATMNSCx #elsie
user: reviewer2
message: Ultricies Vulputate Bibendum Parturient
com15:
test_hash: 6NjnourLE6Y #richard
user: manager
message: gibberish
com16:
test_hash: egPomAuVDeCEp #henry
user: admin
message: word.
com17:
test_hash: 6NjnourLE6Y #richard
user: reviewer
message: more gibberish

View File

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

View File

@ -8,4 +8,8 @@ class RecruiterMailerPreview < ActionMailer::Preview
def candidate_submitted
RecruiterMailer.candidate_submitted Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn
end
def candidate_reviewed
RecruiterMailer.candidate_reviewed Candidate.find_by(test_hash: 's6oFExZliYYFx') # Stacy
end
end

View File

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

View File

@ -3,6 +3,7 @@
class UserMailerPreview < ActionMailer::Preview
def password_reset
user = User.find_by(email: 'alan.admin@mailinator.com')
user.send(:gen_reset_token)
UserMailer.password_reset user
end

View File

@ -12,11 +12,24 @@ class RecruiterMailerTest < ActionMailer::TestCase
end
test "candidate_submitted" do
candidate = candidates :dawn
candidate = candidates :elsie
manager = users :manager
mail = RecruiterMailer.candidate_submitted candidate
assert_match candidate.name, mail.subject
assert_equal [candidate.recruiter.email], mail.to
assert_equal [ENV["default_mail_from"]], mail.from
assert_match candidate.name, mail.body.encoded
assert_match manager.name, mail.body.encoded
end
test "candidate_reviewed" do
candidate = candidates :stacy
mail = RecruiterMailer.candidate_reviewed candidate
assert_match candidate.name, mail.subject
assert_equal [candidate.recruiter.email], mail.to
assert_equal [ENV["default_mail_from"]], mail.from
assert_match candidate.review_status, mail.body.encoded
assert_match candidate.name, mail.body.encoded
assert_match candidate.review_comments, mail.body.encoded
end
end

View File

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

View File

@ -6,6 +6,7 @@ class CandidateTest < ActiveSupport::TestCase
candidate = Candidate.create(name: 'new name',
email: 'test@mailinator.com',
experience: '0-3',
project: 'Client',
quiz_id: quizzes(:fed).id)
assert candidate.test_hash.present?
@ -16,6 +17,8 @@ class CandidateTest < ActiveSupport::TestCase
candidate = Candidate.create(name: 'new name',
email: email,
experience: '0-3',
project: 'Client',
position: 'full-time',
recruiter_id: users(:recruiter).id,
quiz_id: quizzes(:fed).id)

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'test_helper'
class QuizCommentTest < ActiveSupport::TestCase
test "the truth" do
assert QuizComment
end
test "user to comments association" do
manager = users(:manager)
assert_equal 5, manager.quiz_comments.size
end
test "candidate to comments association" do
candidate = candidates(:elsie)
assert_equal 6, candidate.quiz_comments.size
end
test 'comment to user' do
comment = quiz_comments(:com1)
assert_match 'Wade', comment.candidate.name
assert_match 'Tina', comment.user.name
end
end

View File

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

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'test_helper'
class QuizCommentPolicyTest < PolicyAssertions::Test
test 'should require current_user' do
assert_raise Pundit::NotAuthorizedError do
QuizCommentPolicy.new(nil, User.first).create?
end
end
def test_create
candidate = candidates(:stacy)
comment = QuizComment.new(test_hash: candidate.test_hash)
assert_permit users(:manager), comment
assert_permit users(:reviewer), comment
assert_permit users(:admin), comment
refute_permit users(:recruiter), comment
end
def test_update
assert_permit users(:reviewer2), quiz_comments(:com6)
refute_permit users(:reviewer), quiz_comments(:com6)
refute_permit users(:manager), quiz_comments(:com6)
refute_permit users(:admin), quiz_comments(:com6)
refute_permit users(:recruiter), quiz_comments(:com6)
end
end

View File

@ -31,8 +31,8 @@ class ReviewerVotePolicyTest < PolicyAssertions::Test
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(: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)
@ -41,22 +41,14 @@ class ReviewerVotePolicyTest < PolicyAssertions::Test
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(: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
def interview_request
assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:admin), reviewer_votes(:manager_henry)

View File

@ -8,6 +8,8 @@ SimpleCov.start 'rails' do
add_group 'Services & Workers', %w(app/workers app/services)
add_group "Jobs", 'app/jobs'
add_group "Policies", 'app/policies'
add_filter "/lib/utils/" # no need to test dev only utility classes
end
require File.expand_path('../../config/environment', __FILE__)
@ -19,6 +21,7 @@ Dir[Rails.root.join("test/test_helpers/**/*.rb")].each { |f| require f }
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
class ActiveSupport::TestCase
ActiveRecord::Migration.maintain_test_schema!
ActiveRecord::Migration.check_pending!
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.

View File

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

View File

@ -97,7 +97,7 @@ class QuizStatusTest < ActiveSupport::TestCase
dawn = candidates :dawn
status = QuizStatus.new dawn
assert_equal nil, status.current_question_id
assert_nil status.current_question_id
end
test "richard has no_finish_laters" do

View File

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