A very fine release.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

View File

@ -18,11 +18,6 @@ function updateVotes(data){
$("[data-id=my-vote]").html(data.myVote);
}
function updateVeto(data){
$("[data-id=interview-request]").html(data.requestCopy);
$("[data-id=interview-decline]").html(data.declineCopy);
}
$(document).ready(function() {
$('[data-id=ajax-action]').each(function(){ handleAjaxResponse($(this)); });
});
@ -30,7 +25,3 @@ $(document).ready(function() {
$(document).ready(function() {
$('[data-id=vote-count]').each(function(){ handleAjaxResponse($(this), updateVotes); });
});
$(document).ready(function() {
$('[data-id=veto-status]').each(function(){ handleAjaxResponse($(this), updateVeto); });
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,54 +5,41 @@ module Admin
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.cast_yea_on(@candidate)
results = {
message: "Vote Counted",
upCount: @candidate.votes.yea.count,
downCount: @candidate.votes.nay.count,
myVote: "yea"
}
render json: results.to_json
end
def down
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.cast_nay_on(@candidate)
results = {
message: "Vote Counted",
upCount: @candidate.votes.yea.count,
downCount: @candidate.votes.nay.count,
myVote: "nay"
}
render json: results.to_json
end
def approve
def interview_request
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.approve_candidate(@candidate)
results = {
message: "Interview requested!",
requestCopy: "Requested",
declineCopy: "Decline Interview"
}
render json: results.to_json
if interview_params[:review_comments].blank?
refuse_interview_request
else
send_interview_request
end
end
def decline
@candidate = Candidate.find_by(test_hash: params[:test_hash])
authorize ReviewerVote.find_by(user_id: current_user.id, candidate_id: @candidate.id)
current_user.decline_candidate(@candidate)
private
results = {
message: "Interview declined.",
requestCopy: "Request Interview",
declineCopy: "Declined"
}
render json: results.to_json
end
def refuse_interview_request
redirect_to admin_result_path(@candidate.test_hash),
flash: { error: "Must provide a comment" }
end
def send_interview_request
current_user.review_candidate(@candidate, interview_params)
RecruiterMailer.candidate_reviewed(@candidate).deliver_later
redirect_to admin_result_path(@candidate.test_hash),
flash: { notice: "Quiz #{interview_params[:review_status]}" }
end
def interview_params
params.permit(:review_status, :review_comments)
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,30 +2,26 @@
class ReviewerVotePolicy < ApplicationPolicy
# Voting Policy
#
# Only Reviewers, Managers, and Admins, can cast a vote on a quiz result
# Only Reviewers and Managers can cast a vote on a quiz result
#
# Reviewers can vote any quiz they are linked to
# Only Managers, and Admins, can veto a quiz result
def up?
return true if user.acts_as_admin?
return false unless user.commented_on?(record.candidate.test_hash)
return false unless record.candidate.reviewers.include? user
return false if user.admin?
user.acts_as_reviewer?
end
def down?
return true if user.acts_as_admin?
return false unless user.commented_on?(record.candidate.test_hash)
return false unless record.candidate.reviewers.include? user
return false if user.admin?
user.acts_as_reviewer?
end
def approve?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_manager?
end
def decline?
def interview_request?
return true if user.acts_as_admin?
return false unless record.candidate.reviewers.include? user
user.acts_as_manager?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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