Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[162] Build UI - Eval Form Step 2 - Evaluation Criteria Add/Remove Criteria #219

Merged
merged 31 commits into from
Nov 1, 2024
Merged
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
64b9841
162 Initial eval criteria commit for eval form
cpreisinger Oct 18, 2024
16ed9dc
162 Initial temp styling for eval criteria
cpreisinger Oct 18, 2024
13c836e
Merge branch 'dev' of github.com:GSA/Challenge_platform into 162/eval…
cpreisinger Oct 18, 2024
9ade174
164 Add support for binary scale to eval criteria
cpreisinger Oct 22, 2024
765ae15
165 Add support for rating scale to eval criteria
cpreisinger Oct 22, 2024
a22b0e8
165 Fix bug with new criteria option labels
cpreisinger Oct 22, 2024
d77a3cc
165 Option label bug with form validation error
cpreisinger Oct 22, 2024
38d8f2c
165 Fix bug with rating options on new criteria
cpreisinger Oct 22, 2024
2af7b30
61 Accordion titles and collapse validity check
cpreisinger Oct 28, 2024
e55b445
Merge branch 'dev' of github.com:GSA/Challenge_platform into 162/eval…
cpreisinger Oct 28, 2024
ef80db5
Merge branch '165/evaluation-criteria-rating-scale' into 162/eval-for…
cpreisinger Oct 28, 2024
c23e016
61 Fix rspec failure with criteria option labels
cpreisinger Oct 28, 2024
eea695f
162 Initial eval criteria stimulus port
cpreisinger Oct 29, 2024
85725f6
Merge branch 'dev' of github.com:GSA/Challenge_platform into 162/eval…
cpreisinger Oct 29, 2024
db83008
update yarn.lock
stepchud Oct 29, 2024
3394377
61 Update dependency versions
cpreisinger Oct 29, 2024
1f1716f
Merge branch '162/eval-form-evaluation-criteria' of github.com:GSA/Ch…
cpreisinger Oct 29, 2024
027e1c4
set node-version for circleci
stepchud Oct 29, 2024
d3fb1be
61 Make eval criteria respect eval form disabled
cpreisinger Oct 29, 2024
7a7307f
61 Simplify eval criteria js for code climate1
cpreisinger Oct 29, 2024
9e04a71
61 Styling and add max length for eval criteria
cpreisinger Oct 30, 2024
a8afc05
61 Styled add another criteria button
cpreisinger Oct 30, 2024
2ac908c
61 Temp darker accordion styling for criteria
cpreisinger Oct 30, 2024
9d0f5b5
Merge branch 'dev' into 162/eval-form-evaluation-criteria
stepchud Oct 30, 2024
a631c66
Update .circleci/config.yml
stepchud Oct 30, 2024
a2e5321
revert structure.sql
stepchud Oct 30, 2024
ac91b8e
61 Remove unsued targets and code readability
cpreisinger Oct 31, 2024
6e2e07a
Merge branch '162/eval-form-evaluation-criteria' of github.com:GSA/Ch…
cpreisinger Oct 31, 2024
cfeb931
Merge branch 'dev' of github.com:GSA/Challenge_platform into 162/eval…
cpreisinger Oct 31, 2024
2d05253
61 Add the confirmation screen for eval form save
cpreisinger Oct 31, 2024
aba2de0
grammar
stepchud Oct 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -131,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)
@@ -173,7 +173,7 @@ GEM
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.1)
msgpack (1.7.2)
msgpack (1.7.3)
net-http (0.4.1)
uri
net-imap (0.5.0)
@@ -266,13 +266,13 @@ GEM
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)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.1)
@@ -303,14 +303,14 @@ 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-progressbar (1.13.0)
rubyzip (2.3.2)
Empty file removed app/assets/builds/.keep
Empty file.
17 changes: 17 additions & 0 deletions app/assets/stylesheets/application.sass.scss
Original file line number Diff line number Diff line change
@@ -9,3 +9,20 @@ dialog::backdrop {
background-color: black;
border-color: black;
}

#add-criteria-button.usa-button {
background-color: #4d8055;
}

