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/**/* - bin/**/*
- vendor/assets/**/* - vendor/assets/**/*
Lint/Debugger:
AutoCorrect: False
Style/AndOr: Style/AndOr:
Enabled: false 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 'figaro', '~> 1.1.1'
gem 'bcrypt', '~> 3.1.7' gem 'bcrypt', '~> 3.1.7'
gem 'mysql2', '>= 0.3.18', '< 0.5' 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 'jbuilder', '~> 2.6'
gem 'jquery-rails' gem 'jquery-rails'
gem 'json', '~> 2.0.2' gem 'json', '~> 2.0.2'
gem 'kaminari'
gem 'mailjet', '~> 1.3.8' gem 'mailjet', '~> 1.3.8'
gem 'puma', '~> 3.0' gem 'puma', '~> 3.0'
gem 'pundit' gem 'pundit'
@ -53,6 +54,7 @@ group :development, :test do
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
gem 'pry-byebug' gem 'pry-byebug'
gem 'pry-rails' gem 'pry-rails'
gem 'faker'
gem 'brakeman' gem 'brakeman'
gem 'rubocop', '~> 0.42.0' gem 'rubocop', '~> 0.42.0'

View File

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

View File

@ -79,7 +79,7 @@ guard :shell, all_on_start: true do
end end
end end
guard :rubocop, cli: %w(-D -S) do guard :rubocop, cli: %w(-D -S -a) do
watch(/.rubocop.yml/) watch(/.rubocop.yml/)
watch(/.+\.rb$/) watch(/.+\.rb$/)
watch(/Rakefile/) 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); $("[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() { $(document).ready(function() {
$('[data-id=ajax-action]').each(function(){ handleAjaxResponse($(this)); }); $('[data-id=ajax-action]').each(function(){ handleAjaxResponse($(this)); });
}); });
@ -30,7 +25,3 @@ $(document).ready(function() {
$(document).ready(function() { $(document).ready(function() {
$('[data-id=vote-count]').each(function(){ handleAjaxResponse($(this), updateVotes); }); $('[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 { .admin-review {
counter-reset: question; counter-reset: question;
float: left;
width: 66%;
form { form {
margin-left: 2.3em; margin-left: 2.3em;
position: relative; position: relative;
&::before { &::before {
content: counter(question) ") "; content: counter(question) ') ';
counter-increment: question; counter-increment: question;
font-size: 1.25em; font-size: 1.25em;
left: -1.8em; 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 { .review_meta {
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
@ -24,8 +87,32 @@
& > div { flex: 1 1 auto; } & > div { flex: 1 1 auto; }
} }
.review_meta__votes, .review_meta__votes {
.review_meta__vetos { margin-bottom: 15px;
a { padding: 5px; } 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; font-weight: 600;
padding: $small-spacing 0; padding: $small-spacing 0;
text-align: left; 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 { td {

View File

@ -15,7 +15,7 @@ module Admin
if user && user.authenticate(auth_params[:password]) if user && user.authenticate(auth_params[:password])
session[:user] = user.to_i session[:user] = user.to_i
redirect_to admin_path redirect_to session[:request] || admin_path
else else
redirect_to admin_login_path, redirect_to admin_login_path,
flash: { error: "Sorry, incorrect email or password. Please try again." } 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] before_action :collect_quizzes, except: [:login, :auth]
def index def index
@candidates = policy_scope Candidate.order(:name) @candidates = policy_scope Candidate.order("#{sort_column} #{sort_direction}")
.page(params[:page])
end end
def new def new
@ -49,13 +50,14 @@ module Admin
authorize Candidate authorize Candidate
candidate = Candidate.find_by(id: params[:id]) candidate = Candidate.find_by(id: params[:id])
CandidateMailer.welcome(candidate).deliver_later CandidateMailer.welcome(candidate).deliver_later
render json: { message: "Email queued!" }.to_json
end end
private private
def candidate_params 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 end
def collect_quizzes def collect_quizzes
@ -66,5 +68,9 @@ module Admin
CandidateMailer.welcome(candidate).deliver_later CandidateMailer.welcome(candidate).deliver_later
RecruiterMailer.candidate_created(candidate).deliver_later RecruiterMailer.candidate_created(candidate).deliver_later
end end
def sort_column
Candidate.column_names.include?(params[:sort]) ? params[:sort] : 'name'
end
end 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 # frozen_string_literal: true
module Admin module Admin
class ResultController < AdminController class ResultController < AdminController
#
# TODO: change context from Candidate to Quiz # TODO: change context from Candidate to Quiz
# bypass pundit lockdowns until completed # bypass pundit lockdowns until completed
after_action :skip_policy_scope after_action :skip_policy_scope
@ -10,13 +9,30 @@ module Admin
# TODO: Limit results to the quizzes current_user has access to # TODO: Limit results to the quizzes current_user has access to
def index 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 end
def view def view
@candidate = Candidate.find_by(test_hash: params[:test_hash]) @candidate = Candidate.find_by(test_hash: params[:test_hash])
@quiz = @candidate.my_quiz @quiz = @candidate.my_quiz
@status = QuizStatus.new(@candidate) @status = QuizStatus.new(@candidate)
@comments = QuizComment.includes(:user).where(test_hash: @candidate.test_hash).order(:created_at)
@comment = QuizComment.new
end 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
end end

View File

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

View File

@ -5,54 +5,41 @@ module Admin
@candidate = Candidate.find_by(test_hash: params[:test_hash]) @candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id) authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.cast_yea_on(@candidate) 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 end
def down def down
@candidate = Candidate.find_by(test_hash: params[:test_hash]) @candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id) authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.cast_nay_on(@candidate) 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 end
def approve def interview_request
@candidate = Candidate.find_by(test_hash: params[:test_hash]) @candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id) authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.approve_candidate(@candidate)
results = { if interview_params[:review_comments].blank?
message: "Interview requested!", refuse_interview_request
requestCopy: "Requested", else
declineCopy: "Decline Interview" send_interview_request
} end
render json: results.to_json
end end
def decline private
@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 = { def refuse_interview_request
message: "Interview declined.", redirect_to admin_result_path(@candidate.test_hash),
requestCopy: "Request Interview", flash: { error: "Must provide a comment" }
declineCopy: "Declined" end
}
render json: results.to_json def send_interview_request
end 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
end end

View File

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

View File

@ -35,7 +35,7 @@ class QuizController < ApplicationController
private private
def complete_and_email 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 current_candidate.build_reviews
CandidateMailer.submitted(current_candidate).deliver_later CandidateMailer.submitted(current_candidate).deliver_later
RecruiterMailer.candidate_submitted(current_candidate).deliver_later RecruiterMailer.candidate_submitted(current_candidate).deliver_later

View File

@ -44,4 +44,11 @@ module ApplicationHelper
@js_blocks << code_label @js_blocks << code_label
content_for :custom_javascipt, &block content_for :custom_javascipt, &block
end 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 end

View File

@ -1,14 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
class RecruiterMailer < ApplicationMailer class RecruiterMailer < ApplicationMailer
def candidate_created candidate 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 end
def candidate_submitted candidate 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
end end

View File

@ -6,4 +6,24 @@ class ReviewerMailer < ApplicationMailer
mail to: recipients, subject: "Skills Assessment Results - #{@candidate.test_hash}" mail to: recipients, subject: "Skills Assessment Results - #{@candidate.test_hash}"
end 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 end

View File

@ -6,6 +6,7 @@ class Candidate < ApplicationRecord
belongs_to :recruiter, class_name: "User" belongs_to :recruiter, class_name: "User"
has_many :votes, class_name: "ReviewerVote" has_many :votes, class_name: "ReviewerVote"
has_many :reviewers, through: :quiz has_many :reviewers, through: :quiz
has_many :quiz_comments, foreign_key: :test_hash, primary_key: :test_hash
serialize :email, CryptSerializer serialize :email, CryptSerializer
@ -14,6 +15,8 @@ class Candidate < ApplicationRecord
validates :recruiter_id, presence: true validates :recruiter_id, presence: true
validates :name, presence: true validates :name, presence: true
validates :experience, presence: true validates :experience, presence: true
validates :project, presence: true
validates :position, presence: true
validates :email, uniqueness: true, presence: true, email_format: true validates :email, uniqueness: true, presence: true, email_format: true
validates :test_hash, uniqueness: true, presence: true validates :test_hash, uniqueness: true, presence: true
@ -23,12 +26,24 @@ class Candidate < ApplicationRecord
declined: 2 declined: 2
} }
def interview?
return 'yes' if approved?
'no' if declined?
end
def build_reviews def build_reviews
reviewers.each do |reviewer| reviewers.each do |reviewer|
votes.find_or_create_by(user_id: reviewer.id) votes.find_or_create_by(user_id: reviewer.id)
end end
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 def submitted_answers
answers.where(submitted: true) answers.where(submitted: true)
end 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 } validates :user_id, uniqueness: { scope: :candidate_id }
after_save :notify_manager
enum vote: { enum vote: {
undecided: 0, undecided: 0,
yea: 1, yea: 1,
@ -13,6 +15,40 @@ class ReviewerVote < ApplicationRecord
enum veto: { enum veto: {
approved: 1, 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 end

View File

@ -5,6 +5,7 @@ class User < ApplicationRecord
has_many :reviewer_to_quizzes has_many :reviewer_to_quizzes
has_many :quizzes, through: :reviewer_to_quizzes has_many :quizzes, through: :reviewer_to_quizzes
has_many :votes, class_name: 'ReviewerVote' has_many :votes, class_name: 'ReviewerVote'
has_many :quiz_comments
has_many :reviewees, through: :quizzes, source: :candidates has_many :reviewees, through: :quizzes, source: :candidates
@ -18,7 +19,13 @@ class User < ApplicationRecord
save save
end end
def commented_on? test_hash
quiz_comments.where(test_hash: test_hash).count.positive?
end
# Voting # Voting
# TODO: Refactor this out of User, belongs on ReviewerVote
# ie: cast_yea(candidate, user)
def cast_yea_on candidate def cast_yea_on candidate
vote = votes.find_by(candidate_id: candidate.to_i) vote = votes.find_by(candidate_id: candidate.to_i)
vote.vote = :yea vote.vote = :yea
@ -31,20 +38,16 @@ class User < ApplicationRecord
vote.save vote.save
end end
def approve_candidate candidate def review_candidate candidate, parms_hash
candidate = Candidate.find(candidate.to_i) candidate = Candidate.find(candidate.to_i)
vote = votes.find_by(candidate_id: candidate.to_i) vote = votes.find_by(candidate_id: candidate.to_i)
vote.veto = :approved vote.veto = parms_hash[:review_status]
candidate.update_attribute(:review_status, :approved) if vote.save if vote.save
end # skipping validations on candidate because that's not the managers responsibility
candidate.review_comments = parms_hash[:review_comments]
def decline_candidate candidate candidate.update_attribute(:review_status, parms_hash[:review_status])
candidate = Candidate.find(candidate.to_i) end
vote = votes.find_by(candidate_id: candidate.to_i)
vote.veto = :rejected
candidate.update_attribute(:review_status, :declined) if vote.save
end end
def my_vote candidate 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 class ReviewerVotePolicy < ApplicationPolicy
# Voting Policy # 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 # Reviewers can vote any quiz they are linked to
# Only Managers, and Admins, can veto a quiz result # Only Managers, and Admins, can veto a quiz result
def up? 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 unless record.candidate.reviewers.include? user
return false if user.admin?
user.acts_as_reviewer? user.acts_as_reviewer?
end end
def down? 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 unless record.candidate.reviewers.include? user
return false if user.admin?
user.acts_as_reviewer? user.acts_as_reviewer?
end end
def approve? def interview_request?
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 true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user return false unless record.candidate.reviewers.include? user
user.acts_as_manager? user.acts_as_manager?

View File

@ -16,6 +16,24 @@
<%= form.select :experience, experience_options(candidate.experience), include_blank: false %> <%= form.select :experience, experience_options(candidate.experience), include_blank: false %>
</div> </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"> <div class="form-group">
<%= form.label :quiz_id, "Quiz" %> <%= form.label :quiz_id, "Quiz" %>
<%= form.select :quiz_id, quiz_options(quizzes, candidate.quiz_id), include_blank: (quizzes.size > 1) %> <%= 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"> <table cellspacing="0" cellpadding="0">
<tr> <tr>
<th>Candidate</th> <th><%= sortable "name", "Candidate" %></th>
<th>Test ID</th> <th><%= sortable "test_hash", "Test ID" %></th>
<th>Email</th> <th><%= sortable "email" %></th>
<th>Experience</th> <th><%= sortable "experience" %></th>
<th>Progress</th> <th>Progress</th>
<th>Completed</th> <th><%= sortable "completed_at", "Completed" %></th>
<th>Reminded</th> <th><%= sortable "reminded" %></th>
<th>Interview Request</th> <th>Interview?</th>
</tr> </tr>
<% @candidates.each do |candidate| %> <% @candidates.each do |candidate| %>
@ -32,8 +32,9 @@
<td><%= candidate.status %></td> <td><%= candidate.status %></td>
<td><%= candidate.completed ? link_to("Submitted", admin_result_path(candidate.test_hash)) : "" %></td> <td><%= candidate.completed ? link_to("Submitted", admin_result_path(candidate.test_hash)) : "" %></td>
<td><%= candidate.reminded ? "Yes" : "" %></td> <td><%= candidate.reminded ? "Yes" : "" %></td>
<td><%= candidate.review_status unless candidate.pending? %></td> <td><%= candidate.interview? %></td>
</tr> </tr>
<% end %> <% end %>
</table> </table>
<%= paginate @candidates %>
</main> </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? %> <% if current_user.acts_as_reviewer? %>
<div class="review_meta__votes" data-id="vote-count"> <div class="review_meta__votes" data-id="vote-count">
<strong>Votes: </strong> <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>) <% if @candidate.pending? && current_user.commented_on?(@candidate.test_hash) %>
<% end %> <%= link_to admin_up_vote_path(test_hash: @candidate.test_hash), remote: true do %>
<%= link_to admin_down_vote_path(test_hash: @candidate.test_hash), remote: true do %> Yea (<span data-id="up-votes"><%= @candidate.votes.yea.count %></span>)
Nay (<span data-id="down-votes"><%= @candidate.votes.nay.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 %> <% end %>
<small>(Your vote: <span data-id="my-vote"><%= current_user.my_vote(@candidate) %></span>)</small> <small>(Your vote: <span data-id="my-vote"><%= current_user.my_vote(@candidate) %></span>)</small>
</div> </div>
<% end %> <% end %>
<% if current_user.acts_as_manager? %> <% if current_user.acts_as_manager? %>
<div class="review_meta__vetos" data-id="veto-status"> <div class="review_meta__vetos">
<strong>Manager Vetos: </strong> <%= form_tag admin_interview_path(test_hash: @candidate.test_hash) do %>
<%= link_to admin_approve_vote_path(test_hash: @candidate.test_hash), remote: true do %> <strong>Interview: </strong>
<span data-id="interview-request">
<%= @candidate.approved? ? "Requested" : "Request Interview" %> <%= radio_button_tag :review_status, :approved, checked = @candidate.approved? %>
</span> <%= label_tag :review_status_approved, "Yes" %>
<% end %>
<%= link_to admin_decline_vote_path(test_hash: @candidate.test_hash), remote: true do %> <%= radio_button_tag :review_status, :declined, checked = @candidate.declined? %>
<span data-id="interview-decline"> <%= label_tag :review_status_declined, "No" %>
<%= @candidate.declined? ? "Declined" : "Decline Interview" %>
</span> <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 %> <% end %>
</div> </div>
<% else %> <% else %>
<strong>Candidate Interview Status: </strong><%= @candidate.review_status %> <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 %> <% end %>

View File

@ -5,19 +5,24 @@
<main class="summary_tpl"> <main class="summary_tpl">
<table cellspacing="0" cellpadding="0"> <table cellspacing="0" cellpadding="0">
<tr> <tr>
<th>Test ID</th> <th><%= sortable "test_hash", "Test ID" %></th>
<th>Experience</th> <th><%= sortable "name" %></th>
<th><%= sortable "project", "Client/Project" %></th>
<th>Recruiter</th> <th>Recruiter</th>
<th>Interview Request</th> <th><%= sortable "completed_at", "Submitted on" %></th>
<th>Interview?</th>
</tr> </tr>
<% @candidates.each do |candidate| %> <% @candidates.each do |candidate| %>
<tr> <tr>
<td><%= link_to candidate.test_hash, admin_result_path(candidate.test_hash) %></td> <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><%= 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> </tr>
<% end %> <% end %>
</table> </table>
<%= paginate @candidates %>
</main> </main>

View File

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

View File

@ -1,8 +1,8 @@
<table cellspacing="0" cellpadding="0"> <table cellspacing="0" cellpadding="0">
<tr> <tr>
<th>User</th> <th><%= sortable "name", "User" %></th>
<th>Email</th> <th><%= sortable "email" %></th>
<th>Role</th> <th><%= sortable "role" %></th>
<th></th> <th></th>
</tr> </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> <table>
<tr> <tr>
<td class="email-copyright"> <td class="email-copyright">
&copy;2016 All Rights Reserved - Perficient Digital &copy;2016-<%= Time.now.year %> All Rights Reserved - Perficient Digital
</td> </td>
<td class="email-logo"> <td class="email-logo">
<%= image_tag(attachments["perficientdigital-logo.jpg"].url, alt:"Perficient Digital") %> <%= image_tag(attachments["perficientdigital-logo.jpg"].url, alt:"Perficient Digital") %>

View File

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

View File

@ -6,5 +6,6 @@ Candidate name: <%= @candidate.name %>
Candidate email: <%= @candidate.email %> Candidate email: <%= @candidate.email %>
Candidate ID: <%= @candidate.test_hash %> Candidate ID: <%= @candidate.test_hash %>
Years of experience: <%= @candidate.experience %> Years Years of experience: <%= @candidate.experience %> Years
Client/Project: <%= @candidate.project %>
You will be notified when the candidate has finished taking the test. 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> <row>
<columns class="email-body"> <columns class="email-body">
<p><strong><%= @candidate.name %></strong> has completed the Skills Assessment Test.</p> <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> </columns>
</row> </row>

View File

@ -1,4 +1,4 @@
PERFICIENT/digital - Skills Assessment Test PERFICIENT/digital - Skills Assessment Test
<%= @candidate.name %> has completed the 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> <row>
<columns class="email-body"> <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> <p>You can view the results here: <%= link_to nil, admin_result_url(@candidate.test_hash) %>.</p>
</columns> </columns>
</row> </row>

View File

@ -1,5 +1,5 @@
PERFICIENT/digital SKILLS ASSESSMENT RESULTS 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) %>. 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 # frozen_string_literal: true
class Reminder class CandidateReminder
def initialize def initialize
@collection = reminder_collection @collection = reminder_collection
end 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.helper false
g.routes false g.routes false
end end
# let's use lib/utils for dev only utility classes & helpers
config.autoload_paths << "#{Rails.root}/lib/utils"
end end
end end

View File

@ -1,10 +1,81 @@
{ {
"ignored_warnings": [ "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", "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" "note": "false positive"
} }
], ],
"updated": "2016-09-19 09:06:25 -0500", "updated": "2017-03-06 12:16:23 -0600",
"brakeman_version": "3.4.0" "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 # 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 Rails.application.routes.draw do
get "/styleguide", to: "application#styleguide", as: :styleguide get "/styleguide", to: "application#styleguide", as: :styleguide
get "/admin/styleguide", to: "application#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 post "/admin/candidate/new", to: "admin/candidate#create", as: :admin_create_candidate
get "/admin/candidate/:id", to: "admin/candidate#edit", as: :admin_edit_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 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/results", to: "admin/result#index", as: :admin_results
get "/admin/result/:test_hash", to: "admin/result#view", as: :admin_result 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 post "/admin/comment/:test_hash/:id", to: "admin/comment#update", as: :admin_update_comment
get "admin/vote/:test_hash/down", to: "admin/vote#down", as: :admin_down_vote post "/admin/comment/:test_hash", to: "admin/comment#create", as: :admin_create_comment
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/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 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. # 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| create_table "answers", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "candidate_id" t.integer "candidate_id"
@ -30,13 +30,18 @@ ActiveRecord::Schema.define(version: 20161120175737) do
t.string "name" t.string "name"
t.string "email" t.string "email"
t.string "experience" t.string "experience"
t.string "project"
t.string "position"
t.string "skill_needs"
t.integer "recruiter_id" t.integer "recruiter_id"
t.boolean "completed" t.boolean "completed"
t.datetime "completed_at"
t.boolean "reminded" t.boolean "reminded"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "quiz_id" 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 ["quiz_id"], name: "index_candidates_on_quiz_id", using: :btree
t.index ["recruiter_id"], name: "index_candidates_on_recruiter_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 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 t.index ["sort"], name: "index_questions_on_sort", using: :btree
end 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| create_table "quizzes", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "unit" t.string "unit"
t.string "dept" 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| create_table "reviewer_votes", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "candidate_id" t.integer "candidate_id"
t.integer "user_id" t.integer "user_id"
t.integer "vote", default: 0, null: false t.integer "vote", default: 0, null: false
t.integer "veto", default: 0, null: false t.integer "veto", default: 0, null: false
t.datetime "last_reminded" t.datetime "created_at", null: false
t.boolean "locked", default: false, null: false t.datetime "updated_at", 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 t.index ["candidate_id", "user_id"], name: "index_reviewer_votes_on_candidate_id_and_user_id", unique: true, using: :btree
end end

View File

@ -2,7 +2,13 @@
namespace :reminders do namespace :reminders do
desc "send reminders to stagnate quizes" desc "send reminders to stagnate quizes"
task send_all: :environment do 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 reminders.send_all
end end
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 assert_redirected_to admin_url
end 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 test "should FAIL auth" do
post admin_auth_url, params: { auth: post admin_auth_url, params: { auth:
{ email: 'alan.admin@mailinator.com', password: 'b@d9a$$werD' } } { email: 'alan.admin@mailinator.com', password: 'b@d9a$$werD' } }

View File

@ -23,8 +23,14 @@ module Admin
assert_enqueued_jobs 2 do assert_enqueued_jobs 2 do
assert_difference("Candidate.count") do assert_difference("Candidate.count") do
post admin_create_candidate_path, params: { candidate: post admin_create_candidate_path, params: { candidate: {
{ name: 'new name', email: 'test@mailinator.com', experience: '0-3', quiz_id: quizzes(:fed).id } } name: 'new name',
email: 'test@mailinator.com',
experience: '0-3',
position: 'full-time',
project: 'client project',
quiz_id: quizzes(:fed).id
} }
end end
end end
assert_redirected_to admin_candidates_path 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 module Admin
class VoteControllerTest < ActionDispatch::IntegrationTest 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) auth_user users(:reviewer)
henry = candidates(:henry) 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 assert_difference("Candidate.find(#{henry.id}).votes.yea.count", 1) do
get admin_up_vote_url(henry.test_hash) get admin_up_vote_url(henry.test_hash)
end end
@ -14,7 +34,7 @@ module Admin
end end
test "reviewer can down vote henry" do test "reviewer can down vote henry" do
auth_user users(:reviewer) auth_user users(:reviewer2)
henry = candidates(:henry) henry = candidates(:henry)
assert_difference("Candidate.find(#{henry.id}).votes.nay.count", 1) do assert_difference("Candidate.find(#{henry.id}).votes.nay.count", 1) do
@ -24,7 +44,7 @@ module Admin
end end
test "reviewer can change vote on henry" do test "reviewer can change vote on henry" do
auth_user users(:reviewer) auth_user users(:reviewer2)
henry = candidates(:henry) henry = candidates(:henry)
get admin_up_vote_url(henry.test_hash) get admin_up_vote_url(henry.test_hash)
@ -36,24 +56,30 @@ module Admin
assert_response :success assert_response :success
end end
test "manager can approve henry" do test "manager can approve henry and notify recruiter" do
auth_user users(:manager) auth_user users(:manager)
henry = candidates(:henry) 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_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 end
test "manager can decline henry" do test "approve fails without comment" do
auth_user users(:manager) auth_user users(:manager)
henry = candidates(:henry) henry = candidates(:henry)
get admin_decline_vote_url(henry.test_hash)
assert_equal 1, henry.votes.rejected.count assert_enqueued_jobs 0 do
assert_equal 'declined', Candidate.find(henry.to_i).review_status post admin_interview_url(henry.test_hash), params: { review_status: 'approved' }
assert_response :success end
assert_match 'comment', flash[:error]
assert_redirected_to admin_result_url(henry.test_hash)
end end
end end
end end

View File

@ -9,4 +9,11 @@ class AdminControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to admin_login_path assert_redirected_to admin_login_path
assert_match 'not authorized', flash[:error] assert_match 'not authorized', flash[:error]
end 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 end

View File

@ -696,3 +696,197 @@ wade10:
created_at: <%= DateTime.now() - 36.hours - 40.minutes %> created_at: <%= DateTime.now() - 36.hours - 40.minutes %>
updated_at: <%= DateTime.now() - 36.hours - 20.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 name: Roy Cruz
email: <%= CryptSerializer.dump 'roy.cruz@mailinator.com' %> email: <%= CryptSerializer.dump 'roy.cruz@mailinator.com' %>
experience: 0-3 experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: false completed: false
@ -14,6 +17,9 @@ gillian: # Gillian has not begun the test
name: Gillian Anderson name: Gillian Anderson
email: <%= CryptSerializer.dump 'gillian.anderson@mailinator.com' %> email: <%= CryptSerializer.dump 'gillian.anderson@mailinator.com' %>
experience: 4-6 experience: 4-6
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: false completed: false
@ -24,6 +30,9 @@ martha: # Martha has not begun the test
name: Martha Watts name: Martha Watts
email: <%= CryptSerializer.dump 'martha.watts@mailinator.com' %> email: <%= CryptSerializer.dump 'martha.watts@mailinator.com' %>
experience: 4-6 experience: 4-6
project: Client/Project
position: 'contract'
skill_needs: 'angular'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: false completed: false
@ -34,6 +43,9 @@ dawn: # Dawn has completed, and been reminded, but not submitted the test
name: Dawn Hopkins name: Dawn Hopkins
email: <%= CryptSerializer.dump 'dawn.hopkins@mailinator.com' %> email: <%= CryptSerializer.dump 'dawn.hopkins@mailinator.com' %>
experience: 0-2 experience: 0-2
project: Client/Project
position: 'contract'
skill_needs: 'javascript'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: false completed: false
@ -44,6 +56,9 @@ peggy: # Peggy has completed, and been reminded, but not submitted the test
name: Peggy Blisters name: Peggy Blisters
email: <%= CryptSerializer.dump 'peggy.blisters@mailinator.com' %> email: <%= CryptSerializer.dump 'peggy.blisters@mailinator.com' %>
experience: 0-2 experience: 0-2
project: Client/Project
position: 'contract'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: false completed: false
@ -54,17 +69,25 @@ richard: # Richard has completed AND submitted the test
name: Richard Burns name: Richard Burns
email: <%= CryptSerializer.dump 'richard.burns@mailinator.com' %> email: <%= CryptSerializer.dump 'richard.burns@mailinator.com' %>
experience: 15+ experience: 15+
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: true completed: true
completed_at: <%= DateTime.current - 20.days %>
reminded: false reminded: false
test_hash: 6NjnourLE6Y test_hash: 6NjnourLE6Y
review_status: 1 review_status: 1
review_comments: "Some reasons why or why not, but here they are."
juan: # Juan has chosen "finish later" for live coders juan: # Juan has chosen "finish later" for live coders
name: Juan Campbell name: Juan Campbell
email: <%= CryptSerializer.dump 'juan.campbell@mailinator.com' %> email: <%= CryptSerializer.dump 'juan.campbell@mailinator.com' %>
experience: 15+ experience: 15+
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: false completed: false
@ -75,20 +98,29 @@ stacy: # Stacy has completed AND submitted the test
name: Stacy Scott name: Stacy Scott
email: <%= CryptSerializer.dump 'stacy.scott@mailinator.com' %> email: <%= CryptSerializer.dump 'stacy.scott@mailinator.com' %>
experience: 7-9 experience: 7-9
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: true completed: true
completed_at: <%= DateTime.current - 13.hours %>
reminded: false reminded: false
test_hash: s6oFExZliYYFx test_hash: s6oFExZliYYFx
review_status: 2 review_status: 2
review_comments: "Some reasons why or why not, but here they are."
henry: # Henry has completed AND submitted the test henry: # Henry has completed AND submitted the test
name: Henry Butler name: Henry Butler
email: <%= CryptSerializer.dump 'henry.butler@mailinator.com' %> email: <%= CryptSerializer.dump 'henry.butler@mailinator.com' %>
experience: 4-6 experience: 4-6
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: true completed: true
completed_at: <%= DateTime.current - 3.days %>
reminded: false reminded: false
test_hash: egPomAuVDeCEp test_hash: egPomAuVDeCEp
@ -96,16 +128,51 @@ wade: # Wade has completed AND submitted the test
name: Wade Armstrong name: Wade Armstrong
email: <%= CryptSerializer.dump 'wade.armstrong@mailinator.com' %> email: <%= CryptSerializer.dump 'wade.armstrong@mailinator.com' %>
experience: 0-3 experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'css/html'
recruiter: recruiter recruiter: recruiter
quiz: fed quiz: fed
completed: true completed: true
completed_at: <%= DateTime.current - 8.days %>
reminded: false reminded: false
test_hash: BkSkpapJnkz2N 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 gustov: # Gustov is NOT for FED
name: Gustov name: Gustov
email: <%= CryptSerializer.dump 'gustov@mailinator.com' %> email: <%= CryptSerializer.dump 'gustov@mailinator.com' %>
experience: 0-3 experience: 0-3
project: Client/Project
position: 'full-time'
skill_needs: 'C#, SQL'
recruiter: recruiter recruiter: recruiter
quiz: admin quiz: admin
completed: false 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 candidate: wade
user: reviewer2 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 def candidate_submitted
RecruiterMailer.candidate_submitted Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn RecruiterMailer.candidate_submitted Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn
end end
def candidate_reviewed
RecruiterMailer.candidate_reviewed Candidate.find_by(test_hash: 's6oFExZliYYFx') # Stacy
end
end end

View File

@ -4,4 +4,18 @@ class ReviewerMailerPreview < ActionMailer::Preview
def candidate_submission def candidate_submission
ReviewerMailer.candidate_submission Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn ReviewerMailer.candidate_submission Candidate.find_by(test_hash: 'OvP0ZqGKwJ0') # Dawn
end 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 end

View File

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

View File

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

View File

@ -11,4 +11,33 @@ class ReviewerMailerTest < ActionMailer::TestCase
assert_equal [ENV["default_mail_from"]], mail.from assert_equal [ENV["default_mail_from"]], mail.from
assert_match candidate.test_hash, mail.body.encoded assert_match candidate.test_hash, mail.body.encoded
end 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 end

View File

@ -6,6 +6,7 @@ class CandidateTest < ActiveSupport::TestCase
candidate = Candidate.create(name: 'new name', candidate = Candidate.create(name: 'new name',
email: 'test@mailinator.com', email: 'test@mailinator.com',
experience: '0-3', experience: '0-3',
project: 'Client',
quiz_id: quizzes(:fed).id) quiz_id: quizzes(:fed).id)
assert candidate.test_hash.present? assert candidate.test_hash.present?
@ -16,6 +17,8 @@ class CandidateTest < ActiveSupport::TestCase
candidate = Candidate.create(name: 'new name', candidate = Candidate.create(name: 'new name',
email: email, email: email,
experience: '0-3', experience: '0-3',
project: 'Client',
position: 'full-time',
recruiter_id: users(:recruiter).id, recruiter_id: users(:recruiter).id,
quiz_id: quizzes(:fed).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 class ReviewerVoteTest < ActiveSupport::TestCase
test "the truth" do test "the truth" do
assert ReviewerVoteTest assert ReviewerVote
end end
test "richard has 3 votes" do test "richard has 3 votes" do
@ -12,10 +12,11 @@ class ReviewerVoteTest < ActiveSupport::TestCase
assert_equal 3, richard.votes.size assert_equal 3, richard.votes.size
end end
test "manager has 4 votes" do test "manager has a vote for every completed quiz" do
manager = users(:manager) manager = users(:manager)
completed_count = Candidate.where(completed: true).count
assert_equal 4, manager.votes.size assert_equal completed_count, manager.votes.size
end end
test "richard has been approved" do test "richard has been approved" do
@ -31,4 +32,22 @@ class ReviewerVoteTest < ActiveSupport::TestCase
assert stacy.declined? assert stacy.declined?
refute stacy.approved? refute stacy.approved?
end 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 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 def test_up
assert_permit users(:manager), reviewer_votes(:manager_richard) assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:reviewer), reviewer_votes(:reviewer_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(:recruiter), reviewer_votes(:manager_henry)
refute_permit users(:reviewer), reviewer_votes(:gustov) refute_permit users(:reviewer), reviewer_votes(:gustov)
refute_permit users(:manager), reviewer_votes(:gustov) refute_permit users(:manager), reviewer_votes(:gustov)
@ -41,22 +41,14 @@ class ReviewerVotePolicyTest < PolicyAssertions::Test
def test_down def test_down
assert_permit users(:manager), reviewer_votes(:manager_richard) assert_permit users(:manager), reviewer_votes(:manager_richard)
assert_permit users(:reviewer), reviewer_votes(:reviewer_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(:recruiter), reviewer_votes(:manager_henry)
refute_permit users(:reviewer), reviewer_votes(:gustov) refute_permit users(:reviewer), reviewer_votes(:gustov)
refute_permit users(:manager), reviewer_votes(:gustov) refute_permit users(:manager), reviewer_votes(:gustov)
end end
def approve def interview_request
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(:manager), reviewer_votes(:manager_richard)
assert_permit users(:admin), reviewer_votes(:manager_henry) 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 'Services & Workers', %w(app/workers app/services)
add_group "Jobs", 'app/jobs' add_group "Jobs", 'app/jobs'
add_group "Policies", 'app/policies' add_group "Policies", 'app/policies'
add_filter "/lib/utils/" # no need to test dev only utility classes
end end
require File.expand_path('../../config/environment', __FILE__) 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)] Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
class ActiveSupport::TestCase class ActiveSupport::TestCase
ActiveRecord::Migration.maintain_test_schema!
ActiveRecord::Migration.check_pending! ActiveRecord::Migration.check_pending!
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.

View File

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

View File

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