diff --git a/app/controllers/evaluators_controller.rb b/app/controllers/evaluators_controller.rb index 123f4523..97f67cd9 100644 --- a/app/controllers/evaluators_controller.rb +++ b/app/controllers/evaluators_controller.rb @@ -13,6 +13,8 @@ def index end def create + @evaluator_invitation = EvaluatorInvitation.new(evaluator_invitation_params) + result = evaluator_service.process_evaluator_invitation( evaluator_invitation_params[:email], evaluator_invitation_params @@ -20,12 +22,11 @@ def create if result[:success] redirect_to phase_evaluators_path(@phase), notice: result[:message] - else - flash.now[:alert] = result[:message] - @evaluator_invitations = @phase.evaluator_invitations - @existing_evaluators = @phase.evaluators - render :index + return end + + handle_failed_invitation(result) + render :index, status: :unprocessable_entity end def destroy @@ -64,7 +65,18 @@ def evaluator_service def evaluator_invitation_params params.require(:evaluator_invitation).permit( - :first_name, :last_name, :email, :challenge_id, :phase_id, :last_invite_sent + :full_name, :email, :challenge_id, :phase_id, :last_invite_sent ) end + + def handle_failed_invitation(result) + @evaluator_invitations = @phase.evaluator_invitations + @existing_evaluators = @phase.evaluators + + if result[:evaluator_invitation].present? + @evaluator_invitation = result[:evaluator_invitation] + else + @evaluator_invitation.errors.add(:base, result[:message]) + end + end end diff --git a/app/javascript/controllers/evaluation_form_controller.js b/app/javascript/controllers/evaluation_form_controller.js index 0113cfef..59e298ed 100644 --- a/app/javascript/controllers/evaluation_form_controller.js +++ b/app/javascript/controllers/evaluation_form_controller.js @@ -69,46 +69,4 @@ export default class extends Controller { ); accordions.forEach((content) => content.removeAttribute("hidden")); } - - validatePresence(e) { - const target = e.target; - const formGroup = target.closest(".usa-form-group"); - const fieldName = target.dataset.fieldName || target.id; - - const label = this.findLabel(target, formGroup); - - if (!target.value) { - this.addErrorClasses(target, label); - this.updateErrorMessage(fieldName, "can't be blank"); - } else { - this.removeErrorClasses(target, label); - this.updateErrorMessage(fieldName, ""); - } - } - - findLabel(target, formGroup) { - const isSelect = - target.tagName === "SELECT" || - target.classList.contains("usa-combo-box__input"); - const isRadio = target.type === "radio"; - - const labelId = isSelect ? target.name : target.id; - const labelQuery = isRadio ? "legend" : `label[for="${labelId}"]`; - - return formGroup.querySelector(labelQuery); - } - - addErrorClasses(target, label) { - target.classList.add("border-secondary"); - if (label) label.classList.add("text-secondary"); - } - - removeErrorClasses(target, label) { - target.classList.remove("border-secondary"); - if (label) label.classList.remove("text-secondary"); - } - - updateErrorMessage(field, message) { - document.getElementById(field + "_error").innerHTML = message; - } } diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js new file mode 100644 index 00000000..4e0d7a73 --- /dev/null +++ b/app/javascript/controllers/form_validation_controller.js @@ -0,0 +1,46 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="form-validation" +export default class extends Controller { + validatePresence(e) { + const target = e.target; + const formGroup = target.closest(".usa-form-group"); + const fieldName = target.dataset.fieldName || target.id; + const label = this.findLabel(target, formGroup); + + if (!target.value) { + this.addErrorClasses(target, label); + this.updateErrorMessage(fieldName, "can't be blank"); + } else { + this.removeErrorClasses(target, label); + this.updateErrorMessage(fieldName, ""); + } + } + + findLabel(target, formGroup) { + const isSelect = target.tagName === "SELECT" || target.classList.contains("usa-combo-box__input"); + const isRadio = target.type === "radio"; + + const labelId = isSelect ? target.name : target.id; + const labelQuery = isRadio ? "legend" : `label[for="${labelId}"]`; + + return formGroup.querySelector(labelQuery); + } + + addErrorClasses(target, label) { + target.classList.add("border-secondary"); + if (label) label.classList.add("text-secondary"); + } + + removeErrorClasses(target, label) { + target.classList.remove("border-secondary"); + if (label) label.classList.remove("text-secondary"); + } + + updateErrorMessage(field, message) { + const errorElement = document.getElementById(field + "_error"); + if (errorElement) { + errorElement.innerHTML = message; + } + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 7e86587e..6e928fed 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -13,6 +13,9 @@ application.register("evaluation-criteria", EvaluationCriteriaController); import EvaluationFormController from "./evaluation_form_controller"; application.register("evaluation-form", EvaluationFormController); +import FormValidationController from "./form_validation_controller"; +application.register("form-validation", FormValidationController); + import HotdogController from "./hotdog_controller"; application.register("hotdog", HotdogController); diff --git a/app/models/evaluator_invitation.rb b/app/models/evaluator_invitation.rb index b22ab9c1..0b4633c3 100644 --- a/app/models/evaluator_invitation.rb +++ b/app/models/evaluator_invitation.rb @@ -24,4 +24,14 @@ class EvaluatorInvitation < ApplicationRecord validates :last_invite_sent, presence: true validates :email, uniqueness: { scope: [:challenge_id, :phase_id] } + + def full_name=(name) + names = name.to_s.strip.split(/\s+/, 2) + self.first_name = names[0] + self.last_name = names[1] + end + + def full_name + "#{first_name} #{last_name}".strip + end end diff --git a/app/services/evaluator_management_service.rb b/app/services/evaluator_management_service.rb index 30bd8fe5..64f66c8b 100644 --- a/app/services/evaluator_management_service.rb +++ b/app/services/evaluator_management_service.rb @@ -8,6 +8,7 @@ def initialize(challenge, phase) end def process_evaluator_invitation(email, invitation_params) + @invitation_params = invitation_params user = User.find_by(email:) user ? add_existing_user_as_evaluator(user) : handle_invitation(email, invitation_params) end @@ -44,6 +45,24 @@ def resend_invitation(invitation) private + def update_name_for_existing_user(user) + names = @invitation_params[:full_name].to_s.strip.split(/\s+/, 2) + if names.length < 2 + return { + success: false, + message: "First and last name are required" + } + end + + first_name, last_name = names + user.update( + first_name:, + last_name: + ) + + { success: true } + end + def add_existing_user_as_evaluator(user) if @phase.evaluators.include?(user) return { @@ -61,6 +80,9 @@ def add_existing_user_as_evaluator(user) } end + updated_name = update_name_for_existing_user(user) + return updated_name unless updated_name[:success] + cpe = ChallengePhasesEvaluator.find_or_create_by(challenge: @challenge, phase: @phase, user:) if cpe.persisted? @@ -101,7 +123,8 @@ def create_new_invitation(invitation_params) else { success: false, - message: invitation.errors.full_messages.join(", ") + message: invitation.errors.full_messages.join(", "), + evaluator_invitation: invitation } end end diff --git a/app/views/evaluation_forms/_evaluation_criterion_fields.html.erb b/app/views/evaluation_forms/_evaluation_criterion_fields.html.erb index 406c6a8e..34966098 100644 --- a/app/views/evaluation_forms/_evaluation_criterion_fields.html.erb +++ b/app/views/evaluation_forms/_evaluation_criterion_fields.html.erb @@ -32,7 +32,7 @@ disabled: is_template || form_disabled, data: { "evaluation-criteria-target": "titleField", - "action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence" + "action": "form-validation#validatePresence focusout->form-validation#validatePresence" } %> @@ -55,7 +55,7 @@ disabled: is_template || form_disabled, data: { "evaluation-criteria-target": "descriptionField", - "action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence" + "action": "form-validation#validatePresence focusout->form-validation#validatePresence" } %>
@@ -83,7 +83,7 @@ "evaluation-criteria-target": "pointsOrWeightField", action: " input->evaluation-criteria#checkPointsOrWeightMax blur->evaluation-criteria#checkPointsOrWeightMax - evaluation-form#validatePresence focusout->evaluation-form#validatePresence + form-validation#validatePresence focusout->form-validation#validatePresence " } %> @@ -111,7 +111,7 @@ disabled: is_template || form_disabled, data: { "evaluation-criteria-target": "scoringTypeRadio", - action: "click->evaluation-criteria#toggleScoringType evaluation-form#validatePresence", + action: "click->evaluation-criteria#toggleScoringType form-validation#validatePresence", field_name: criteria_field_id(f, "scoring_type", is_template) } %> @@ -187,7 +187,7 @@ value: f.object.option_labels[index.to_s], disabled: disabledOptionLabel || form_disabled, data: { - "action": "evaluation-form#validatePresence focusout->evaluation-form#validatePresence", + "action": "form-validation#validatePresence focusout->form-validation#validatePresence", "evaluation-criteria-target": "optionLabelInput" } %> diff --git a/app/views/evaluation_forms/_form.html.erb b/app/views/evaluation_forms/_form.html.erb index 41440bc7..a4355e46 100644 --- a/app/views/evaluation_forms/_form.html.erb +++ b/app/views/evaluation_forms/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with(model: evaluation_form, data: { controller: "evaluation-form modal" }) do |form| %> +<%= form_with(model: evaluation_form, data: { controller: "evaluation-form modal form-validation" }) do |form| %> <% if evaluation_form.errors.any? %>View submissions assigned to an evalutor.
Create and manage a list of evaluators for the challenge.
Evaluators will not be assigned to submissions until you assign in the submission detail view.
- <%= form_with(model: EvaluatorInvitation.new, url: phase_evaluators_path(@phase), method: :post, local: true) do |form| %> + <%= form_with(model: @evaluator_invitation || EvaluatorInvitation.new, url: phase_evaluators_path(@phase), method: :post, local: true, data: { controller: "form-validation modal" }, html: { novalidate: true }) do |form| %> <%= form.hidden_field :challenge_id, value: @challenge.id %> <%= form.hidden_field :phase_id, value: @phase.id %> <%= form.hidden_field :last_invite_sent, value: Time.current %> + + <% if form.object.errors.any? %> +