// #criteria-list .usa-accordion__button {
// background-color: #162e51;
// background-image: url(asset-path('../builds/images/usa-icons-bg/add--white.svg', image)),linear-gradient(transparent,transparent);
// }

// #criteria-list .usa-accordion__button {
// background-color: #162e51;
// }

// #criteria-list .usa-accordion__button[aria-expanded=true] {
// background-image: url(assets/images/usa-icons-bg/remove-white.svg),linear-gradient(transparent,transparent);
// }
7 changes: 6 additions & 1 deletion app/controllers/evaluation_forms_controller.rb
Original file line number Diff line number Diff line change
@@ -76,7 +76,12 @@ def set_evaluation_forms
def evaluation_form_params
permitted = params.require(:evaluation_form).
permit(:title, :instructions, :phase_id, :status, :comments_required,
:weighted_scoring, :publication_date, :closing_date, :challenge_id)
:weighted_scoring, :publication_date, :closing_date, :challenge_id,
evaluation_criteria_attributes: [
:id, :title, :description, :points_or_weight, :scoring_type,
:option_range_start, :option_range_end, :_destroy,
{ option_labels: {} }
])
closing_date = parse_closing_date(permitted[:closing_date])
closing_date ? permitted.merge({ closing_date: }) : permitted
end
20 changes: 20 additions & 0 deletions app/helpers/evaluation_forms_helper.rb
Original file line number Diff line number Diff line change
@@ -14,6 +14,26 @@ def inline_error(evaluation_form, field)
tag.span(error, class: "text-secondary font-body-2xs", id: "evaluation_form_#{field}_error")
end

def criteria_field_id(form, attribute, is_template)
prefix = "evaluation_form_evaluation_criteria_attributes"

if is_template
"#{prefix}_NEW_CRITERIA_#{attribute}"
else
"#{prefix}_#{form.options[:child_index]}_#{attribute}"
end
end

def criteria_field_name(form, attribute, is_template)
prefix = "evaluation_form[evaluation_criteria_attributes]"

if is_template
"#{prefix}[NEW_CRITERIA][#{attribute}]"
else
"#{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
2 changes: 1 addition & 1 deletion app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import "./controllers"
import "./controllers";
221 changes: 221 additions & 0 deletions app/javascript/controllers/evaluation_criteria_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// app/javascript/controllers/evaluation_criteria_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
static targets = [
"criteriaList",
"template",
"addButton",
"criteriaRow",
"scoringRadio",
"binaryOptions",
"ratingOptions",
"scaleOptions",
"hiddenOptionRangeStart",
"hiddenOptionRangeEnd",
"selectOptionRangeStart",
"selectOptionRangeEnd",
"criteriaLabelRow",
];

connect() {
this.counter = this.criteriaRowTargets.length;
}

addCriteria() {
this.counter++;
const newCriteria = this.templateTarget.cloneNode(true);

this.replacePlaceholders(newCriteria);
this.enableInputs(newCriteria);

this.criteriaListTarget.appendChild(newCriteria);

this.updateCriteriaTitles();
}

removeCriteria(event) {
const row = event.target.closest(".criteria-row");
const destroyField = row.querySelector(".destroy-evaluation-criteria");

if (destroyField) {
row.style.display = "none";
destroyField.value = "true";
this.disableInputs(row);
} else {
row.remove();
}

if (this.visibleRows().length == 0) {
return this.addCriteria();
}

this.updateCriteriaTitles();
}

toggleScoringType(event) {
const row = event.target.closest(".criteria-row");
const scoringType = row.querySelector(".scoring-type-radio:checked").value;

this.updateScoringOptions(row, scoringType);
}

toggleOptionRange(event) {
const row = event.target.closest(".criteria-row");
const start = row.querySelector(
".option-range-select.option-range-start"
).value;
const end = row.querySelector(
".option-range-select.option-range-end"
).value;

this.toggleOptionLabels(row, start, end);
}

