diff --git a/Gemfile b/Gemfile index d8447a6..90f6e82 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,7 @@ group :development, :test do gem 'byebug', platform: :mri gem 'pry-byebug' gem 'pry-rails' + gem 'table_print' gem 'faker' gem 'brakeman' diff --git a/Gemfile.lock b/Gemfile.lock index bbe87c3..a9e6a8f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -280,6 +280,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + table_print (1.5.6) thor (0.19.4) thread_safe (0.3.5) tilt (2.0.5) @@ -348,6 +349,7 @@ DEPENDENCIES simplecov spring spring-watcher-listen (~> 2.0.0) + table_print turbolinks (~> 5) tzinfo-data uglifier (>= 1.3.0) diff --git a/app/controllers/admin/result_controller.rb b/app/controllers/admin/result_controller.rb index 48bb2b5..c3eeee0 100644 --- a/app/controllers/admin/result_controller.rb +++ b/app/controllers/admin/result_controller.rb @@ -1,20 +1,16 @@ # 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 + # TODO: bypass pundit authorization until a result wrapper class if sorted after_action :skip_authorization - # + # needed for :view - # TODO: Limit results to the quizzes current_user has access to def index 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]) + @candidates = policy_scope(:result).includes(:recruiter) + .order("#{sort_with_case} #{sort_direction}") + .page(params[:page]) end def view diff --git a/app/controllers/candidate_controller.rb b/app/controllers/candidate_controller.rb index 0cd4f4c..e052666 100644 --- a/app/controllers/candidate_controller.rb +++ b/app/controllers/candidate_controller.rb @@ -45,6 +45,7 @@ class CandidateController < ApplicationController end def send_to_oops + redirect_to welcome_path and return if current_candidate && current_candidate.stale? redirect_to oops_path if current_candidate end end diff --git a/app/models/candidate.rb b/app/models/candidate.rb index 56083a1..8587779 100644 --- a/app/models/candidate.rb +++ b/app/models/candidate.rb @@ -48,6 +48,17 @@ class Candidate < ApplicationRecord answers.where(submitted: true) end + def last_answered_at + return Time.current unless submitted_answers.count.positive? + submitted_answers.order(updated_at: :desc).first.updated_at + end + + def stale? + return true unless answers.count.positive? + minutes_since_answered = (Time.current.minus_with_coercion(last_answered_at) / 60).round + minutes_since_answered > 45 + end + def answered_questions answers.where.not(answer: nil) .where("answers.answer not like '%later:%'") diff --git a/app/policies/quiz_policy.rb b/app/policies/quiz_policy.rb index 6f80113..f70114d 100644 --- a/app/policies/quiz_policy.rb +++ b/app/policies/quiz_policy.rb @@ -25,10 +25,10 @@ class QuizPolicy < ApplicationPolicy class Scope < Scope def resolve - if user.reviewer? - scope.joins(:reviewers).where('reviewer_to_quizzes.user_id = ?', user.id) - else + if user.acts_as_recruiter? scope + else + scope.joins(:reviewers).where('reviewer_to_quizzes.user_id = ?', user.id) end end end diff --git a/app/policies/result_policy.rb b/app/policies/result_policy.rb new file mode 100644 index 0000000..60934df --- /dev/null +++ b/app/policies/result_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +class ResultPolicy < Struct.new(:user, :result) + # Result Access Policy + # + # Only Admins and Recruiters can view all results + # Managers and Reviewers can view any completed quiz they are linked to + + attr_reader :user, :record + + def initialize(user, record) + raise Pundit::NotAuthorizedError, "Must be logged in." unless user + @user = user + @record = record + end + + def index? + true + end + + # def view? + # return true if user.acts_as_recruiter? + # user.reviewees.include? record + # end + + class Scope + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + if user.acts_as_recruiter? + Candidate.where(completed: true) + else + user.reviewees.where(completed: true) + end + end + end +end diff --git a/db/seeds.rb b/db/seeds.rb index e1186d7..fe97609 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -8,13 +8,13 @@ User.create( name: 'admin', - email: 'pdr.admin@mailinator.com', + email: 'pda.admin@mailinator.com', password_digest: BCrypt::Password.create("this is the admin password"), role: 'admin' ) Quiz.create( - name: 'PDR Standard FED Screening', + name: 'PDA Standard FED Screening', unit: 'FED', - dept: 'PDR' + dept: 'PDA' ) diff --git a/erd.pdf b/erd.pdf index 35d725d..306fb65 100644 Binary files a/erd.pdf and b/erd.pdf differ diff --git a/test/controllers/admin/auth_controller_test.rb b/test/controllers/admin/auth_controller_test.rb index 477200c..73f5299 100644 --- a/test/controllers/admin/auth_controller_test.rb +++ b/test/controllers/admin/auth_controller_test.rb @@ -47,7 +47,7 @@ module Admin test "recruiter should auth to dashboard" do post admin_auth_url, params: { auth: - { email: 'pdr.recruiter@mailinator.com', password: 'password' } } + { email: 'pda.recruiter@mailinator.com', password: 'password' } } assert_redirected_to admin_url end diff --git a/test/controllers/admin/candidate_controller/index_test.rb b/test/controllers/admin/candidate_controller/index_test.rb index cd6ebe0..d14ef40 100644 --- a/test/controllers/admin/candidate_controller/index_test.rb +++ b/test/controllers/admin/candidate_controller/index_test.rb @@ -25,7 +25,7 @@ module Admin auth_recruiter get admin_candidates_url assert_response :success - assert_select "a[href='#{admin_edit_candidate_path(candidates(:martha))}']" + assert_select "a[href='#{admin_edit_candidate_path(candidates(:gillian))}']" end end end diff --git a/test/fixtures/answers.yml b/test/fixtures/answers.yml index 69019e2..de1b7be 100644 --- a/test/fixtures/answers.yml +++ b/test/fixtures/answers.yml @@ -698,8 +698,8 @@ wade10: 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 + question: fed1 + answer: 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. saved: 0 submitted: true created_at: <%= DateTime.now() - 36.hours - 22.minutes %> @@ -890,3 +890,87 @@ elsie10: created_at: <%= DateTime.now() - 36.hours - 40.minutes %> updated_at: <%= DateTime.now() - 36.hours - 20.minutes %> + +########################## +############# Studio Quiz +ethan1: + candidate: ethan + question: studio1 + answer: 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. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 36.hours - 22.minutes %> + updated_at: <%= DateTime.now() - 36.hours - 22.minutes %> + +ethan2: + candidate: ethan + question: studio2 + answer: Vestibulum id ligula porta felis euismod semper. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 36.hours - 24.minutes %> + updated_at: <%= DateTime.now() - 36.hours - 4.minutes %> + +ethan3: + candidate: ethan + question: studio3 + answer: Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 36.hours - 26.minutes %> + updated_at: <%= DateTime.now() - 36.hours - 6.minutes %> + +adele1: + candidate: adele + question: studio1 + answer: 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. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 16.hours - 22.minutes %> + updated_at: <%= DateTime.now() - 16.hours - 22.minutes %> + +adele2: + candidate: adele + question: studio2 + answer: Vestibulum id ligula porta felis euismod semper. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 16.hours - 24.minutes %> + updated_at: <%= DateTime.now() - 16.hours - 4.minutes %> + +adele3: + candidate: adele + question: studio3 + answer: Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 16.hours - 26.minutes %> + updated_at: <%= DateTime.now() - 16.hours - 6.minutes %> + +carl1: + candidate: carl + question: studio1 + answer: 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. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 22.minutes %> + updated_at: <%= DateTime.now() - 22.minutes %> + +carl2: + candidate: carl + question: studio2 + answer: Vestibulum id ligula porta felis euismod semper. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 24.minutes %> + updated_at: <%= DateTime.now() - 4.minutes %> + +carl3: + candidate: carl + question: studio3 + answer: Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue. + saved: 0 + submitted: true + created_at: <%= DateTime.now() - 26.minutes %> + updated_at: <%= DateTime.now() - 6.minutes %> + diff --git a/test/fixtures/candidates.yml b/test/fixtures/candidates.yml index d298cc5..0e70cec 100644 --- a/test/fixtures/candidates.yml +++ b/test/fixtures/candidates.yml @@ -179,3 +179,42 @@ gustov: # Gustov is NOT for FED reminded: false test_hash: kp6tfghjyapJnkz2N +ethan: # Completed quiz for studio + name: Ethan Woodward + email: <%= CryptSerializer.dump 'ethan.woodward@mailinator.com' %> + experience: 0-3 + project: Studio Client + position: 'full-time' + skill_needs: 'Angular, HTML' + recruiter: recruiter + quiz: studio + completed: true + reminded: false + test_hash: vNgQo2c5/HZL2CN + +adele: # Completed quiz for studio + name: Adele Kearney + email: <%= CryptSerializer.dump 'adele.kearney@mailinator.com' %> + experience: 0-3 + project: Studio Client + position: 'full-time' + skill_needs: 'Angular, HTML' + recruiter: recruiter + quiz: studio + completed: true + reminded: false + test_hash: 37GmHL0Odjwv + +carl: # Completed quiz for studio + name: Carl Mitchell + email: <%= CryptSerializer.dump 'carle.mitchell@mailinator.com' %> + experience: 0-3 + project: Studio Client + position: 'full-time' + skill_needs: 'Angular, HTML' + recruiter: recruiter + quiz: studio + completed: true + reminded: false + test_hash: hANPsTL1XHcmi + diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml index 4380750..33a23f7 100644 --- a/test/fixtures/questions.yml +++ b/test/fixtures/questions.yml @@ -150,3 +150,32 @@ admin1: sort: 0 active: true + +studio1: + quiz: studio + question: 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Maecenas faucibus mollis interdum.' + category: Ipsum + input_type: text + input_options: + sort: 0 + active: true + +studio2: + quiz: studio + question: 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Maecenas faucibus mollis interdum.' + category: Magna + input_type: text + input_options: + sort: 0 + active: true + + +studio3: + quiz: studio + question: 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Maecenas faucibus mollis interdum.' + category: Commodo + input_type: text + input_options: + sort: 0 + active: true + diff --git a/test/fixtures/quizzes.yml b/test/fixtures/quizzes.yml index f4bd5fa..5f4aa1d 100644 --- a/test/fixtures/quizzes.yml +++ b/test/fixtures/quizzes.yml @@ -1,11 +1,16 @@ # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html fed: - name: PDR Standard FED Screening - unit: PDR + name: PDA Standard FED Screening + unit: PD Agency dept: FED admin: name: An extra quiz not assigned to anyone - unit: PDR + unit: PD dept: NOPE + +studio: + name: Studio Screening One + unit: PD Studio + dept: FED diff --git a/test/fixtures/reviewer_to_quizzes.yml b/test/fixtures/reviewer_to_quizzes.yml index cb91027..45b4b22 100644 --- a/test/fixtures/reviewer_to_quizzes.yml +++ b/test/fixtures/reviewer_to_quizzes.yml @@ -11,3 +11,11 @@ two: three: user: manager quiz: fed + +four: + user: studio_manager + quiz: studio + +five: + user: studio_reviewer + quiz: studio diff --git a/test/fixtures/reviewer_votes.yml b/test/fixtures/reviewer_votes.yml index a8ab4c1..440ac4b 100644 --- a/test/fixtures/reviewer_votes.yml +++ b/test/fixtures/reviewer_votes.yml @@ -93,3 +93,29 @@ reviewer2_elsie: candidate: elsie user: reviewer2 + +studio_reviewer_carle: + candidate: carle + user: studio_reviewer + +studio_reviewer_ethan: + candidate: ethan + user: studio_reviewer + +studio_reviewer_adele: + candidate: adele + user: studio_reviewer + + +studio_manager_carle: + candidate: carle + user: studio_manager + +studio_manager_ethan: + candidate: ethan + user: studio_manager + +studio_manager_adele: + user: studio_manager + candidate: adele + diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 94e21e3..ef8fbdf 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -2,7 +2,7 @@ recruiter: name: Sam Recruiter - email: pdr.recruiter@mailinator.com + email: pda.recruiter@mailinator.com password_digest: <%= BCrypt::Password.create("password", cost: 4) %> role: recruiter @@ -29,3 +29,16 @@ admin: email: alan.admin@mailinator.com password_digest: <%= BCrypt::Password.create("password", cost: 4) %> role: admin + +studio_manager: + name: Studio Manager + email: studio.manager@mailinator.com + password_digest: <%= BCrypt::Password.create("password", cost: 4) %> + role: manager + +studio_reviewer: + name: Studio Reviewer + email: studio.reviewer@mailinator.com + password_digest: <%= BCrypt::Password.create("password", cost: 4) %> + role: reviewer + diff --git a/test/models/candidate_test.rb b/test/models/candidate_test.rb index 5b9ea44..4d5154e 100644 --- a/test/models/candidate_test.rb +++ b/test/models/candidate_test.rb @@ -34,4 +34,23 @@ class CandidateTest < ActiveSupport::TestCase candidate.build_reviews assert_equal 3, candidate.votes.count end + + test 'can get last answer timestamp' do + candidate = candidates(:roy) + roy_last = answers(:roy2).updated_at + + assert_equal roy_last, candidate.last_answered_at + end + + test 'gillian is stale with no answers' do + candidate = candidates(:gillian) + + assert candidate.stale? + end + + test 'roy is stale with answers' do + candidate = candidates(:roy) + + assert candidate.stale? + end end diff --git a/test/models/reviewer_vote_test.rb b/test/models/reviewer_vote_test.rb index d5a9ef3..2ea670c 100644 --- a/test/models/reviewer_vote_test.rb +++ b/test/models/reviewer_vote_test.rb @@ -14,7 +14,7 @@ class ReviewerVoteTest < ActiveSupport::TestCase test "manager has a vote for every completed quiz" do manager = users(:manager) - completed_count = Candidate.where(completed: true).count + completed_count = 6 assert_equal completed_count, manager.votes.size end diff --git a/test/policies/quiz_policy_test.rb b/test/policies/quiz_policy_test.rb index e5eb7be..505f9f0 100644 --- a/test/policies/quiz_policy_test.rb +++ b/test/policies/quiz_policy_test.rb @@ -15,7 +15,7 @@ class QuizPolicyTest < PolicyAssertions::Test test 'should allow manager to scope' do scope = QuizPolicy::Scope.new(users(:manager), Quiz).resolve - assert_equal Quiz.count, scope.count + assert_equal users(:manager).quizzes.count, scope.count end test 'should allow reviewer to scope' do diff --git a/test/policies/result_policy_test.rb b/test/policies/result_policy_test.rb new file mode 100644 index 0000000..fdccb60 --- /dev/null +++ b/test/policies/result_policy_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require 'test_helper' + +class ResultPolicyTest < PolicyAssertions::Test + def test_index + assert_permit users(:admin), :result + assert_permit users(:recruiter), :result + assert_permit users(:manager), :result + assert_permit users(:reviewer), :result + end + + test 'should allow admin to scope' do + scope = ResultPolicy::Scope.new(users(:admin), Candidate).resolve + assert_equal Candidate.where(completed: true).count, scope.count + end + + test 'should allow recruiter to scope' do + scope = ResultPolicy::Scope.new(users(:recruiter), Candidate).resolve + assert_equal Candidate.where(completed: true).count, scope.count + end + + test 'should not allow fed.reviewer to scope studio results' do + reviewer = users(:reviewer) + scope = ResultPolicy::Scope.new(reviewer, Candidate).resolve + assert_equal reviewer.reviewees.where(completed: true).count, scope.count + end +end diff --git a/test/test_helpers/auth_test_helper.rb b/test/test_helpers/auth_test_helper.rb index 152a7b6..03f6ace 100644 --- a/test/test_helpers/auth_test_helper.rb +++ b/test/test_helpers/auth_test_helper.rb @@ -21,7 +21,7 @@ module AuthTestHelper def auth_recruiter post admin_auth_url, params: { auth: - { email: 'pdr.recruiter@mailinator.com', password: 'password' } } + { email: 'pda.recruiter@mailinator.com', password: 'password' } } end def auth_reviewer diff --git a/test/workers/reviewer_reminder_test.rb b/test/workers/reviewer_reminder_test.rb index c4399dd..5a6cbf0 100644 --- a/test/workers/reviewer_reminder_test.rb +++ b/test/workers/reviewer_reminder_test.rb @@ -4,7 +4,7 @@ require 'test_helper' class ReviewerReminderTest < ActiveSupport::TestCase test "collection is created with results" do reminders = ReviewerReminder.new - assert_equal 6, reminders.size + assert_equal 8, reminders.size end test "each reminder has needed attributes" do