merge in Quiz Review Commenting system

completes #87
This commit is contained in:
Mark Moser 2017-02-14 11:33:43 -06:00
commit d8cab3314c
27 changed files with 537 additions and 39 deletions

View File

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

28
.sass-lint.yml Normal file
View File

@ -0,0 +1,28 @@
# https://github.com/sasstools/sass-lint/tree/master/docs/rules
files:
include: app/assets/stylesheets/**/*.scss
options:
formatter: stylish
merge-default-rules: true
rules:
class-name-format: 0
id-name-format: 0
leading-zero:
- 1
- include: true
no-duplicate-properties:
- 1
-
exclude:
- src # for @font mixins
no-qualifying-elements:
- 1
- allow-element-with-attribute: true # input[type='email'] but not div.class-name
quotes: 0
no-vendor-prefixes:
-
excluded-identifiers:
- -moz-osx-font-smoothing
- -webkit-font-smoothing

View File

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

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,59 @@
}
.review-comments {
float: right;
padding-left: 30px;
width: 33%;
.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) {

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

@ -19,6 +19,8 @@ module Admin
@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
end
end

View File

@ -19,4 +19,11 @@ class ReviewerMailer < ApplicationMailer
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

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

View File

@ -0,0 +1,19 @@
# 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?
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

@ -0,0 +1,21 @@
<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>
<% 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,25 @@
<%= render partial: 'shared/form_model_errors', locals: { obj: comment } %>
<% 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

@ -2,42 +2,52 @@
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>Client/Project:</strong> <%= @candidate.project %><br />
<strong>Recruiter Email:</strong> <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %><br />
<div class="review_meta">
<div>
<strong>Test ID:</strong> <%= @candidate.test_hash %><br />
<strong>Years of Experience:</strong> <%= @candidate.experience %><br />
<strong>Client/Project:</strong> <%= @candidate.project %><br />
<strong>Recruiter Email:</strong> <%= mail_to @candidate.recruiter.name, @candidate.recruiter.email %><br />
</div>
<div><%= render partial: 'voting' %></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="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 class="review-comments">
<h2 class="prft-heading">Comments</h2>
<%= render partial: 'comment', collection: @comments, locals: { test_hash: @candidate.test_hash } %>
<% if policy(QuizComment).new? %>
<%= render partial: 'comment_form', locals: {comment: @comment, test_hash: @candidate.test_hash } %>
<% end %>
</div>
</div>

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

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

@ -52,6 +52,9 @@ Rails.application.routes.draw do
get "/admin/results", to: "admin/result#index", as: :admin_results
get "/admin/result/:test_hash", to: "admin/result#view", as: :admin_result
post "/admin/comment/:test_hash/:id", to: "admin/comment#update", as: :admin_update_comment
post "/admin/comment/:test_hash", to: "admin/comment#create", as: :admin_create_comment
get "admin/vote/:test_hash/up", to: "admin/vote#up", as: :admin_up_vote, defaults: { format: 'json' }
get "admin/vote/:test_hash/down", to: "admin/vote#down", as: :admin_down_vote, defaults: { format: 'json' }
get "admin/vote/:test_hash/approve", to: "admin/vote#approve", as: :admin_approve_vote, defaults: { format: 'json' }

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

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170208212526) do
ActiveRecord::Schema.define(version: 20170210165110) do
create_table "answers", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "candidate_id"
@ -60,6 +60,14 @@ ActiveRecord::Schema.define(version: 20170208212526) do
t.index ["sort"], name: "index_questions_on_sort", using: :btree
end
create_table "quiz_comments", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "user_id"
t.string "test_hash"
t.text "message", limit: 65535
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "quizzes", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "unit"
t.string "dept"

View File

@ -0,0 +1,90 @@
# 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 "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

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

@ -0,0 +1,72 @@
# 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

View File

@ -14,4 +14,8 @@ class ReviewerMailerPreview < ActionMailer::Preview
def notify_manager
ReviewerMailer.notify_manager Candidate.find_by(test_hash: 'OvP0ZqGKwJ0').id # Dawn
end
def new_comment
ReviewerMailer.new_comment QuizComment.first
end
end

View File

@ -30,4 +30,14 @@ class ReviewerMailerTest < ActionMailer::TestCase
assert_equal [ENV["default_mail_from"]], mail.from
assert_match candidate.test_hash, mail.body.encoded
end
test "comment notification" do
comment = quiz_comments(:com5)
mail = ReviewerMailer.new_comment comment
assert_match "Comment", mail.subject
assert_match comment.test_hash, mail.subject
assert_equal comment.candidate.reviewers.map(&:email), mail.to
assert_equal [ENV["default_mail_from"]], mail.from
assert_match comment.test_hash, mail.body.encoded
end
end

View File

@ -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 4, manager.quiz_comments.size
end
test "candidate to comments association" do
candidate = candidates(:elsie)
assert_equal 6, candidate.quiz_comments.size
end
test 'comment to user' do
comment = quiz_comments(:com1)
assert_match 'Wade', comment.candidate.name
assert_match 'Tina', comment.user.name
end
end

View File

@ -3,7 +3,7 @@ require 'test_helper'
class ReviewerVoteTest < ActiveSupport::TestCase
test "the truth" do
assert ReviewerVoteTest
assert ReviewerVote
end
test "richard has 3 votes" do

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