replacePlaceholders(newCriteria) {
newCriteria.setAttribute("data-evaluation-criteria-target", "criteriaRow");
newCriteria.style.display = "block";
newCriteria.removeAttribute("id");

let accordionButton = newCriteria.querySelector(".usa-accordion__button");
let accordionContent = newCriteria.querySelector(".usa-accordion__content");

let accordionId = accordionContent
.getAttribute("id")
.replace("NEW_CRITERIA", this.counter);

accordionButton.setAttribute("aria-controls", accordionId);
accordionContent.setAttribute("id", accordionId);

newCriteria.querySelectorAll("[id]").forEach((el) => {
el.id = el.id.replace("NEW_CRITERIA", this.counter);
});

newCriteria.querySelectorAll("[name]").forEach((el) => {
el.name = el.name.replace("NEW_CRITERIA", this.counter);
});

newCriteria.querySelectorAll("label").forEach((label) => {
label.setAttribute(
"for",
label.getAttribute("for").replace("NEW_CRITERIA", this.counter)
);
});
}

updateCriteriaTitles() {
this.visibleRows().forEach((row, index) => {
const numberElement = row.querySelector(".criteria-number");
numberElement.textContent = index + 1;
});
}

updateScoringOptions(row, scoringType) {
const options = {
scaleOptions: row.querySelector(".criteria-scale-options"),
binaryOptions: row.querySelector(".criteria-binary-options"),
ratingOptions: row.querySelector(".criteria-rating-options"),
scaleOptionLabels: row.querySelector(".criteria-scale-option-labels"),
};

console.log(options);

switch (scoringType) {
case "binary":
this.showBinaryOptions(options);
this.toggleOptionLabels(row, 0, 1);
break;
case "rating":
this.showRatingOptions(row, options);
break;
default:
this.hideAllOptions(options);
break;
}
}

showBinaryOptions(options) {
options.scaleOptions.style.display = "block";
options.binaryOptions.style.display = "block";
options.ratingOptions.style.display = "none";
this.enableInputs(options.binaryOptions);
this.disableInputs(options.ratingOptions);
}

showRatingOptions(row, options) {
options.scaleOptions.style.display = "block";
options.binaryOptions.style.display = "none";
options.ratingOptions.style.display = "block";
this.enableInputs(options.ratingOptions);
this.disableInputs(options.binaryOptions);
const start = parseInt(
row.querySelector(".option-range-select.option-range-start").value
);
const end = parseInt(
row.querySelector(".option-range-select.option-range-end").value
);
this.toggleOptionLabels(row, start, end);
}

hideAllOptions(options) {
options.scaleOptions.style.display = "none";
options.binaryOptions.style.display = "none";
options.ratingOptions.style.display = "none";
this.disableInputs(options.binaryOptions);
this.disableInputs(options.ratingOptions);
this.disableInputs(options.scaleOptionLabels);
}

toggleOptionLabels(row, start, end) {
row
.querySelectorAll(".criteria-option-label-row")
.forEach((labelRow, index) => {
labelRow.style.display =
index >= start && index <= end ? "flex" : "none";
const input = labelRow.querySelector("input");
input.disabled = !(index >= start && index <= end);
});
}

disableInputs(container) {
container.querySelectorAll("input, select, textarea").forEach((input) => {
if (input.type != "hidden") {
input.disabled = true;
}
});
}

enableInputs(container) {
container.querySelectorAll("input, select, textarea").forEach((input) => {
input.disabled = false;
});
}

visibleRows() {
return this.criteriaRowTargets.filter(
(row) => row.style.display !== "none"
);
}

validateInputs(event) {
const section = document.getElementById(
event.target
.closest(".usa-accordion__button")
.getAttribute("aria-controls")
);

if (this.checkRequiredFields(section)) {
return true;
} else {
event.preventDefault();
event.stopPropagation();
return false;
}
}

checkRequiredFields(section) {
return Array.from(section.querySelectorAll("[required]")).every((field) =>
field.checkValidity() ? true : field.reportValidity()
);
}
}
9 changes: 5 additions & 4 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
@@ -2,8 +2,9 @@
// Run that command whenever you add a new controller or create them with
// ./bin/rails generate stimulus controllerName

