diff --git a/app/javascript/__tests__/dashboard.test.js b/app/javascript/__tests__/dashboard.test.js index cf46553753..ae6ec8c4fc 100644 --- a/app/javascript/__tests__/dashboard.test.js +++ b/app/javascript/__tests__/dashboard.test.js @@ -3,14 +3,14 @@ import { defineCaseContactsTable } from '../src/dashboard' require('jest') -var $ = require('jquery') -var _ = require('lodash') +const $ = require('jquery') +const _ = require('lodash') global.$ = $ describe('dashboard.js', () => { describe('DataTable tests', () => { function initializeDocumentWithTable (id, numberOfRows = 1, classes = '') { - var mockTable = `` + + let mockTable = `
` + '' + '' + '' diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 556066bdf0..3e873fa48d 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -23,6 +23,7 @@ require('select2/dist/css/select2') require('src/case_contact') require('src/case_emancipation') +require('src/casa_case') require('src/emancipations') require('src/select') require('src/dashboard') diff --git a/app/javascript/src/casa_case.js b/app/javascript/src/casa_case.js new file mode 100644 index 0000000000..89cc7eb1ab --- /dev/null +++ b/app/javascript/src/casa_case.js @@ -0,0 +1,15 @@ +function add_court_mandate_input () { + const list = '#mandates-list-container' + const index = $(`${list} textarea`).length + + const textarea_html = `` + + $(list).append(textarea_html) + $(list).children(':last').trigger('focus') +} + +$('document').ready(() => { + $('button#add-mandate-button').on('click', add_court_mandate_input) +}) diff --git a/app/javascript/src/dashboard.js b/app/javascript/src/dashboard.js index 368697c747..07cb29c3c7 100644 --- a/app/javascript/src/dashboard.js +++ b/app/javascript/src/dashboard.js @@ -1,5 +1,5 @@ /* global $ */ -var defineCaseContactsTable = function () { +const defineCaseContactsTable = function () { $('table#case_contacts').DataTable( { scrollX: true, @@ -9,7 +9,7 @@ var defineCaseContactsTable = function () { ) } -var defineSupervisorsDataTable = function () { +const defineSupervisorsDataTable = function () { $('table#supervisors').DataTable( { columnDefs: [ @@ -29,11 +29,11 @@ $('document').ready(() => { return true } - var statusArray = [] - var assignedToVolunteerArray = [] - var assignedToMoreThanOneVolunteerArray = [] - var assignedToTransitionYouthArray = [] - var caseNumberPrefixArray = [] + const statusArray = [] + const assignedToVolunteerArray = [] + const assignedToMoreThanOneVolunteerArray = [] + const assignedToTransitionYouthArray = [] + const caseNumberPrefixArray = [] $('.status-options').find('input[type="checkbox"]').each(function () { if ($(this).is(':checked')) { @@ -65,12 +65,12 @@ $('document').ready(() => { } }) - var status = data[3] - var assignedToVolunteer = (data[5] !== '' && data[5].split(',').length >= 1) ? 'Yes' : 'No' - var assignedToMoreThanOneVolunteer = (data[5] !== '' && data[5].split(',').length > 1) ? 'Yes' : 'No' - var assignedToTransitionYouth = data[4] - var regex = /^(CINA|TPR)/g - var caseNumberPrefix = data[0].match(regex) ? data[0].match(regex)[0] : '' + const status = data[3] + const assignedToVolunteer = (data[5] !== '' && data[5].split(',').length >= 1) ? 'Yes' : 'No' + const assignedToMoreThanOneVolunteer = (data[5] !== '' && data[5].split(',').length > 1) ? 'Yes' : 'No' + const assignedToTransitionYouth = data[4] + const regex = /^(CINA|TPR)/g + const caseNumberPrefix = data[0].match(regex) ? data[0].match(regex)[0] : '' if (statusArray.includes(status) && assignedToVolunteerArray.includes(assignedToVolunteer) && @@ -106,7 +106,7 @@ $('document').ready(() => { const editSupervisorPath = id => `/supervisors/${id}/edit` const editVolunteerPath = id => `/volunteers/${id}/edit` const casaCasePath = id => `/casa_cases/${id}` - var volunteersTable = $('table#volunteers').DataTable({ + const volunteersTable = $('table#volunteers').DataTable({ autoWidth: false, stateSave: false, columns: [ @@ -226,12 +226,12 @@ $('document').ready(() => { // Because the table saves state, we have to check/uncheck modal inputs based on what // columns are visible volunteersTable.columns().every(function (index) { - var columnVisible = this.visible() + const columnVisible = this.visible() if (columnVisible) { $('#visibleColumns input[data-column="' + index + '"]').prop('checked', true) } else { $('#visibleColumns input[data-column="' + index + '"]').prop('checked', false) } }) - var casaCasesTable = $('table#casa-cases').DataTable({ + const casaCasesTable = $('table#casa-cases').DataTable({ autoWidth: false, stateSave: false, columnDefs: [], @@ -241,7 +241,7 @@ $('document').ready(() => { }) casaCasesTable.columns().every(function (index) { - var columnVisible = this.visible() + const columnVisible = this.visible() if (columnVisible) { $('#visibleColumns input[data-column="' + index + '"]').prop('checked', true) @@ -279,11 +279,11 @@ $('document').ready(() => { $('input.toggle-visibility').on('click', function (e) { // Get the column API object and toggle the visibility - var column = volunteersTable.column($(this).attr('data-column')) + const column = volunteersTable.column($(this).attr('data-column')) column.visible(!column.visible()) volunteersTable.columns.adjust().draw() - var caseColumn = casaCasesTable.column($(this).attr('data-column')) + const caseColumn = casaCasesTable.column($(this).attr('data-column')) caseColumn.visible(!caseColumn.visible()) casaCasesTable.columns.adjust().draw() }) diff --git a/app/javascript/src/emancipations.js b/app/javascript/src/emancipations.js index d193ee3346..cb34391add 100644 --- a/app/javascript/src/emancipations.js +++ b/app/javascript/src/emancipations.js @@ -1,5 +1,5 @@ $('document').ready(() => { - var emancipationsTable = $('table#all-case-emancipations').DataTable({ + const emancipationsTable = $('table#all-case-emancipations').DataTable({ autoWidth: false, searching: false, stateSave: false, diff --git a/app/javascript/src/stylesheets/pages/casa_cases.scss b/app/javascript/src/stylesheets/pages/casa_cases.scss index dd0f4bd337..85ee36f0cd 100644 --- a/app/javascript/src/stylesheets/pages/casa_cases.scss +++ b/app/javascript/src/stylesheets/pages/casa_cases.scss @@ -19,4 +19,47 @@ body.casa_cases { width: 130px; padding-bottom: 15px; } + + .court-mandates { + textarea { + display: block; + margin-bottom: 5px; + } + + textarea, .add-court-mandate { + width: 500px; + } + } + + .add-court-mandate-container { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 10px; + } + + #add-mandate-button { + background-color: #{$primary}; + color: white; + + font-size: 1.25em; + + padding: 0.25em 1.5em; + border-radius: 10px; + border: none; + + :hover { + cursor: pointer; + } + } } + +@media only screen and (max-width: 640px) { + body.casa_cases { + .court-mandates { + textarea, .add-court-mandate { + width: 100%; + } + } + } +} \ No newline at end of file diff --git a/app/models/casa_case.rb b/app/models/casa_case.rb index 7fbd716742..e24672e5f5 100644 --- a/app/models/casa_case.rb +++ b/app/models/casa_case.rb @@ -38,6 +38,9 @@ class CasaCase < ApplicationRecord has_many :contact_types, through: :casa_case_contact_types, source: :contact_type accepts_nested_attributes_for :casa_case_contact_types + has_many :case_court_mandates, -> { order "id asc" }, dependent: :destroy + accepts_nested_attributes_for :case_court_mandates, reject_if: :all_blank + enum court_report_status: {not_submitted: 0, submitted: 1, in_review: 2, completed: 3}, _prefix: :court_report scope :ordered, -> { order(updated_at: :desc) } diff --git a/app/models/case_court_mandate.rb b/app/models/case_court_mandate.rb new file mode 100644 index 0000000000..9fe3c91f1a --- /dev/null +++ b/app/models/case_court_mandate.rb @@ -0,0 +1,24 @@ +class CaseCourtMandate < ApplicationRecord + belongs_to :casa_case + + validates :mandate_text, presence: true +end + +# == Schema Information +# +# Table name: case_court_mandates +# +# id :bigint not null, primary key +# mandate_text :string +# created_at :datetime not null +# updated_at :datetime not null +# casa_case_id :bigint not null +# +# Indexes +# +# index_case_court_mandates_on_casa_case_id (casa_case_id) +# +# Foreign Keys +# +# fk_rails_... (casa_case_id => casa_cases.id) +# diff --git a/app/policies/casa_case_policy.rb b/app/policies/casa_case_policy.rb index a6eb887373..aebc533e1d 100644 --- a/app/policies/casa_case_policy.rb +++ b/app/policies/casa_case_policy.rb @@ -47,6 +47,7 @@ def can_see_filters? alias_method :update_hearing_type?, :admin_or_supervisor? alias_method :update_judge?, :admin_or_supervisor? alias_method :update_court_report_due_date?, :admin_or_supervisor? + alias_method :update_court_mandates?, :admin_or_supervisor? def permitted_attributes common_attrs = [ @@ -58,8 +59,10 @@ def permitted_attributes case @user when CasaAdmin common_attrs.concat(%i[case_number birth_month_year_youth court_date court_report_due_date hearing_type_id judge_id]) + common_attrs << {case_court_mandates_attributes: %i[mandate_text id]} when Supervisor common_attrs.concat(%i[court_date court_report_due_date hearing_type_id judge_id]) + common_attrs << {case_court_mandates_attributes: %i[mandate_text id]} else common_attrs end diff --git a/app/views/casa_cases/_form.html.erb b/app/views/casa_cases/_form.html.erb index c0e3ba229b..39f26b6115 100644 --- a/app/views/casa_cases/_form.html.erb +++ b/app/views/casa_cases/_form.html.erb @@ -115,6 +115,32 @@ CasaCase.court_report_statuses&.map { |status| [status.first.humanize, status.first] } %> + <% if casa_case.persisted? %> +
+ <%= form.label :case_court_mandates, "Court Mandates" %> + + <% if policy(casa_case).update_court_mandates? %> +
+ <%= form.fields_for :case_court_mandates do |ff| %> + <%= ff.text_area :mandate_text %> + <% end %> +
+
+ + Add a court mandate +
+ <% else %> +
+ <% @casa_case.case_court_mandates.each do |mandate| %> + + <% end %> +
+ <% end %> +
+ <% end %> + <% if Pundit.policy(current_user, casa_case).update_contact_types? %>

<%= form.label :contact_types %>

diff --git a/babel.config.js b/babel.config.js index 915b26cb1b..2510c3ba05 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,9 +1,9 @@ module.exports = function (api) { - var validEnv = ['development', 'test', 'production'] - var currentEnv = api.env() - var isDevelopmentEnv = api.env('development') - var isProductionEnv = api.env('production') - var isTestEnv = api.env('test') + const validEnv = ['development', 'test', 'production'] + const currentEnv = api.env() + const isDevelopmentEnv = api.env('development') + const isProductionEnv = api.env('production') + const isTestEnv = api.env('test') if (!validEnv.includes(currentEnv)) { throw new Error( diff --git a/db/migrate/20210223133248_create_case_court_mandates.rb b/db/migrate/20210223133248_create_case_court_mandates.rb new file mode 100644 index 0000000000..90835cea82 --- /dev/null +++ b/db/migrate/20210223133248_create_case_court_mandates.rb @@ -0,0 +1,10 @@ +class CreateCaseCourtMandates < ActiveRecord::Migration[6.1] + def change + create_table :case_court_mandates do |t| + t.string :mandate_text + t.references :casa_case, foreign_key: true, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3ca2d0acb2..dd82bd9acf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_01_17_185614) do +ActiveRecord::Schema.define(version: 2021_02_23_133248) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -145,6 +145,14 @@ t.check_constraint "(miles_driven IS NOT NULL) OR (NOT want_driving_reimbursement)", name: "want_driving_reimbursement_only_when_miles_driven" end + create_table "case_court_mandates", force: :cascade do |t| + t.string "mandate_text" + t.bigint "casa_case_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["casa_case_id"], name: "index_case_court_mandates_on_casa_case_id" + end + create_table "contact_type_groups", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "name", null: false @@ -289,6 +297,7 @@ add_foreign_key "case_assignments", "users", column: "volunteer_id" add_foreign_key "case_contacts", "casa_cases" add_foreign_key "case_contacts", "users", column: "creator_id" + add_foreign_key "case_court_mandates", "casa_cases" add_foreign_key "emancipation_options", "emancipation_categories" add_foreign_key "followups", "users", column: "creator_id" add_foreign_key "judges", "casa_orgs" diff --git a/spec/factories/casa_case.rb b/spec/factories/casa_case.rb index 59743bae81..f3efeb98f3 100644 --- a/spec/factories/casa_case.rb +++ b/spec/factories/casa_case.rb @@ -7,6 +7,7 @@ hearing_type judge court_report_status { :not_submitted } + case_court_mandates { [] } trait :with_case_assignments do after(:create) do |casa_case, _| diff --git a/spec/factories/case_court_mandates.rb b/spec/factories/case_court_mandates.rb new file mode 100644 index 0000000000..8a5ba0aa61 --- /dev/null +++ b/spec/factories/case_court_mandates.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :case_court_mandate do + mandate_text { Faker::Lorem.paragraph(sentence_count: 5, supplemental: true, random_sentences_to_add: 20) } + end +end diff --git a/spec/models/casa_case_spec.rb b/spec/models/casa_case_spec.rb index 28e4c4ac67..525c0d8b82 100644 --- a/spec/models/casa_case_spec.rb +++ b/spec/models/casa_case_spec.rb @@ -13,6 +13,7 @@ it { is_expected.to belong_to(:judge).optional } it { is_expected.to validate_presence_of(:case_number) } it { is_expected.to validate_uniqueness_of(:case_number).scoped_to(:casa_org_id).case_insensitive } + it { is_expected.to have_many(:case_court_mandates).dependent(:destroy) } it { is_expected.to have_many(:volunteers).through(:case_assignments) } describe ".ordered" do diff --git a/spec/models/case_court_mandate_spec.rb b/spec/models/case_court_mandate_spec.rb new file mode 100644 index 0000000000..06dfa53553 --- /dev/null +++ b/spec/models/case_court_mandate_spec.rb @@ -0,0 +1,9 @@ +require "rails_helper" + +RSpec.describe CaseCourtMandate, type: :model do + subject { build(:case_court_mandate) } + + it { is_expected.to belong_to(:casa_case) } + + it { is_expected.to validate_presence_of(:mandate_text) } +end diff --git a/spec/requests/casa_cases_spec.rb b/spec/requests/casa_cases_spec.rb index 3c84b73495..eafec9ecff 100644 --- a/spec/requests/casa_cases_spec.rb +++ b/spec/requests/casa_cases_spec.rb @@ -7,6 +7,8 @@ let(:valid_attributes) { {case_number: "1234", transition_aged_youth: true, casa_org_id: organization.id, hearing_type_id: hearing_type.id, judge_id: judge.id} } let(:invalid_attributes) { {case_number: nil} } let(:casa_case) { create(:casa_case, casa_org: organization, case_number: "111") } + let(:mandate_texts) { ["1-New Mandate Text One", "0-New Mandate Text Two"] } + let(:mandates_attributes) { {"0" => {mandate_text: mandate_texts[0]}, "1" => {mandate_text: mandate_texts[1]}} } before { sign_in user } @@ -120,17 +122,44 @@ ) end - context "with invalid parameters" do - it "does not create a new CasaCase" do - expect { post casa_cases_url, params: {casa_case: invalid_attributes} }.to change( - CasaCase, - :count - ).by(0) + describe "invalid request" do + context "with invalid parameters" do + it "does not create a new CasaCase" do + expect { post casa_cases_url, params: {casa_case: invalid_attributes} }.to change( + CasaCase, + :count + ).by(0) + end + + it "renders a successful response (i.e. to display the 'new' template)" do + post casa_cases_url, params: {casa_case: invalid_attributes} + expect(response).to be_successful + end end - it "renders a successful response (i.e. to display the 'new' template)" do - post casa_cases_url, params: {casa_case: invalid_attributes} - expect(response).to be_successful + context "with case_court_mandates_attributes being passed as a parameter" do + let(:invalid_params) do + attributes = valid_attributes + attributes[:case_court_mandates_attributes] = mandates_attributes + {casa_case: attributes} + end + + it "Creates a new CasaCase, but no CaseCourtMandate" do + expect { post casa_cases_url, params: invalid_params }.to change( + CasaCase, + :count + ).by(1) + + expect { post casa_cases_url, params: invalid_params }.not_to change( + CaseCourtMandate, + :count + ) + end + + it "renders a successful response (i.e. to display the 'new' template)" do + post casa_cases_url, params: {casa_case: invalid_params} + expect(response).to be_successful + end end end end @@ -140,7 +169,8 @@ { case_number: "12345", hearing_type_id: hearing_type.id, - judge_id: judge.id + judge_id: judge.id, + case_court_mandates_attributes: mandates_attributes } } @@ -151,6 +181,8 @@ expect(casa_case.case_number).to eq "12345" expect(casa_case.hearing_type).to eq hearing_type expect(casa_case.judge).to eq judge + expect(casa_case.case_court_mandates[0].mandate_text).to eq mandate_texts[0] + expect(casa_case.case_court_mandates[1].mandate_text).to eq mandate_texts[1] end it "redirects to the casa_case" do @@ -167,6 +199,43 @@ end end + describe "court mandates" do + context "when the user tries to make an existing mandate empty" do + let(:mandates_updated) do + { + case_court_mandates_attributes: { + "0" => { + mandate_text: "New Mandate Text One Updated" + }, + "1" => { + mandate_text: "" + } + } + } + end + + before do + patch casa_case_url(casa_case), params: {casa_case: new_attributes} + casa_case.reload + + mandates_updated[:case_court_mandates_attributes]["0"][:id] = casa_case.case_court_mandates[0].id + mandates_updated[:case_court_mandates_attributes]["1"][:id] = casa_case.case_court_mandates[1].id + end + + it "does not update the first mandate" do + expect { patch casa_case_url(casa_case), params: {casa_case: mandates_updated} }.not_to( + change { casa_case.reload.case_court_mandates[0].mandate_text } + ) + end + + it "does not update the second mandate" do + expect { patch casa_case_url(casa_case), params: {casa_case: mandates_updated} }.not_to( + change { casa_case.reload.case_court_mandates[1].mandate_text } + ) + end + end + end + it "does not update across organizations" do other_org = create(:casa_org) other_casa_case = create(:casa_case, case_number: "abc", casa_org: other_org) @@ -296,7 +365,8 @@ case_number: "12345", court_report_status: :submitted, hearing_type_id: hearing_type.id, - judge_id: judge.id + judge_id: judge.id, + case_court_mandates_attributes: mandates_attributes } } @@ -310,6 +380,7 @@ expect(casa_case.case_number).to eq "111" expect(casa_case.hearing_type).to_not eq hearing_type expect(casa_case.judge).to_not eq judge + expect(casa_case.case_court_mandates.size).to be 0 end it "redirects to the casa_case" do @@ -392,14 +463,18 @@ end describe "PATCH /update" do - let(:new_attributes) { {case_number: "12345", court_report_status: :completed} } + let(:new_attributes) { {case_number: "12345", court_report_status: :completed, case_court_mandates_attributes: mandates_attributes} } context "with valid parameters" do it "updates fields (except case_number)" do patch casa_case_url(casa_case), params: {casa_case: new_attributes} casa_case.reload + expect(casa_case.case_number).to eq "111" expect(casa_case.court_report_completed?).to be true + + expect(casa_case.case_court_mandates[0].mandate_text).to eq mandate_texts[0] + expect(casa_case.case_court_mandates[1].mandate_text).to eq mandate_texts[1] end it "redirects to the casa_case" do diff --git a/spec/system/casa_cases/edit_spec.rb b/spec/system/casa_cases/edit_spec.rb index 0664749791..f393871519 100644 --- a/spec/system/casa_cases/edit_spec.rb +++ b/spec/system/casa_cases/edit_spec.rb @@ -21,12 +21,16 @@ expect(casa_case).not_to be_court_report_submitted end - it "edits case" do + it "edits case", js: true do visit casa_case_path(casa_case.id) click_on "Edit Case Details" expect(page).to have_select("Hearing type") expect(page).to have_select("Judge") select "Submitted", from: "casa_case_court_report_status" + + page.find("#add-mandate-button").click + find("#mandates-list-container").first("textarea").send_keys("Court Mandate Text One") + click_on "Update CASA Case" expect(page).to have_text("Submitted") expect(page).to have_text("Court Date") @@ -34,6 +38,7 @@ expect(page).to have_text("Day") expect(page).to have_text("Month") expect(page).to have_text("Year") + expect(page).to have_text("Court Mandate Text One") expect(page).not_to have_text("Deactivate Case") end @@ -81,7 +86,7 @@ sign_in supervisor end - it "edits case" do + it "edits case", js: true do visit casa_case_path(casa_case) expect(page).to have_text("Court Report Status: Not submitted") visit edit_casa_case_path(casa_case) @@ -95,6 +100,9 @@ select "September", from: "casa_case_court_report_due_date_2i" select next_year, from: "casa_case_court_report_due_date_1i" + page.find("#add-mandate-button").click + find("#mandates-list-container").first("textarea").send_keys("Court Mandate Text One") + click_on "Update CASA Case" has_checked_field? "Youth" has_no_checked_field? "Supervisor" @@ -106,6 +114,7 @@ expect(page).to have_text("Year") expect(page).to have_text("November") expect(page).to have_text("September") + expect(page).to have_text("Court Mandate Text One") visit casa_case_path(casa_case) @@ -362,6 +371,8 @@ def sign_in_and_assign_volunteer expect(page).not_to have_text("Year") expect(page).not_to have_text("Deactivate Case") + expect(page).not_to have_css("#add-mandate-button") + visit casa_case_path(casa_case) expect(page).to have_text("Court Report Status: Submitted") end
Column1 Column2 Column3