diff --git a/.circleci/config.yml b/.circleci/config.yml index 9f7a25b6..a10030c2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,7 +71,7 @@ jobs: rspec: executor: name: test_executor - + environment: PHOENIX_URI: http://localhost:4000 @@ -123,6 +123,9 @@ jobs: executor: name: test_executor + environment: + - CC_TEST_REPORTER_ID: "a561562e78b6f0b3e346d4811eef2b020f977cd089bf459f102f6229dd100703" + steps: - restore_cache: key: repo-{{ .Environment.CIRCLE_SHA1 }} diff --git a/.env_login b/.env_login index 42d5b0d4..56b421dd 100644 --- a/.env_login +++ b/.env_login @@ -6,5 +6,3 @@ export LOGOUT_REDIRECT_EVAL_URL=http://localhost:3000/ export PHOENIX_URI="http://localhost:4000" export LOGIN_SECRET="f4d3c40a00a8e6ed72fae5204d9ddacd40f087865d40803a6fcfb935591a271838533f06081067dac24c0085c74123e7e1c8b3e0ab562c6645b17eb769854d0d" export JWT_SECRET="fc28c5738ca45162f61126e770a8fbdbd938d0fedcfe8fbb9f851b855b0264866364a9130e96aca8b1977e9f58edf064f1aa435ceccf415ff22fd3c24adba320" - -export APP_DOMAIN="localhost" diff --git a/.envrc b/.envrc index 4fbb075a..dd85f90b 100644 --- a/.envrc +++ b/.envrc @@ -5,5 +5,8 @@ use nix mkdir -p .nix-bundler export BUNDLE_PATH=./.nix-bundler +# hook for rbenv locally +which rbenv &> /dev/null && eval "$(rbenv init - -zsh)" + # Login Env Vars source .env_login diff --git a/Gemfile b/Gemfile index e4893ae2..1bedb549 100644 --- a/Gemfile +++ b/Gemfile @@ -5,13 +5,13 @@ source "https://rubygems.org" ruby "3.2.4" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.2.0" +gem "rails", "~> 7.2.1", ">= 7.2.1.1" # Use postgresql as the database for Active Record gem "pg" # Use the Puma web server [https://github.com/puma/puma] -gem "puma", ">= 5.0" +gem "puma", ">= 6.4.3" # Use the popular Faraday HTTP library gem "faraday" @@ -20,7 +20,7 @@ gem "faraday" gem "jwt" # Use simple asset pipeline -gem "propshaft", "~> 0.9.1" +gem "propshaft", "~> 1.1.0" gem "cssbundling-rails", "~> 1.4" gem "jsbundling-rails", "~> 1.3" @@ -59,9 +59,6 @@ group :development, :test do gem 'pry' - # add the Ruby LSP package so it's bundled with the rest of the gems and available to VS Code - gem "ruby-lsp" - # rubocop and specific extensions used by VS Code gem "rubocop", ">= 1.66.0" gem "rubocop-performance", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0a442425..d7b75653 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,29 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.1) - actionpack (= 7.2.1) - activesupport (= 7.2.1) + actioncable (7.2.1.2) + actionpack (= 7.2.1.2) + activesupport (= 7.2.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.1) - actionpack (= 7.2.1) - activejob (= 7.2.1) - activerecord (= 7.2.1) - activestorage (= 7.2.1) - activesupport (= 7.2.1) + actionmailbox (7.2.1.2) + actionpack (= 7.2.1.2) + activejob (= 7.2.1.2) + activerecord (= 7.2.1.2) + activestorage (= 7.2.1.2) + activesupport (= 7.2.1.2) mail (>= 2.8.0) - actionmailer (7.2.1) - actionpack (= 7.2.1) - actionview (= 7.2.1) - activejob (= 7.2.1) - activesupport (= 7.2.1) + actionmailer (7.2.1.2) + actionpack (= 7.2.1.2) + actionview (= 7.2.1.2) + activejob (= 7.2.1.2) + activesupport (= 7.2.1.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.1) - actionview (= 7.2.1) - activesupport (= 7.2.1) + actionpack (7.2.1.2) + actionview (= 7.2.1.2) + activesupport (= 7.2.1.2) nokogiri (>= 1.8.5) racc rack (>= 2.2.4, < 3.2) @@ -32,35 +32,35 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.1) - actionpack (= 7.2.1) - activerecord (= 7.2.1) - activestorage (= 7.2.1) - activesupport (= 7.2.1) + actiontext (7.2.1.2) + actionpack (= 7.2.1.2) + activerecord (= 7.2.1.2) + activestorage (= 7.2.1.2) + activesupport (= 7.2.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.1) - activesupport (= 7.2.1) + actionview (7.2.1.2) + activesupport (= 7.2.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.1) - activesupport (= 7.2.1) + activejob (7.2.1.2) + activesupport (= 7.2.1.2) globalid (>= 0.3.6) - activemodel (7.2.1) - activesupport (= 7.2.1) - activerecord (7.2.1) - activemodel (= 7.2.1) - activesupport (= 7.2.1) + activemodel (7.2.1.2) + activesupport (= 7.2.1.2) + activerecord (7.2.1.2) + activemodel (= 7.2.1.2) + activesupport (= 7.2.1.2) timeout (>= 0.4.0) - activestorage (7.2.1) - actionpack (= 7.2.1) - activejob (= 7.2.1) - activerecord (= 7.2.1) - activesupport (= 7.2.1) + activestorage (7.2.1.2) + actionpack (= 7.2.1.2) + activejob (= 7.2.1.2) + activerecord (= 7.2.1.2) + activesupport (= 7.2.1.2) marcel (~> 1.0) - activesupport (7.2.1) + activesupport (7.2.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -77,12 +77,14 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - axe-core-api (4.10.0) + axe-core-api (4.10.1) dumb_delegator + ostruct virtus - axe-core-rspec (4.10.0) - axe-core-api (= 4.10.0) + axe-core-rspec (4.10.1) + axe-core-api (= 4.10.1) dumb_delegator + ostruct virtus axiom-types (0.1.1) descendants_tracker (~> 0.0.4) @@ -129,7 +131,7 @@ GEM erubi (1.13.0) factory_bot (6.5.0) activesupport (>= 5.0.0) - faker (3.4.2) + faker (3.5.1) i18n (>= 1.8.11, < 2) faraday (2.12.0) faraday-net_http (>= 2.0, < 3.4) @@ -153,12 +155,12 @@ GEM activesupport (>= 5.0.0) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.7.2) + json (2.7.4) jwt (2.9.3) base64 language_server-protocol (3.17.0.3) logger (1.6.1) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -174,7 +176,7 @@ GEM msgpack (1.7.3) net-http (0.4.1) uri - net-imap (0.4.17) + net-imap (0.5.0) date net-protocol net-pop (0.1.2) @@ -196,13 +198,13 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) + ostruct (0.6.0) parallel (1.26.3) parser (3.3.5.0) ast (~> 2.4.1) racc - pg (1.5.8) - prism (1.2.0) - propshaft (0.9.1) + pg (1.5.9) + propshaft (1.1.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack @@ -224,20 +226,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.2.1) - actioncable (= 7.2.1) - actionmailbox (= 7.2.1) - actionmailer (= 7.2.1) - actionpack (= 7.2.1) - actiontext (= 7.2.1) - actionview (= 7.2.1) - activejob (= 7.2.1) - activemodel (= 7.2.1) - activerecord (= 7.2.1) - activestorage (= 7.2.1) - activesupport (= 7.2.1) + rails (7.2.1.2) + actioncable (= 7.2.1.2) + actionmailbox (= 7.2.1.2) + actionmailer (= 7.2.1.2) + actionpack (= 7.2.1.2) + actiontext (= 7.2.1.2) + actionview (= 7.2.1.2) + activejob (= 7.2.1.2) + activemodel (= 7.2.1.2) + activerecord (= 7.2.1.2) + activestorage (= 7.2.1.2) + activesupport (= 7.2.1.2) bundler (>= 1.15.0) - railties (= 7.2.1) + railties (= 7.2.1.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -249,9 +251,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.2.1) - actionpack (= 7.2.1) - activesupport (= 7.2.1) + railties (7.2.1.2) + actionpack (= 7.2.1.2) + activesupport (= 7.2.1.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -259,15 +261,13 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rbs (3.6.1) - logger rdoc (6.7.0) psych (>= 4.0.0) regexp_parser (2.9.2) reline (0.5.10) io-console (~> 0.5) - rexml (3.3.8) - rspec-core (3.13.1) + rexml (3.3.9) + rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) @@ -286,7 +286,7 @@ GEM rspec-support (3.13.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.66.1) + rubocop (1.67.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -303,20 +303,15 @@ GEM rubocop-performance (1.22.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.26.2) + rubocop-rails (2.27.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (3.1.0) + rubocop-rspec (3.2.0) rubocop (~> 1.61) - ruby-lsp (0.20.0) - language_server-protocol (~> 3.17.0) - prism (>= 1.2, < 2.0) - rbs (>= 3, < 4) - sorbet-runtime (>= 0.5.10782) ruby-progressbar (1.13.0) rubyzip (2.3.2) securerandom (0.3.1) @@ -331,14 +326,13 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sorbet-runtime (0.5.11602) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.1) thor (1.3.2) thread_safe (0.3.6) timeout (0.4.1) - turbo-rails (2.0.10) + turbo-rails (2.0.11) actionpack (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) @@ -366,7 +360,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.0) + zeitwerk (2.7.1) PLATFORMS aarch64-linux @@ -392,10 +386,10 @@ DEPENDENCIES jsbundling-rails (~> 1.3) jwt pg - propshaft (~> 0.9.1) + propshaft (~> 1.1.0) pry - puma (>= 5.0) - rails (~> 7.2.0) + puma (>= 6.4.3) + rails (~> 7.2.1, >= 7.2.1.1) rails-controller-testing rspec-rails rspec_junit_formatter @@ -405,7 +399,6 @@ DEPENDENCIES rubocop-rails (>= 2.26.0) rubocop-rake rubocop-rspec - ruby-lsp selenium-webdriver (>= 4.24.0) simplecov (~> 0.17.0) stimulus-rails diff --git a/app/controllers/evaluation_forms_controller.rb b/app/controllers/evaluation_forms_controller.rb index 0827796d..e0cd999c 100644 --- a/app/controllers/evaluation_forms_controller.rb +++ b/app/controllers/evaluation_forms_controller.rb @@ -41,7 +41,7 @@ def update respond_to do |format| if @evaluation_form.update(evaluation_form_params) format.html do - redirect_to evaluation_form_url(@evaluation_form), notice: I18n.t("evaluation_form_saved") + redirect_to evaluation_forms_url, notice: I18n.t("evaluation_form_saved") end format.json { render :show, status: :ok, location: @evaluation_form } else diff --git a/app/controllers/manage_submissions_controller.rb b/app/controllers/manage_submissions_controller.rb index e684cb41..318a1911 100644 --- a/app/controllers/manage_submissions_controller.rb +++ b/app/controllers/manage_submissions_controller.rb @@ -2,5 +2,7 @@ class ManageSubmissionsController < ApplicationController before_action -> { authorize_user('challenge_manager') } - def index; end + def index + @challenges = current_user.challenge_manager_challenges + end end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index d3e201ca..515b190c 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -7,7 +7,8 @@ def dashboard_cards_by_role { image_path: 'emoji_events', href: Rails.configuration.phx_interop[:phx_uri], alt: 'challenges', title: 'Challenges', subtitle: 'Create and manage challenges.' }, { image_path: 'star_half', href: 'manage_submissions', - alt: 'submissions', title: 'Submissions', subtitle: 'Manage submissions, evaluations, and evaluators.' }, + alt: 'submissions and evaluations', title: 'Submissions & Evaluations', subtitle: + 'Manage submissions, evaluations, and evaluators.' }, { image_path: 'check_circle_outline', href: 'evaluation_forms', alt: 'evaluation forms', title: 'Evaluation Forms', subtitle: 'Create and manage evaluation forms.' }, { image_path: 'mail', href: "#{Rails.configuration.phx_interop[:phx_uri]}/messages", @@ -19,7 +20,7 @@ def dashboard_cards_by_role ], evaluator: [ { image_path: 'star_half', href: 'evaluations', - alt: 'submissions', title: 'Submissions', + alt: 'submissions and evaluations', title: 'Submissions & Evaluations', subtitle: 'View submissions assigned to me and provide evaluations.' }, { image_path: 'support', href: 'https://www.challenge.gov/cm-user-guide/', alt: 'resources', title: 'Resources', subtitle: 'Learn how to make the most of the platform.' } diff --git a/app/helpers/evaluation_forms_helper.rb b/app/helpers/evaluation_forms_helper.rb index 94420370..ed34b279 100644 --- a/app/helpers/evaluation_forms_helper.rb +++ b/app/helpers/evaluation_forms_helper.rb @@ -33,4 +33,8 @@ def criteria_field_name(form, attribute, is_template) "#{prefix}[#{form.options[:child_index]}][#{attribute}]" end end + + def eval_form_disabled?(evaluation_form) + evaluation_form.valid? && evaluation_form.phase.end_date < Time.zone.today + end end diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb index 7d0a6687..d1d21798 100644 --- a/app/helpers/navigation_helper.rb +++ b/app/helpers/navigation_helper.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true module NavigationHelper - def utility_menu_link(image_path, href, alt, button_label) + def utility_menu_link(image_path, href, _alt, button_label) link_to(href, class: "display-flex flex-align-center flex-column desktop:flex-row " \ - "margin-x-1 desktop:margin-x-3 text-white text-no-wrap") do + "margin-x-1 desktop:margin-x-3 text-white + width-9 tablet:width-auto tablet:text-no-wrap text-center") do image_tag( "images/usa-icons/#{image_path}.svg", class: "usa-icon--size-4 desktop:usa-icon--size-3 icon-white desktop:margin-right-1", - alt: + alt: "" ) + tag.span(button_label, class: "display-none desktop:display-block") + tag.span(button_label, class: "desktop:display-none", style: "font-size: 0.7rem") diff --git a/app/models/challenge.rb b/app/models/challenge.rb index 030552b7..5472be04 100644 --- a/app/models/challenge.rb +++ b/app/models/challenge.rb @@ -99,6 +99,9 @@ class Challenge < ApplicationRecord has_many :phases, -> { order(:start_date) }, inverse_of: :challenge, dependent: :destroy has_many :submissions, dependent: :destroy has_many :submission_exports, dependent: :destroy + has_many :evaluator_invitations, dependent: :destroy + has_many :challenge_phases_evaluators, dependent: :destroy + has_many :evaluators, through: :challenge_phases_evaluators, source: :user # JSON fields attribute :types, :jsonb diff --git a/app/models/challenge_phases_evaluator.rb b/app/models/challenge_phases_evaluator.rb new file mode 100644 index 00000000..d2389a27 --- /dev/null +++ b/app/models/challenge_phases_evaluator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: challenge_phases_evaluators +# +# id :bigint not null, primary key +# challenge_id :bigint not null +# phase_id :bigint not null +# user_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +class ChallengePhasesEvaluator < ApplicationRecord + belongs_to :challenge + belongs_to :phase + belongs_to :user +end diff --git a/app/models/evaluator_invitation.rb b/app/models/evaluator_invitation.rb new file mode 100644 index 00000000..42ffa177 --- /dev/null +++ b/app/models/evaluator_invitation.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: evaluator_invitations +# +# id :bigint not null, primary key +# challenge_id :bigint not null +# phase_id :bigint not null +# first_name :string not null +# last_name :string not null +# email :string not null +# last_invite_sent :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# +class EvaluatorInvitation < ApplicationRecord + belongs_to :challenge + belongs_to :phase + + validates :first_name, presence: true + validates :last_name, presence: true + validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :last_invite_sent, presence: true + + validates :email, uniqueness: { scope: [:challenge_id, :phase_id] } +end diff --git a/app/models/phase.rb b/app/models/phase.rb index 2d773320..bb4193be 100644 --- a/app/models/phase.rb +++ b/app/models/phase.rb @@ -24,8 +24,12 @@ class Phase < ApplicationRecord belongs_to :challenge # More relations from phoenix app - # has_many :submissions + has_many :submissions, dependent: :destroy + has_one :evaluation_form, dependent: :destroy # has_one :winner, class_name: 'PhaseWinner' + has_many :evaluator_invitations, dependent: :destroy + has_many :challenge_phases_evaluators, dependent: :destroy + has_many :evaluators, through: :challenge_phases_evaluators, source: :user # Attributes attribute :uuid, :uuid diff --git a/app/models/submission.rb b/app/models/submission.rb new file mode 100644 index 00000000..36b2ec41 --- /dev/null +++ b/app/models/submission.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Submission < ApplicationRecord + enum :status, { draft: "draft", submitted: "submitted" } + enum :judging_status, { not_selected: "not_selected", selected: "selected", qualified: "qualified", winner: "winner" } + + # Associations + belongs_to :submitter, class_name: 'User' + belongs_to :challenge + belongs_to :phase + belongs_to :manager, class_name: 'User' + + # Fields + attribute :title, :string + attribute :brief_description, :string + attribute :description, :string + attribute :external_url, :string + attribute :terms_accepted, :boolean, default: nil + attribute :review_verified, :boolean, default: nil + + # Validations + validates :title, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 5f90cd1c..7b182c39 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -48,6 +48,8 @@ class User < ApplicationRecord dependent: :destroy has_many :submission_documents, class_name: 'Submissions::Document', dependent: :destroy has_many :message_context_statuses, dependent: :destroy + has_many :challenge_phases_evaluators, dependent: :destroy + has_many :evaluated_phases, through: :challenge_phases_evaluators, source: :phase attribute :role, :string attribute :status, :string, default: 'pending' diff --git a/app/views/dashboard/_dashboard_card.html.erb b/app/views/dashboard/_dashboard_card.html.erb index 8bc4bf15..1c53778f 100644 --- a/app/views/dashboard/_dashboard_card.html.erb +++ b/app/views/dashboard/_dashboard_card.html.erb @@ -5,7 +5,7 @@ <%= image_tag( "images/usa-icons/#{card[:image_path]}.svg", class: "usa-icon--size-7 icon-white margin-x-2", - alt: card[:alt] + alt: "" )%> <div class="margin-right-2" > <h2><%= card[:title] %></h2> diff --git a/app/views/evaluation_forms/_form.html.erb b/app/views/evaluation_forms/_form.html.erb index 3cacf9fd..ac43659f 100644 --- a/app/views/evaluation_forms/_form.html.erb +++ b/app/views/evaluation_forms/_form.html.erb @@ -15,6 +15,7 @@ challenge = evaluation_form.challenge phase = evaluation_form.phase combo_box_default_value = "#{challenge.id}.#{phase.id}.#{phase.end_date.strftime("%m/%d/%Y")}" + disabled = eval_form_disabled?(evaluation_form) end %> <ol class="usa-process-list"> @@ -26,7 +27,7 @@ </p> <div> <%= form.label :title, class: "usa-label" %> - <%= form.text_field :title, maxlength: 150, class: "usa-input", data: {"action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence"} %> + <%= form.text_field :title, maxlength: 150, class: "usa-input", data: {"action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence"}, disabled: disabled %> </div> <%= inline_error(evaluation_form, :title) %> @@ -34,7 +35,7 @@ <label class="usa-label" for="challenge-combo">Select a challenge</label> <div class="usa-hint">Choose the challenge this form will evaluate.</div> <div class="usa-combo-box" data-default-value="<%= combo_box_default_value %>"> - <select id="challenge-combo" class="usa-select" name="challenge-combo" data-action="evaluation-form#handleChallengeSelect"> + <select class="usa-select" <%= if disabled then "disabled" end %> id="challenge-combo" title="challenge-combo" data-action="evaluation-form#handleChallengeSelect"> <% current_user.challenge_manager_challenges.each do |challenge| %> <% challenge.phases.each do |phase| %> <option value="<%= "#{challenge.id}.#{phase.id}.#{phase.end_date.strftime("%m/%d/%Y")}" %>"><%= challenge_phase_title(challenge, phase) %></option> @@ -52,7 +53,7 @@ <div class="usa-character-count"> <div class="usa-form-group"> <%= form.label :instructions, class: "usa-label" %> - <%= form.text_area :instructions, class: "usa-textarea usa-character-count__field", maxlength: 3000, rows: "7", data: {"action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence"} %> + <%= form.text_area :instructions, class: "usa-textarea usa-character-count__field", maxlength: 3000, rows: "7", data: {"action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence"}, disabled: disabled%> </div> <span id="with-hint-textarea-info" class="usa-character-count__message">You can enter up to 3000 characters</span> </div> @@ -65,7 +66,7 @@ Build your evaluation criteria and scoring options. </p> <div class="usa-checkbox"> - <%= form.check_box :comments_required, class: "usa-checkbox__input", id: "evaluation_form_comments_required" %> + <%= form.check_box :comments_required, class: "usa-checkbox__input", id: "evaluation_form_comments_required", disabled: disabled %> <label class="usa-checkbox__label" for="evaluation_form_comments_required"> Require evaluators to provide comments on their scores. (optional) <%= image_tag 'images/usa-icons/help_outline.svg', width: 16, height: 16, alt: 'Help for comments required' %> @@ -80,11 +81,11 @@ </legend> <div class="display-flex flex-row"> <div class="usa-radio margin-right-205"> - <input class="usa-radio__input" id="point_scale" type="radio" name="evaluation_form[weighted_scoring]" value="false" <%= 'checked' unless evaluation_form.weighted_scoring %>> + <input class="usa-radio__input" id="point_scale" type="radio" name="evaluation_form[weighted_scoring]" value="false" <%= 'checked' unless evaluation_form.weighted_scoring %> <%= 'disabled' unless !disabled %>> <label class="usa-radio__label font-sans-xs" for="point_scale">Point Scale</label> </div> <div class="usa-radio"> - <input class="usa-radio__input" id="weighted_scale" type="radio" name="evaluation_form[weighted_scoring]" value="true" <%= 'checked' if evaluation_form.weighted_scoring %>> + <input class="usa-radio__input" id="weighted_scale" type="radio" name="evaluation_form[weighted_scoring]" value="true" <%= 'checked' if evaluation_form.weighted_scoring %> <%= 'disabled' unless !disabled %>> <label class="usa-radio__label font-sans-xs" for="weighted_scale">Weighted Scale (%)</label> </div> </div> diff --git a/app/views/evaluation_forms/_table.html.erb b/app/views/evaluation_forms/_table.html.erb index 27e71b32..f2151c6d 100644 --- a/app/views/evaluation_forms/_table.html.erb +++ b/app/views/evaluation_forms/_table.html.erb @@ -10,7 +10,7 @@ <% @evaluation_forms.each do |evaluation_form| %> <tr> <th data-label="Form Title" scope="row"> - <%= link_to evaluation_form do %> + <%= link_to edit_evaluation_form_path(evaluation_form) do %> <%= evaluation_form.title %> <% end %> </th> diff --git a/app/views/evaluation_forms/edit.html.erb b/app/views/evaluation_forms/edit.html.erb index 03d62c26..27efdc86 100644 --- a/app/views/evaluation_forms/edit.html.erb +++ b/app/views/evaluation_forms/edit.html.erb @@ -1,5 +1,5 @@ <% content_for :title, "Editing evaluation form" %> -<h1>Editing evaluation form</h1> +<h1><%= if eval_form_disabled?(@evaluation_form) then "View" else "Editing" end %> evaluation form</h1> <%= render "form", evaluation_form: @evaluation_form %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index e9f70579..b6db8a9f 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -99,7 +99,7 @@ aria-label="Official website of the United States government" <em class="usa-logo__text" ><a href="/" title="challenge.gov"> <span class="site-title--long"> - <img width="300" class="usa-header__logo-img margin-left-4 desktop:margin-left-2 desktop:margin-bottom-2" src="<%= image_path("challenge-logo.svg") %>" alt="challenge logo" /> + <img width="300" class="usa-header__logo-img desktop:margin-bottom-2" src="<%= image_path("challenge-logo.svg") %>" alt="challenge logo" /> </span> </a></em > diff --git a/app/views/layouts/_utility_menu.erb b/app/views/layouts/_utility_menu.erb index cb25a84b..0a770e20 100644 --- a/app/views/layouts/_utility_menu.erb +++ b/app/views/layouts/_utility_menu.erb @@ -3,11 +3,11 @@ <%= utility_menu_link('grid_view', dashboard_path, 'dashboard', 'Dashboard') %> <% if current_user.role == "challenge_manager" %> <%= utility_menu_link('emoji_events', Rails.configuration.phx_interop[:phx_uri], 'challenges', 'Challenges') %> - <%= utility_menu_link('star_half', manage_submissions_path, 'Manage Submissions', 'Submissions') %> + <%= utility_menu_link('star_half', manage_submissions_path, 'Manage Submissions and Evaluations', 'Submissions & Evaluations') %> <%= utility_menu_link('check_circle_outline', evaluation_forms_path, 'Evaluation Forms', 'Evaluation Forms') %> <% end %> <% if current_user.role == "evaluator" %> - <%= utility_menu_link('star_half', evaluations_path, 'Evaluate Submissions', 'Submissions') %> + <%= utility_menu_link('star_half', evaluations_path, 'Evaluate Submissions', 'Submissions & Evaluations') %> <% end %> <%= utility_menu_link('support', 'https://www.challenge.gov/cm-user-guide/', 'Resources', 'Resources') %> <div class="display-none desktop:display-flex flex-align-center margin-x-1 margin-top-1"> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c7908dc6..82cc5812 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ <!DOCTYPE html> <html lang="en"> <head> - <title>RailsNew</title> + <title>Challenge.gov</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> diff --git a/app/views/manage_submissions/_table.html.erb b/app/views/manage_submissions/_table.html.erb new file mode 100644 index 00000000..674aa9f7 --- /dev/null +++ b/app/views/manage_submissions/_table.html.erb @@ -0,0 +1,52 @@ +<table class="usa-table usa-table--stacked-header usa-table--borderless width-full"> + <thead> + <tr> + <th scope="col">Challenge</th> + <th scope="col">Number of Submissions</th> + <th scope="col">Evaluation Form</th> + <th scope="col">Evaluations are due by</th> + </tr> + </thead> + <tbody> + <% @challenges.each do |challenge| %> + <% challenge.phases.each do |phase| %> + <tr> + <th data-label="Form Title" scope="row"> + <%= challenge_phase_title(challenge, phase) %> + </th> + <td data-label="Number of Submissions"> + <%= phase.submissions.length %> + </td> + <td data-label="Evaluation Form"> + <% if phase.evaluation_form %> + <%= link_to edit_evaluation_form_path(phase.evaluation_form) do %> + <%= phase.evaluation_form.title %> + <% end %> + <% else %> + N/A + <% end %> + </td> + <td data-label="Evaluations are due by"> + <% if phase.evaluation_form %> + <%= phase.evaluation_form.closing_date %> + <% else %> + N/A + <% end %> + </td> + <td> + <div class="display-flex flex-no-wrap grid-row grid-gap-1"> + <button class="usa-button font-body-2xs text-no-wrap"> + Manage Evaluators + </button> + <% unless phase.submissions.empty? %> + <button class="usa-button font-body-2xs text-no-wrap"> + View Submissions + </button> + <% end %> + </div> + </td> + </tr> + <% end %> + <% end %> + </tbody> + </table> \ No newline at end of file diff --git a/app/views/manage_submissions/index.html.erb b/app/views/manage_submissions/index.html.erb index d3797af9..bbf6ceae 100644 --- a/app/views/manage_submissions/index.html.erb +++ b/app/views/manage_submissions/index.html.erb @@ -1,7 +1,10 @@ -<div class="usa-card__container col-md-6"> - <div class="usa-card__body"> - <h1>Manage Submissions</h1> +<h1>Submissions & Evaluations</h1> +<p class="text-normal">View challenge submissions and manage your list of evaluators for each challenge.</p> - <p>Placeholder text for Manage Submissions.</p> +<% if @challenges.empty? %> + <div class="text-normal"> + <p>You currently do not have any challenges.</p> </div> -</div> \ No newline at end of file +<% else %> + <%= render partial: "table", locals: { challenges: @challenges } %> +<% end %> \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 342165ce..5aeb8cb4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,7 +8,7 @@ # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) -module RailsNew +module ChallengePlatform class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.1 diff --git a/db/migrate/20241001143033_change_null_values_on_evaluation_forms.rb b/db/migrate/20241001143033_change_null_values_on_evaluation_forms.rb index e0b1be3b..e3fc8f30 100644 --- a/db/migrate/20241001143033_change_null_values_on_evaluation_forms.rb +++ b/db/migrate/20241001143033_change_null_values_on_evaluation_forms.rb @@ -1,5 +1,8 @@ class ChangeNullValuesOnEvaluationForms < ActiveRecord::Migration[7.2] def change + # reset both tables due to foreign_key constraint + execute('DELETE FROM evaluation_criteria;') + execute('DELETE FROM evaluation_forms;') change_column_null :evaluation_forms, :title, false change_column_null :evaluation_forms, :instructions, false change_column_null :evaluation_forms, :challenge_phase, false diff --git a/db/migrate/20241014214843_create_challenge_phases_evaluators.rb b/db/migrate/20241014214843_create_challenge_phases_evaluators.rb new file mode 100644 index 00000000..50e365fa --- /dev/null +++ b/db/migrate/20241014214843_create_challenge_phases_evaluators.rb @@ -0,0 +1,11 @@ +class CreateChallengePhasesEvaluators < ActiveRecord::Migration[7.2] + def change + create_table :challenge_phases_evaluators do |t| + t.references :challenge, null: false, foreign_key: true + t.references :phase, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241018150049_create_evaluator_invitations.rb b/db/migrate/20241018150049_create_evaluator_invitations.rb new file mode 100644 index 00000000..865db7fa --- /dev/null +++ b/db/migrate/20241018150049_create_evaluator_invitations.rb @@ -0,0 +1,16 @@ +class CreateEvaluatorInvitations < ActiveRecord::Migration[7.2] + def change + create_table :evaluator_invitations do |t| + t.references :challenge, null: false, foreign_key: true + t.references :phase, null: false, foreign_key: true + t.string :first_name, null: false + t.string :last_name, null: false + t.string :email, null: false + t.datetime :last_invite_sent + + t.timestamps + end + + add_index :evaluator_invitations, [:challenge_id, :phase_id, :email], unique: true + end +end diff --git a/db/structure.sql b/db/structure.sql index 668ac499..11f62eb6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -183,6 +183,39 @@ CREATE SEQUENCE public.challenge_owners_id_seq ALTER SEQUENCE public.challenge_owners_id_seq OWNED BY public.challenge_managers.id; +-- +-- Name: challenge_phases_evaluators; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.challenge_phases_evaluators ( + id bigint NOT NULL, + challenge_id bigint NOT NULL, + phase_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: challenge_phases_evaluators_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.challenge_phases_evaluators_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: challenge_phases_evaluators_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.challenge_phases_evaluators_id_seq OWNED BY public.challenge_phases_evaluators.id; + + -- -- Name: challenges; Type: TABLE; Schema: public; Owner: - -- @@ -397,6 +430,42 @@ CREATE SEQUENCE public.evaluation_forms_id_seq ALTER SEQUENCE public.evaluation_forms_id_seq OWNED BY public.evaluation_forms.id; +-- +-- Name: evaluator_invitations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.evaluator_invitations ( + id bigint NOT NULL, + challenge_id bigint NOT NULL, + phase_id bigint NOT NULL, + first_name character varying NOT NULL, + last_name character varying NOT NULL, + email character varying NOT NULL, + last_invite_sent timestamp(6) without time zone, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: evaluator_invitations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.evaluator_invitations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: evaluator_invitations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.evaluator_invitations_id_seq OWNED BY public.evaluator_invitations.id; + + -- -- Name: federal_partners; Type: TABLE; Schema: public; Owner: - -- @@ -1161,6 +1230,13 @@ ALTER TABLE ONLY public.certification_log ALTER COLUMN id SET DEFAULT nextval('p ALTER TABLE ONLY public.challenge_managers ALTER COLUMN id SET DEFAULT nextval('public.challenge_owners_id_seq'::regclass); +-- +-- Name: challenge_phases_evaluators id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_phases_evaluators ALTER COLUMN id SET DEFAULT nextval('public.challenge_phases_evaluators_id_seq'::regclass); + + -- -- Name: challenges id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1189,6 +1265,13 @@ ALTER TABLE ONLY public.evaluation_criteria ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.evaluation_forms ALTER COLUMN id SET DEFAULT nextval('public.evaluation_forms_id_seq'::regclass); +-- +-- Name: evaluator_invitations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.evaluator_invitations ALTER COLUMN id SET DEFAULT nextval('public.evaluator_invitations_id_seq'::regclass); + + -- -- Name: federal_partners id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1362,6 +1445,14 @@ ALTER TABLE ONLY public.challenge_managers ADD CONSTRAINT challenge_owners_pkey PRIMARY KEY (id); +-- +-- Name: challenge_phases_evaluators challenge_phases_evaluators_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_phases_evaluators + ADD CONSTRAINT challenge_phases_evaluators_pkey PRIMARY KEY (id); + + -- -- Name: challenges challenges_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1394,6 +1485,14 @@ ALTER TABLE ONLY public.evaluation_forms ADD CONSTRAINT evaluation_forms_pkey PRIMARY KEY (id); +-- +-- Name: evaluator_invitations evaluator_invitations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.evaluator_invitations + ADD CONSTRAINT evaluator_invitations_pkey PRIMARY KEY (id); + + -- -- Name: federal_partners federal_partners_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1584,6 +1683,34 @@ CREATE UNIQUE INDEX agency_members_user_id_index ON public.agency_members USING CREATE UNIQUE INDEX challenges_custom_url_index ON public.challenges USING btree (custom_url); +-- +-- Name: idx_on_challenge_id_phase_id_email_b0ae3723d2; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_on_challenge_id_phase_id_email_b0ae3723d2 ON public.evaluator_invitations USING btree (challenge_id, phase_id, email); + + +-- +-- Name: index_challenge_phases_evaluators_on_challenge_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_challenge_phases_evaluators_on_challenge_id ON public.challenge_phases_evaluators USING btree (challenge_id); + + +-- +-- Name: index_challenge_phases_evaluators_on_phase_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_challenge_phases_evaluators_on_phase_id ON public.challenge_phases_evaluators USING btree (phase_id); + + +-- +-- Name: index_challenge_phases_evaluators_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_challenge_phases_evaluators_on_user_id ON public.challenge_phases_evaluators USING btree (user_id); + + -- -- Name: index_evaluation_criteria_on_evaluation_form_id; Type: INDEX; Schema: public; Owner: - -- @@ -1605,6 +1732,20 @@ CREATE INDEX index_evaluation_forms_on_challenge_id ON public.evaluation_forms U CREATE INDEX index_evaluation_forms_on_phase_id ON public.evaluation_forms USING btree (phase_id); +-- +-- Name: index_evaluator_invitations_on_challenge_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_evaluator_invitations_on_challenge_id ON public.evaluator_invitations USING btree (challenge_id); + + +-- +-- Name: index_evaluator_invitations_on_phase_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_evaluator_invitations_on_phase_id ON public.evaluator_invitations USING btree (phase_id); + + -- -- Name: message_contexts_context_context_id_audience_parent_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -1773,6 +1914,14 @@ ALTER TABLE ONLY public.evaluation_forms ADD CONSTRAINT fk_rails_1c5ee6cafd FOREIGN KEY (phase_id) REFERENCES public.phases(id); +-- +-- Name: challenge_phases_evaluators fk_rails_252b3aeac2; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_phases_evaluators + ADD CONSTRAINT fk_rails_252b3aeac2 FOREIGN KEY (user_id) REFERENCES public.users(id); + + -- -- Name: evaluation_forms fk_rails_28ad57fb81; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1789,6 +1938,38 @@ ALTER TABLE ONLY public.evaluation_criteria ADD CONSTRAINT fk_rails_a39b8fa483 FOREIGN KEY (evaluation_form_id) REFERENCES public.evaluation_forms(id); +-- +-- Name: challenge_phases_evaluators fk_rails_ba136003c3; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_phases_evaluators + ADD CONSTRAINT fk_rails_ba136003c3 FOREIGN KEY (phase_id) REFERENCES public.phases(id); + + +-- +-- Name: evaluator_invitations fk_rails_c4f5e767a9; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.evaluator_invitations + ADD CONSTRAINT fk_rails_c4f5e767a9 FOREIGN KEY (phase_id) REFERENCES public.phases(id); + + +-- +-- Name: evaluator_invitations fk_rails_d623dba270; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.evaluator_invitations + ADD CONSTRAINT fk_rails_d623dba270 FOREIGN KEY (challenge_id) REFERENCES public.challenges(id); + + +-- +-- Name: challenge_phases_evaluators fk_rails_e27fcb2d4d; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_phases_evaluators + ADD CONSTRAINT fk_rails_e27fcb2d4d FOREIGN KEY (challenge_id) REFERENCES public.challenges(id); + + -- -- Name: message_context_statuses message_context_statuses_message_context_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1996,8 +2177,10 @@ ALTER TABLE ONLY public.winners SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +(20241018150049), (20241017172408), (20241015140056), +(20241014214843), (20241001143033), (20240927010020), (20240917010803), diff --git a/spec/factories/challenge.rb b/spec/factories/challenge.rb index 30ebd428..53b873d5 100644 --- a/spec/factories/challenge.rb +++ b/spec/factories/challenge.rb @@ -22,7 +22,7 @@ end_date { 3.months.from_now } auto_publish_date { 1.week.from_now } published_on { 2.weeks.from_now } - custom_url { Faker::Internet.url(host: "example.com", path: "/custom") } + custom_url { Faker::Internet.url(host: "example.com", path: "/custom/#{SecureRandom.hex(8)}") } external_url { Faker::Internet.url(host: "example.com", path: "/external") } agency_name { Faker::Company.name } fiscal_year { "2024" } diff --git a/spec/factories/challenge_phases_evaluator.rb b/spec/factories/challenge_phases_evaluator.rb new file mode 100644 index 00000000..73eb8d95 --- /dev/null +++ b/spec/factories/challenge_phases_evaluator.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :challenge_phases_evaluator do + # Associations + association :challenge + association :phase + association :user + end +end diff --git a/spec/factories/evaluator_invitation.rb b/spec/factories/evaluator_invitation.rb new file mode 100644 index 00000000..3a08477a --- /dev/null +++ b/spec/factories/evaluator_invitation.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :evaluator_invitation do + # Associations + association :challenge + association :phase + + # Fields + first_name { Faker::Name.first_name } + last_name { Faker::Name.last_name } + email { Faker::Internet.email } + last_invite_sent { Time.current } + end +end diff --git a/spec/models/challenge_phases_evaluator_spec.rb b/spec/models/challenge_phases_evaluator_spec.rb new file mode 100644 index 00000000..b4c720c8 --- /dev/null +++ b/spec/models/challenge_phases_evaluator_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +RSpec.describe ChallengePhasesEvaluator, type: :model do + let(:challenge) { create(:challenge) } + let(:phase) { create(:phase, challenge:) } + let(:user) { create(:user, role: :evaluator) } + + it "can be created with valid attributes" do + evaluator = build(:challenge_phases_evaluator, challenge:, phase:, user:) + expect(evaluator).to be_valid + expect { evaluator.save! }.to change { described_class.count }.by(1) + end + + it "can be destroyed" do + evaluator = create(:challenge_phases_evaluator, challenge:, phase:, user:) + expect { evaluator.destroy }.to change { described_class.count }.by(-1) + end + + it "associates the user as an evaluator for the challenge" do + create(:challenge_phases_evaluator, challenge:, phase:, user:) + expect(challenge.evaluators).to include(user) + end + + it "requires a challenge" do + evaluator = build(:challenge_phases_evaluator, challenge: nil) + expect(evaluator).not_to be_valid + expect(evaluator.errors[:challenge]).to include("must exist") + end + + it "requires a phase" do + evaluator = build(:challenge_phases_evaluator, phase: nil) + expect(evaluator).not_to be_valid + expect(evaluator.errors[:phase]).to include("must exist") + end + + it "requires a user" do + evaluator = build(:challenge_phases_evaluator, user: nil) + expect(evaluator).not_to be_valid + expect(evaluator.errors[:user]).to include("must exist") + end + + it "allows multiple evaluators for the same challenge and phase" do + challenge = create(:challenge) + phase = create(:phase, challenge:) + user1 = create(:user, role: :evaluator) + user2 = create(:user, role: :evaluator) + + create(:challenge_phases_evaluator, challenge:, phase:, user: user1) + evaluator2 = build(:challenge_phases_evaluator, challenge:, phase:, user: user2) + + expect(evaluator2).to be_valid + expect { evaluator2.save! }.not_to raise_error + end +end diff --git a/spec/models/evaluator_invitation_spec.rb b/spec/models/evaluator_invitation_spec.rb new file mode 100644 index 00000000..68e0b82b --- /dev/null +++ b/spec/models/evaluator_invitation_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe EvaluatorInvitation, type: :model do + let(:challenge) { create(:challenge) } + let(:phase) { create(:phase, challenge:) } + + it "can be created with valid attributes" do + invitation = build(:evaluator_invitation, challenge:, phase:) + expect(invitation).to be_valid + expect { invitation.save! }.to change { described_class.count }.by(1) + end + + it "can be destroyed" do + invitation = create(:evaluator_invitation, challenge:, phase:) + expect { invitation.destroy }.to change { described_class.count }.by(-1) + end + + it "validates uniqueness of email within challenge and phase" do + create(:evaluator_invitation, challenge:, phase:, email: "test@example.com") + duplicate_invitation = build(:evaluator_invitation, challenge:, phase:, email: "test@example.com") + expect(duplicate_invitation).not_to be_valid + expect(duplicate_invitation.errors[:email]).to include("has already been taken") + end + + it "allows the same email for different challenges or phases" do + create(:evaluator_invitation, challenge:, phase:, email: "test@example.com") + different_challenge = create(:challenge) + different_phase = create(:phase, challenge: different_challenge) + + invitation_different_challenge = build(:evaluator_invitation, challenge: different_challenge, phase:, + email: "test@example.com") + expect(invitation_different_challenge).to be_valid + + invitation_different_phase = build(:evaluator_invitation, challenge:, phase: different_phase, + email: "test@example.com") + expect(invitation_different_phase).to be_valid + end + + it "requires a first name" do + invitation = build(:evaluator_invitation, first_name: nil) + expect(invitation).not_to be_valid + expect(invitation.errors[:first_name]).to include("can't be blank") + end + + it "requires a last name" do + invitation = build(:evaluator_invitation, last_name: nil) + expect(invitation).not_to be_valid + expect(invitation.errors[:last_name]).to include("can't be blank") + end + + it "requires a valid email" do + invitation = build(:evaluator_invitation, email: "invalid_email") + expect(invitation).not_to be_valid + expect(invitation.errors[:email]).to include("is invalid") + end +end diff --git a/spec/requests/manage_submissions_spec.rb b/spec/requests/manage_submissions_spec.rb index fe833df5..063fb1a9 100644 --- a/spec/requests/manage_submissions_spec.rb +++ b/spec/requests/manage_submissions_spec.rb @@ -27,15 +27,34 @@ end context "when logged in as a challenge manager" do - before do - create_and_log_in_user(role: "challenge_manager") - end + let(:challenge_user) { create_user(role: "challenge_manager") } + + before { log_in_user(challenge_user) } it "renders the index view with the correct header" do get manage_submissions_path expect(response).to have_http_status(:success) - expect(response.body).to include("Manage Submissions") + expect(response.body).to include("Submissions & Evaluations") + expect(response.body).to include("View challenge submissions") + end + + it "renders an empty list" do + get manage_submissions_path + + expect(response.body).to include("You currently do not have any challenges.") + end + + it "renders a list of challenges" do + agency = Agency.create!(name: "Gandalf and Sons", acronym: "GAD") + challenge = Challenge.create!(user: challenge_user, agency:, title: "Turning monster energy into pepto bismol") + phase = create_phase(challenge_id: challenge.id) + ChallengeManager.create(user: challenge_user, challenge:) + create_evaluation_form(title: "Frodo", challenge_id: challenge.id, phase_id: phase.id) + + get manage_submissions_path + expect(response.body).to include("Turning monster energy into pepto bismol") + expect(response.body).to include("Frodo") end end