import { application } from "./application"

import EvaluationFormController from "./evaluation_form_controller"
application.register("evaluation-form", EvaluationFormController)
import { application } from "./application";

import EvaluationFormController from "./evaluation_form_controller";
import EvaluationCriteriaController from "./evaluation_criteria_controller";
application.register("evaluation-form", EvaluationFormController);
application.register("evaluation-criteria", EvaluationCriteriaController);
6 changes: 3 additions & 3 deletions app/models/evaluation_criterion.rb
Original file line number Diff line number Diff line change
@@ -29,12 +29,12 @@ class EvaluationCriterion < ApplicationRecord
enum :scoring_type, { numeric: 0, rating: 1, binary: 2 }
attribute :option_range_start, :integer
attribute :option_range_end, :integer
attribute :option_labels, :json, default: -> { [] }
attribute :option_labels, :json, default: -> { {} }
attribute :evaluation_form_id, :integer

# Validations
validates :title, :description, :points_or_weight, presence: true
validates :title, length: { maximum: 150 }
validates :description, length: { maximum: 1000 }
validates :points_or_weight, numericality: { only_integer: true }
validates :title,
uniqueness: { scope: :evaluation_form_id, message: I18n.t("evaluation_criterion_unique_title_in_form") }
end
24 changes: 23 additions & 1 deletion app/models/evaluation_form.rb
Original file line number Diff line number Diff line change
@@ -18,7 +18,10 @@
class EvaluationForm < ApplicationRecord
belongs_to :challenge
belongs_to :phase
has_many :evaluation_criteria, dependent: :destroy, class_name: "EvaluationCriterion"
has_many :evaluation_criteria, lambda {
order(:created_at)
}, class_name: 'EvaluationCriterion', dependent: :destroy, inverse_of: :evaluation_form
accepts_nested_attributes_for :evaluation_criteria, allow_destroy: true

scope :by_user, lambda { |user|
joins(challenge: :challenge_manager_users).
@@ -28,4 +31,23 @@ class EvaluationForm < ApplicationRecord
validates :title, presence: true, length: { maximum: 150 }
validates :instructions, presence: true
validates :closing_date, presence: true

validate :criteria_weights_must_sum_to_one_hundred, if: :weighted_scoring?
validate :validate_unique_criteria_titles

def validate_unique_criteria_titles
titles = evaluation_criteria.reject(&:marked_for_destruction?).map(&:title)

return unless titles.uniq.length != titles.length

errors.add(:base, I18n.t("evaluation_criterion_unique_title_in_form_error"))
end

def criteria_weights_must_sum_to_one_hundred
total_weight = evaluation_criteria.reject(&:marked_for_destruction?).sum(&:points_or_weight)

return unless total_weight != 100

errors.add(:base, I18n.t("evaluation_form_criteria_weight_total_error"))
end
end
192 changes: 192 additions & 0 deletions app/views/evaluation_forms/_evaluation_criterion_fields.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<h4 class="usa-accordion__heading">
<button
type="button"
class="usa-accordion__button display-flex flex-justify icon-white"
aria-expanded="<%= f.object.persisted? ? false : true %>"
aria-controls="<%= criteria_field_id(f, "accordion", is_template) %>"
data-evaluation-criteria-target="accordionButton"
data-action="click->evaluation-criteria#validateInputs"
>
<div>Criteria <span class="criteria-number" data-evaluation-criteria-target="criteriaNumber"><%= f.options[:child_index] + 1 %></span></div>
</button>
</h4>

<div id="<%= criteria_field_id(f, "accordion", is_template) %>" class="usa-accordion__content" data-evaluation-criteria-target="accordionContent">
<% if f.object.persisted? %>
<%= f.hidden_field :id, value: f.object.id %>
<%= f.hidden_field :_destroy, class: "destroy-evaluation-criteria", data: {"evaluation-criteria-target": "destroyField"} %>
<% end %>

<div class="usa-form-group margin-top-0">
<%= f.label :title, "Criteria title", for: criteria_field_id(f, "title", is_template), class: "text-bold" %>
<span style="color: darkred;">*</span>
<%= image_tag "images/usa-icons/help_outline.svg", width: 16, height: 16, alt: "Help for criteria title" %>

<%= f.text_field :title,
id: criteria_field_id(f, "title", is_template),
name: criteria_field_name(f, "title", is_template),
class: "usa-input",
placeholder: "Add criteria title here",
maxlength: 150,
required: true,
disabled: is_template || form_disabled,
data: {"evaluation-criteria-target": "titleField"}
%>
</div>

<div class="usa-character-count">
<div class="usa-form-group">
<%= f.label :description, "Criteria description", for: criteria_field_id(f, "description", is_template), class: "text-bold" %>
<span style="color: darkred;">*</span>
<%= image_tag "images/usa-icons/help_outline.svg", width: 16, height: 16, alt: "Help for criteria title" %>

<%= f.text_area :description,
id: criteria_field_id(f, "description", is_template),
name: criteria_field_name(f, "description", is_template),
class: "usa-textarea usa-character-count__field",
placeholder: "Add criteria description here",
maxlength: 1000,
required: true,
disabled: is_template || form_disabled,
data: {"evaluation-criteria-target": "descriptionField"}
%>
</div>
<span class="usa-character-count__message">You can enter up to 1000 characters</span>
</div>

<div class="usa-form-group display-flex flex-align-center">
<%= f.number_field :points_or_weight,
id: criteria_field_id(f, "points_or_weight", is_template),
name: criteria_field_name(f, "points_or_weight", is_template),
class: "usa-input flex-1 margin-top-0 margin-right-2 font-sans-lg",
min: 1,
step: 1,
placeholder: "Add criteria points/weight here",
required: true,
disabled: is_template || form_disabled,
data: {"evaluation-criteria-target": "pointsOrWeightField"}
%>

<div class="flex-7 font-sans-lg">
<%= f.label :points_or_weight, "Criteria Points/Weight",
for: criteria_field_id(f, "points_or_weight", is_template),
class: "text-bold"
%>

<span style="color: darkred;">*</span>

<%= image_tag "images/usa-icons/help_outline.svg",
width: 16, height: 16, alt: "Help for criteria title",
class: "flex-0"
%>
</div>
</div>

<div class="usa-form-group">
<%= f.label :scoring_type, "Scoring Type", for: criteria_field_id(f, "scoring_type", is_template), class: "font-sans-lg text-bold" %>
<span style="color: darkred;">*</span>
<%= image_tag "images/usa-icons/help_outline.svg", width: 16, height: 16, alt: "Help for criteria title" %>

<% scoring_types = { "numeric" => "Numeric score / points", "rating" => "Rating scale", "binary" => "Binary scale" } %>
<div>
<% scoring_types.each do |value, label| %>
<div class="usa-radio">
<%= f.radio_button :scoring_type, value,
id: criteria_field_id(f, "scoring_type_#{value}", is_template),
name: criteria_field_name(f, :scoring_type, is_template),
class: "usa-radio__input scoring-type-radio",
checked: f.object.scoring_type == value,
required: true,
disabled: is_template || form_disabled,
data: {
"evaluation-criteria-target": "scoringTypeRadio",
action: "click->evaluation-criteria#toggleScoringType"
}
%>

<%= label_tag criteria_field_id(f, "scoring_type_#{value}", is_template), label, class: "usa-radio__label" %>
</div>
<% end %>
</div>
</div>

<div class="criteria-scale-options usa-form-group" style="<%= f.object.numeric? || !f.object.persisted? || is_template ? 'display: none;' : '' %>" data-evaluation-criteria-target="scaleOptions">
<%= f.label :rating_scale_options, "Rating Scale Options", for: criteria_field_id(f, "rating_scale_options", is_template), class: "text-bold" %>

<div class="criteria-binary-options" style="<%= f.object.binary? ? '' : 'display: none;'%>" data-evaluation-criteria-target="binaryOptions">
<%= f.hidden_field :option_range_start,
id: criteria_field_id(f, "option_range_start", is_template),
name: criteria_field_name(f, "option_range_start", is_template),
class: "option-range-start",
disabled: !f.object.binary? || form_disabled,
value: 0,
data: {"evaluation-criteria-target": "hiddenOptionRangeStart"}
%>
<%= f.hidden_field :option_range_end,
id: criteria_field_id(f, "option_range_end", is_template),
name: criteria_field_name(f, "option_range_end", is_template),
class: "option-range-end",
disabled: !f.object.binary? || form_disabled,
value: 1,
data: {"evaluation-criteria-target": "hiddenOptionRangeEnd"}
%>
</div>

<div class="criteria-rating-options" style="<%= f.object.rating? ? '' : 'display: none;' %>" data-evaluation-criteria-target="ratingOptions">
<div class="display-flex flex-align-center margin-top-2">
<%= f.select :option_range_start, options_for_select((0..1).to_a, f.object.option_range_start), {},
id: criteria_field_id(f, "option_range_start", is_template),
name: criteria_field_name(f, "option_range_start", is_template),
class: "usa-select margin-0 height-auto width-auto font-sans-md text-bold option-range-select option-range-start",
disabled: !f.object.rating? || form_disabled,
include_blank: true,
data: {
"evaluation-criteria-target": "selectOptionRangeStart",
action: "change->evaluation-criteria#toggleOptionRange"
}
%>
<span class="margin-x-2 text-bold font-sans-md">to</span>
<%= f.select :option_range_end, options_for_select((2..10).to_a, f.object.option_range_end), {},
id: criteria_field_id(f, "option_range_end", is_template),
name: criteria_field_name(f, "option_range_end", is_template),
class: "usa-select margin-0 height-auto width-auto font-sans-md text-bold option-range-select option-range-end",
disabled: !f.object.rating? || form_disabled,
include_blank: true,
data: {
"evaluation-criteria-target": "selectOptionRangeEnd",
action: "change->evaluation-criteria#toggleOptionRange"
}
%>
</div>
</div>

<div class="criteria-scale-option-labels" data-evaluation-criteria-target="optionLabels">
<% (0..10).each do |index| %>
<% disabledOptionLabel = is_template || f.object.numeric? || ((f.object.option_range_start && index < f.object.option_range_start) || (f.object.option_range_end && index > f.object.option_range_end)) %>

<div class="display-flex flex-align-center flex-justify margin-top-2 criteria-option-label-row"
style="<%= disabledOptionLabel ? 'display: none;' : '' %>" data-evaluation-criteria-target="optionLabelRow">
<%= label_tag criteria_field_id(f, "option_labels", is_template) ++ "_#{index}", index, class: "font-sans-lg text-bold text-base-light" %>

<%= f.text_field "option_labels",
id: criteria_field_id(f, "option_labels", is_template) ++ "_#{index}",
name: criteria_field_name(f, "option_labels", is_template) ++ "[#{index}]",
class: "usa-input margin-0",
value: f.object.option_labels[index.to_s],
required: true,
disabled: disabledOptionLabel || form_disabled,
data: {"evaluation-criteria-target": "optionLabelInput"}
%>
</div>
<% end %>
</div>
</div>

<% if !form_disabled %>
<div class="margin-top-2">
<%= button_tag type: "button", class: "delete-criteria-button usa-button usa-button--unstyled", title: "Delete criteria", data: {action: "click->evaluation-criteria#removeCriteria"} do %>
Remove criteria
<% end %>
</div>
<% end %>
</div>
51 changes: 48 additions & 3 deletions app/views/evaluation_forms/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
<%= form_with(model: evaluation_form, data: { controller: "evaluation-form" }) do |form| %>
<% if evaluation_form.errors.any? %>
<div style="color: red">
<h2><%= pluralize(evaluation_form.errors.count, "error") %> prohibited this evaluation form from being saved:</h2>

<ul>
<% evaluation_form.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<%
if evaluation_form.challenge && evaluation_form.phase
challenge = evaluation_form.challenge
@@ -80,6 +91,43 @@
</div>
</fieldset>
</div>

<div data-controller="evaluation-criteria">
<div id="criteria-list" class="margin-top-4 usa-accordion usa-accordion__multiselectable" data-allow-multiple data-evaluation-criteria-target="criteriaList">
<% if evaluation_form.evaluation_criteria.empty? %>
<%= form.fields_for :evaluation_criteria, EvaluationCriterion.new do |ec| %>
<div class="criteria-row margin-bottom-2" data-evaluation-criteria-target="criteriaRow">
<%= render partial: 'evaluation_criterion_fields', locals: { f: ec, is_template: false, form_disabled: false } %>
</div>
<% end %>
<% else %>
<%= form.fields_for :evaluation_criteria, include_id: false do |ec| %>
<div class="criteria-row margin-bottom-2" style="<%= ec.object.marked_for_destruction? ? "display: none;" : "" %>" data-evaluation-criteria-target="criteriaRow">
<%= render partial: 'evaluation_criterion_fields', locals: { f: ec, is_template: false, form_disabled: disabled } %>
</div>
<% end %>
<% end %>
</div>

<% if !disabled %>
<div class="usa-accordion">
<div id="criteria-template" class="criteria-row margin-bottom-2 display-none" data-evaluation-criteria-target="template">
<%= form.fields_for :evaluation_criteria, EvaluationCriterion.new do |ec| %>
<%= render partial: 'evaluation_criterion_fields', locals: { f: ec, is_template: true, form_disabled: disabled } %>
<% end %>
</div>
</div>

<%= button_tag type: "button", id: "add-criteria-button", class: "usa-button width-full display-flex flex-column", title: "Add another criteria", data: {action: "click->evaluation-criteria#addCriteria"} do %>
<%= image_tag(
"images/usa-icons/add_circle.svg",
class: "usa-icon icon-white circle-4 margin-bottom-1",
alt: "Add criteria plus icon"
)%>
<div class="font-sans-xs">Add another criteria</div>
<% end %>
<% end %>
</div>
</li>
<li class="usa-process-list__item">
<h4 class="usa-process-list__heading">Evaluation Period</h4>
@@ -109,9 +157,6 @@
</li>
</ol>




<div class="display-flex flex-wrap">
<button type="submit" name="commit" class="mobile-lg:width-mobile usa-button font-body-2xs text-no-wrap mobile-lg:margin-left-2 mobile-lg:margin-bottom-4" style="background-color: #4d8055">
<%= image_tag(
5 changes: 3 additions & 2 deletions config/initializers/inflections.rb
Original file line number Diff line number Diff line change
@@ -5,12 +5,13 @@
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end
inflect.irregular "criterion", "criteria"
end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
4 changes: 3 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -35,4 +35,6 @@ en:
login_error: "There was an issue with logging in. Please try again."
please_try_again: "Please try again."
session_expired_alert: "Your session has expired. Please log in again."
evaluation_criterion_unique_title_in_form: "must be unique within the same form."
evaluation_criterion_unique_title_in_form_error: "Evaluation criteria title must be unique within the same form."
evaluation_form_criteria_weight_total_error: "The total weight of all evaluation criteria must add up to 100 when weighted scoring is enabled."
evaluation_criteria_form_title_placeholder: "Add criteria title here"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class RemoveUniqueTitleIndexFromEvaluationCriteria < ActiveRecord::Migration[7.2]
def change
# remove_index :evaluation_criteria, name: 'index_evaluation_criteria_on_evaluation_form_id_and_title'
remove_index :evaluation_criteria, [:evaluation_form_id, :title]
end
end
10 changes: 2 additions & 8 deletions db/structure.sql
Original file line number Diff line number Diff line change
@@ -685,7 +685,7 @@ CREATE TABLE public.oban_jobs (
attempted_by text[],
discarded_at timestamp without time zone,
priority integer DEFAULT 0 NOT NULL,
tags text[] DEFAULT ARRAY[]::text[],
tags character varying(255)[] DEFAULT ARRAY[]::character varying[],
meta jsonb DEFAULT '{}'::jsonb,
cancelled_at timestamp without time zone,
CONSTRAINT attempt_range CHECK (((attempt >= 0) AND (attempt <= max_attempts))),
@@ -1766,13 +1766,6 @@ CREATE INDEX index_challenge_phases_evaluators_on_user_id ON public.challenge_ph
CREATE INDEX index_evaluation_criteria_on_evaluation_form_id ON public.evaluation_criteria USING btree (evaluation_form_id);


--
-- Name: index_evaluation_criteria_on_evaluation_form_id_and_title; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX index_evaluation_criteria_on_evaluation_form_id_and_title ON public.evaluation_criteria USING btree (evaluation_form_id, title);


--
-- Name: index_evaluation_forms_on_challenge_id; Type: INDEX; Schema: public; Owner: -
--
@@ -2264,6 +2257,7 @@ SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
(20241023195356),
(20241018150049),
(20241017172408),
(20241015140056),
(20241014214843),
(20241001143033),
14 changes: 12 additions & 2 deletions spec/factories/evaluation_criteria.rb
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
scoring_type { [:numeric, :rating, :binary].sample }
option_range_start { nil }
option_range_end { nil }
option_labels { [] }
option_labels { {} }

# Factory options
trait :numeric do
@@ -32,10 +32,20 @@
when "rating"
criterion.option_range_start = 0
criterion.option_range_end = 4
criterion.option_labels = {
"0" => "Option 1",
"1" => "Option 2",
"2" => "Option 3",
"3" => "Option 4",
"4" => "Option 5"
}
when "binary"
criterion.option_range_start = 0
criterion.option_range_end = 1
criterion.option_labels = %w[no yes]
criterion.option_labels = {
"0" => "No",
"1" => "Yes"
}
end
end
end
10 changes: 1 addition & 9 deletions spec/models/evaluation_criterion_spec.rb
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
RSpec.describe EvaluationCriterion, type: :model do
let(:challenge) { create(:challenge) }
let(:phase) { create(:phase, challenge:) }
let(:evaluation_form) { create(:evaluation_form, challenge:, phase:) }
let(:evaluation_form) { create(:evaluation_form, challenge:, phase:, weighted_scoring: false) }
let(:evaluation_criterion) { create(:evaluation_criterion, evaluation_form:) }

describe "creating an evaluation criterion" do
@@ -28,14 +28,6 @@
expect(evaluation_criterion).not_to be_valid
expect(evaluation_criterion.errors[:points_or_weight]).to include("can't be blank")
end

it "validates uniqueness of title within the same evaluation form" do
create(:evaluation_criterion, evaluation_form:, title: "Unique Title")
duplicate_criterion = build(:evaluation_criterion, evaluation_form:, title: "Unique Title")

expect(duplicate_criterion).not_to be_valid
expect(duplicate_criterion.errors[:title]).to include(I18n.t("evaluation_criterion_unique_title_in_form"))
end
end

describe "updating an evaluation criterion" do
23 changes: 23 additions & 0 deletions spec/requests/evaluation_forms_spec.rb
Original file line number Diff line number Diff line change
@@ -119,10 +119,33 @@

context "when updating weighted scoring in scale type" do
it "updates the weighted_scoring attribute" do
create(:evaluation_criterion, evaluation_form: evaluation_form, points_or_weight: 100)

patch evaluation_form_path(evaluation_form), params: { evaluation_form: { weighted_scoring: true } }
evaluation_form.reload
expect(evaluation_form.weighted_scoring).to be_truthy
end

it "fails if criteria doesn't add up to 100" do
evaluation_form = create(:evaluation_form, weighted_scoring: false)
evaluation_criterion = create(:evaluation_criterion, evaluation_form:, points_or_weight: 100)

patch evaluation_form_path(evaluation_form), params: {
evaluation_form: {
weighted_scoring: true,
evaluation_criteria_attributes: {
"0" => {
id: evaluation_criterion.id,
points_or_weight: 50
}
}
}
}

evaluation_form.reload

expect(evaluation_form.weighted_scoring).to be_falsey
end
end
end
end
3,068 changes: 958 additions & 2,110 deletions yarn.lock

Large diffs are not rendered by default.