Repository: rubyforgood/casa Branch: main Commit: 10fb1a929d95 Files: 1723 Total size: 62.6 MB Directory structure: gitextract_7qaocwti/ ├── .allow_skipping_tests ├── .better-html.yml ├── .browserslistrc ├── .devcontainer/ │ ├── Dockerfile │ ├── devcontainer.json │ ├── docker-compose.yml │ └── post-create.sh ├── .dockerignore ├── .erb_lint.yml ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── chore.md │ │ ├── config.yml │ │ ├── documentation.md │ │ ├── feature_request.md │ │ ├── flaky_test.md │ │ └── problem_validation.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── autoapproval.yml │ ├── dependabot.yml │ ├── instructions/ │ │ ├── copilot-review.instructions.md │ │ └── ruby.instructions.md │ ├── labeler.yml │ └── workflows/ │ ├── add-labels-based-on-column.yml │ ├── after-deploy.yml │ ├── codeql-analysis.yml │ ├── combine_and_report.yml │ ├── docker.yml │ ├── erb_lint.yml │ ├── factory_bot_lint.yml │ ├── issue-auto-close-done.yml │ ├── issue-auto-unassign.yml │ ├── label.yml │ ├── npm_lint_and_test.yml │ ├── rake-after_party.yml │ ├── remove-helped-wanted.yml │ ├── remove-label-based-on-column.yml │ ├── rspec.yml │ ├── ruby_lint.yml │ ├── security.yml │ ├── spec_checker.yml │ ├── stale.yml │ ├── toc.yml │ └── yaml_lint.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc.yml ├── .rspec ├── .rspec_parallel ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── .slugignore ├── .standard.yml ├── .standard_todo.yml ├── .tool-versions ├── .yamllint.yml ├── CONTRIBUTING.md ├── DEPLOY_CHECKLIST.md ├── Dockerfile ├── Gemfile ├── LICENSE.md ├── PROSOPITE_TODO.md ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── SECURITY.md ├── app/ │ ├── assets/ │ │ ├── builds/ │ │ │ └── .keep │ │ ├── config/ │ │ │ └── manifest.js │ │ └── stylesheets/ │ │ ├── actiontext.css │ │ ├── android_app_associations.css │ │ ├── application.scss │ │ ├── base/ │ │ │ ├── breakpoints.scss │ │ │ └── variables.scss │ │ ├── pages/ │ │ │ ├── all_casa_admin_dashboard.scss │ │ │ ├── casa_cases.scss │ │ │ ├── casa_org.scss │ │ │ ├── case_contacts.scss │ │ │ ├── case_contacts_form.scss │ │ │ ├── case_court_reports.scss │ │ │ ├── court_dates.scss │ │ │ ├── emancipation.scss │ │ │ ├── feature_flags.scss │ │ │ ├── login.scss │ │ │ ├── password.scss │ │ │ ├── patch_notes.scss │ │ │ ├── reimbursements.scss │ │ │ ├── supervisors.scss │ │ │ └── volunteers.scss │ │ └── shared/ │ │ ├── dashboard.scss │ │ ├── data_tables_overide.scss │ │ ├── flashes.scss │ │ ├── footer.scss │ │ ├── form.scss │ │ ├── header.scss │ │ ├── layout.scss │ │ ├── navbar.scss │ │ ├── noscript.css │ │ ├── notifier.scss │ │ ├── select2_additional_styles.scss │ │ ├── sidebar.scss │ │ ├── tomselect_additional_styles.scss │ │ ├── truncated_text_component.scss │ │ ├── typography.scss │ │ ├── utilities.scss │ │ └── validated_form_component.scss │ ├── blueprints/ │ │ └── api/ │ │ └── v1/ │ │ └── session_blueprint.rb │ ├── callbacks/ │ │ └── case_contact_metadata_callback.rb │ ├── channels/ │ │ └── application_cable/ │ │ ├── channel.rb │ │ └── connection.rb │ ├── components/ │ │ ├── badge_component.html.erb │ │ ├── badge_component.rb │ │ ├── dropdown_menu_component.html.erb │ │ ├── dropdown_menu_component.rb │ │ ├── form/ │ │ │ ├── hour_minute_duration_component.html.erb │ │ │ ├── hour_minute_duration_component.rb │ │ │ ├── multiple_select/ │ │ │ │ ├── item_component.html.erb │ │ │ │ └── item_component.rb │ │ │ ├── multiple_select_component.html.erb │ │ │ └── multiple_select_component.rb │ │ ├── local_time_component.html.erb │ │ ├── local_time_component.rb │ │ ├── modal/ │ │ │ ├── body_component.html.erb │ │ │ ├── body_component.rb │ │ │ ├── footer_component.html.erb │ │ │ ├── footer_component.rb │ │ │ ├── group_component.html.erb │ │ │ ├── group_component.rb │ │ │ ├── header_component.html.erb │ │ │ ├── header_component.rb │ │ │ ├── open_button_component.html.erb │ │ │ ├── open_button_component.rb │ │ │ ├── open_link_component.html.erb │ │ │ └── open_link_component.rb │ │ ├── notification_component.html.erb │ │ ├── notification_component.rb │ │ ├── sidebar/ │ │ │ ├── group_component.html.erb │ │ │ ├── group_component.rb │ │ │ ├── link_component.html.erb │ │ │ └── link_component.rb │ │ ├── truncated_text_component.html.erb │ │ └── truncated_text_component.rb │ ├── controllers/ │ │ ├── additional_expenses_controller.rb │ │ ├── all_casa_admins/ │ │ │ ├── casa_admins_controller.rb │ │ │ ├── casa_orgs_controller.rb │ │ │ ├── dashboard_controller.rb │ │ │ ├── patch_notes_controller.rb │ │ │ └── sessions_controller.rb │ │ ├── all_casa_admins_controller.rb │ │ ├── android_app_associations_controller.rb │ │ ├── api/ │ │ │ └── v1/ │ │ │ ├── base_controller.rb │ │ │ └── users/ │ │ │ └── sessions_controller.rb │ │ ├── application_controller.rb │ │ ├── banners_controller.rb │ │ ├── bulk_court_dates_controller.rb │ │ ├── casa_admins_controller.rb │ │ ├── casa_cases_controller.rb │ │ ├── casa_org_controller.rb │ │ ├── case_assignments_controller.rb │ │ ├── case_contact_reports_controller.rb │ │ ├── case_contacts/ │ │ │ ├── case_contacts_new_design_controller.rb │ │ │ ├── followups_controller.rb │ │ │ └── form_controller.rb │ │ ├── case_contacts_controller.rb │ │ ├── case_court_orders_controller.rb │ │ ├── case_court_reports_controller.rb │ │ ├── case_groups_controller.rb │ │ ├── checklist_items_controller.rb │ │ ├── concerns/ │ │ │ ├── accessible.rb │ │ │ ├── court_date_params.rb │ │ │ ├── loads_case_contacts.rb │ │ │ ├── organizational.rb │ │ │ └── users/ │ │ │ └── time_zone.rb │ │ ├── contact_topic_answers_controller.rb │ │ ├── contact_topics_controller.rb │ │ ├── contact_type_groups_controller.rb │ │ ├── contact_types_controller.rb │ │ ├── court_dates_controller.rb │ │ ├── custom_org_links_controller.rb │ │ ├── dashboard_controller.rb │ │ ├── emancipation_checklists_controller.rb │ │ ├── emancipations_controller.rb │ │ ├── error_controller.rb │ │ ├── followup_reports_controller.rb │ │ ├── fund_requests_controller.rb │ │ ├── health_controller.rb │ │ ├── hearing_types_controller.rb │ │ ├── imports_controller.rb │ │ ├── judges_controller.rb │ │ ├── languages_controller.rb │ │ ├── learning_hour_topics_controller.rb │ │ ├── learning_hour_types_controller.rb │ │ ├── learning_hours/ │ │ │ └── volunteers_controller.rb │ │ ├── learning_hours_controller.rb │ │ ├── learning_hours_reports_controller.rb │ │ ├── mileage_rates_controller.rb │ │ ├── mileage_reports_controller.rb │ │ ├── missing_data_reports_controller.rb │ │ ├── notes_controller.rb │ │ ├── notifications_controller.rb │ │ ├── other_duties_controller.rb │ │ ├── placement_reports_controller.rb │ │ ├── placement_types_controller.rb │ │ ├── placements_controller.rb │ │ ├── preference_sets_controller.rb │ │ ├── reimbursements_controller.rb │ │ ├── reports_controller.rb │ │ ├── static_controller.rb │ │ ├── supervisor_volunteers_controller.rb │ │ ├── supervisors_controller.rb │ │ ├── users/ │ │ │ ├── invitations_controller.rb │ │ │ ├── passwords_controller.rb │ │ │ └── sessions_controller.rb │ │ ├── users_controller.rb │ │ └── volunteers_controller.rb │ ├── datatables/ │ │ ├── application_datatable.rb │ │ ├── case_contact_datatable.rb │ │ ├── reimbursement_datatable.rb │ │ ├── supervisor_datatable.rb │ │ └── volunteer_datatable.rb │ ├── decorators/ │ │ ├── android_app_association_decorator.rb │ │ ├── application_decorator.rb │ │ ├── casa_case_decorator.rb │ │ ├── case_assignment_decorator.rb │ │ ├── case_contact_decorator.rb │ │ ├── case_contacts/ │ │ │ └── form_decorator.rb │ │ ├── contact_type_decorator.rb │ │ ├── court_date_decorator.rb │ │ ├── learning_hour_decorator.rb │ │ ├── learning_hour_topic_decorator.rb │ │ ├── other_duty_decorator.rb │ │ ├── patch_note_decorator.rb │ │ ├── placement_decorator.rb │ │ ├── user_decorator.rb │ │ └── volunteer_decorator.rb │ ├── documents/ │ │ └── templates/ │ │ ├── default_report_template.docx │ │ ├── emancipation_checklist_template.docx │ │ ├── howard_county_report_template.docx │ │ ├── montgomery_report_template.docx │ │ ├── montgomery_report_template_062022.docx │ │ └── prince_george_report_template.docx │ ├── helpers/ │ │ ├── all_casa_admins/ │ │ │ └── casa_orgs_helper.rb │ │ ├── api_base_helper.rb │ │ ├── application_helper.rb │ │ ├── banner_helper.rb │ │ ├── case_contacts_helper.rb │ │ ├── contact_types_helper.rb │ │ ├── court_dates_helper.rb │ │ ├── court_orders_helper.rb │ │ ├── date_helper.rb │ │ ├── emancipations_helper.rb │ │ ├── followup_helper.rb │ │ ├── learning_hours_helper.rb │ │ ├── mileage_rates_helper.rb │ │ ├── notifications_helper.rb │ │ ├── other_duties_helper.rb │ │ ├── phone_number_helper.rb │ │ ├── preference_sets_helper.rb │ │ ├── report_helper.rb │ │ ├── request_header_helper.rb │ │ ├── sidebar_helper.rb │ │ ├── sms_body_helper.rb │ │ ├── template_helper.rb │ │ ├── ui_helper.rb │ │ └── volunteer_helper.rb │ ├── javascript/ │ │ ├── __mocks__/ │ │ │ ├── fileMock.js │ │ │ └── styleMock.js │ │ ├── __tests__/ │ │ │ ├── add_to_calendar_button.test.js │ │ │ ├── casa_case.test.js │ │ │ ├── case_button_states.test.js │ │ │ ├── case_contact.test.js │ │ │ ├── case_emancipations.test.js │ │ │ ├── dashboard.test.js │ │ │ ├── notifier.test.js │ │ │ ├── password_confirmation.test.js │ │ │ ├── read_more.test.js │ │ │ ├── require_communication_preference.test.js │ │ │ ├── setup-jest.js │ │ │ ├── time_zone.test.js │ │ │ ├── two_minute_warning_session_timeout.test.js │ │ │ ├── type_checker.test.js │ │ │ └── validated_form.test.js │ │ ├── all_casa_admin.js │ │ ├── application.js │ │ ├── controllers/ │ │ │ ├── alert_controller.js │ │ │ ├── application.js │ │ │ ├── autosave_controller.js │ │ │ ├── casa_nested_form_controller.js │ │ │ ├── case_contact_form_controller.js │ │ │ ├── court_order_form_controller.js │ │ │ ├── disable_form_controller.js │ │ │ ├── dismiss_controller.js │ │ │ ├── hello_controller.js │ │ │ ├── icon_toggle_controller.js │ │ │ ├── index.js │ │ │ ├── multiple_select_controller.js │ │ │ ├── navbar_controller.js │ │ │ ├── select_all_controller.js │ │ │ ├── sidebar_controller.js │ │ │ ├── sidebar_group_controller.js │ │ │ └── truncated_text_controller.js │ │ ├── datatable.js │ │ ├── jQueryGlobalizer.js │ │ ├── src/ │ │ │ ├── add_to_calendar_button.js │ │ │ ├── all_casa_admin/ │ │ │ │ ├── patch_notes.js │ │ │ │ └── tables.js │ │ │ ├── casa_case.js │ │ │ ├── casa_org.js │ │ │ ├── case_contact.js │ │ │ ├── case_emancipation.js │ │ │ ├── dashboard.js │ │ │ ├── display_app_metric.js │ │ │ ├── emancipations.js │ │ │ ├── import.js │ │ │ ├── learning_hours.js │ │ │ ├── new_casa_case.js │ │ │ ├── notifier.js │ │ │ ├── password_confirmation.js │ │ │ ├── read_more.js │ │ │ ├── reimbursements.js │ │ │ ├── reports.js │ │ │ ├── require_communication_preference.js │ │ │ ├── select.js │ │ │ ├── session_timeout_poller.js │ │ │ ├── sms_reactivation_toggle.js │ │ │ ├── time_zone.js │ │ │ ├── tooltip.js │ │ │ ├── type_checker.js │ │ │ └── validated_form.js │ │ └── sweet-alert-confirm.js │ ├── jobs/ │ │ └── application_job.rb │ ├── lib/ │ │ └── importers/ │ │ ├── case_importer.rb │ │ ├── file_importer.rb │ │ ├── supervisor_importer.rb │ │ └── volunteer_importer.rb │ ├── mailers/ │ │ ├── application_mailer.rb │ │ ├── casa_admin_mailer.rb │ │ ├── fund_request_mailer.rb │ │ ├── learning_hours_mailer.rb │ │ ├── supervisor_mailer.rb │ │ ├── user_mailer.rb │ │ └── volunteer_mailer.rb │ ├── models/ │ │ ├── additional_expense.rb │ │ ├── address.rb │ │ ├── all_casa_admin.rb │ │ ├── all_casa_admins/ │ │ │ └── casa_org_metrics.rb │ │ ├── api_credential.rb │ │ ├── application_record.rb │ │ ├── banner.rb │ │ ├── casa_admin.rb │ │ ├── casa_case.rb │ │ ├── casa_case_contact_type.rb │ │ ├── casa_case_emancipation_category.rb │ │ ├── casa_case_emancipation_option.rb │ │ ├── casa_org.rb │ │ ├── case_assignment.rb │ │ ├── case_contact.rb │ │ ├── case_contact_contact_type.rb │ │ ├── case_contact_report.rb │ │ ├── case_court_order.rb │ │ ├── case_court_report.rb │ │ ├── case_court_report_context.rb │ │ ├── case_group.rb │ │ ├── case_group_membership.rb │ │ ├── checklist_item.rb │ │ ├── concerns/ │ │ │ ├── CasaCase/ │ │ │ │ └── validations.rb │ │ │ ├── api.rb │ │ │ ├── by_organization_scope.rb │ │ │ └── roles.rb │ │ ├── contact_topic.rb │ │ ├── contact_topic_answer.rb │ │ ├── contact_type.rb │ │ ├── contact_type_group.rb │ │ ├── court_date.rb │ │ ├── custom_org_link.rb │ │ ├── emancipation_category.rb │ │ ├── emancipation_option.rb │ │ ├── followup.rb │ │ ├── fund_request.rb │ │ ├── health.rb │ │ ├── hearing_type.rb │ │ ├── judge.rb │ │ ├── language.rb │ │ ├── learning_hour.rb │ │ ├── learning_hour_topic.rb │ │ ├── learning_hour_type.rb │ │ ├── learning_hours_report.rb │ │ ├── login_activity.rb │ │ ├── mileage_rate.rb │ │ ├── mileage_report.rb │ │ ├── missing_data_report.rb │ │ ├── note.rb │ │ ├── other_duty.rb │ │ ├── patch_note.rb │ │ ├── patch_note_group.rb │ │ ├── patch_note_type.rb │ │ ├── placement.rb │ │ ├── placement_type.rb │ │ ├── preference_set.rb │ │ ├── sent_email.rb │ │ ├── sms_notification_event.rb │ │ ├── supervisor.rb │ │ ├── supervisor_volunteer.rb │ │ ├── user.rb │ │ ├── user_language.rb │ │ ├── user_reminder_time.rb │ │ ├── user_sms_notification_event.rb │ │ └── volunteer.rb │ ├── notifications/ │ │ ├── base_notifier.rb │ │ ├── delivery_methods/ │ │ │ └── sms.rb │ │ ├── emancipation_checklist_reminder_notifier.rb │ │ ├── followup_notifier.rb │ │ ├── followup_resolved_notifier.rb │ │ ├── reimbursement_complete_notifier.rb │ │ ├── volunteer_birthday_notifier.rb │ │ └── youth_birthday_notifier.rb │ ├── policies/ │ │ ├── additional_expense_policy.rb │ │ ├── application_policy.rb │ │ ├── bulk_court_date_policy.rb │ │ ├── casa_admin_policy.rb │ │ ├── casa_case_policy.rb │ │ ├── casa_org_policy.rb │ │ ├── case_assignment_policy.rb │ │ ├── case_contact_policy.rb │ │ ├── case_court_order_policy.rb │ │ ├── case_court_report_policy.rb │ │ ├── case_group_policy.rb │ │ ├── checklist_item_policy.rb │ │ ├── contact_topic_answer_policy.rb │ │ ├── contact_topic_policy.rb │ │ ├── contact_type_group_policy.rb │ │ ├── contact_type_policy.rb │ │ ├── court_date_policy.rb │ │ ├── custom_org_link_policy.rb │ │ ├── dashboard_policy.rb │ │ ├── followup_policy.rb │ │ ├── fund_request_policy.rb │ │ ├── hearing_type_policy.rb │ │ ├── import_policy.rb │ │ ├── judge_policy.rb │ │ ├── language_policy.rb │ │ ├── learning_hour_policy.rb │ │ ├── learning_hour_topic_policy.rb │ │ ├── learning_hour_type_policy.rb │ │ ├── nil_class_policy.rb │ │ ├── note_policy.rb │ │ ├── notification_policy.rb │ │ ├── other_duty_policy.rb │ │ ├── patch_note_policy.rb │ │ ├── placement_policy.rb │ │ ├── placement_type_policy.rb │ │ ├── reimbursement_policy.rb │ │ ├── supervisor_policy.rb │ │ ├── supervisor_volunteer_policy.rb │ │ ├── user_policy.rb │ │ └── volunteer_policy.rb │ ├── presenters/ │ │ ├── base_presenter.rb │ │ └── case_contact_presenter.rb │ ├── services/ │ │ ├── additional_expense_params_service.rb │ │ ├── backfill_followupable_service.rb │ │ ├── casa_case_change_service.rb │ │ ├── case_contacts_contact_dates.rb │ │ ├── case_contacts_export_csv_service.rb │ │ ├── court_report_due_sms_reminder_service.rb │ │ ├── court_report_format_contact_date.rb │ │ ├── create_all_casa_admin_service.rb │ │ ├── create_casa_admin_service.rb │ │ ├── deployment/ │ │ │ └── backfill_case_contact_started_metadata_service.rb │ │ ├── emancipation_checklist_download_html.rb │ │ ├── emancipation_checklist_reminder_service.rb │ │ ├── failed_import_csv_service.rb │ │ ├── fdf_inputs_service.rb │ │ ├── followup_export_csv_service.rb │ │ ├── followup_service.rb │ │ ├── inactive_messages_service.rb │ │ ├── learning_hours_export_csv_service.rb │ │ ├── mileage_export_csv_service.rb │ │ ├── missing_data_export_csv_service.rb │ │ ├── no_contact_made_sms_reminder_service.rb │ │ ├── placement_export_csv_service.rb │ │ ├── preference_set_table_state_service.rb │ │ ├── short_url_service.rb │ │ ├── sms_reminder_service.rb │ │ ├── svg_sanitizer_service.rb │ │ ├── twilio_service.rb │ │ ├── volunteer_birthday_reminder_service.rb │ │ └── volunteers_emails_export_csv_service.rb │ ├── validators/ │ │ ├── casa_org_validator.rb │ │ ├── court_report_validator.rb │ │ ├── url_validator.rb │ │ └── user_validator.rb │ ├── values/ │ │ ├── all_casa_admin_parameters.rb │ │ ├── banner_parameters.rb │ │ ├── casa_admin_parameters.rb │ │ ├── case_contact_parameters.rb │ │ ├── supervisor_parameters.rb │ │ ├── user_parameters.rb │ │ └── volunteer_parameters.rb │ └── views/ │ ├── active_storage/ │ │ └── blobs/ │ │ └── _blob.html.erb │ ├── all_casa_admins/ │ │ ├── casa_admins/ │ │ │ ├── _form.html.erb │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── casa_orgs/ │ │ │ ├── new.html.erb │ │ │ └── show.html.erb │ │ ├── dashboard/ │ │ │ └── show.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── passwords/ │ │ │ └── new.html.erb │ │ ├── patch_notes/ │ │ │ ├── _patch_note.html.erb │ │ │ ├── _patch_note.json.jbuilder │ │ │ ├── index.html.erb │ │ │ └── index.json.jbuilder │ │ ├── sessions/ │ │ │ └── new.html.erb │ │ └── shared/ │ │ └── _links.html.erb │ ├── banners/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── bulk_court_dates/ │ │ └── new.html.erb │ ├── casa_admin_mailer/ │ │ ├── account_setup.html.erb │ │ └── deactivation.html.erb │ ├── casa_admins/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── casa_cases/ │ │ ├── _calendar_button.html.erb │ │ ├── _court_dates.html.erb │ │ ├── _filter.html.erb │ │ ├── _form.html.erb │ │ ├── _generate_report_modal.html.erb │ │ ├── _inactive_case.html.erb │ │ ├── _placements.html.erb │ │ ├── _thank_you_modal.html.erb │ │ ├── _volunteer_assignment.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ └── show.xlsx.axlsx │ ├── casa_org/ │ │ ├── _contact_topics.html.erb │ │ ├── _contact_type_groups.html.erb │ │ ├── _contact_types.html.erb │ │ ├── _custom_org_links.html.erb │ │ ├── _hearing_types.html.erb │ │ ├── _judges.html.erb │ │ ├── _languages.html.erb │ │ ├── _learning_hour_topics.html.erb │ │ ├── _learning_hour_types.html.erb │ │ ├── _placement_types.html.erb │ │ ├── _sent_emails.html.erb │ │ └── edit.html.erb │ ├── case_assignments/ │ │ └── index.html.erb │ ├── case_contacts/ │ │ ├── _case_contact.html.erb │ │ ├── _confirm_note_content_dialog.html.erb │ │ ├── _followup.html.erb │ │ ├── case_contacts_new_design/ │ │ │ └── index.html.erb │ │ ├── drafts.html.erb │ │ ├── form/ │ │ │ ├── _contact_topic_answer.html.erb │ │ │ ├── _contact_types.html.erb │ │ │ └── details.html.erb │ │ └── index.html.erb │ ├── case_court_reports/ │ │ ├── _generate_docx.html.erb │ │ └── index.html.erb │ ├── case_groups/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── checklist_items/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── contact_topics/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── contact_type_groups/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── contact_types/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── court_dates/ │ │ ├── _fields.html.erb │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── custom_org_links/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── devise/ │ │ ├── invitations/ │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── mailer/ │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── email_changed.html.erb │ │ │ ├── invitation_instructions.html.erb │ │ │ ├── invitation_instructions.text.erb │ │ │ ├── password_change.html.erb │ │ │ ├── reset_password_instructions.html.erb │ │ │ └── unlock_instructions.html.erb │ │ ├── passwords/ │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── sessions/ │ │ │ └── new.html.erb │ │ └── shared/ │ │ └── _links.html.erb │ ├── emancipation_checklists/ │ │ └── index.html.erb │ ├── emancipations/ │ │ ├── download.html.erb │ │ └── show.html.erb │ ├── error/ │ │ └── index.html.erb │ ├── fund_request_mailer/ │ │ └── send_request.html.erb │ ├── fund_requests/ │ │ └── new.html.erb │ ├── health/ │ │ └── index.html.erb │ ├── hearing_types/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── imports/ │ │ ├── _cases.html.erb │ │ ├── _csv_error_modal.html.erb │ │ ├── _sms_opt_in_modal.html.erb │ │ ├── _supervisors.html.erb │ │ ├── _volunteers.html.erb │ │ └── index.html.erb │ ├── judges/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── languages/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── layouts/ │ │ ├── _all_casa_admin_sidebar.html.erb │ │ ├── _banner.html.erb │ │ ├── _flash_messages.html.erb │ │ ├── _header.html.erb │ │ ├── _login_header.html.erb │ │ ├── _mobile_navbar.html.erb │ │ ├── _sidebar.html.erb │ │ ├── action_text/ │ │ │ └── contents/ │ │ │ └── _content.html.erb │ │ ├── application.html.erb │ │ ├── components/ │ │ │ └── _notifier.html.erb │ │ ├── devise.html.erb │ │ ├── footers/ │ │ │ ├── _logged_in.html.erb │ │ │ └── _not_logged_in.html.erb │ │ ├── fund_layout.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb │ ├── learning_hour_topics/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── learning_hour_types/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── learning_hours/ │ │ ├── _confirm_note.html.erb │ │ ├── _form.html.erb │ │ ├── _learning_hours_table.html.erb │ │ ├── _supervisor_admin_learning_hours.html.erb │ │ ├── _volunteer_learning_hours.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ └── volunteers/ │ │ └── show.html.erb │ ├── learning_hours_mailer/ │ │ └── learning_hours_report_email.html.erb │ ├── mileage_rates/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── notes/ │ │ └── edit.html.erb │ ├── notifications/ │ │ ├── _notification.html.erb │ │ ├── _patch_notes.html.erb │ │ └── index.html.erb │ ├── other_duties/ │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── placement_types/ │ │ ├── _fields.html.erb │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── placements/ │ │ ├── _fields.html.erb │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── reimbursements/ │ │ ├── _datatable.html.erb │ │ ├── _filter_trigger.html.erb │ │ ├── _occurred_at_filter_input.html.erb │ │ ├── _reimbursement_complete.html.erb │ │ ├── _table.html.erb │ │ └── index.html.erb │ ├── reports/ │ │ ├── _filter.html.erb │ │ └── index.html.erb │ ├── shared/ │ │ ├── _additional_expense_form.html.erb │ │ ├── _court_order_form.html.erb │ │ ├── _court_order_list.erb │ │ ├── _edit_form.html.erb │ │ ├── _emancipation_link.html.erb │ │ ├── _error_messages.html.erb │ │ ├── _favicons.html.erb │ │ ├── _invite_login.html.erb │ │ └── _manage_volunteers.html.erb │ ├── static/ │ │ └── index.html.erb │ ├── supervisor_mailer/ │ │ ├── _active_volunteer_info.html.erb │ │ ├── _active_volunteers.html.erb │ │ ├── _additional_notes.html.erb │ │ ├── _no_recent_sign_in.html.erb │ │ ├── _pending_volunteers.html.erb │ │ ├── _recently_unassigned_volunteers.html.erb │ │ ├── _summary_header.html.erb │ │ ├── account_setup.html.erb │ │ ├── reimbursement_request_email.html.erb │ │ └── weekly_digest.html.erb │ ├── supervisors/ │ │ ├── _manage_active.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── user_mailer/ │ │ └── password_changed_reminder.html.erb │ ├── users/ │ │ ├── _edit_profile.erb │ │ ├── _languages.html.erb │ │ └── edit.html.erb │ ├── volunteer_mailer/ │ │ ├── account_setup.html.erb │ │ ├── case_contacts_reminder.html.erb │ │ └── court_report_reminder.html.erb │ └── volunteers/ │ ├── _form.html.erb │ ├── _manage_active.html.erb │ ├── _manage_cases.erb │ ├── _manage_supervisor.erb │ ├── _notes.html.erb │ ├── _send_reminder_button.html.erb │ ├── _volunteer_reminder_form.erb │ ├── edit.html.erb │ ├── index.html.erb │ └── new.html.erb ├── app.json ├── babel.config.js ├── bin/ │ ├── asset_bundling_scripts/ │ │ ├── build_js.js │ │ └── logger.js │ ├── brakeman │ ├── bundle │ ├── delayed_job │ ├── dev │ ├── git_hooks/ │ │ ├── README.md │ │ ├── build-assets │ │ ├── lint │ │ ├── logger │ │ ├── migrate-all │ │ ├── update-branch │ │ └── update-dependencies │ ├── lint │ ├── login │ ├── npm │ ├── rails │ ├── rake │ ├── rspec │ ├── setup │ ├── spring │ └── update ├── cc-test-reporter ├── code-of-conduct.md ├── config/ │ ├── application.rb │ ├── boot.rb │ ├── brakeman.ignore │ ├── cable.yml │ ├── credentials/ │ │ ├── development.key │ │ ├── development.yml.enc │ │ ├── production.yml.enc │ │ ├── qa.key │ │ ├── qa.yml.enc │ │ ├── test.key │ │ └── test.yml.enc │ ├── database.yml │ ├── docker.env │ ├── environment.rb │ ├── environments/ │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers/ │ │ ├── after_party.rb │ │ ├── all_casa_admin_access.rb │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── authtrail.rb │ │ ├── backtrace_silencers.rb │ │ ├── blueprinter.rb │ │ ├── bugsnag.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── cors.rb │ │ ├── date_formats.rb │ │ ├── devise.rb │ │ ├── extensions.rb │ │ ├── filter_parameter_logging.rb │ │ ├── flipper.rb │ │ ├── generators.rb │ │ ├── inflections.rb │ │ ├── lograge.rb │ │ ├── mime_types.rb │ │ ├── pagy.rb │ │ ├── permissions_policy.rb │ │ ├── prosopite.rb │ │ ├── rack_attack.rb │ │ ├── rswag_api.rb │ │ ├── rswag_ui.rb │ │ ├── sent_email_event.rb │ │ ├── strong_migrations.rb │ │ └── wrap_parameters.rb │ ├── locales/ │ │ ├── devise.en.yml │ │ ├── devise_invitable.en.yml │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── scout_apm.yml │ ├── spring.rb │ └── storage.yml ├── config.ru ├── data/ │ └── inputs_fdf.erb ├── db/ │ ├── migrate/ │ │ ├── 20200329050100_create_casa_cases.rb │ │ ├── 20200329062155_devise_create_users.rb │ │ ├── 20200329064203_add_role_to_user.rb │ │ ├── 20200329071025_change_casa_case_teen_to_required.rb │ │ ├── 20200329071327_change_casa_case_number_to_required.rb │ │ ├── 20200329071626_add_unique_index_on_case_case_number.rb │ │ ├── 20200329074655_create_supervisor_volunteers.rb │ │ ├── 20200329081206_create_case_assignments.rb │ │ ├── 20200329085225_create_versions.rb │ │ ├── 20200329095031_create_casa_orgs.rb │ │ ├── 20200329095154_add_casa_org_to_user.rb │ │ ├── 20200329102102_devise_create_all_casa_admins.rb │ │ ├── 20200329175337_create_case_contacts.rb │ │ ├── 20200330231711_add_volunteer_reference_to_casa_cases.rb │ │ ├── 20200405112910_remove_volunteer_id_from_casa_case.rb │ │ ├── 20200420004403_add_medium_type_and_contact_made_to_case_contacts.rb │ │ ├── 20200422180727_replace_contact_type_with_contact_types_on_case_contact.rb │ │ ├── 20200423154018_change_teen_program_to_transition_aged_youth.rb │ │ ├── 20200423204147_add_name_to_user.rb │ │ ├── 20200525220759_add_driving_fields_to_case_contact.rb │ │ ├── 20200726185103_devise_invitable_add_to_users.rb │ │ ├── 20200729002247_add_casa_org_to_casa_case.rb │ │ ├── 20200801170524_add_type_to_user.rb │ │ ├── 20200801192923_remove_role_from_user.rb │ │ ├── 20200818220659_add_is_active_to_supervisor_volunteers.rb │ │ ├── 20200830011647_add_notes_to_case_contacts.rb │ │ ├── 20200905192934_remove_other_type_text_from_case_contacts.rb │ │ ├── 20200906145045_add_display_name_to_casa_orgs.rb │ │ ├── 20200906145725_add_address_to_casa_orgs.rb │ │ ├── 20200906145830_add_footer_links_to_casa_orgs.rb │ │ ├── 20200906150641_create_casa_org_logos.rb │ │ ├── 20200906184455_add_banner_color_to_casa_org_logos.rb │ │ ├── 20200907142411_create_task_records.rb │ │ ├── 20200917175655_add_default_value_to_case_contact_miles_driven.rb │ │ ├── 20200918115741_set_miles_driven_as_not_nullable.rb │ │ ├── 20200922144730_add_birth_month_year_youth_to_casa_cases.rb │ │ ├── 20200922150754_create_contact_type.rb │ │ ├── 20200922170308_link_case_contacts_to_contact_types.rb │ │ ├── 20200924192310_create_casa_case_contact_type.rb │ │ ├── 20200925180941_add_active_to_contact_type_groups.rb │ │ ├── 20200925181042_add_active_to_contact_types.rb │ │ ├── 20200928233606_add_court_report_submitted_to_casa_cases.rb │ │ ├── 20201002192636_add_court_date_to_casa_cases.rb │ │ ├── 20201004165322_add_court_report_due_date_to_casa_cases.rb │ │ ├── 20201005191326_create_hearing_types.rb │ │ ├── 20201013171632_remove_contact_types_from_case_contacts.rb │ │ ├── 20201019120548_create_active_storage_tables.active_storage.rb │ │ ├── 20201020095451_add_hearing_type_to_court_cases.rb │ │ ├── 20201020220412_create_emancipation_categories.rb │ │ ├── 20201021024459_create_emancipation_options.rb │ │ ├── 20201021034012_create_join_table_casa_cases_emancipaton_options.rb │ │ ├── 20201021143642_add_active_column_to_casa_cases_table.rb │ │ ├── 20201022034445_add_foreign_key_constraints_to_case_emancipation_join_table.rb │ │ ├── 20201023233638_create_judges.rb │ │ ├── 20201023234325_add_active_to_judge.rb │ │ ├── 20201024003821_add_name_to_judge.rb │ │ ├── 20201024113046_create_past_court_dates.rb │ │ ├── 20201025162142_add_judge_to_court_cases.rb │ │ ├── 20201108142333_add_court_report_status_to_casa_cases.rb │ │ ├── 20201120103041_remove_remember_created_at_from_users.rb │ │ ├── 20201120103146_remove_remember_created_at_from_all_casa_admins.rb │ │ ├── 20201120215756_remove_court_report_submitted_from_casa_cases.rb │ │ ├── 20201123100716_remove_case_number_index_from_casa_cases.rb │ │ ├── 20201123112651_add_case_number_index_scoped_by_casa_org_to_casa_cases.rb │ │ ├── 20201222125441_add_service_name_to_active_storage_blobs.active_storage.rb │ │ ├── 20201222125442_create_active_storage_variant_records.active_storage.rb │ │ ├── 20201226024029_create_casa_case_emancipation_categories.rb │ │ ├── 20210105155534_create_followups.rb │ │ ├── 20210107181908_add_id_and_timestamps_to_case_emancipation_options_table.rb │ │ ├── 20210109231411_drop_casa_org_logos_table.rb │ │ ├── 20210117185614_create_notifications.rb │ │ ├── 20210223133248_create_case_court_mandates.rb │ │ ├── 20210308195135_change_is_active_name.rb │ │ ├── 20210330182538_add_devise_trackable_columns_to_users.rb │ │ ├── 20210401161710_add_deleted_at_to_case_contacts.rb │ │ ├── 20210401182359_add_implementation_status_to_edit_case_court_mandates.rb │ │ ├── 20210502172706_add_devise_invitable_to_all_casa_admins.rb │ │ ├── 20210521151549_add_casa_case_details_to_past_court_dates.rb │ │ ├── 20210521194321_add_past_court_date_to_case_court_mandates.rb │ │ ├── 20210526233058_create_sent_emails.rb │ │ ├── 20210624125750_add_note_to_followups.rb │ │ ├── 20210913142024_set_display_name_as_not_nullable.rb │ │ ├── 20210925140028_add_slug_to_casa_orgs_and_casa_cases.rb │ │ ├── 20211001204053_rename_court_mandates_to_court_orders.rb │ │ ├── 20211007144114_add_show_driving_reimbursement_to_casa_orgs.rb │ │ ├── 20211008170357_add_text_to_case_court_orders.rb │ │ ├── 20211008170724_migrate_case_court_orders_mandate_text_to_text.rb │ │ ├── 20211008174527_remove_mandate_text_from_case_court_orders.rb │ │ ├── 20211011195857_rename_past_court_date_to_court_date.rb │ │ ├── 20211012180102_change_casa_cases_court_date_to_reference.rb │ │ ├── 20211023165907_add_reimbursement_complete_to_case_contacts.rb │ │ ├── 20211024011923_create_mileage_rates.rb │ │ ├── 20211024060815_create_preference_sets.rb │ │ ├── 20211025143709_create_healths.rb │ │ ├── 20211029032305_add_casa_org_to_mileage_rate.rb │ │ ├── 20211029033530_remove_user_required_from_mileage_rate.rb │ │ ├── 20211203181342_create_additional_expenses.rb │ │ ├── 20211230033457_create_delayed_jobs.rb │ │ ├── 20220105030922_create_feature_flags.rb │ │ ├── 20220127055733_create_notes.rb │ │ ├── 20220223035901_remove_null_check_deprecated_field.rb │ │ ├── 20220226040507_create_fund_request.rb │ │ ├── 20220303183053_create_other_duty.rb │ │ ├── 20220323145733_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ │ ├── 20220324141758_create_learning_hours.rb │ │ ├── 20220402201247_add_phone_number_to_users.rb │ │ ├── 20220406011016_add_sms_notification_preferences_to_users.rb │ │ ├── 20220406011144_add_email_notification_preferences_to_users.rb │ │ ├── 20220409184741_add_fund_request.rb │ │ ├── 20220411180242_add_uniqueness_constraint_to_feature_flag_name.rb │ │ ├── 20220509224425_add_columns_to_casa_orgs.rb │ │ ├── 20220513084954_add_date_in_care_to_casa_cases.rb │ │ ├── 20220513111133_delete_versions.rb │ │ ├── 20220519210423_create_sms_notification_events.rb │ │ ├── 20220519233803_create_user_sms_notification_events.rb │ │ ├── 20220526011848_create_user_reminder_times.rb │ │ ├── 20220602215632_create_addresses.rb │ │ ├── 20220607184910_add_hide_old_contacts_to_case_assignment.rb │ │ ├── 20220610221701_create_checklist_items.rb │ │ ├── 20220615015056_create_patch_note_types.rb │ │ ├── 20220616021404_add_checklist_updated_date_to_hearing_types.rb │ │ ├── 20220618042137_create_patch_note_groups.rb │ │ ├── 20220622022147_create_patch_notes.rb │ │ ├── 20220820231119_create_languages.rb │ │ ├── 20220826130829_create_languages_users_join_table.rb │ │ ├── 20220924181447_remove_all_empty_languages.rb │ │ ├── 20221002103627_add_user_foreign_key_to_other_duties.rb │ │ ├── 20221002103754_validate_add_user_foreign_key_to_other_duties.rb │ │ ├── 20221003202112_add_court_report_due_date_to_court_dates.rb │ │ ├── 20221011044911_add_user_languages.rb │ │ ├── 20221012203806_populate_user_languages_from_languages_users.rb │ │ ├── 20230121174227_remove_court_data_from_casa_cases.rb │ │ ├── 20230220210146_add_phone_number_to_all_casa_admin_resource.rb │ │ ├── 20230316152808_remove_phone_number_from_all_casa_admin_resource.rb │ │ ├── 20230326225216_create_placement_types.rb │ │ ├── 20230326225230_create_placements.rb │ │ ├── 20230327154626_add_confirmable_to_users.rb │ │ ├── 20230327155053_add_email_confirmation_and_old_emails_to_users.rb │ │ ├── 20230405202939_remove_email_confirmation_from_users.rb │ │ ├── 20230412103356_add_casa_case_to_placements.rb │ │ ├── 20230420212437_add_table_columns_display_to_preference_set.rb │ │ ├── 20230610153139_add_receive_reimbursement_email_to_users.rb │ │ ├── 20230615155223_add_twilio_enabled_to_casa_orgs.rb │ │ ├── 20230621161252_add_additional_expenses_to_casa_orgs.rb │ │ ├── 20230627210040_add_allow_reimbursement_to_case_assignments.rb │ │ ├── 20230704123327_add_foreign_key_constraints_to_mileage_rates.rb │ │ ├── 20230710025852_add_token_to_users.rb │ │ ├── 20230712080040_add_foreign_key_creator_id_to_note.rb │ │ ├── 20230728135743_create_action_text_tables.action_text.rb │ │ ├── 20230728140249_create_banners.rb │ │ ├── 20230729143126_create_learning_hour_types.rb │ │ ├── 20230729145310_add_reference_learning_hour_types.rb │ │ ├── 20230729145351_add_foreign_key_learning_hour_types.rb │ │ ├── 20230729145419_validate_foreign_key_learning_hour_types.rb │ │ ├── 20230729154529_create_case_groups.rb │ │ ├── 20230729154545_create_case_group_memberships.rb │ │ ├── 20230729213608_remove_hearing_type_id_and_judge_id_from_casa_cases.rb │ │ ├── 20230730103110_remove_learning_type.rb │ │ ├── 20230809002819_drop_languages_users.rb │ │ ├── 20230817144910_create_learning_hour_topics.rb │ │ ├── 20230819124840_add_learning_hour_topic_id_to_learning_hour.rb │ │ ├── 20230819132316_add_learning_topic_active_to_casa_org.rb │ │ ├── 20230822152341_drop_jwt_denylist_table.rb │ │ ├── 20230902021531_add_monthly_learning_hours_report_to_user.rb │ │ ├── 20230903182657_add_foreign_key_casa_case_to_placement.rb │ │ ├── 20231102181027_add_birthdays_to_users.rb │ │ ├── 20231125150721_status_for_case_contacts.rb │ │ ├── 20240216013254_add_contact_topics.rb │ │ ├── 20240415160842_add_followupable_to_followups.rb │ │ ├── 20240507022441_create_login_activities.rb │ │ ├── 20240509104733_create_noticed_tables.noticed.rb │ │ ├── 20240509104734_add_notifications_count_to_noticed_event.noticed.rb │ │ ├── 20240531172823_add_expires_at_to_banner.rb │ │ ├── 20240610071054_add_other_duties_enabled_to_casa_org.rb │ │ ├── 20240621165358_create_flipper_tables.rb │ │ ├── 20240622020203_drop_feature_flags.rb │ │ ├── 20240716194609_add_metadata_to_case_contacts.rb │ │ ├── 20241017050129_remove_contact_topic_answer_contact_topic_id_null_constraint.rb │ │ ├── 20250207080433_remove_token_from_users.rb │ │ ├── 20250207080511_create_api_credentials.rb │ │ ├── 20250208160513_remove_plain_text_tokens_from_api_credentials.rb │ │ ├── 20250331032424_validate_constraint_mileage_rates.rb │ │ ├── 20250331033339_validate_constraint_notes.rb │ │ ├── 20250331033350_validate_constraint_placements.rb │ │ ├── 20250331033418_remove_duplicate_indexindex_emancipation_options_on_emancipation_category_id.rb │ │ ├── 20250331033441_remove_duplicate_indexindex_user_languages_on_language_id.rb │ │ ├── 20250404200715_create_custom_org_links.rb │ │ ├── 20250507011754_remove_unused_indexes.rb │ │ ├── 20250528092341_trim_whitespace_from_custom_org_links.rb │ │ ├── 20250702142004_add_exclude_from_court_report_to_contact_topics.rb │ │ ├── 20260210233737_rename_casa_cases_emancipaton_options_to_casa_case_emancipaton_options.rb │ │ ├── 20260211001655_rename_casa_cases_emancipaton_options_to_casa_case_emancipaton_options_follow_up.rb │ │ └── 20260414132818_add_deleted_at_to_contact_topic_answers.rb │ ├── schema.rb │ ├── seeds/ │ │ ├── api_credential_data.rb │ │ ├── casa_org_populator_presets.rb │ │ ├── db_populator.rb │ │ ├── default_contact_topics.yml │ │ ├── emancipation_data.rb │ │ ├── emancipation_options_prune.rb │ │ ├── patch_note_group_data.rb │ │ ├── patch_note_type_data.rb │ │ └── placement_data.rb │ └── seeds.rb ├── doc/ │ ├── API.md │ ├── CONTRIBUTING.md │ ├── DOCKER.md │ ├── LINUX_SETUP.md │ ├── MAC_SETUP.md │ ├── NIX_SETUP.md │ ├── SECURITY.md │ ├── WSL_SETUP.md │ ├── architecture-decisions/ │ │ ├── 0001-record-architecture-decisions.md │ │ ├── 0002-disallow-ui-sign-ups.md │ │ ├── 0003-multiple-user-tables.md │ │ ├── 0004-use-bootstrap.md │ │ ├── 0005-android-app-as-a-pwa.md │ │ ├── 0006-few-controller-tests.md │ │ ├── 0007-inline-css-for-email-views.md │ │ └── 0008-controller-specs.txt │ ├── casa_db_diagram_patch_notes.kra │ ├── code-of-conduct.md │ ├── db_diagram_schema_code/ │ │ ├── part_1.txt │ │ └── part_2.txt │ └── productsense.md ├── docker/ │ ├── brakeman │ ├── build │ ├── build-assets │ ├── build-log │ ├── console │ ├── nuke │ ├── nukec │ ├── run │ ├── sandbox │ ├── seed │ ├── server │ ├── server-log │ ├── test │ └── test-log ├── docker-compose.yml ├── docker-entrypoint.sh ├── flake.nix ├── gemset.nix ├── jest.config.js ├── lib/ │ ├── assets/ │ │ └── .keep │ ├── ext/ │ │ └── pdf_forms.rb │ ├── generators/ │ │ └── rails/ │ │ └── policy/ │ │ ├── USAGE │ │ ├── policy_generator.rb │ │ └── templates/ │ │ ├── policy.rb.tt │ │ └── policy_spec.rb.tt │ ├── mailers/ │ │ ├── debug_preview_mailer.rb │ │ └── previews/ │ │ ├── casa_admin_mailer_preview.rb │ │ ├── concerns/ │ │ │ └── mailer_preview.rb │ │ ├── devise_mailer_preview.rb │ │ ├── fund_request_mailer_preview.rb │ │ ├── learning_hours_mailer_preview.rb │ │ ├── supervisor_mailer_preview.rb │ │ └── volunteer_mailer_preview.rb │ └── tasks/ │ ├── auto_annotate_models.rake │ ├── case_contact_types_reminder.rb │ ├── check_controller_tests.rake │ ├── court_report_due_reminder.rake │ ├── data_post_processors/ │ │ ├── case_contact_populator.rb │ │ ├── contact_topic_populator.rb │ │ ├── contact_type_populator.rb │ │ └── sms_notification_event_populator.rb │ ├── deployment/ │ │ ├── 20200913155303_populate_missing_display_names.rake │ │ ├── 20201005203140_populate_contact_type_groups_and_contact_types.rake │ │ ├── 20201005203405_populate_case_contact_contact_type.rake │ │ ├── 20201210004047_populate_emancipation_data.rake │ │ ├── 20201215183231_emancipation_category_options.rake │ │ ├── 20210415012736_task_name.rake │ │ ├── 20210507200644_fix_transition_aged_youth_for_nil_birth_month_year_youth.rake │ │ ├── 20210811052058_update_montgomery_court_report_template.rake │ │ ├── 20210925143244_populate_slugs_for_orgs_and_cases.rake │ │ ├── 20220409174149_enable_feature_flag_prince_george_fund_request.rake │ │ ├── 20220521015108_populate_sms_notification_events.rake │ │ ├── 20220615020226_create_initial_patch_note_types.rake │ │ ├── 20220629015841_update_montgomery_court_report_template.rake │ │ ├── 20220902180609_populate_languages.rake │ │ ├── 20221003224029_migrate_court_report_due_date_to_court_dates.rake │ │ ├── 20221009032756_add_medium_type_to_howard_court_report_template.rake │ │ ├── 20221104005957_create_initial_patch_note_groups.rake │ │ ├── 20230110000515_update_howard_court_report_template.rake │ │ ├── 20230114184424_update_howard_court_report_temp2.rake │ │ ├── 20230114184852_update_howard_court_report_temp3.rake │ │ ├── 20230208031806_update_howard_court_report_temp4.rake │ │ ├── 20230409222823_add_confrimation_to_existing_users.rake │ │ ├── 20230419224330_backfill_user_prefernce_sets.rake │ │ ├── 20230720000759_update_howard_court_report_fix.rake │ │ ├── 20231125151610_set_case_contacts_as_active.rake │ │ ├── 20240416171009_backfill_followup_followupable_id_and_type_from_case_contact_id.rake │ │ ├── 20240420230126_update_org_templates.rake │ │ ├── 20240604121427_migrate_notifications.rake │ │ ├── 20240720232939_backfill_case_contact_started_metadata.rake │ │ ├── 20250226015042_populate_new_api_and_refresh_token.rake │ │ └── 99991023145114_store_deploy_time.rake │ ├── development/ │ │ └── notifications.rake │ ├── emancipation_checklist_reminder.rake │ ├── example_recurring_task.rb │ ├── lint_factory_bot.rake │ ├── monthly_learning_hours_report.rake │ ├── no_contact_made_reminder.rb │ ├── post_gc_stat_to_discord.rake │ ├── recurring_jobs.rake │ ├── scheduler.rake │ ├── send_case_contact_types_reminder.rake │ ├── send_no_contact_made_reminder.rake │ ├── send_supervisor_digest.rake │ ├── supervisor_weekly_digest.rb │ ├── test_checker.rake │ ├── volunteer_birthday_reminder.rake │ └── youth_birthday_reminder.rake ├── log/ │ └── .keep ├── noop ├── package.json ├── postcss.config.js ├── public/ │ ├── 403.html │ ├── 404.html │ ├── 406-unsupported-browser.html │ ├── 422.html │ ├── 500.html │ ├── assets/ │ │ ├── css/ │ │ │ ├── lineicons.css │ │ │ └── main.css │ │ └── scss/ │ │ ├── _common.scss │ │ ├── _default.scss │ │ ├── _mixin.scss │ │ ├── _sidebar.scss │ │ ├── _variables.scss │ │ ├── alerts/ │ │ │ └── _alerts.scss │ │ ├── auth/ │ │ │ ├── _signin.scss │ │ │ └── _signup.scss │ │ ├── buttons/ │ │ │ └── _buttons.scss │ │ ├── calendar/ │ │ │ └── _calendar.scss │ │ ├── cards/ │ │ │ └── _cards.scss │ │ ├── dashboards/ │ │ │ └── _dashboards.scss │ │ ├── forms/ │ │ │ └── _form-elements.scss │ │ ├── header/ │ │ │ └── _header.scss │ │ ├── icons/ │ │ │ └── _icons.scss │ │ ├── invoice/ │ │ │ └── _invoice.scss │ │ ├── main.scss │ │ ├── notification/ │ │ │ └── _notification.scss │ │ ├── settings/ │ │ │ └── _settings.scss │ │ ├── tables/ │ │ │ └── _tables.scss │ │ └── typography/ │ │ └── _typography.scss │ ├── casa_cases.csv │ ├── robots.txt │ ├── sms-terms-conditions.html │ ├── supervisors.csv │ └── volunteers.csv ├── scripts/ │ ├── generate_github_issues_for_missing_spec.rb │ └── import_casa_case_date_of_birth.rb ├── spec/ │ ├── .prosopite_ignore │ ├── blueprints/ │ │ └── api/ │ │ └── v1/ │ │ └── session_blueprint_spec.rb │ ├── callbacks/ │ │ └── case_contact_metadata_callback_spec.rb │ ├── channels/ │ │ └── application_cable/ │ │ ├── channel_spec.rb │ │ └── connection_spec.rb │ ├── components/ │ │ ├── badge_component_spec.rb │ │ ├── dropdown_menu_component_spec.rb │ │ ├── form/ │ │ │ ├── hour_minute_duration_component_spec.rb │ │ │ ├── multiple_select/ │ │ │ │ └── item_component_spec.rb │ │ │ └── multiple_select_component_spec.rb │ │ ├── local_time_component_spec.rb │ │ ├── modal/ │ │ │ ├── body_component_spec.rb │ │ │ ├── footer_component_spec.rb │ │ │ ├── group_component_spec.rb │ │ │ ├── header_component_spec.rb │ │ │ ├── open_button_component_spec.rb │ │ │ └── open_link_component_spec.rb │ │ ├── notification_component_spec.rb │ │ ├── previews/ │ │ │ └── truncated_text_component_preview.rb │ │ ├── sidebar/ │ │ │ ├── group_component_spec.rb │ │ │ └── link_component_spec.rb │ │ └── truncated_text_component_spec.rb │ ├── config/ │ │ └── initializers/ │ │ └── rack_attack_spec.rb │ ├── controllers/ │ │ ├── README.md │ │ ├── application_controller_spec.rb │ │ ├── concerns/ │ │ │ ├── accessible_spec.rb │ │ │ ├── court_date_params_spec.rb │ │ │ ├── loads_case_contacts_spec.rb │ │ │ ├── organizational_spec.rb │ │ │ └── users/ │ │ │ └── time_zone_spec.rb │ │ ├── emancipations_controller_spec.rb │ │ ├── learning_hours/ │ │ │ └── volunteers_controller_spec.rb │ │ └── users/ │ │ └── sessions_controller_spec.rb │ ├── datatables/ │ │ ├── application_datatable_spec.rb │ │ ├── case_contact_datatable_spec.rb │ │ ├── reimbursement_datatable_spec.rb │ │ ├── supervisor_datatable_spec.rb │ │ └── volunteer_datatable_spec.rb │ ├── decorators/ │ │ ├── android_app_association_decorator_spec.rb │ │ ├── application_decorator_spec.rb │ │ ├── casa_case_decorator_spec.rb │ │ ├── case_assignment_decorator_spec.rb │ │ ├── case_contact_decorator_spec.rb │ │ ├── case_contacts/ │ │ │ └── form_decorator_spec.rb │ │ ├── contact_type_decorator_spec.rb │ │ ├── court_date_decorator_spec.rb │ │ ├── learning_hour_decorator_spec.rb │ │ ├── learning_hour_topic_decorator_spec.rb │ │ ├── other_duty_decorator_spec.rb │ │ ├── patch_note_decorator_spec.rb │ │ ├── placement_decorator_spec.rb │ │ ├── user_decorator_spec.rb │ │ └── volunteer_decorator_spec.rb │ ├── documents/ │ │ └── templates/ │ │ └── prince_george_report_template_spec.rb │ ├── factories/ │ │ ├── additional_expenses.rb │ │ ├── addresses.rb │ │ ├── all_casa_admins.rb │ │ ├── api_credential.rb │ │ ├── banners.rb │ │ ├── casa_admins.rb │ │ ├── casa_case_contact_types.rb │ │ ├── casa_case_emancipation_categories.rb │ │ ├── casa_case_emancipation_options.rb │ │ ├── casa_cases.rb │ │ ├── casa_orgs.rb │ │ ├── case_assignments.rb │ │ ├── case_contact_contact_type.rb │ │ ├── case_contacts.rb │ │ ├── case_court_orders.rb │ │ ├── case_court_report_context.rb │ │ ├── case_group_memberships.rb │ │ ├── case_groups.rb │ │ ├── checklist_items.rb │ │ ├── contact_topic_answers.rb │ │ ├── contact_topics.rb │ │ ├── contact_type_group.rb │ │ ├── contact_types.rb │ │ ├── court_dates.rb │ │ ├── custom_org_links.rb │ │ ├── emancipation_categories.rb │ │ ├── emancipation_options.rb │ │ ├── followups.rb │ │ ├── fund_requests.rb │ │ ├── healths.rb │ │ ├── hearing_types.rb │ │ ├── judges.rb │ │ ├── languages.rb │ │ ├── learning_hour_topics.rb │ │ ├── learning_hour_types.rb │ │ ├── learning_hours.rb │ │ ├── login_activities.rb │ │ ├── mileage_rates.rb │ │ ├── notes.rb │ │ ├── notifications.rb │ │ ├── notifiers.rb │ │ ├── other_duty.rb │ │ ├── patch_note_groups.rb │ │ ├── patch_note_types.rb │ │ ├── patch_notes.rb │ │ ├── placement_types.rb │ │ ├── placements.rb │ │ ├── preference_sets.rb │ │ ├── sent_emails.rb │ │ ├── sms_notification_events.rb │ │ ├── supervisor_volunteer.rb │ │ ├── supervisors.rb │ │ ├── user_languages.rb │ │ ├── user_reminder_time.rb │ │ ├── user_sms_notification_events.rb │ │ ├── users.rb │ │ └── volunteers.rb │ ├── fixtures/ │ │ └── files/ │ │ ├── casa_cases.csv │ │ ├── casa_cases_without_case_number.csv │ │ ├── default_past_court_date_template.docx │ │ ├── existing_casa_case.csv │ │ ├── generic.csv │ │ ├── no_rows.csv │ │ ├── sample_report.docx │ │ ├── supervisor_volunteers.csv │ │ ├── supervisors.csv │ │ ├── supervisors_invalid_phone_numbers.csv │ │ ├── supervisors_without_display_names.csv │ │ ├── supervisors_without_email.csv │ │ ├── supervisors_without_phone_numbers.csv │ │ ├── volunteers.csv │ │ ├── volunteers_invalid_phone_numbers.csv │ │ ├── volunteers_without_display_names.csv │ │ ├── volunteers_without_email.csv │ │ └── volunteers_without_phone_numbers.csv │ ├── helpers/ │ │ ├── all_casa_admins/ │ │ │ └── casa_orgs_helper_spec.rb │ │ ├── api_base_helper_spec.rb │ │ ├── application_helper_spec.rb │ │ ├── banner_helper_spec.rb │ │ ├── case_contacts_helper_spec.rb │ │ ├── contact_types_helper_spec.rb │ │ ├── court_dates_helper_spec.rb │ │ ├── court_orders_helper_spec.rb │ │ ├── date_helper_spec.rb │ │ ├── emancipations_helper_spec.rb │ │ ├── followup_helper_spec.rb │ │ ├── learning_hours_helper_spec.rb │ │ ├── mileage_rates_helper_spec.rb │ │ ├── notifications_helper_spec.rb │ │ ├── other_duties_helper_spec.rb │ │ ├── phone_number_helper_spec.rb │ │ ├── preference_sets_helper_spec.rb │ │ ├── report_helper_spec.rb │ │ ├── request_header_helper_spec.rb │ │ ├── sidebar_helper_spec.rb │ │ ├── sms_body_helper_spec.rb │ │ ├── template_helper_spec.rb │ │ ├── ui_helper_spec.rb │ │ └── volunteer_helper_spec.rb │ ├── jobs/ │ │ └── application_job_spec.rb │ ├── lib/ │ │ ├── importers/ │ │ │ ├── case_importer_spec.rb │ │ │ ├── file_importer_spec.rb │ │ │ ├── supervisor_importer_spec.rb │ │ │ └── volunteer_importer_spec.rb │ │ └── tasks/ │ │ ├── case_contact_types_reminder_spec.rb │ │ ├── data_post_processors/ │ │ │ ├── case_contact_populator_spec.rb │ │ │ ├── contact_topic_populator_spec.rb │ │ │ └── contact_type_populator_spec.rb │ │ ├── no_contact_made_reminder_spec.rb │ │ └── supervisor_weekly_digest_spec.rb │ ├── mailers/ │ │ ├── application_mailer_spec.rb │ │ ├── casa_admin_mailer_spec.rb │ │ ├── fund_request_mailer_spec.rb │ │ ├── learning_hours_mailer_spec.rb │ │ ├── previews/ │ │ │ ├── casa_admin_mailer_preview_spec.rb │ │ │ ├── devise_mailer_preview_spec.rb │ │ │ ├── supervisor_mailer_preview_spec.rb │ │ │ └── volunteer_mailer_preview_spec.rb │ │ ├── supervisor_mailer_spec.rb │ │ ├── user_mailer_spec.rb │ │ └── volunteer_mailer_spec.rb │ ├── models/ │ │ ├── acts_as_paranoid_spec.rb │ │ ├── additional_expense_spec.rb │ │ ├── address_spec.rb │ │ ├── all_casa_admin_spec.rb │ │ ├── all_casa_admins/ │ │ │ └── casa_org_metrics_spec.rb │ │ ├── api_credential_spec.rb │ │ ├── application_record_spec.rb │ │ ├── banner_spec.rb │ │ ├── casa_admin_spec.rb │ │ ├── casa_case_contact_type_spec.rb │ │ ├── casa_case_emancipation_category_spec.rb │ │ ├── casa_case_emancipation_option_spec.rb │ │ ├── casa_case_spec.rb │ │ ├── casa_org_spec.rb │ │ ├── case_assignment_spec.rb │ │ ├── case_contact_contact_type_spec.rb │ │ ├── case_contact_report_spec.rb │ │ ├── case_contact_spec.rb │ │ ├── case_court_order_spec.rb │ │ ├── case_court_report_context_spec.rb │ │ ├── case_court_report_spec.rb │ │ ├── case_group_membership_spec.rb │ │ ├── case_group_spec.rb │ │ ├── checklist_item_spec.rb │ │ ├── concerns/ │ │ │ ├── CasaCase/ │ │ │ │ └── validations_spec.rb │ │ │ ├── api_spec.rb │ │ │ ├── by_organization_scope_spec.rb │ │ │ └── roles_spec.rb │ │ ├── contact_topic_answer_spec.rb │ │ ├── contact_topic_spec.rb │ │ ├── contact_type_group_spec.rb │ │ ├── contact_type_spec.rb │ │ ├── court_date_spec.rb │ │ ├── custom_org_link_spec.rb │ │ ├── emancipation_category_spec.rb │ │ ├── emancipation_option_spec.rb │ │ ├── followup_spec.rb │ │ ├── fund_request_spec.rb │ │ ├── health_spec.rb │ │ ├── hearing_type_spec.rb │ │ ├── judge_spec.rb │ │ ├── language_spec.rb │ │ ├── learning_hour_spec.rb │ │ ├── learning_hour_topic_spec.rb │ │ ├── learning_hour_type_spec.rb │ │ ├── learning_hours_report_spec.rb │ │ ├── login_activity_spec.rb │ │ ├── mileage_rate_spec.rb │ │ ├── mileage_report_spec.rb │ │ ├── missing_data_report_spec.rb │ │ ├── note_spec.rb │ │ ├── other_duty_spec.rb │ │ ├── patch_note_group_spec.rb │ │ ├── patch_note_spec.rb │ │ ├── patch_note_type_spec.rb │ │ ├── placement_spec.rb │ │ ├── placement_type_spec.rb │ │ ├── preference_set_spec.rb │ │ ├── sent_email_spec.rb │ │ ├── sms_notification_event_spec.rb │ │ ├── soft_deleted_model_shared_example_coverage_spec.rb │ │ ├── supervisor_spec.rb │ │ ├── supervisor_volunteer_spec.rb │ │ ├── user_language_spec.rb │ │ ├── user_reminder_time.rb │ │ ├── user_sms_notification_event_spec.rb │ │ ├── user_spec.rb │ │ └── volunteer_spec.rb │ ├── notifications/ │ │ ├── base_notifier_spec.rb │ │ ├── delivery_methods/ │ │ │ └── sms_spec.rb │ │ ├── emancipation_checklist_reminder_notifier_spec.rb │ │ ├── followup_notifier_spec.rb │ │ ├── followup_resolved_notifier_spec.rb │ │ ├── reimbursement_complete_notifier_spec.rb │ │ ├── volunteer_birthday_notifier_spec.rb │ │ └── youth_birthday_notifier_spec.rb │ ├── policies/ │ │ ├── additional_expense_policy_spec.rb │ │ ├── application_policy_spec.rb │ │ ├── bulk_court_date_policy_spec.rb │ │ ├── casa_admin_policy_spec.rb │ │ ├── casa_case_policy/ │ │ │ └── scope_spec.rb │ │ ├── casa_case_policy_spec.rb │ │ ├── casa_org_policy_spec.rb │ │ ├── case_assignment_policy_spec.rb │ │ ├── case_contact_policy_spec.rb │ │ ├── case_court_order_policy_spec.rb │ │ ├── case_court_report_policy_spec.rb │ │ ├── case_group_policy_spec.rb │ │ ├── checklist_item_policy_spec.rb │ │ ├── contact_topic_answer_policy_spec.rb │ │ ├── contact_topic_policy_spec.rb │ │ ├── contact_type_group_policy_spec.rb │ │ ├── contact_type_policy_spec.rb │ │ ├── court_date_policy_spec.rb │ │ ├── custom_org_link_policy_spec.rb │ │ ├── dashboard_policy_spec.rb │ │ ├── followup_policy_spec.rb │ │ ├── fund_request_policy_spec.rb │ │ ├── hearing_type_policy_spec.rb │ │ ├── import_policy_spec.rb │ │ ├── judge_policy_spec.rb │ │ ├── language_policy_spec.rb │ │ ├── learning_hour_policy/ │ │ │ └── scope_spec.rb │ │ ├── learning_hour_policy_spec.rb │ │ ├── learning_hour_topic_policy_spec.rb │ │ ├── learning_hour_type_policy_spec.rb │ │ ├── nil_class_policy_spec.rb │ │ ├── note_policy_spec.rb │ │ ├── notification_policy_spec.rb │ │ ├── other_duty_policy_spec.rb │ │ ├── patch_note_policy_spec.rb │ │ ├── placement_policy_spec.rb │ │ ├── placement_type_policy_spec.rb │ │ ├── reimbursement_policy_spec.rb │ │ ├── supervisor_policy_spec.rb │ │ ├── supervisor_volunteer_policy_spec.rb │ │ ├── user_policy/ │ │ │ └── scope_spec.rb │ │ ├── user_policy_spec.rb │ │ └── volunteer_policy_spec.rb │ ├── presenters/ │ │ ├── base_presenter_spec.rb │ │ └── case_contact_presenter_spec.rb │ ├── rails_helper.rb │ ├── requests/ │ │ ├── additional_expenses_spec.rb │ │ ├── all_casa_admins/ │ │ │ ├── casa_admins_spec.rb │ │ │ ├── casa_orgs_spec.rb │ │ │ ├── dashboard_spec.rb │ │ │ ├── patch_notes_spec.rb │ │ │ └── sessions_spec.rb │ │ ├── all_casa_admins_spec.rb │ │ ├── android_app_associations_spec.rb │ │ ├── api/ │ │ │ └── v1/ │ │ │ ├── base_spec.rb │ │ │ └── users/ │ │ │ └── sessions_spec.rb │ │ ├── banners_spec.rb │ │ ├── bulk_court_dates_spec.rb │ │ ├── casa_admins_spec.rb │ │ ├── casa_cases_spec.rb │ │ ├── casa_org_spec.rb │ │ ├── case_assignments_spec.rb │ │ ├── case_contact_reports_spec.rb │ │ ├── case_contacts/ │ │ │ ├── case_contacts_new_design_spec.rb │ │ │ ├── followups_spec.rb │ │ │ └── form_spec.rb │ │ ├── case_contacts_spec.rb │ │ ├── case_court_orders_spec.rb │ │ ├── case_court_reports_spec.rb │ │ ├── case_groups_spec.rb │ │ ├── checklist_items_spec.rb │ │ ├── contact_topic_answers_spec.rb │ │ ├── contact_topics_spec.rb │ │ ├── contact_type_groups_spec.rb │ │ ├── contact_types_spec.rb │ │ ├── court_dates_spec.rb │ │ ├── custom_org_links_spec.rb │ │ ├── dashboard_spec.rb │ │ ├── emancipation_checklists_spec.rb │ │ ├── emancipations_request_spec.rb │ │ ├── error_spec.rb │ │ ├── followup_reports_spec.rb │ │ ├── fund_requests_spec.rb │ │ ├── health_spec.rb │ │ ├── hearing_types_spec.rb │ │ ├── imports_spec.rb │ │ ├── judges_spec.rb │ │ ├── languages_spec.rb │ │ ├── learning_hour_topics_spec.rb │ │ ├── learning_hour_types_spec.rb │ │ ├── learning_hours_reports_spec.rb │ │ ├── learning_hours_spec.rb │ │ ├── mileage_rates_spec.rb │ │ ├── mileage_reports_spec.rb │ │ ├── missing_data_reports_spec.rb │ │ ├── notes_spec.rb │ │ ├── notifications_spec.rb │ │ ├── other_duties_spec.rb │ │ ├── placement_reports_spec.rb │ │ ├── placement_types_spec.rb │ │ ├── placements_spec.rb │ │ ├── preference_sets_spec.rb │ │ ├── reimbursements_spec.rb │ │ ├── reports_spec.rb │ │ ├── static_spec.rb │ │ ├── supervisor_volunteers_spec.rb │ │ ├── supervisors_spec.rb │ │ ├── users/ │ │ │ ├── invitations_spec.rb │ │ │ └── passwords_spec.rb │ │ ├── users_spec.rb │ │ └── volunteers_spec.rb │ ├── routing/ │ │ └── all_casa_admins/ │ │ └── patch_notes_routing_spec.rb │ ├── seeds/ │ │ └── seeds_spec.rb │ ├── services/ │ │ ├── additional_expense_params_service_spec.rb │ │ ├── backfill_followupable_service_spec.rb │ │ ├── casa_case_change_service_spec.rb │ │ ├── case_contacts_contact_dates_spec.rb │ │ ├── case_contacts_export_csv_service_spec.rb │ │ ├── court_report_due_sms_reminder_service_spec.rb │ │ ├── court_report_format_contact_date_spec.rb │ │ ├── create_all_casa_admin_service_spec.rb │ │ ├── create_casa_admin_service_spec.rb │ │ ├── deployment/ │ │ │ └── backfill_case_contact_started_metadata_service_spec.rb │ │ ├── emancipation_checklist_download_html_spec.rb │ │ ├── emancipation_checklist_reminder_service_spec.rb │ │ ├── failed_import_csv_service_spec.rb │ │ ├── fdf_inputs_service_spec.rb │ │ ├── followup_export_csv_service_spec.rb │ │ ├── followup_service_spec.rb │ │ ├── inactive_messages_service_spec.rb │ │ ├── learning_hours_export_csv_service_spec.rb │ │ ├── mileage_export_csv_service_spec.rb │ │ ├── missing_data_export_csv_service_spec.rb │ │ ├── no_contact_made_sms_reminder_service_spec.rb │ │ ├── placement_export_csv_service_spec.rb │ │ ├── preference_set_table_state_service_spec.rb │ │ ├── short_url_service_spec.rb │ │ ├── sms_reminder_service_spec.rb │ │ ├── svg_sanitizer_service_spec.rb │ │ ├── twilio_service_spec.rb │ │ ├── volunteer_birthday_reminder_service_spec.rb │ │ └── volunteers_emails_export_csv_service_spec.rb │ ├── spec_helper.rb │ ├── support/ │ │ ├── api_helper.rb │ │ ├── capybara.rb │ │ ├── case_court_report_helpers.rb │ │ ├── datatable_helper.rb │ │ ├── download_helpers.rb │ │ ├── factory_bot.rb │ │ ├── fill_in_case_contact_fields.rb │ │ ├── flipper_helper.rb │ │ ├── pretender_context.rb │ │ ├── prosopite.rb │ │ ├── pundit_helper.rb │ │ ├── rack_attack.rb │ │ ├── request_helpers.rb │ │ ├── rspec_retry.rb │ │ ├── session_helper.rb │ │ ├── shared_examples/ │ │ │ ├── shows_court_dates_links.rb │ │ │ ├── shows_error_for_invalid_phone_numbers.rb │ │ │ └── soft_deleted_model.rb │ │ ├── stubbed_requests/ │ │ │ ├── short_io_api.rb │ │ │ ├── twilio_api.rb │ │ │ └── webmock_helper.rb │ │ ├── twilio_helper.rb │ │ └── user_input_helpers.rb │ ├── swagger_helper.rb │ ├── system/ │ │ ├── all_casa_admins/ │ │ │ ├── all_casa_admin_spec.rb │ │ │ ├── edit_spec.rb │ │ │ ├── password_change_spec.rb │ │ │ ├── patch_notes/ │ │ │ │ └── index_spec.rb │ │ │ └── sessions/ │ │ │ └── new_spec.rb │ │ ├── application/ │ │ │ └── timeout_warning_spec.rb │ │ ├── banners/ │ │ │ ├── dismiss_spec.rb │ │ │ └── new_spec.rb │ │ ├── bulk_court_dates/ │ │ │ └── new_spec.rb │ │ ├── casa_admins/ │ │ │ ├── edit_spec.rb │ │ │ ├── index_spec.rb │ │ │ └── new_spec.rb │ │ ├── casa_cases/ │ │ │ ├── additional_index_spec.rb │ │ │ ├── edit_spec.rb │ │ │ ├── emancipation/ │ │ │ │ └── show_spec.rb │ │ │ ├── fund_requests/ │ │ │ │ └── new_spec.rb │ │ │ ├── index_spec.rb │ │ │ ├── new_spec.rb │ │ │ ├── show_more_spec.rb │ │ │ └── show_spec.rb │ │ ├── casa_org/ │ │ │ └── edit_spec.rb │ │ ├── case_contacts/ │ │ │ ├── additional_expenses_spec.rb │ │ │ ├── case_contacts_new_design_spec.rb │ │ │ ├── contact_topic_answers_spec.rb │ │ │ ├── drafts_spec.rb │ │ │ ├── edit_spec.rb │ │ │ ├── followups/ │ │ │ │ ├── create_spec.rb │ │ │ │ └── resolve_spec.rb │ │ │ ├── index_spec.rb │ │ │ └── new_spec.rb │ │ ├── case_court_reports/ │ │ │ └── index_spec.rb │ │ ├── case_groups/ │ │ │ └── case_groups_spec.rb │ │ ├── checklist_items/ │ │ │ ├── destroy_spec.rb │ │ │ ├── edit_spec.rb │ │ │ └── new_spec.rb │ │ ├── components/ │ │ │ └── truncated_text_component_spec.rb │ │ ├── contact_types/ │ │ │ ├── edit_spec.rb │ │ │ └── new_spec.rb │ │ ├── court_dates/ │ │ │ ├── edit_spec.rb │ │ │ ├── new_spec.rb │ │ │ └── view_spec.rb │ │ ├── dashboard/ │ │ │ └── show_spec.rb │ │ ├── deep_link/ │ │ │ └── deep_link_spec.rb │ │ ├── devise/ │ │ │ └── passwords/ │ │ │ └── new_spec.rb │ │ ├── emancipations/ │ │ │ └── show_spec.rb │ │ ├── hearing_types/ │ │ │ └── new_spec.rb │ │ ├── imports/ │ │ │ └── index_spec.rb │ │ ├── judges/ │ │ │ └── new_spec.rb │ │ ├── languages/ │ │ │ └── languages_spec.rb │ │ ├── learning_hours/ │ │ │ ├── edit_spec.rb │ │ │ ├── index_spec.rb │ │ │ ├── new_spec.rb │ │ │ └── volunteers/ │ │ │ └── show_spec.rb │ │ ├── mileage_rates/ │ │ │ └── mileage_rates_spec.rb │ │ ├── notifications/ │ │ │ └── index_spec.rb │ │ ├── other_duties/ │ │ │ └── new_spec.rb │ │ ├── placements/ │ │ │ ├── destroy_spec.rb │ │ │ ├── edit_spec.rb │ │ │ ├── index_spec.rb │ │ │ └── new_spec.rb │ │ ├── reimbursements/ │ │ │ └── reimbursements_spec.rb │ │ ├── reports/ │ │ │ ├── export_data_spec.rb │ │ │ └── index_spec.rb │ │ ├── sessions/ │ │ │ ├── destroy_spec.rb │ │ │ ├── login_spec.rb │ │ │ └── new_spec.rb │ │ ├── static/ │ │ │ └── index_spec.rb │ │ ├── supervisors/ │ │ │ ├── edit_spec.rb │ │ │ ├── index_spec.rb │ │ │ └── new_spec.rb │ │ ├── users/ │ │ │ └── edit_spec.rb │ │ └── volunteers/ │ │ ├── edit_spec.rb │ │ ├── index_spec.rb │ │ ├── invite_spec.rb │ │ ├── new_spec.rb │ │ └── notes/ │ │ └── edit_spec.rb │ ├── validators/ │ │ ├── casa_org_validator_spec.rb │ │ ├── court_report_validator_spec.rb │ │ ├── url_validator_spec.rb │ │ └── user_validator_spec.rb │ ├── values/ │ │ ├── all_casa_admin_parameters_spec.rb │ │ ├── banner_parameters_spec.rb │ │ ├── casa_admin_parameters_spec.rb │ │ ├── case_contact_parameters_spec.rb │ │ ├── supervisor_parameters_spec.rb │ │ ├── user_parameters_spec.rb │ │ └── volunteer_parameters_spec.rb │ └── views/ │ ├── all_casa_admins/ │ │ ├── casa_orgs/ │ │ │ ├── new.html.erb_spec.rb │ │ │ └── show.html.erb_spec.rb │ │ ├── edit.html.erb_spec.rb │ │ └── patch_notes/ │ │ └── index.html.erb_spec.rb │ ├── banners/ │ │ └── new.html.erb_spec.rb │ ├── bulk_court_date/ │ │ └── new.html.erb_spec.rb │ ├── casa_admins/ │ │ ├── admins_table.html.erb_spec.rb │ │ └── edit.html.erb_spec.rb │ ├── casa_cases/ │ │ ├── edit.html.erb_spec.rb │ │ ├── index.html.erb_spec.rb │ │ ├── new.html.erb_spec.rb │ │ └── show.html.erb_spec.rb │ ├── casa_orgs/ │ │ └── edit.html.erb_spec.rb │ ├── case_contacts/ │ │ ├── case_contact.html.erb_spec.rb │ │ └── index.html.erb_spec.rb │ ├── case_court_reports/ │ │ └── index.html.erb_spec.rb │ ├── checklist_items/ │ │ ├── edit.html.erb_spec.rb │ │ └── new.html.erb_spec.rb │ ├── court_dates/ │ │ ├── edit.html.erb_spec.rb │ │ ├── new.html.erb_spec.rb │ │ └── show.html.erb_spec.rb │ ├── devise/ │ │ ├── invitations/ │ │ │ ├── edit.html.erb_spec.rb │ │ │ └── new.html.erb_spec.rb │ │ └── passwords/ │ │ └── new.html.erb_spec.rb │ ├── emancipations/ │ │ └── show.html.erb_spec.rb │ ├── hearing_types/ │ │ ├── edit.html.erb_spec.rb │ │ └── new.html.erb_spec.rb │ ├── judges/ │ │ └── new.html.erb_spec.rb │ ├── layouts/ │ │ ├── application.html.erb_spec.rb │ │ ├── footer.html.erb_spec.rb │ │ ├── header.html.erb_spec.rb │ │ └── sidebar.html.erb_spec.rb │ ├── mileage_rates/ │ │ └── index.html.erb_spec.rb │ ├── notifications/ │ │ └── index.html.erb_spec.rb │ ├── other_duties/ │ │ ├── edit.html.erb_spec.rb │ │ └── new.html.erb_spec.rb │ ├── placement_types/ │ │ ├── edit.html.erb_spec.rb │ │ └── new.html.erb_spec.rb │ ├── placements/ │ │ ├── edit.html.erb_spec.rb │ │ ├── index.html.erb_spec.rb │ │ └── new.html.erb_spec.rb │ ├── reimbursements/ │ │ └── index.html.erb_spec.rb │ ├── supervisor_mailer/ │ │ └── weekly_digest.html.erb_spec.rb │ ├── supervisors/ │ │ ├── edit.html.erb_spec.rb │ │ ├── index.html.erb_spec.rb │ │ └── new.html.erb_spec.rb │ ├── templates/ │ │ └── email_templates_spec.rb │ └── volunteers/ │ ├── edit.html.erb_spec.rb │ ├── index.html.erb_spec.rb │ └── new.html.erb_spec.rb ├── storage/ │ └── .keep ├── swagger/ │ └── v1/ │ └── swagger.yaml └── vendor/ ├── .keep └── pdftk/ ├── bin/ │ └── pdftk └── lib/ └── libgcj.so.12 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .allow_skipping_tests ================================================ ================================================ FILE: .better-html.yml ================================================ # defaults ================================================ FILE: .browserslistrc ================================================ defaults ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/devcontainers/ruby:dev-3.3-bookworm RUN apt-get update && apt-get install -y vim curl gpg postgresql postgresql-contrib tzdata imagemagick ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/postgres { "dockerComposeFile": "docker-compose.yml", "forwardPorts": [3000, 5432], "workspaceFolder": "/workspaces/casa", "service": "app", "customizations": { "vscode": { "extensions": ["Shopify.ruby-extensions-pack"], "settings": { "rubyLsp.rubyVersionManager": { "identifier": "rvm" } } } }, "containerEnv": { "DATABASE_HOST": "postgres", "POSTGRES_USER": "postgres", "POSTGRES_PASSWORD": "postgres" }, "features": { "ghcr.io/devcontainers/features/github-cli:1": { "version": "latest" }, "ghcr.io/devcontainers/features/node:1": { "version": "24.13.0" } }, "postCreateCommand": ".devcontainer/post-create.sh" } ================================================ FILE: .devcontainer/docker-compose.yml ================================================ version: "3.8" services: app: build: context: .. dockerfile: .devcontainer/Dockerfile volumes: - ../..:/workspaces:cached # Overrides default command so things don't shut down after the process ends. command: sleep infinity networks: - default postgres: image: postgres:latest restart: always networks: - default volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_USER: postgres POSTGRES_DB: postgres POSTGRES_PASSWORD: postgres # Note that these ports are ignored by the devcontainer. # Instead, the ports are specified in .devcontainer/devcontainer.json. # ports: # - "5432:5432" volumes: postgres-data: ================================================ FILE: .devcontainer/post-create.sh ================================================ RUBY_VERSION="$(cat .ruby-version | tr -d '\n')" # copy the file only if it doesn't already exist cp -n .devcontainer/.env.codespaces .env # If the project's required ruby version changes from 4.0.2, this command # will download and compile the correct version, but it will take a long time. if [ "$RUBY_VERSION" != "4.0.2" ]; then rvm install $RUBY_VERSION rvm use $RUBY_VERSION echo "Ruby $RUBY_VERSION installed" fi bin/setup ================================================ FILE: .dockerignore ================================================ /.bundle /log/* /tmp/* !/log/.keep !/tmp/.keep /storage/* !/storage/.keep /public/assets .byebug_history /config/master.key /public/packs /public/packs-test /node_modules /npm-debug.log /npm-error.log ================================================ FILE: .erb_lint.yml ================================================ EnableDefaultLinters: true exclude: - '**/vendor/**/*' linters: ErbSafety: enabled: true better_html_config: .better-html.yml Rubocop: enabled: true rubocop_config: Layout/LineLength: Enabled: false only: - Layout/LineLength ================================================ FILE: .github/CODEOWNERS ================================================ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners * @compwron @FireLemons @elasticspoon /app/javascript/ @schoork @FireLemons @elasticspoon ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [rubyforgood] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 🪲Bug report about: 🔨What needs fixing? ✨ title: "Bug: " labels: ["Type: Bug", "Help Wanted"] --- ## Impacted User Types - volunteers? - supervisors? - admins? - all casa admins? ## Environment ex: staging, desktop web, Safari ## Current Behavior ex: When I click "generate report," no report is generated. _Please include a screenshot!_ ## Expected Behavior ex: When I click "generate report", a downloadable report should be generated. ## How to Replicate ex: 1. - Log in as an admin or supervisor. 2. - Click on "Generate Reports" in the left sidebar menu. 3. - Filter by a volunteer who has logged at least one case contact. 4. - Click "Generate report" at the bottom of the page. ## How to access the QA site _Login Details:_ [Link to QA site](https://casa-qa.herokuapp.com/) Login Emails: - volunteer1@example.com view site as a volunteer - supervisor1@example.com view site as a supervisor - casa_admin1@example.com view site as an admin - all_casa_admin1@example.com view site as an all casa admin - go to `/all_casa_admins/sign_in` password for all users: 12345678 ### Questions? Join Slack! We highly recommend that you join us in [slack](https:https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q) #casa channel to ask questions quickly. And [discord](https://discord.gg/qJcw2RZH8Q) for office hours (currently Tuesday 5-7pm Pacific), stakeholder news, and upcoming new issues. ================================================ FILE: .github/ISSUE_TEMPLATE/chore.md ================================================ **Description** **Existing gem(s), file(s) needing updating, changing or deleting** **New file(s) needing creating** ### Questions? Join Slack! We highly recommend that you join us in [slack](https:https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q) #casa channel to ask questions quickly and hear about office hours (currently Tuesday 5-7pm Pacific), stakeholder news, and upcoming new issues. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 📄Documentation addition/change about: 🔨What needs documenting? ✨ url: https://github.com/rubyforgood/casa/issues/new?template=documentation.md&projects=rubyforgood/casa/1 - name: 🧹Chore about: 🔨What needs updating or removing? ✨ url: https://github.com/rubyforgood/casa/issues/new?template=chore.md&projects=rubyforgood/casa/1 - name: 🙏Problem Validation about: 🔨Describe a stakeholder's issue✨ url: https://github.com/rubyforgood/casa/issues/new?template=problem_validation.md&projects=rubyforgood/casa/1 ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.md ================================================ **Description** **Existing file(s) needing changing** **New file(s) needing creating** ### Questions? Join Slack! We highly recommend that you join us in [slack](https:https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q) #casa channel to ask questions quickly and hear about office hours (currently Tuesday 5-7pm Pacific), stakeholder news, and upcoming new issues. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 💡Feature request about: 🔨What needs building? ✨ labels: ["✨ New Feature", "Help Wanted"] --- ## What type(s) of user does this feature affect? - volunteers? - supervisors? - admins? - all casa admins? ## Description ## Screenshots of current behavior, if any You can paste images on the clipboard here ## How to access the QA site _Login Details:_ [Link to QA site](https://casa-qa.herokuapp.com/) Login Emails: - volunteer1@example.com view site as a volunteer - supervisor1@example.com view site as a supervisor - casa_admin1@example.com view site as an admin - all_casa_admin1@example.com view site as an all casa admin - go to `/all_casa_admins/sign_in` password for all users: 12345678 ### Questions? Join Slack! We highly recommend that you join us in [slack](https:https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q) #casa channel to ask questions quickly. And [discord](https://discord.gg/qJcw2RZH8Q) for office hours (currently Tuesday 5-7pm Pacific), stakeholder news, and upcoming new issues. ================================================ FILE: .github/ISSUE_TEMPLATE/flaky_test.md ================================================ --- name: Flaky Test about: one of the tests is inconsistent and likely producing false positives title: "Bug: Flaky Test" labels: ["Type: Bug", "Help Wanted"] --- Flaky tests are defined as tests that return both passes and failures despite no changes to the code or the test itself Fix the test so it runs consistently. ### CI Workflow rspec or rspec in docker? ### Sample Error Output: ``` ``` ### Questions? Join Slack! We highly recommend that you join us in [slack](https:https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q) #casa channel to ask questions quickly and hear about office hours (currently Tuesday 5-7pm Pacific), stakeholder news, and upcoming new issues. ================================================ FILE: .github/ISSUE_TEMPLATE/problem_validation.md ================================================ Please use this template to document problems mentioned by CASA stakeholders so we make the best decisions for the solution. **Which role has this problem (AllCasaAdmin, CasaAdmin, Supervisor, Volunteer)?** **What is the problem they are facing? (short description)** **When does the problem happen? At what moment in the workflow or after how long?** **Attach a design mockup if possible** ### Questions? Join Slack! We highly recommend that you join us in [slack](https:https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q) #casa channel to ask questions quickly and hear about office hours (currently Tuesday 5-7pm Pacific), stakeholder news, and upcoming new issues. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### What github issue is this PR for, if any? Resolves #XXXX ### What changed, and _why_? ### How is this **tested**? (please write rspec and jest tests!) 💖💪 _Note: if you see a flake in your test build in github actions, please post in slack #casa "Flaky test: " :) 💪_ _Note: We love [capybara](https://rubydoc.info/github/teamcapybara/capybara) tests! If you are writing both haml/js and ruby, please try to test your work with tests at every level including system tests like https://github.com/rubyforgood/casa/tree/main/spec/system_ ### Screenshots please :) _Run your local server and take a screenshot of your work! Try to include the URL of the page as well as the contents of the page._ ### Feelings gif (optional) _What gif best describes your feeling working on this issue? https://giphy.com/ How to embed:_ `![alt text](https://media.giphy.com/media/1nP7ThJFes5pgXKUNf/giphy.gif)` ================================================ FILE: .github/autoapproval.yml ================================================ from_owner: - dependabot-preview ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates # Basic set up for three package managers version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" ignore: # Keep this locked to 1.77.6 until # bootstrap 5.3.4 is out to silence # sass deprecation warnings # https://getbootstrap.com/docs/versions/ - dependency-name: "sass" versions: ["1.77.*", "1.78.*"] - package-ecosystem: "bundler" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/instructions/copilot-review.instructions.md ================================================ --- applyTo: "**" --- # CASA Code Review Instructions You are reviewing pull requests for CASA (Court Appointed Special Advocates), a Ruby on Rails application that helps CASA volunteers track their work with youth in foster care. This is an open-source project maintained by Ruby for Good with contributors of all experience levels. Be constructive and kind. ## Tech Stack - **Backend**: Rails 7.2, Ruby 3.x, PostgreSQL - **Frontend**: Stimulus (Hotwire) + Turbo, Bootstrap 5, ESBuild - **Auth**: Devise + Devise-Invitable (no UI sign-ups; admin invitation only) - **Authorization**: Pundit (policy-based) - **Background Jobs**: Delayed Job - **Feature Flags**: Flipper - **Testing**: RSpec + Capybara (Ruby), Jest (JavaScript) - **Linting**: Standard.rb (Ruby), StandardJS (JavaScript), erb-lint (ERB) - **View Layer**: ERB templates, ViewComponent, Draper decorators ## Architecture Rules ### Authorization & Multi-Tenancy - All controller actions must be authorized via Pundit (`authorize` / `policy_scope`). - Data must be scoped to the current user's `casa_org`. Never allow cross-organization data access. - User roles: `AllCasaAdmin`, `CasaAdmin`, `Supervisor`, `Volunteer`. Each has a separate model (not STI). - Permission changes must include or update the corresponding Pundit policy file in `app/policies/`. ### Controllers - Controllers should be thin. Complex logic belongs in service objects or models. - Controllers must call `authorize` and typically use `after_action :verify_authorized`. - Use `policy_scope` for index actions to scope records to the current org/user. ### Models - Models use `include ByOrganizationScope` for org-scoped queries where applicable. - Prefer scopes over class methods for query logic. - Soft deletes via the Paranoia gem: `destroy` marks records deleted, does not hard-delete. - Use Strong Migrations: flag any unsafe migration operations (e.g., removing a column without `safety_assured`, adding an index without `algorithm: :concurrently`). ### Views & Frontend - Use ERB (not HAML). Complex view logic should live in a Draper decorator (`app/decorators/`), not in the template. - Use ViewComponent (`app/components/`) for reusable UI elements. - Style with Bootstrap 5 utility classes and existing project SCSS. UI changes should match the rest of the site. - JavaScript should use Stimulus controllers. Avoid inline ` ================================================ FILE: app/views/casa_cases/show.xlsx.axlsx ================================================ column_names = [ "Internal Contact Number", "Duration Minutes", "Contact Types", "Contact Made", "Contact Medium", "Occurred At", "Added To System At", "Miles Driven", "Wants Driving Reimbursement", "Casa Case Number", "Creator Email", "Creator Name", "Supervisor Name", "Case Contact Notes" ] wb = xlsx_package.workbook case_contacts = @casa_case.decorate.case_contacts_ordered_by_occurred_at wb.add_worksheet(name: @casa_case.case_number) do |sheet| sheet.add_row column_names case_contacts.each do |case_contact| sheet.add_row [ case_contact.id, case_contact.duration_minutes, case_contact.contact_types.map(&:name).join("|"), case_contact.contact_made, case_contact.medium_type, case_contact.occurred_at.strftime("%B %d, %Y"), case_contact.created_at.strftime("%F %T UTC"), case_contact.miles_driven, case_contact.want_driving_reimbursement, @casa_case.case_number, case_contact.creator.email, case_contact.creator.display_name, case_contact.creator.supervisor&.display_name, case_contact.notes ] end end ================================================ FILE: app/views/casa_org/_contact_topics.html.erb ================================================

Contact Topics

<% @contact_topics.each do |contact_topic| %> <% id = "contact_topic-#{contact_topic.id}" %> <%= render(Modal::GroupComponent.new(id: id)) do |component| %> <% component.with_header(text: "Delete Contact Topic?", id: id) %> <% component.with_body(text: [ "This topic and its related questions will be deleted and will no longer be presented while filling out case contacts.", "This will not affect case contacts that have already been created."]) %> <% component.with_footer do %> <%= link_to soft_delete_contact_topic_path(contact_topic), method: :delete, class: "btn-sm main-btn danger-btn btn-hover ms-auto" do %> Delete Court Report Topic <% end %> <% end %> <% end %> <% end %>
Question Details Active?
<%= contact_topic.question %> <%= contact_topic.details %> <%= contact_topic.active ? "Yes" : "No" %> <%= render(DropdownMenuComponent.new(menu_title: "Actions Menu", hide_label: true)) do %>
  • <%= link_to "Edit", edit_contact_topic_path(contact_topic), class: "dropdown-item" %>
  • <%= render(Modal::OpenLinkComponent.new(text: "Delete", target: id, klass: "dropdown-item")) %>
  • <% end %>
    ================================================ FILE: app/views/casa_org/_contact_type_groups.html.erb ================================================

    Contact Type Groups

    <% contact_type_groups.each do |group| %> <% end %>
    Name Active? Actions
    <%= group.name %> <%= group.active? ? "Yes" : "No" %> <%= link_to edit_contact_type_group_path(group) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_contact_types.html.erb ================================================

    Contact Types

    <% contact_types.each do |contact_type| %> <% end %>
    Name Group Active? Actions
    <%= contact_type.name %> <%= contact_type.contact_type_group.name %> <%= contact_type.active? ? "Yes" : "No" %> <%= link_to edit_contact_type_path(contact_type) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_custom_org_links.html.erb ================================================

    Custom Links

    <% if @custom_org_links.blank? %> <% else %> <% @custom_org_links.each do |custom_link| %> <% end %> <% end %>
    Display Text URL Active? Actions
    No custom links have been added for this organization.
    <%= custom_link.text %> <%= truncate(custom_link.url, length: 90) %> <%= custom_link.active ? "Yes" : "No" %> <%= link_to edit_custom_org_link_path(custom_link) do %>
    <% end %> <%= link_to custom_org_link_path(custom_link), method: :delete do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_hearing_types.html.erb ================================================

    Hearing Types

    <% @hearing_types.each do |hearing_type| %> <% end %>
    Name Checklist Active? Actions
    <%= hearing_type.name %> <%= "#{hearing_type.checklist_updated_date}" %> <%= hearing_type.active ? "Yes" : "No" %> <%= link_to edit_hearing_type_path(hearing_type) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_judges.html.erb ================================================

    Judge

    <% @judges.each do |judge| %> <% end %>
    Name Active? Actions
    <%= judge.name %> <%= judge.active ? "Yes" : "No" %> <%= link_to edit_judge_path(judge) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_languages.html.erb ================================================

    Languages

    A list of languages volunteers can choose from to add to their profile to let supervisors and admins know they can speak the language.

    <%= link_to new_language_path, class: "btn-sm main-btn primary-btn btn-hover" do %> New Language <% end %>
    <% languages.each do |lang| %> <% end %>
    Name Actions
    English (default language)
    <%= lang.name %> <%= link_to edit_language_path(lang) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_learning_hour_topics.html.erb ================================================

    Learning Topic

    <% @learning_hour_topics.each do |learning_hour_topic| %> <% end %>
    Name Actions
    <%= learning_hour_topic.name %> <%= link_to edit_learning_hour_topic_path(learning_hour_topic) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_learning_hour_types.html.erb ================================================

    Type of Learning

    <% @learning_hour_types.each do |learning_hour_type| %> <% end %>
    Name Active? Actions
    <%= learning_hour_type.name %> <%= learning_hour_type.active ? "Yes" : "No" %> <%= link_to edit_learning_hour_type_path(learning_hour_type) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_placement_types.html.erb ================================================

    Placement Types

    <% placement_types.each do |placement_type| %> <% end %>
    Name Actions
    <%= placement_type.name %> <%= link_to edit_placement_type_path(placement_type) do %>
    <% end %>
    ================================================ FILE: app/views/casa_org/_sent_emails.html.erb ================================================

    Sent Emails

    <% @sent_emails.each do |sent_email| %> <% end %>
    Mailer Type Category Recipient Time Sent
    <%= sent_email.mailer_type %> <%= sent_email.category %> <%= sent_email.user.display_name %> <<%= sent_email.sent_address %>> <%= to_user_timezone(sent_email.created_at).strftime("%l:%M%P %d %b %Y") %>
    ================================================ FILE: app/views/casa_org/edit.html.erb ================================================

    Editing CASA Organization

    <%= form_with(model: current_organization, local: true) do |form| %> <%= render "/shared/error_messages", resource: current_organization %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= form.label :display_name, "Display name" %> <%= form.text_field :display_name, class: "form-control" %>
    <%= form.label :address, "Address" %> <%= form.text_field :address, class: "form-control" %>
    <%= form.label :logo, "Logo" %> <%= form.file_field :logo, class: "form-control h-auto", accept: ".png,.gif,.jpg,.jpeg,.webp,.svg" %>
    <%= form.label :court_report_template, "Court report template" %>
    <%= form.file_field :court_report_template, class: "form-control" %>
    <% if current_organization.court_report_template.attached? %> <% ActiveStorage::Current.url_options = { host: request.base_url } %> <%= link_to 'Download Current Template', current_organization.court_report_template.url(only_path: true), class: "btn btn-info" %> <% end %>

    Organization Features

    <%= form.check_box :show_driving_reimbursement, class: 'form-check-input' %> <%= form.label :show_driving_reimbursement, "Show driving reimbursement", class: 'form-check-label mb-2' %>
    <%= form.check_box :learning_topic_active, class: 'form-check-input' %> <%= form.label :learning_topic_active, "Enable Learning Topic", class: 'form-check-label mb-2' %>
    <%= form.check_box :other_duties_enabled, class: 'form-check-input' %> <%= form.label :other_duties_enabled, "Enable Other Duties", class: 'form-check-label mb-2' %>
    <%= form.check_box :twilio_enabled, class: 'form-check-input accordionTwilio' %> <%= form.label :twilio_enabled, "Enable Twilio", class: 'form-check-label mb-2' %>
    <%# Twilio Form Begin %>
    <%= form.label :twilio_phone_number, "Twilio Phone Number" %> <%= form.text_field :twilio_phone_number, class: "form-control", required: true %>
    <%= form.label :twilio_account_sid, "Twilio Account SID" %> <%= form.text_field :twilio_account_sid, class: "form-control", required: true %>
    <%= form.label :twilio_api_key_sid, "Twilio API Key SID" %> <%= form.text_field :twilio_api_key_sid, class: "form-control", required: true %>
    <%= form.label :twilio_api_key_secret, "Twilio API Key Secret" %> <%= form.text_field :twilio_api_key_secret, class: "form-control", required: true %>
    <%# Twilio Form End %> <% if Flipper.enabled?(:show_additional_expenses) %>
    <%= form.check_box :additional_expenses_enabled, class: 'form-check-input' %> <%= form.label :additional_expenses_enabled, "Volunteers can add Other Expenses", class: 'form-check-label mb-2' %>
    <% end %>
    <%= button_tag( type: "submit", class: "btn-sm main-btn primary-btn btn-hover" ) do %> Submit <% end %>
    <% end %>
    <%= render "languages", languages: current_organization.languages %>
    <%= render "custom_org_links" %>

    Manage Contact Types

    <%= render "contact_type_groups", contact_type_groups: @contact_type_groups %> <%= render "contact_types", contact_types: @contact_types %>

    Manage Court Details

    <%= render "hearing_types" %> <%= render "judges" %> <%= render "sent_emails" %>

    Manage Learning Hours

    <%= render "learning_hour_types" %>
    <%= render "learning_hour_topics" %>

    Manage Case Contact Topics

    <%= render "contact_topics" %>

    Manage Case Placement Types

    <%= render "placement_types", placement_types: @placement_types %>
    ================================================ FILE: app/views/case_assignments/index.html.erb ================================================

    <%= "Editing #{@volunteer.display_name}" %>


    <% if @volunteer.casa_cases %> <% @volunteer.case_assignments.each do |assignment| %> <% end %>
    Case Number Transition Aged Youth Actions
    Case Number <%= assignment.casa_case.case_number %> Transition Aged Youth <%= assignment.casa_case.decorate.transition_aged_youth %> <%= button_to "Unassign Volunteer", volunteer_case_assignment_path(@volunteer, assignment), method: :delete, class: "btn btn-danger" %>
    <% end %>

    Assign a New Volunteer

    <%= form_with(model: CaseAssignment.new, url: volunteer_case_assignments_path) do |form| %>
    <%= form.submit "Assign Case", class: 'btn btn-primary' %> <% end %>
    ================================================ FILE: app/views/case_contacts/_case_contact.html.erb ================================================
    <%= contact.decorate.medium_icon_classes %>">
    "> <%= "[DELETE] " if policy(contact).restore? && contact.deleted? %> <%= contact.decorate.contact_groups %> <% if !contact.active? %> Draft <% end %> <%= link_to("undelete", restore_case_contact_path(contact.id), method: :post, data: { turbo: false }, class: "btn btn-info") if policy(contact).restore? && contact.deleted? %>
    <%= contact.decorate.contact_types %>
    <%= contact.decorate.subheading %>
      <% contact.contact_topic_answers.reject { _1.value.blank? }.each do |answer| %>
    • <%= render TruncatedTextComponent.new(answer.value, label: answer.contact_topic.question) %>
    • <% end %> <% if contact.notes %>
    • <%= render TruncatedTextComponent.new(contact.notes, label: "Additional Notes") %>
    • <% end %>
    <% if Pundit.policy(current_user, contact).update? %> <%= render "case_contacts/followup", contact: contact, followup: contact.requested_followup %>
    <%= link_to edit_case_contact_path(contact), class: "text-danger", data: { turbo: false } do %> Edit <% end %>
    <% end %>
    <% if policy(contact).destroy? && !contact.deleted? %> <%= link_to case_contact_path(contact.id), class: "main-btn btn-sm danger-btn btn-hover", method: :delete, data: { turbo: false } do %> Delete <% end %> <% end %>
    Created by: <% if policy(contact).edit? %> <% if current_user.volunteer? %> <%= contact.creator&.display_name %> <% else %> <% if contact.creator&.supervisor? %> <%= link_to contact.creator&.display_name, edit_supervisor_path(contact.creator), data: { turbo: false } %> <% elsif contact.creator&.casa_admin? %> <%= link_to contact.creator&.display_name, edit_users_path, data: { turbo: false } %> <% else %> <%= link_to contact.creator&.display_name, edit_volunteer_path(contact.creator), data: { turbo: false } %> <% end %> <% end %> <% else %> <%= contact.creator&.display_name %> <% end %>
    ================================================ FILE: app/views/case_contacts/_confirm_note_content_dialog.html.erb ================================================ ================================================ FILE: app/views/case_contacts/_followup.html.erb ================================================
    <% if followup %> <%= button_to resolve_followup_path(followup), method: :patch, class: "main-btn btn-sm success-btn btn-hover", id:"resolve", data: { turbo: false } do %> Resolve Reminder <% end %> <% else %> <% end %>
    ================================================ FILE: app/views/case_contacts/case_contacts_new_design/index.html.erb ================================================

    Case Contacts

    <%= link_to new_case_contact_path, class: "main-btn primary-btn btn-sm btn-hover" do %> New Case Contact <% end %>
    <% @presenter.case_contacts.each do |casa_case_id, case_contacts| %> <% case_contacts.each do |case_contact| %> <% end %> <% end %>
    Date
    Case
    Relationship
    Medium
    Created By
    Contacted
    Topics
    Draft
    <%= I18n.l(case_contact[:occurred_at], format: :full) if case_contact[:occurred_at].present? %> <%= @presenter.display_case_number(casa_case_id) %> <%= case_contact.decorate.contact_types_comma_separated %> <%= case_contact.medium_type&.capitalize %> <% if policy(case_contact).edit? %> <% if current_user.volunteer? %> <%= case_contact.creator&.display_name %> <% else %> <% if case_contact.creator&.supervisor? %> <%= link_to case_contact.creator&.display_name, edit_supervisor_path(case_contact.creator), data: { turbo: false } %> <% elsif case_contact.creator&.casa_admin? %> <%= link_to case_contact.creator&.display_name, edit_users_path, data: { turbo: false } %> <% else %> <%= link_to case_contact.creator&.display_name, edit_volunteer_path(case_contact.creator), data: { turbo: false } %> <% end %> <% end %> <% else %> <%= case_contact.creator&.display_name %> <% end %> <% if case_contact.contact_made %> <% else %> <% end %> <%= "(#{"%02d:%02d" % [case_contact.duration_minutes / 60, case_contact.duration_minutes % 60]})" if case_contact.duration_minutes %> <%= case_contact.contact_topics.map(&:question).join(" | ") %> <% if !case_contact.active? %> Draft <% end %>
    ================================================ FILE: app/views/case_contacts/drafts.html.erb ================================================ <%= render partial: "case_contacts/case_contact", collection: @case_contacts, as: :contact %> ================================================ FILE: app/views/case_contacts/form/_contact_topic_answer.html.erb ================================================
    <%= form.label :contact_topic_id, "Discussion Topic", class: "form-label" %> <%= form.select(:contact_topic_id, select_options, {include_blank: "Select a discussion topic"}, class: ["form-select", "contact-topic-id-select"], data: { action: "change->case-contact-form#onContactTopicSelect", case_contact_form_target: "contactTopicSelect" } ) %>
    <%= form.label :value, "Discussion Notes", class: "form-label" %> <%= form.text_area( :value, data: { action: "input->autosave#save" }, rows: 3, class: ["form-control", "contact-topic-answer-input"], placeholder: "Enter discussion notes here to be included in the court report. Refer to individuals by role, not by name." ) %>
    <%= form.hidden_field(:id, value: form.object.id) %> <%= form.hidden_field :_destroy %>
    ================================================ FILE: app/views/case_contacts/form/_contact_types.html.erb ================================================

    Choose from the available options below by searching or selecting from the dropdown menu.

    <%= render(Form::MultipleSelectComponent.new( form: form, name: :contact_type_ids, options: options.decorate.map { |ct| ct.hash_for_multi_select_with_cases(casa_cases&.pluck(:id)) }, selected_items: selected_items, render_option_subtext: true, show_all_option: true, )) %>
    ================================================ FILE: app/views/case_contacts/form/details.html.erb ================================================

    <%= @case_contact.decorate.form_title %>

    <%= form_with( model: @case_contact, url: wizard_path(nil, case_contact_id: @case_contact.id), id: "case-contact-form", data: { controller: "case-contact-form", "autosave-target": "form", }, local: true ) do |form| %> <%= render "/shared/error_messages", resource: @case_contact %>

    Details

    <%= render(Form::MultipleSelectComponent.new( form: form, name: :draft_case_ids, options: @casa_cases.decorate.map { |casa_case| casa_case.hash_for_multi_select }, selected_items: @case_contact.draft_case_ids, render_option_subtext: current_user.supervisor?, placeholder_term: "case(s)" )) %>

    <%= form.label :occurred_at, class: "form-label" do %> Contact Date* <% end %>

    <% min_date = CaseContact::MINIMUM_DATE %> <% current_date = Time.zone.today %> <%= form.date_field(:occurred_at, required: true, max: (current_date + 1.day).to_fs(:iso8601), min: min_date.to_fs(:iso8601), class: "form-control") %>

    Contact Type(s)*

    <% casa_case_ids = @casa_cases.pluck(:id) %> <% @grouped_contact_types.each do |group_name, contact_types| %>
    <%= group_name %> <%= form.collection_check_boxes(:contact_type_ids, contact_types, :id, :name) do |b| %>
    <%= b.check_box(class: ["form-check-input", "contact-form-type-checkbox"]) %> <%= b.label(class: "form-check-label") %> <%= b.object.last_time_used_with_cases(casa_case_ids) %>
    <% end %>
    <% end %>
    <%= form.check_box :contact_made, class: "form-check-input" %> <%= form.label :contact_made, class: "form-check-label" do %>

    Contact was made

    <% end %>

    Contact Medium*

    <%= form.collection_radio_buttons(:medium_type, contact_mediums, 'value', 'label') do |b| %>
    <%= b.radio_button(class: "form-check-input") %> <%= b.label(class: "form-check-label") %>
    <% end %>

    Contact Duration

    <%= render(Form::HourMinuteDurationComponent.new( form: form, hour_value: duration_hours(@case_contact), minute_value: duration_minutes(@case_contact)) ) %>

    Notes

    <% if @contact_topics.empty? %>

    <% if current_user.casa_admin? %> Visit <%= link_to "Manage Case Contact Topics", edit_casa_org_path(current_organization, anchor: "case-contact-topics") %> to set your organization Court report topics. <% else %> Your organization has not set any Court Report Topics yet. Contact your admin to learn more. <% end %>

    <% else %> <% select_options = @contact_topics.map { |topic| [topic.question, topic.id] } %> <% if form.object.contact_topic_answers.any? %> <%= form.fields_for :contact_topic_answers do |topic_fields| %> <%= render("contact_topic_answer", form: topic_fields, select_options:) %> <% end %> <% end %>
    <% end %> <% if form.object.notes.present? || @contact_topics.empty? %>
    <%= form.label :notes, "Additional Notes", class: "form-label" %> <%= form.text_area( :notes, data: { action: "input->autosave#save" }, rows: 5, class: ["form-control"], ) %>
    <% end %>
    <% org_driving_reimbursement = current_organization.show_driving_reimbursement %> <% show_driving_reimbursement = org_driving_reimbursement && show_volunteer_reimbursement(@casa_cases) %> <% org_additional_expenses = current_organization.additional_expenses_enabled %> <% show_additional_expenses = org_additional_expenses && Pundit.policy(current_user, @case_contact).additional_expenses_allowed? %> <% if show_driving_reimbursement %>

    Reimbursement

    <% if Flipper.enabled?(:reimbursement_warning, current_organization) %>

    Volunteers are reimbursed at the federal mileage rate.
    Please note that there is a $35.00 per month cap per volunteer for your mileage. We aim to mail your reimbursement to you via check within 14-28 business days of your request for reimbursement.
    <% end %>
    <%= form.check_box(:want_driving_reimbursement, class: "form-check-input", data: { case_contact_form_target: "wantDrivingReimbursement", action: "click->case-contact-form#setReimbursementFormVisibility", } ) %> <%= form.label :want_driving_reimbursement, class: "form-check-label" do %>

    Request travel or other reimbursement

    <% end %>
    <%= form.label :miles_driven, class: "form-label" do %> Total Miles Driven* <% end %> <%= form.number_field(:miles_driven, min: "0", max: 10000, placeholder: "0", class: "form-control", data: { case_contact_form_target: "milesDriven" }, ) %>
    <%= form.label :volunteer_address, class: "form-label" do %> Mailing Address for Reimbursement Check* <% end %> <%= form.text_area(:volunteer_address, value: @case_contact.decorate.address_of_volunteer, disabled: @case_contact.address_field_disabled?, placeholder: "Enter mailing address", class: "form-control", data: { case_contact_form_target: "volunteerAddress" }, ) %> <% if @case_contact.address_field_disabled? %> <%= @case_contact.decorate.ambiguous_volunteer_address_message %> <% end %>
    <% if show_additional_expenses %>
    <% if form.object.additional_expenses.any? %> <%= form.fields_for :additional_expenses do |expense_fields| %> <%= render "shared/additional_expense_form", form: expense_fields %> <% end %> <% end %>
    <% end %>
    <% end %>
    <%= form.fields_for :metadata do |metadata_form| %> <%= metadata_form.check_box :create_another, class: "form-check-input" %> <%= metadata_form.label :create_another, class: "form-check-label d-inline align-bottom" do %> Create Another <% end %> <% end %>
    <%= button_tag type: "submit", class: "btn-sm main-btn primary-btn btn-hover" do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/case_contacts/index.html.erb ================================================
    <%= render "casa_cases/thank_you_modal" %>

    Case Contacts

    <%= link_to new_case_contact_path, class: "main-btn secondary-btn btn-sm btn-hover" do %> New Case Contact <% end %>
    <%= form_for_filterrific @filterrific, url: case_contacts_path, html: {class: "my-4 filter-form"}, remote: true, data: {turbo: true, turbo_frame: :case_contacts, turbo_action: :advance } do |f| %> <%= hidden_field_tag 'casa_case_id', params[:casa_case_id] %>
    Filter by
    <%= f.label :sorted_by %>
    <%= f.select(:sorted_by, @filterrific.select_options[:sorted_by], {}, {class: "filter-input"}) %>
    <%= f.check_box :no_drafts, class: "form-check-input case-contact-contact-type filter-input" %>
    <%= link_to("Reset filters", reset_filterrific_url, class: "btn-sm main-btn dark-btn-outline btn-hove", data: { turbo: true, turbo_frame: :case_contacts, turbo_action: :advance }) %>
    <% collapse_class = expand_filters? ? "collapse show" : "collapse" %>

    <%= f.label "Starting from", for: "filterrific_occurred_starting_at" %> <%= f.date_field(:occurred_starting_at, class: "filter-input") %>
    <%= f.label "Ending at", for: "filterrific_occurred_ending_at" %> <%= f.date_field(:occurred_ending_at, class: "filter-input") %>

    <% @current_organization_groups.each do |group| %>
    <%= group.name %>
    <% group.contact_types.each do |contact_type| %>
    <%= f.check_box :contact_type, {multiple: true, class: "form-check-input case-contact-contact-type filter-input"}, contact_type.id, nil %>
    <% end %>
    <% end %>

    Other filters

    <%= f.label :contact_medium %>
    <%= f.select(:contact_medium, options_from_collection_for_select(contact_mediums, "value", "label"), {include_blank: "Display all", class: "filter-input"}) %>
    <%= f.label :want_driving_reimbursement %>
    <%= f.select(:want_driving_reimbursement, @presenter.boolean_select_options, {include_blank: "Display all", class: "filter-input"}) %>
    <%= f.label :contact_made %>
    <%= f.select(:contact_made, @presenter.boolean_select_options, {include_blank: "Display all", class: "filter-input"}) %>
    <% end %> <%= turbo_frame_tag :case_contacts do %> <% @presenter.case_contacts.each do |casa_case_id, data| %>

    <%= @presenter.display_case_number(casa_case_id) %>

    <%= render partial: "case_contacts/case_contact", collection: data, as: :contact %>
    <% end %> <% if @presenter.case_contacts.empty? %> <% if params.key?(:casa_case_id) %>

    <%= @presenter.display_case_number(params[:casa_case_id].to_i) %>

    <%= params[:filterrific] ? "No case contacts have been found." : "You have no case contacts for this case. \ Please click New Case Contact button above to create a case contact for your youth!" %>
    <% else %>
    <%= params[:filterrific] ? "No case contacts have been found." : "You have no case contacts for this case. \ Please click New Case Contact button above to create a case contact for your youth!" %>
    <% end %> <% end %> <%== pagy_bootstrap_nav(@pagy) %> <% end %> ================================================ FILE: app/views/case_court_reports/_generate_docx.html.erb ================================================ <%= form_with url: generate_case_court_reports_path, local: false do |form| %> <% id = "generate-docx-report-modal" %> <%= render(Modal::OpenButtonComponent.new(target: id, klass: "btn generate-report-button")) do %> Word Document Logo

    Download Court Report as .docx

    The Court Report is pre-filled with information for your case. You can select among currently active cases assigned to you. The document is in Microsoft Word format (.docx).

    <% end %> <%= render(Modal::GroupComponent.new(id: id)) do |component| %> <% component.with_header(text: "Download Court Report as a .docx", id: id, klass: "content-1") %> <% component.with_body do %>

    To download a court report, choose an active case and specify the date range.

    <%= form.label :case_selection, "Case" %> <% select_options = @assigned_cases.map { |casa_case| casa_case.decorate.court_report_select_option } %> <% show_search = !current_user.volunteer? %> <% select_case_prompt = show_search ? "Search by volunteer name or case number" : "Select case number" %> <% select2_class = show_search ? " select2" : "" %>
    <%= select_tag :case_number, options_for_select(select_options), prompt: select_case_prompt, include_blank: false, id: "case-selection", class: "custom-select#{select2_class}", required: true, data: { dropdown_parent: "##{id}", width: "100%" } %>

    Case selection is required.

    <%= form.hidden_field :time_zone, id: "user-time-zone" %>
    <%= form.text_field :start_date, value: Time.zone.now.strftime(::DateHelper::RUBY_MONTH_DAY_YEAR_FORMAT), data: { provide: "datepicker", date_format: ::DateHelper::JQUERY_MONTH_DAY_YEAR_FORMAT }, class: "form-control" %>
    <%= form.text_field :end_date, value: Time.zone.now.strftime(::DateHelper::RUBY_MONTH_DAY_YEAR_FORMAT), data: { provide: "datepicker", date_format: ::DateHelper::JQUERY_MONTH_DAY_YEAR_FORMAT }, class: "form-control" %>
    <% end %> <% component.with_footer do %> <%= button_tag type: :submit, data: { button_name: "Generate Report" }, id: "btnGenerateReport", class: "main-btn primary-btn btn-hover btn-sm", onclick: "setTimeZone()" do %> Generate Report <% end %> <% end %> <% end %> <% end %> ================================================ FILE: app/views/case_court_reports/index.html.erb ================================================

    Generate Reports

    Court Reports
    <%= render "case_court_reports/generate_docx" %>
    ================================================ FILE: app/views/case_groups/_form.html.erb ================================================

    <% if case_group.persisted? %> Edit Case Group <% else %> New Case Group <% end %>

    <%= form_with model: case_group do |form| %> <%= render "shared/error_messages", resource: case_group %>
    <%= form.label :name %> <%= form.text_field :name, required: true %>
    <%= form.label :casa_case_ids, 'Cases' %> <%= form.select( :casa_case_ids, current_organization.casa_cases.map { |casa_case| [sanitize("#{casa_case.case_number} - #{volunteer_badge(casa_case, current_user)}"), casa_case.id] }, { include_hidden: false, autocomplete: "off" }, { class: "form-control", multiple: true, data: { "multiple-select-target": "select" } } ) %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/case_groups/edit.html.erb ================================================ <%= render 'case_groups/form', case_group: @case_group %> ================================================ FILE: app/views/case_groups/index.html.erb ================================================

    Case Groups

    Case Groups

    Case groups are used to bulk create court dates for all cases in a group. For example, if siblings attend the same court data you can create a case group for them and then use the <%= link_to 'Bulk Court Date', new_bulk_court_date_path %> form to create a court date for all of them.

    <%= link_to new_case_group_path, class: "btn-sm main-btn primary-btn btn-hover" do %> New Case Group <% end %>
    <% @case_groups.each do |case_group| %> <% end %>
    Name Case Numbers Updated At Actions
    <%= case_group.name %>
      <% case_group.casa_cases.each do |casa_case| %>
    • <%= link_to casa_case.case_number, casa_case_path(casa_case) %> <%= volunteer_badge(casa_case, current_user) %>
    • <% end %>
    <%= time_ago_in_words(case_group.updated_at) %> ago <%= link_to edit_case_group_path(case_group) do %>
    <% end %> <%= link_to 'Delete', case_group_path(case_group), class: 'btn btn-danger', method: :delete, data: { confirm: "Are you sure that you want to delete this case group?" } %>
    ================================================ FILE: app/views/case_groups/new.html.erb ================================================ <%= render 'case_groups/form', case_group: @case_group %> ================================================ FILE: app/views/checklist_items/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: [hearing_type, checklist_item], local: true) do |form| %>
    <%= render "/shared/error_messages", resource: checklist_item %>
    <%= form.label :category %> <%= form.text_field :category, class: "form-control", required: true %>
    <%= form.label :description %> <%= form.text_field :description, class: "form-control", required: true %>
    <%= form.check_box :mandatory, class: 'form-check-input' %> <%= form.label :mandatory, "Mandatory" %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %> <% end %>
    ================================================ FILE: app/views/checklist_items/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Edit this checklist item", hearing_type: @hearing_type, checklist_item: @checklist_item} %> ================================================ FILE: app/views/checklist_items/new.html.erb ================================================ <%= render partial: "form", locals: {title: "Add a new checklist item", hearing_type: @hearing_type, checklist_item: @checklist_item} %> ================================================ FILE: app/views/contact_topics/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: contact_topic, local: true) do |form| %> <%= form.hidden_field :casa_org_id %>
    <%= render "/shared/error_messages", resource: contact_topic %>
    <%= form.label :question, "Question" %> <%= form.text_field :question, class: "form-control", required: true %>
    <%= form.label :details, "Details?" %> <%= form.text_area :details, rows: 5, class: "form-control", required: true %>
    <%= form.check_box :active, class: 'form-check-input' %> <%= form.label :active, "Active?", class: 'form-check-label' %>
    <%= form.check_box :exclude_from_court_report, class: 'form-check-input' %> <%= form.label :exclude_from_court_report, "Exclude from Court Report?", class: 'form-check-label' %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/contact_topics/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Contact Topic", contact_topic: @contact_topic} %> ================================================ FILE: app/views/contact_topics/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Contact Topic", contact_topic: @contact_topic} %> ================================================ FILE: app/views/contact_type_groups/_form.html.erb ================================================

    <%= title %>

    Case contact groups are used to group case contact types. For example a Family group could contain the case contact types: parent, grandpa, aunt.

    <%= form_with(model: contact_type_group, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: contact_type_group %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control" %>
    <%= form.check_box :active, class: 'form-check-input' %> <%= form.label :active, "Active", class: 'form-check-label' %>
    <%= button_tag( type: "submit" , class: "btn-sm main-btn primary-btn btn-hover" ) do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/contact_type_groups/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Edit Contact Type Groups", contact_type_group: @contact_type_group} %> ================================================ FILE: app/views/contact_type_groups/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Contact Type Group", contact_type_group: @contact_type_group} %> ================================================ FILE: app/views/contact_types/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: contact_type, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: contact_type %>
    Case contact types are the types of people that can be contacted for a case contact. Some examples are parents, teachers, and social workers.
    <%= form.label :name, "Name" %> <%= form.text_field :name %>
    <%= form.label :contact_type_group, "Contact type group" %>
    <%= form.select :contact_type_group_id, set_group_options %>
    <%= form.check_box :active, class: 'form-check-input' %> <%= form.label :active, "Active", class: 'form-check-label' %>
    <%= button_tag( type: "submit" , class: "btn-sm main-btn primary-btn btn-hover" ) do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/contact_types/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Editing", contact_type: @contact_type} %> ================================================ FILE: app/views/contact_types/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Case Contact Type", contact_type: @contact_type} %> ================================================ FILE: app/views/court_dates/_fields.html.erb ================================================
    <%= form.label :date, "Add Court Date" %> <%= form.date_field :date, value: court_date.date&.to_date || Time.zone.now, class: "form-control" %>
    <%= form.label :court_report_due_date, "Add Court Report Due Date" %> <%= form.date_field :court_report_due_date, value: court_date.court_report_due_date&.to_date, class: "form-control" %>
    <%= form.label :judge_id, "Judge" %>
    <%= form.collection_select( :judge_id, Judge.for_organization(current_organization), :id, :name, {include_hidden: false, include_blank: "-Select Judge-"}, {class: "form-control"} ) %>
    <%= form.label :hearing_type_id, "Hearing type" %>
    <%= form.collection_select( :hearing_type_id, HearingType.active.for_organization(current_organization), :id, :name, {include_hidden: false, include_blank: "-Select Hearing Type-"}, {class: "form-control"} ) %>
    <%= render partial: "shared/court_order_list", locals: {casa_case: casa_case, siblings_casa_cases: nil, form: form, resource: 'court_date'} %>
    ================================================ FILE: app/views/court_dates/_form.html.erb ================================================
    <%= form_with(model: court_date, url: [casa_case, court_date], local: true, data: { controller: "court-order-form", nested_form_wrapper_selector_value: ".nested-form-wrapper" }) do |form| %> <%= render "/shared/error_messages", resource: court_date %>
    Case Number: <%= link_to casa_case.case_number, casa_case %>
    <%= render 'court_dates/fields', court_date: court_date, form: form, casa_case: casa_case %>
    <% end %>
    ================================================ FILE: app/views/court_dates/edit.html.erb ================================================

    Editing Court Date

    <%= render 'form', casa_case: @casa_case, court_date: @court_date %> ================================================ FILE: app/views/court_dates/new.html.erb ================================================

    New Court Date

    <%= render 'form', casa_case: @casa_case, court_date: @court_date %> ================================================ FILE: app/views/court_dates/show.html.erb ================================================

    Court Date

    Case Number:
    <%= link_to "#{@casa_case.case_number}", casa_case_path(@casa_case) %>
    Court Report Due Date:
    <%= I18n.l(@court_date.court_report_due_date, format: :full, default: "None") %>
    Judge:
    <%= @court_date.judge&.name || "None" %>
    Hearing Type:
    <%= @court_date.hearing_type&.name || "None" %>
    Court Orders:
    <% if @court_date.case_court_orders.any? %>
    <% @court_date.case_court_orders.each do |court_order| %> <% end %>
    Case Order Text Implementation Status
    <%= court_order.text %> <%= court_order.implementation_status&.humanize %>
    <% else %>

    There are no court orders associated with this court date.

    <% end %>
    <%= link_to casa_case_court_date_path(@casa_case, @court_date, format: :docx), class: "btn-sm main-btn primary-btn btn-hover" do %> Download Report (.docx) <% end %> <% if policy(:court_date).destroy? && @court_date.date > Time.now %> <%= link_to [@casa_case, @court_date], method: :delete, data: { confirm: 'Are you sure?' },class: "btn-sm main-btn danger-btn-outline btn-hover ms-auto" do %> Delete Future Court Date <% end %> <% end %>
    ================================================ FILE: app/views/custom_org_links/_form.html.erb ================================================

    <%= title %>

    <%= form_with model: @custom_org_link, local: true do |form| %>
    <%= render "/shared/error_messages", resource: @custom_org_link %>
    <%= form.label :name, "Display Text" %> <%= form.text_field :text, class: "form-control", required: true %>
    <%= form.label :name, "URL" %> <%= form.text_field :url, class: "form-control", required: true %>
    <%= form.check_box :active, as: :boolean, class: 'form-check-input' %> <%= form.label :active, "Active?", class: 'form-check-label' %>
    <%= button_tag type: "submit", class: "btn-sm main-btn primary-btn btn-hover" do %> <%= action %> <% end %>
    <% end %>
    ================================================ FILE: app/views/custom_org_links/edit.html.erb ================================================ <%= render partial: "form", locals: { title: "Edit Custom Link", action: 'Update' } %> ================================================ FILE: app/views/custom_org_links/new.html.erb ================================================ <%= render partial: "form", locals: { title: "New Custom Link", action: 'Create' } %> ================================================ FILE: app/views/devise/invitations/edit.html.erb ================================================

    Set password

    <%= form_with(model: resource, as: resource_name, url: invitation_path(resource_name), local: true, html: {method: :put}) do |f| %> <%= render "/shared/error_messages", resource: resource %> <%= f.hidden_field :invitation_token, readonly: true %> <% if f.object.class.require_password_on_accepting %>
    <%= f.label :password %> <%= f.password_field :password, placeholder: "Password", required: true %>
    <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, placeholder: "Password Confirmation", required: true %>
    <% end %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm" ) do %> Set my password <% end %>
    <% end %>
    ================================================ FILE: app/views/devise/invitations/new.html.erb ================================================

    Send invitation

    <%= form_with(model: resource, as: resource_name, url: invitation_path(resource_name), html: {method: :post}) do |f| %> <%= render "/shared/error_messages", resource: resource %> <% resource.class.invite_key_fields.each do |field| -%>
    <%= f.label field %> <%= f.text_field field, placeholder: field %>
    <% end -%>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm" ) do %> Send an invitation <% end %>
    <% end %>
    ================================================ FILE: app/views/devise/mailer/confirmation_instructions.html.erb ================================================

    Click here to confirm your email.

    <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

    If you weren't expecting this email, please disregard.

    ================================================ FILE: app/views/devise/mailer/email_changed.html.erb ================================================ <% if @resource.try(:unconfirmed_email?) %>

    Your CASA account's email has been updated to <%= @resource.unconfirmed_email %>.

    <% else %>

    Your CASA account's email has been updated to <%= @resource.email %>.

    <% end %> ================================================ FILE: app/views/devise/mailer/invitation_instructions.html.erb ================================================ <% if @resource.is_a?(User) %> <% else %> <% end %>
    A <%= @resource.casa_org.display_name %>’s County <%= @resource.type %> console account has been created for you. This console is for logging the time you spend and actions you take on your CASA case. You can log activity with your CASA youth, their family members, their foster family or placement, the DSS worker, your Case Supervisor and others associated with your CASA case (such as teachers and therapists).
    Your console account is associated with this email. If this is not the correct email to use, please stop here and contact your Case Supervisor to change the email address. If you are ready to get started, please set your password. This is the first step to accessing your new <%= @resource.type %> account.
    A CASA console admin account has been created for you. Your console account is associated with this email. If you are ready to get started, please set your password. This is the first step to accessing your new account.

    <%= link_to "Set your password", accept_invitation_url(@resource, invitation_token: @token) %>

    This invitation will expire on <%= I18n.l(@resource.invitation_due_at, format: :full, default: nil) %> <% if @resource.is_a?(AllCasaAdmin) %> (one week). <% elsif @resource.is_a?(Supervisor) || @resource.is_a?(CasaAdmin) %> (two weeks). <% elsif @resource.is_a?(Volunteer) %> (one year). <% end %>
    ================================================ FILE: app/views/devise/mailer/invitation_instructions.text.erb ================================================ <% if @resource.is_a?(User) %> A <%= @resource.casa_org.display_name %>’s County <%= @resource.type %> console account has been created for you. This console is for logging the time you spend and actions you take on your CASA case. You can log activity with your CASA youth, their family members, their foster family or placement, the DSS worker, your Case Supervisor and others associated with your CASA case (such as teachers and therapists). Your console account is associated with this email. If this is not the correct email to use, please stop here and contact your Case Supervisor to change the email address. If you are ready to get started, please set your password. This is the first step to accessing your new <%= @resource.type %> account. <% else %> A CASA console admin account has been created for you. Your console account is associated with this email. If you are ready to get started, please set your password. This is the first step to accessing your new account. <%= "This invitation will be due in #{I18n.l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')}." %> <% end %> <%= link_to "Set your password", accept_invitation_url(@resource, invitation_token: @token) %> ================================================ FILE: app/views/devise/mailer/password_change.html.erb ================================================

    Hello <%= @resource.email %>!

    We're contacting you to notify you that your password has been changed.

    ================================================ FILE: app/views/devise/mailer/reset_password_instructions.html.erb ================================================ Actionable emails e.g. reset password
    Court Appointed Special Advocate (CASA)
    Logo
    If you've lost your password or wish to reset it, click the button below. If you didn’t request this, you can safely ignore this email.
    Your password must be reset within the next <%= User.reset_password_within.inspect %>. If you do not reset your password within the next <%= User.reset_password_within.inspect %>, request a new password reset message from the login page.
    " target="_blank" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #00447c; margin: 0; border-color: #00447c; border-style: solid; border-width: 10px 20px;">Reset your password
    ================================================ FILE: app/views/devise/mailer/unlock_instructions.html.erb ================================================

    Hello <%= @resource.email %>!

    Your account has been locked due to an excessive number of unsuccessful sign in attempts.

    Click the link below to unlock your account:

    <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

    ================================================ FILE: app/views/devise/passwords/edit.html.erb ================================================

    Change your password

    <%= form_with(model: resource, as: resource_name, url: password_path(resource_name), method: :put) do |f| %> <%= render "/shared/error_messages", resource: resource %> <%= f.hidden_field :reset_password_token %>
    <%= f.label :password, "New password" %> <% if @minimum_password_length %> (<%= @minimum_password_length %> characters minimum) <% end %> <%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "password-new", placeholder: "New Password", required: true, minlength: @minimum_password_length %>
    <%= f.label :password_confirmation, "Confirm new password" %> <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "password-confirmation", placeholder: "Confirm Password", required: true, minlength: @minimum_password_length %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm submit-password" ) do %> Change my password <% end %>
    <% end %> <%= render "devise/shared/links" %>
    ================================================ FILE: app/views/devise/passwords/new.html.erb ================================================

    Forgot your password?

    <%= form_with(model: resource, as: resource_name, url: password_path(resource_name), html: {method: :post}) do |f| %> <%= render "/shared/error_messages", resource: resource %>

    Please enter email or phone number to receive reset instructions.

    <%= f.label :email %> <%= f.email_field :email, placeholder: "Email", autofocus: true, autocomplete: "email" %>
    <%= f.label :phone_number %> <%= f.text_field :phone_number, placeholder: "Phone Number", autofocus: true, autocomplete: "phone number" %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm" ) do %> Send me reset password instructions <% end %>
    <% end %> <%= render "devise/shared/links" %>
    ================================================ FILE: app/views/devise/sessions/new.html.erb ================================================
    Sign In

    Not registered but want to join? casa@rubyforgood.org

    <%= form_with(model: resource, as: resource_name, url: session_path(resource_name)) do |f| %>
    <% if resource.errors.any? %> <%= render "/shared/error_messages", resource: resource %> <% else %> <%= render "layouts/flash_messages" %> <% end %>
    <%= f.label "Email", for: "email" %> <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: "Email", class: "form-control", required: true, id:"email" %>
    <%= f.label :password %> <%= f.password_field :password, autocomplete: "current-password", placeholder: "Password", class: "form-control", required: true %>
    <%= link_to new_password_path(resource_name), class: "hover-underline" do %> Forgot your password? <% end %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover w-100 text-center", id: "log-in" ) do %> Log In <% end %>
    <% end %>
    ================================================ FILE: app/views/devise/shared/_links.html.erb ================================================ <%- if controller_name != 'sessions' %> <%= link_to new_session_path(resource_name), class: "btn-sm main-btn info-btn btn-hover" do %> Log in <% end %>
    <% end %> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> <%= link_to "Sign up", new_registration_path(resource_name) %>
    <% end %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%= link_to "Forgot your password?", new_password_path(resource_name), class: 'btn-sm main-btn primary-btn-outline btn-hover' %>
    <% end %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> Want to add your CASA? Email: <%= mail_to "casa@rubyforgood.org" %>
    <% end %> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
    <% end %> <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
    <% end %> <%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
    <% end %> <% end %> ================================================ FILE: app/views/emancipation_checklists/index.html.erb ================================================

    Emancipation Checklists

    <% @casa_transitioning_cases.each do |casa_case| %> <% end %>
    Case Number
    Checklist
    <%= link_to(casa_case.case_number, casa_case, title: "Details") %> <%= link_to "Checklist", casa_case_emancipation_path(casa_case) %>
    ================================================ FILE: app/views/emancipations/download.html.erb ================================================ <% @emancipation_form_data.each do |category| %> <% if category.emancipation_options.any? %> <% end %> <% end %>
    <%= emancipation_category_checkbox_checked_download(@current_case, category) %> <%= category.name %>
    <% category.emancipation_options.each do |option| %> <% end %>
    <% if category.mutually_exclusive? %> <%= emancipation_option_radio_checked_download(@current_case, option) %> <% else %> <%= emancipation_option_checkbox_checked_download(@current_case, option) %> <% end %> <%= option.name %>
    ================================================ FILE: app/views/emancipations/show.html.erb ================================================

    Emancipation Checklist

    <%= link_to @current_case.case_number, casa_case_path(@current_case) %>
    <%= link_to casa_case_emancipation_path(@current_case, format: :docx), class: "main-btn primary-btn btn-sm btn-hover" do %> Download Checklist <% end %>
    <% @emancipation_form_data.each do |category| %>
    ">
    <%= tag.input( type: "checkbox", class: "emancipation-category-check-box form-check-input", value: category.id, checked: emancipation_category_checkbox_checked(@current_case, category)) %>
    <%= category.name %>
    <% if category.emancipation_options.count > 0 %> <%= emancipation_category_collapse_icon(@current_case, category) %> <% end %>
    <% category.emancipation_options.each do |option| %>
    <% if category.mutually_exclusive %> <%= tag.input( type: "radio", id: "O#{option.id}", class: "emancipation-radio-button", name: "C#{category.id}", value: option.id, checked: emancipation_option_checkbox_checked(@current_case, option)) %> <% else %> <%= tag.input( type: "checkbox", id: "O#{option.id}", class: "emancipation-option-check-box", value: option.id, checked: emancipation_option_checkbox_checked(@current_case, option)) %> <% end %>
    <% end %>
    <% end %>
    ================================================ FILE: app/views/error/index.html.erb ================================================

    Intentional Error Test

    Click the button below to trigger an intentional test exception.

    <%= button_to "Trigger Exception", error_path, method: :post, class: "btn btn-danger", data: { turbo: false } %>
    ================================================ FILE: app/views/fund_request_mailer/send_request.html.erb ================================================ Fund Request: see attached PDF

    Submitter email: <%= @inputs["submitter_email"] %>
    Youth name: <%= @inputs["youth_name"] %>
    Payment amount: <%= @inputs["payment_amount"] %>
    Deadline: <%= @inputs["deadline"] %>
    Request purpose: <%= @inputs["request_purpose"] %>
    Payee name: <%= @inputs["payee_name"] %>
    Requested by and relationship: <%= @inputs["requested_by_and_relationship"] %>
    Other funding source sought: <%= @inputs["other_funding_source_sought"] %>
    Impact: <%= @inputs["impact"] %>
    Extra information: <%= @inputs["extra_information"] %>
    ================================================ FILE: app/views/fund_requests/new.html.erb ================================================

    New Fund Request

    <%= form_with(model: @fund_request, local: true, url: casa_case_fund_request_path(@casa_case), method: :post) do |form| %> <% if @fund_request.errors.any? %> <% @fund_request.errors.full_messages.each do |msg| %>

    <%= msg %>

    <% end %> <% end %>
    <%= form.label :submitter_email, "Your email" %> <%= form.email_field :submitter_email, class: "form-control", required: false, value: current_user.email %>
    <%= form.label :youth_name, "Name or case number of youth" %> <%= form.text_field :youth_name, class: "form-control", required: false, value: @casa_case&.case_number %>
    <%= form.label :payment_amount, "Amount of payment*" %> <%= form.text_field :payment_amount, class: "form-control", required: false %>
    <%= form.label :deadline, "Deadline / date needed" %> <%= form.text_field :deadline, class: "form-control", required: false %>
    <%= form.label :request_purpose, "Request is for..." %> <%= form.text_area :request_purpose, class: "form-control", required: false %>
    <%= form.label :payee_name, "Name of payee**" %> <%= form.text_field :payee_name, class: "form-control", required: false %>
    <%= form.label :requested_by_and_relationship, "Requested by & relationship to youth" %> <%= form.text_field :requested_by_and_relationship, class: "form-control", required: false, value: "#{current_user.display_name} CASA Volunteer" %>
    <%= form.label :other_funding_source_sought, "Other source of funding available/sought please include status of these requests, if applicable." %> <%= form.text_area :other_funding_source_sought, class: "form-control", required: false %>
    <%= form.label :impact, "How will this funding positively impact the personal goals or aspirations of the youth? If this is for emergency funding, please share any support that is or can be in place to maintain stability or alleviate the emergency moving forward. If funding is for a program or a service, please describe how this will support the youth in the short or long-term." %> <%= form.text_area :impact, class: "form-control", required: false %>
    <%= form.label :extra_information, "Please use this space if it is necessary/helpful to provide additional information that will assist us in understanding the need and making a decision." %> <%= form.text_area :extra_information, class: "form-control", required: false %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover", required: false) do %> Submit Fund Request <% end %> <% end %>
    * Please provide all documentation available for the requested expense (brochure, receipt, estimate, invoice, proof of online enrollment information, etc.). If additional funds have been secured, please provide evidence of the funding commitment (an email, a partial payment on a bill, etc).
    **Payment will be made directly to the vendor for all services. If the payment must be made directly to the youth, please be sure to include why this is the preferred method of payment.
    ================================================ FILE: app/views/health/index.html.erb ================================================
    ================================================ FILE: app/views/hearing_types/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: hearing_type, local: true) do |form| %> <%= render "/shared/error_messages", resource: hearing_type %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= form.check_box :active, class: 'form-check-input' %> <%= form.label :active, class: 'form-check-label' %>
    <% if !hearing_type.id.nil? %>

    Checklist

    <% hearing_type.checklist_items.each do |checklist_item| %> <% end %>
    Description Category Mandatory Actions
    <%= checklist_item.description %> <%= checklist_item.category %> <%= checklist_item.mandatory ? "Yes" : "Optional" %> <%= link_to edit_hearing_type_checklist_item_path(hearing_type, checklist_item) do %>
    <% end %> <%= link_to hearing_type_checklist_item_path(hearing_type, checklist_item), method: :delete, data: { confirm: "Are you sure that you want to delete this checklist item?" } do %>
    <% end %>
    <% end %>
    <%= button_tag( type: "submit", class: "btn-sm main-btn primary-btn btn-hover" ) do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/hearing_types/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Edit", hearing_type: @hearing_type} %> ================================================ FILE: app/views/hearing_types/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Hearing Type", hearing_type: @hearing_type} %> ================================================ FILE: app/views/imports/_cases.html.erb ================================================

    1. <%= link_to "/casa_cases.csv", format: :csv, download: "casa_cases.csv" do %> Download and reference example Case CSV file <% end %>

    2. Upload your CSV file

    <%= form_with(url: imports_path, local: :true) do |f| %> <%= f.hidden_field :import_type, value: "casa_case" %> <%= f.file_field :file, id: 'case-file', accept: 'text/csv', class: 'form-control mt-4', type: 'file', style: "margin: auto;" %> <%= button_tag id: "case-import-button", class: "main-btn primary-btn btn-hover pull-right", disabled: true, data: {disable_with: " Importing File"} do %> Import Cases CSV <% end %> <% end %>
    ================================================ FILE: app/views/imports/_csv_error_modal.html.erb ================================================ ================================================ FILE: app/views/imports/_sms_opt_in_modal.html.erb ================================================ ================================================ FILE: app/views/imports/_supervisors.html.erb ================================================

    1. <%= link_to "/supervisors.csv", format: :csv, download: "supervisors.csv" do %> Download and reference example Supervisor CSV file <% end %>

    2. Upload your CSV file

    <%= form_with(url: imports_path, local: :true, id: "supervisor-import-form") do |f| %> <%= f.hidden_field :import_type, value: "supervisor" %> <%= f.file_field :file, id: 'supervisor-file', accept: 'text/csv', class: 'form-control mt-4', type: 'file', style: "margin: auto;" %> <%= render "sms_opt_in_modal", { form: f } if @sms_opt_in_warning == "supervisor" %> <%= button_tag id: "supervisor-import-button", class: "main-btn primary-btn btn-hover pull-right", disabled: true, data: { disable_with: "
    Importing File"} do %> Import Supervisors CSV <% end %> <% end %>
    ================================================ FILE: app/views/imports/_volunteers.html.erb ================================================

    1. <%= link_to "/volunteers.csv", format: :csv, download: "volunteers.csv" do %> Download and reference example Volunteer CSV file <% end %>

    2. Upload your CSV file

    <%= form_with(url: imports_path, local: :true, id: "volunteer-import-form") do |f| %> <%= f.hidden_field :import_type, value: "volunteer" %> <%= f.hidden_field :sms_opt_in, value: false %> <%= f.file_field :file, id: 'volunteer-file', accept: 'text/csv', class: 'form-control mt-4', type: 'file', style: "margin: auto;" %> <%= render "sms_opt_in_modal", { form: f } if @sms_opt_in_warning == "volunteer" %> <%= button_tag id: "volunteer-import-button", class: "main-btn primary-btn btn-hover pull-right", disabled: true, data: { disable_with: "
    Importing File" } do %> Import Volunteers CSV <% end %> <% end %>
    ================================================ FILE: app/views/imports/index.html.erb ================================================

    Imports

    System Imports

    You should import files in the following order:

    1. Volunteers
    2. Supervisors
    3. Cases
    <%= content_tag :div, role: "tabpanel", id: "volunteer", class: [ "tab-pane", "fade", ("show" if @import_type == "volunteer"), ("active" if @import_type == "volunteer") ].compact, aria: {labelledby: "volunteer-tab"} do %>
    <%= render "volunteers" %> <%- end %> <%= content_tag :div, role: "tabpanel", id: "supervisor", class: [ "tab-pane", "fade", ("show" if @import_type == "supervisor"), ("active" if @import_type == "supervisor") ].compact, aria: {labelledby: "supervisor-tab"} do %>
    <%= render "supervisors" %> <%- end %> <%= content_tag :div, role: "tabpanel", id: "casa-case", class: [ "tab-pane", "fade", ("show" if @import_type == "casa_case"), ("active" if @import_type == "casa_case") ].compact, aria: {labelledby: "casa-case-tab"} do %>
    <%= render "cases" %> <%- end %>

    <%= render "csv_error_modal", {import_error: @import_error} if @import_error %>
    ================================================ FILE: app/views/judges/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: judge, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: judge %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= form.check_box :active, class: 'form-check-input' %> <%= form.label :active, "Active?", class: 'form-check-label' %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/judges/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Judge", judge: @judge} %> ================================================ FILE: app/views/judges/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Judge", judge: @judge} %> ================================================ FILE: app/views/languages/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: language, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: language %>
    A list of languages volunteers can add to their profile to let supervisors and admins know they can speak the language.

    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/languages/edit.html.erb ================================================ <%= render partial: "form", locals: { title: "Edit Language", language: @language } %> ================================================ FILE: app/views/languages/new.html.erb ================================================ <%= render partial: "form", locals: { title: "New Language", language: @language } %> ================================================ FILE: app/views/layouts/_all_casa_admin_sidebar.html.erb ================================================
    ================================================ FILE: app/views/layouts/_banner.html.erb ================================================ <% if @active_banner %>
    <%= @active_banner.content %>
    Dismiss
    <% end %> ================================================ FILE: app/views/layouts/_flash_messages.html.erb ================================================
    <% flash.delete(:timedout).each do |key, value| %> <% end %>
    ================================================ FILE: app/views/layouts/_header.html.erb ================================================
    <%= render 'layouts/banner' %> <% if current_user != true_user %> <%= link_to stop_impersonating_volunteers_path, method: :post, class: "pt-4 pb-4 bg-danger", style: "padding-left: 15px; display: block;" do %> You (<%= true_user.display_name %>) are signed in as <%= current_user.display_name %>. Click here to stop impersonating. <% end %> <% end %>
    <%= link_to notifications_path do %> <% end %>
    <%= render 'layouts/flash_messages' %> ================================================ FILE: app/views/layouts/_login_header.html.erb ================================================
    ================================================ FILE: app/views/layouts/_mobile_navbar.html.erb ================================================ ================================================ FILE: app/views/layouts/_sidebar.html.erb ================================================ ================================================ FILE: app/views/layouts/action_text/contents/_content.html.erb ================================================
    <%= yield -%>
    ================================================ FILE: app/views/layouts/application.html.erb ================================================ CASA Volunteer Tracking <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= og_tag :title, content: "CASA Volunteer Tracking" %> <%= og_tag :description, content: "Volunteer activity tracking for CASA volunteers, supervisors, and administrators." %> <%= og_tag :url, content: root_url %> <%= og_tag :image, content: image_url('login.jpg') %> <%= render 'shared/favicons' %> <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <% if all_casa_admin_signed_in? %> <%= javascript_include_tag "all_casa_admin", "data-turbo-track": "reload", defer: true %> <% end %> <% if all_casa_admin_signed_in? %> <%= render 'layouts/all_casa_admin_sidebar' %> <% elsif signed_in? %> <%= render 'layouts/sidebar' %> <% end %>
    <% if all_casa_admin_signed_in? %> <% elsif signed_in? %> <%= render 'layouts/header' %> <% end %>
    <%= yield %>
    <%= render "layouts/components/notifier" %>
    ================================================ FILE: app/views/layouts/components/_notifier.html.erb ================================================
    ================================================ FILE: app/views/layouts/devise.html.erb ================================================ CASA Volunteer Tracker <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <% if all_casa_admin_signed_in? %> <%= javascript_include_tag "all_casa_admin", "data-turbo-track": "reload", defer: true %> <% end %>

    Welcome Back

    Sign in to your Existing account to continue

    <%= yield %>
    ================================================ FILE: app/views/layouts/footers/_logged_in.html.erb ================================================ ================================================ FILE: app/views/layouts/footers/_not_logged_in.html.erb ================================================ ================================================ FILE: app/views/layouts/fund_layout.html.erb ================================================ <%= yield %> ================================================ FILE: app/views/layouts/mailer.html.erb ================================================ Actionable emails e.g. reset password
    Court Appointed Special Advocate (CASA) / <%= @casa_organization&.name %>
    <%= yield %>
    ================================================ FILE: app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: app/views/learning_hour_topics/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: learning_hour_topic, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: learning_hour_topic %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/learning_hour_topics/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Learning Topic", learning_hour_topic: @learning_hour_topic} %> ================================================ FILE: app/views/learning_hour_topics/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Learning Topic", learning_hour_topic: @learning_hour_topic} %> ================================================ FILE: app/views/learning_hour_types/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: learning_hour_type, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: learning_hour_type %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= form.check_box :active, class: 'form-check-input' %> <%= form.label :active, "Active?", class: 'form-check-label' %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/learning_hour_types/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Type of Learning", learning_hour_type: @learning_hour_type} %> ================================================ FILE: app/views/learning_hour_types/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Type of Learning", learning_hour_type: @learning_hour_type} %> ================================================ FILE: app/views/learning_hours/_confirm_note.html.erb ================================================ ================================================ FILE: app/views/learning_hours/_form.html.erb ================================================ <%= form_with(model: learning_hour, url: learning_hour.persisted? ? learning_hour_path(learning_hour) : learning_hours_path, local: true, html: { id: "learning-hours-form"}) do |form| %>
    <%= render "/shared/error_messages", resource: learning_hour %>
    <%= form.hidden_field :user_id, value: current_user.id %>
    <%= form.label :name, "Learning Hours Title" %> <%= form.text_field :name, placeholder: "-- Enter a title --", value: learning_hour.name, class:"mr-5", required: true %>
    <%= form.label :learning_hour_type_id, "Type of Learning" %>
    <%= form.collection_select :learning_hour_type_id, LearningHourType.for_organization(current_user.casa_org).active, :id, :name, prompt: "Select learning type", value: learning_hour.learning_hour_type_id %>
    <% if current_user.casa_org.learning_topic_active %>
    <%= form.label :learning_hour_topic_id, "Learning Topic" %>
    <%= form.collection_select :learning_hour_topic_id, LearningHourTopic.for_organization(current_user.casa_org), :id, :name, prompt: "Select learning topic", value: learning_hour.learning_hour_topic_id %>
    <% end %>
    Learning Duration
    <%= render(Form::HourMinuteDurationComponent.new(form: form, hour_value: learning_hour.duration_hours, minute_value: learning_hour.duration_minutes)) %>
    <%= form.label :occurred_at, "Occurred On" %>:
    <% occurred_at = learning_hour.occurred_at || Date.today %>
    <%= form.date_field :occurred_at, value: occurred_at.to_date, class: "form-control label-font-weight" %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm wide_button" ) do %> <% if learning_hour.persisted? %> Update Learning Hours Entry <% else %> Create New Learning Hours Entry <% end %> <% end %>
    <% end %> ================================================ FILE: app/views/learning_hours/_learning_hours_table.html.erb ================================================
    <% if current_user.casa_org.learning_topic_active %> <% end %> <% learning_hours.order(occurred_at: :desc).each do |learning_hour| %> <% if current_user.casa_org.learning_topic_active %> <% end %> <% end %>
    Title Learning TypeLearning TopicDate Time Spent
    <% if current_user.volunteer? %> <%= link_to learning_hour.name, learning_hour_path(id: learning_hour.id) %> <% else %> <%= learning_hour.name %> <% end %> <%= learning_hour.learning_hour_type.name %> <%= learning_hour.learning_hour_topic&.name %> <%= learning_hour.occurred_at.strftime("%B %-d, %Y") %> <% if (learning_hour.duration_hours > 0) %> <%= learning_hour.duration_hours %> hr <% end %> <%= learning_hour.duration_minutes %> min
    ================================================ FILE: app/views/learning_hours/_supervisor_admin_learning_hours.html.erb ================================================

    Learning Hours

    <% @learning_hours.each do |learning_hour| %> <% end %>
    Volunteer
    Time Completed YTD
    <%= link_to(learning_hour.display_name, learning_hours_volunteer_path(learning_hour.user_id)) %> <%= format_time(learning_hour.total_time_spent) %>
    ================================================ FILE: app/views/learning_hours/_volunteer_learning_hours.html.erb ================================================

    Learning Hours

    <%= link_to new_learning_hour_path, class: "btn-sm main-btn primary-btn btn-hover" do %> Record Learning Hours <% end %>
    <%= render "learning_hours_table", learning_hours: %> ================================================ FILE: app/views/learning_hours/edit.html.erb ================================================

    Edit Learning Hours

    <%= render 'form', learning_hour: @learning_hour, form_type: 'edit' %>
    ================================================ FILE: app/views/learning_hours/index.html.erb ================================================ <% if current_user.volunteer? %> <%= render "volunteer_learning_hours", learning_hours: @learning_hours %> <% else %> <%= render "supervisor_admin_learning_hours" %> <% end %> ================================================ FILE: app/views/learning_hours/new.html.erb ================================================

    Record Learning Hours

    <%= render 'form', learning_hour: @learning_hour, form_type: 'new' %>
    ================================================ FILE: app/views/learning_hours/show.html.erb ================================================

    <%= @learning_hour.name %>

    Title Learning Type Date Time Spent Action
    <%= @learning_hour.name %> <%= @learning_hour.learning_hour_type.name %> <%= I18n.l(@learning_hour.occurred_at, format: :full) %> <% if (@learning_hour.duration_hours > 0) %> <%= @learning_hour.duration_hours %> hr <% end %> <%= @learning_hour.duration_minutes %> min <%= link_to edit_learning_hour_path, class: "text-primary" do %>Edit<% end %> | <%= link_to learning_hour_path, class: "text-danger", method: :delete, data: { confirm: "Are you sure to delete this learning record?" } do %>Delete<% end %>
    ================================================ FILE: app/views/learning_hours/volunteers/show.html.erb ================================================

    <%= @volunteer.display_name %>'s Learning Hours

    <%= render "learning_hours/learning_hours_table", learning_hours: @learning_hours %> ================================================ FILE: app/views/learning_hours_mailer/learning_hours_report_email.html.erb ================================================ Actionable emails e.g. reset password

    Learning hours have been added this week!

    Please see the attached full report or export the full report <%= link_to "here", reports_url %>

    ================================================ FILE: app/views/mileage_rates/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: mileage_rate, local: true, id: :new_mileage_rate) do |form| %>
    <%= render "/shared/error_messages", resource: mileage_rate %>
    <%= form.label :effective_date, 'Effective date' %> <%= form.date_field :effective_date, value: mileage_rate.effective_date, class: "form-control" %>
    <%= form.label :amount, 'Amount' %> <%= form.number_field :amount, required: true, placeholder: '0.0', min: 0, step: 0.01, class: "form-control", aria: { label: "Amount in US Dollars" } %>
    <%= form.check_box :is_active, class: 'form-check-input' %> <%= form.label :is_active, "Currently active?", class: "form-check-label" %>
    <%= form.submit "Save Mileage Rate", class: "main-btn primary-btn btn-hover" %>
    <% end %>
    ================================================ FILE: app/views/mileage_rates/edit.html.erb ================================================ <%= render partial: "form", locals: { mileage_rate: @mileage_rate, form_url: mileage_rate_path(@mileage_rate), title: "Edit Milage Rate" } %> ================================================ FILE: app/views/mileage_rates/index.html.erb ================================================

    Mileage Rates

    <% @mileage_rates.each do |mileage_rate| %> <% end %>
    Effective date
    Amount
    Active?
    Actions
    <%= I18n.l(mileage_rate.effective_date, format: :full, default: :nil) %> <%= number_to_currency(mileage_rate.amount) %> <%= mileage_rate.is_active? ? 'Yes' : 'No' %> <%= link_to edit_mileage_rate_path(mileage_rate) do %>
    <% end %>
    ================================================ FILE: app/views/mileage_rates/new.html.erb ================================================ <%= render partial: "form", locals: { mileage_rate: @mileage_rate, form_url: mileage_rates_path, title: "New Mileage Rate" } %> ================================================ FILE: app/views/notes/edit.html.erb ================================================ <%= form_with(model: @note, local: true, url: volunteer_note_path(@volunteer, @note)) do |form| %>

    Update Volunteer Note

    <%= form.text_area :content, rows: 5, placeholder: "Enter a note regarding the volunteer. These notes are only visible to CASA administrators and supervisors.", class: "form-control" %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm", id: "note-submit" ) do %> Update Note <% end %> <% end %> ================================================ FILE: app/views/notifications/_notification.html.erb ================================================
    <%= notification_icon(notification) %>
    <%= notification.title %>
    <%= time_ago_in_words(notification.created_at) %> ago
    <%= simple_format notification.message %>
    ================================================ FILE: app/views/notifications/_patch_notes.html.erb ================================================

    Patch Notes

    <%= time_ago_in_words(deploy_time) %> ago
    <% patch_notes.each do |type, notes_per_type| %>
    <%= type %>
      <% notes_per_type.each do |note_text| %>
    • <%= simple_format note_text %>
    • <% end %>
    <% end %>
    ================================================ FILE: app/views/notifications/index.html.erb ================================================

    Notifications

    <% notifications_after_and_including_deploy(@notifications).each do | notification | %> <%= render NotificationComponent.new(notification: notification) %> <% end %> <% unless (@deploy_time.nil? or @patch_notes.empty?) %> <%= render partial: "patch_notes", locals: {deploy_time: @deploy_time, patch_notes: patch_notes_as_hash_keyed_by_type_name(@patch_notes)} %> <% end %> <% notifications_before_deploy(@notifications).each do | notification | %> <%= render NotificationComponent.new(notification: notification) %> <% end %> <% if @notifications.empty? and (@patch_notes.empty? or @deploy_time.nil?) %>
    You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.
    <% end %>
    <% @notifications.each do |notification| %> <% if read_when_seen_notifications.include? notification.type %> <% notification.mark_as_read %> <% end %> <% end %> ================================================ FILE: app/views/other_duties/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: other_duty, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: other_duty %>
    <%= form.label :occurred_at, "Occurred On" %> <% occurred_at = @other_duty.occurred_at || Time.zone.now %> <%= form.date_field :occurred_at, value: occurred_at.to_date %>
    <%= form.number_field :duration_hours, min: 0, size: "10", value: duration_hours(other_duty), required: true %>   hour(s)
    <%= form.number_field :duration_minutes, min: 0, size: "10", value: duration_minutes(other_duty), required: true %>   minute(s)
    <%= form.label :notes, "Enter Notes *" %> <%= form.text_area :notes, rows: 5, placeholder: "Enter notes here", class: "cc-italic form-control", required: true %>
    <%= button_tag( type: "submit", class: "btn-sm main-btn primary-btn btn-hover" ) do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/other_duties/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Editing Duty", other_duty: @other_duty} %> ================================================ FILE: app/views/other_duties/index.html.erb ================================================

    Other Duties

    <% if current_user.volunteer? %> <%= link_to new_other_duty_path, class: "main-btn btn-sm primary-btn btn-hover" do %> New Duty <% end %> <% end %>
    <% if @volunteer_duties.empty? %> There are no duties to display! <% else %> <% @volunteer_duties.each do |volunteer| %> <% if volunteer[:other_duties].any? %> <% unless current_user.id == volunteer[:volunteer].id %> <%= link_to(volunteer[:volunteer].display_name, volunteer_path(volunteer[:volunteer].id)) %> <% end %>
    <% volunteer[:other_duties].decorate.each do |duty| %> <% if current_user.volunteer? %> <% end %> <% end %>
    Duties Occurred Created Duration Title
    <%= I18n.l(duty.occurred_at, format: :full, default: nil) %> <%= I18n.l(duty.created_at, format: :full, default: nil) %> <%= duty.duration_in_minutes %> <%= duty.truncate_notes %><%= link_to "Edit", edit_other_duty_path(duty) %>
    <% end %> <% end %> <% end %>
    ================================================ FILE: app/views/other_duties/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Duty", other_duty: @other_duty} %> ================================================ FILE: app/views/placement_types/_fields.html.erb ================================================
    <%= form.label :placement_started_at, "Placement Started At" %> <%= form.date_field :placement_started_at, value: placement.placement_started_at&.to_date || Time.zone.now, class: "form-control" %>
    <%= form.label :placement_type_id, "Placement Type" %>
    <%= form.collection_select( :placement_type_id, PlacementType.for_organization(current_organization).order_alphabetically, :id, :name, {include_hidden: false, include_blank: "-Select Placement Type-"}, {class: "form-control"} ) %>
    ================================================ FILE: app/views/placement_types/_form.html.erb ================================================

    <%= title %>

    <%= form_with(model: placement_type, local: true) do |form| %>
    <%= render "/shared/error_messages", resource: placement_type %>
    <%= form.label :name, "Name" %> <%= form.text_field :name, class: "form-control", required: true %>
    <%= button_tag(type: "submit", class: "btn-sm main-btn primary-btn btn-hover") do %> Submit <% end %>
    <% end %>
    ================================================ FILE: app/views/placement_types/edit.html.erb ================================================ <%= render partial: "form", locals: {title: "Edit Placement Type", placement_type: @placement_type} %> ================================================ FILE: app/views/placement_types/new.html.erb ================================================ <%= render partial: "form", locals: {title: "New Placement Type", placement_type: @placement_type} %> ================================================ FILE: app/views/placements/_fields.html.erb ================================================
    <%= form.label :placement_started_at, "Placement Started At" %> <%= form.date_field :placement_started_at, value: placement.placement_started_at&.to_date || Time.zone.now, class: "form-control" %>
    <%= form.label :placement_type_id, "Placement Type" %>
    <%= form.collection_select( :placement_type_id, PlacementType.for_organization(current_organization).order_alphabetically, :id, :name, {include_hidden: false, include_blank: "-Select Placement Type-"}, {class: "form-control"} ) %>
    ================================================ FILE: app/views/placements/_form.html.erb ================================================
    <%= render "/shared/error_messages", resource: placement %>
    <%= form_with(model: placement, url: [casa_case, placement], local: true, data: { controller: "placement-form", nested_form_wrapper_selector_value: ".nested-form-wrapper" }) do |form| %>
    Case Number: <%= link_to casa_case.case_number, casa_case %>
    <%= render 'placements/fields', placement: placement, form: form, casa_case: casa_case %>
    <% end %>
    ================================================ FILE: app/views/placements/edit.html.erb ================================================

    Editing Placement

    <%= render 'form', casa_case: @casa_case, placement: @placement %> ================================================ FILE: app/views/placements/index.html.erb ================================================

    Placement History for <%= link_to "#{@casa_case.case_number}", casa_case_path(@casa_case) %>

    <% @placements.each_with_index do |placement, idx| %> <%= render(Modal::GroupComponent.new(id: placement.id)) do |component| %> <% component.with_header(text: "Delete Placement?", id: placement.id) %> <% component.with_footer do %> <%= link_to casa_case_placement_path(@casa_case, placement), method: :delete, class: "btn-sm main-btn danger-btn btn-hover ms-auto" do %> Confirm <% end %> <% end %> <% end %> <% end %>
    Placement Type Date
    <%= placement.placement_type.name %> <%= placement.decorate.formatted_date %> - <% if idx.zero? %> Present <% elsif @placements[idx - 1].placement_started_at %> <%= @placements[idx - 1].decorate.formatted_date %> <% end %> <%= link_to edit_casa_case_placement_path(@casa_case, placement), class: "btn-sm main-btn primary-btn-outline btn-hover ms-auto" do %> Edit <% end %> <%= render(Modal::OpenLinkComponent.new(target: placement.id, klass: "btn-sm main-btn danger-btn-outline btn-hover ms-auto")) do %> Delete <% end %>
    ================================================ FILE: app/views/placements/new.html.erb ================================================

    New Placement

    <%= render 'form', casa_case: @casa_case, placement: @placement %> ================================================ FILE: app/views/placements/show.html.erb ================================================

    Placement

    Case Number:
    <%= link_to "#{@casa_case.case_number}", casa_case_path(@casa_case) %>
    Placement Type:
    <%= @placement.placement_type.name %>
    ================================================ FILE: app/views/reimbursements/_datatable.html.erb ================================================
    Volunteer Case Number Contact Types Occurred At Miles Driven Address Reimbursement Complete
    ================================================ FILE: app/views/reimbursements/_filter_trigger.html.erb ================================================ ================================================ FILE: app/views/reimbursements/_occurred_at_filter_input.html.erb ================================================
    <%= label %> <%= date_field( nil, name, data: { date_end_date: Time.now, date_start_date: @occurred_at_filter_start_date }, class: "form-control" ) %>
    ================================================ FILE: app/views/reimbursements/_reimbursement_complete.html.erb ================================================
    ================================================ FILE: app/views/reimbursements/_table.html.erb ================================================
    <% @grouped_reimbursements.each do |_, reimbursements| %> <% reimbursement = reimbursements.first %> <% end %>
    <%= "Volunteer" %> <%= "Case Number" %> <%= "Contact Types" %> <%= "Occurred At" %> <%= "Miles Driven" %> <%= "Address" %> <%= "Reimbursement Complete" %>
    Volunteer <%= link_to(reimbursement.creator.display_name, volunteer_path(reimbursement.creator)) %> Case Number <% reimbursements.each do |r| %> <%= link_to(r.casa_case.case_number, casa_case_path(r.casa_case)) %>
    <% end %>
    <%= contact_types_list(reimbursement) %> <%= reimbursement.occurred_at.strftime("%B %d %Y") %> <%= reimbursement.miles_driven %> <%= reimbursement.creator.address&.content %> <%= form_with(model: reimbursement, url: reimbursement_mark_as_complete_path(reimbursement), method: "patch") do |form| %> <%= form.label :reimbursement_complete, "Yes" %> <%= form.check_box(:reimbursement_complete, value: reimbursement.reimbursement_complete, onchange: 'this.form.submit();') %> <% end %>
    ================================================ FILE: app/views/reimbursements/index.html.erb ================================================

    Reimbursement Queue

    Filter by:

    <%= render "datatable" %>
    ================================================ FILE: app/views/reports/_filter.html.erb ================================================
    ================================================ FILE: app/views/reports/index.html.erb ================================================

    Export Data

    • <%= form_with url: mileage_reports_path(format: :csv), method: :get do |f| %> <%= button_tag( data: { disable_with: "Downloading Mileage Report" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Mileage Report <% end %> <% end %>
    • <%= form_with url: missing_data_reports_path(format: :csv), method: :get do |f| %> <%= button_tag( data: { disable_with: "Downloading Missing Data Report" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Missing Data Report <% end %> <% end %>
    • <%= form_with url: learning_hours_reports_path(format: :csv), method: :get do |f| %> <%= button_tag( data: { disable_with: "Downloading Learning Hours Report" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Learning Hours Report <% end %> <% end %>
    • <%= form_with url: export_emails_path(format: :csv), method: :get do |f| %> <%= button_tag( data: { disable_with: "Downloading Export Volunteers Emails" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Export Volunteers Emails <% end %> <% end %>
    • <%= form_with url: followup_reports_path(format: :csv), method: :get do |f| %> <%= button_tag( data: { disable_with: "Downloading Followups Report" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Followups Report <% end %> <% end %>
    • <%= form_with url: placement_reports_path(format: :csv), method: :get do |f| %> <%= button_tag( data: { disable_with: "Downloading Placements Report" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Placements Report <% end %> <% end %>
    Case Contacts Report

    This CSV is a listing of all fields for the case contacts of all volunteers. Select the start and end date.


    <%= form_with url: case_contact_reports_path(format: :csv), scope: 'report', method: :get, local: true do |f| %>
    <%= f.date_field :start_date, value: 6.months.ago, class: "form-control" %>
    <%= f.date_field :end_date, value: Date.today, class: "form-control" %>
    <% if current_user&.casa_admin? || current_user&.supervisor? %>
    <%= f.collection_select( :supervisor_ids, Supervisor.where(casa_org: current_user.casa_org), :id, :display_name, {prompt: false, :include_hidden => false}, {class: "form-control form-select select2", id:"multiple-select-field1", multiple:true, data: { placeholder: '-Select Supervisors-' }} ) %>
    <%= f.collection_select( :creator_ids, Volunteer.where(casa_org: current_user.casa_org), :id, :display_name, {prompt: false, :include_hidden => false}, {class: "form-control form-select select2", id:"multiple-select-field2", multiple:true, data: { placeholder: '-Select Volunteers-' }} ) %>
    <%= f.collection_select( :contact_type_ids, ContactType.for_organization(current_user.casa_org), :id, :name, {prompt: false, :include_hidden => false}, {class: "form-control form-select select2", id:"multiple-select-field3", multiple:true, data: { placeholder: '-Select Contact Types-' }} ) %>
    <%= f.collection_select( :contact_type_group_ids, ContactTypeGroup.for_organization(current_user.casa_org), :id, :name, { }, { class: "form-control form-select select2", id:"multiple-select-field4", multiple:true, data: { placeholder: '-Select Contact Type Groups-' } } ) %>
    Want Driving Reimbursement
    <%= f.collection_radio_buttons :want_driving_reimbursement, boolean_choices, :last, :first do |b| %>
    <%= b.radio_button(class: "form-check-input") %> <%= b.label(class: "form-check-label") %>
    <% end %>
    Contact Made
    <%= f.collection_radio_buttons :contact_made, boolean_choices, :last, :first do |b| %>
    <%= b.radio_button(class: "form-check-input") %> <%= b.label(class: "form-check-label") %>
    <% end %>
    Transition Aged Youth
    <%= f.collection_radio_buttons :has_transitioned, boolean_choices, :last, :first do |b| %>
    <%= b.radio_button(class: "form-check-input") %> <%= b.label(class: "form-check-label") %>
    <% end %>
    <% end %> <%= render 'filter', form: f %>
    • <%= button_tag( type: "submit", data: { disable_with: "Downloading Report" }, class: "btn-sm main-btn primary-btn btn-hover report-form-submit" ) do %> Download Report <% end %>
    <% end %>
    ================================================ FILE: app/views/shared/_additional_expense_form.html.erb ================================================
    <%= form.label :other_expense_amount, "Other Expense Amount", class: "form-label" %>
    $ <%= form.number_field(:other_expense_amount, value: number_with_precision(form.object.other_expense_amount, precision: 2), placeholder: "0", min: "0", max: 1000, step: 0.01, class: ["form-control", "expense-amount-input"], ) %>
    <%= form.label :other_expenses_describe, class: "form-label" do %> Other Expense Details* <% end %> <%= form.text_area(:other_expenses_describe, placeholder: "Enter other expense details", class: ["form-control", "expense-describe-input"], data: { action: "input->autosave#save" }, ) %>
    <%= form.hidden_field :id, value: form.object.id %> <%= form.hidden_field :_destroy, data: {case_contact_form_target: "expenseDestroy"} %>
    ================================================ FILE: app/views/shared/_court_order_form.html.erb ================================================
    <%= f.text_area :text, cols: 50, class: "court-order-text-entry" %>
    <%= f.select :implementation_status, court_order_select_options, {include_blank: 'Set Implementation Status'}, {class: 'implementation-status'} %>
    <%= f.hidden_field :_destroy %>
    ================================================ FILE: app/views/shared/_court_order_list.erb ================================================
    <% if siblings_casa_cases && siblings_casa_cases.count >= 1 %>
    <% siblings_casa_cases_options = siblings_casa_cases.map { |scc| [scc.case_number, scc.id] } %>
    <%= form.label :"siblings_casa_cases", "Copy all orders from case: " %>
    <%= form.select :"siblings_casa_cases", siblings_casa_cases_options, {include_blank: true}, {class: "siblings-casa-cases col-3 ml-2"} %>
    <%= button_tag "Copy", type: :button, class: "copy-court-button main-btn primary-btn btn-hover ml-1", id: "copy-court-button" %> <% if casa_case %> <%= form.hidden_field :casa_case, value: casa_case.id %> <% end %>
    <% end %> <%= form.fields_for :case_court_orders do |ff| %> <%= render "shared/court_order_form", f: ff %> <% end %>
    <%= label_tag :selectedCourtOrder, "Court Order Type" %>
    <%= select_tag :selectedCourtOrder, options_for_select(CaseCourtOrder.court_order_options), { include_blank: "Create custom court order", data: {court_order_form_target: "selectedCourtOrder"} } %>
    ================================================ FILE: app/views/shared/_edit_form.html.erb ================================================
    <%= f.label :email, "Email" %> <% if policy(resource).update_user_setting? %> <%= f.text_field :email, placeholder: "Email" %> <% else %> <% end %>
    <%= f.label :display_name, "Display name" %> <% if policy(resource).update_user_setting? %> <%= f.text_field :display_name, placeholder: "Display Name" %> <% else %> <% end %>
    <%= f.label :phone_number, "Phone number" %> <% if policy(resource).update_user_setting? %> <%= f.text_field :phone_number, placeholder: "Phone Number" %> <% else %> <% end %>
    <%= f.label :date_of_birth, "Date of birth" %> <% if policy(resource).update_user_setting? %> <%= f.date_field :date_of_birth, value: resource.date_of_birth, class: "form-control label-font-weight" %> <% else %> <% end %>
    <% if resource.role == "Volunteer" %>
    <%= f.label :address_attributes_content, "Mailing address" %> <% if policy(resource).update_user_setting? %> <%= f.fields_for :address, (resource.address ? nil : Address.new) do |a| %> <%= a.text_field :content, placeholder: "Mailing Address" %> <% end %> <% else %> <%= f.fields_for :address, (resource.address ? nil : Address.new) do |a| %> <%= a.text_field :content, readonly: true, placeholder: "Mailing Address" %> <% end %> <% end %>
    <% end %>
    <%= f.check_box :receive_reimbursement_email, class: "form-check-input" %> <%= f.label :receive_reimbursement_email, "Email Reimbursement Requests", class: "form-check-label" %>
    ================================================ FILE: app/views/shared/_emancipation_link.html.erb ================================================ <%= link_to(casa_case_emancipation_path(casa_case.id), class: "main-btn primary-btn btn-sm emancipation-btn") do %> Emancipation <%= render BadgeComponent.new(text: casa_case.decorate.emancipation_checklist_count, type: :light, margin: false) %> <% end %> ================================================ FILE: app/views/shared/_error_messages.html.erb ================================================ <% if resource.errors.any? %>
    <%= pluralize(resource.errors.count, "error") %> prohibited this <%= @custom_error_header || resource.model_name.human %> from being saved:
    <% end %> ================================================ FILE: app/views/shared/_favicons.html.erb ================================================ <%= favicon_link_tag "apple-icon-57x57.png", rel: "apple-touch-icon", sizes: "57x57" %> <%= favicon_link_tag "apple-icon-60x60.png", rel: "apple-touch-icon", sizes: "60x60" %> <%= favicon_link_tag "apple-icon-72x72.png", rel: "apple-touch-icon", sizes: "72x72" %> <%= favicon_link_tag "apple-icon-76x76.png", rel: "apple-touch-icon", sizes: "76x76" %> <%= favicon_link_tag "apple-icon-114x114.png", rel: "apple-touch-icon", sizes: "114x114" %> <%= favicon_link_tag "apple-icon-120x120.png", rel: "apple-touch-icon", sizes: "120x120" %> <%= favicon_link_tag "apple-icon-144x144.png", rel: "apple-touch-icon", sizes: "144x144" %> <%= favicon_link_tag "apple-icon-152x152.png", rel: "apple-touch-icon", sizes: "152x152" %> <%= favicon_link_tag "apple-icon-180x180.png", rel: "apple-touch-icon", sizes: "180x180" %> <%= favicon_link_tag "android-icon-192x192.png", sizes: "192x192", rel: "icon", type: "image/png" %> <%= favicon_link_tag "favicon-32x32.png", sizes: "32x32", rel: "icon", type: "image/png" %> <%= favicon_link_tag "favicon-96x96.png", sizes: "96x96", rel: "icon", type: "image/png" %> <%= favicon_link_tag "favicon-16x16.png", sizes: "16x16", rel: "icon", type: "image/png" %> "> ================================================ FILE: app/views/shared/_invite_login.html.erb ================================================
    CASA organization <%= resource&.casa_org&.name %>
    Added to system <%= resource&.decorate&.formatted_created_at %>
    Invitation email sent <%= resource&.decorate&.formatted_invitation_sent_at || "never" %>
    Last logged in <%= resource&.decorate&.formatted_current_sign_in_at || "never" %>
    Invitation accepted <%= resource&.decorate&.formatted_invitation_accepted_at || "never" %>
    Password reset last sent <%= resource&.decorate&.formatted_reset_password_sent_at || "never" %>
    Will receive reimbursement request emails <%= resource.receive_reimbursement_email ? "Yes" : "No" %>
    <% if resource.is_a?(Volunteer) %>
    Learning Hours This Year <%= resource.learning_hours_spent_in_one_year %>
    <% end %> ================================================ FILE: app/views/shared/_manage_volunteers.html.erb ================================================

    Manage Volunteers

    <% if show_assigned_volunteers %> <%= yield :table_title %>
    <% if @casa_case %> <% end %> <% if local_assigns[:button_text] == "Hide unassigned" %> <% end %> <%= yield :table_body %>
    Volunteer Name Volunteer EmailStatus Start Date End DateEnable ReimbursementCurrently Assigned ToActions
    <% end %>

    Assign a Volunteer to Case

    disabled <% end %>> <%= form_with(model: assignable_obj.new, url: assign_action) do |form| %>
    <% if @supervisor %> <%= form.hidden_field :supervisor_id, :value => @supervisor.id %> <% end %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-sm btn-hover" ) do %> Assign Volunteer <% end %>
    <% end %>
    <% unless available_volunteers.any? %>

    There are no active, unassigned volunteers available.

    <% end %>
    ================================================ FILE: app/views/static/index.html.erb ================================================ CASA Volunteer Tracking <%= stylesheet_link_tag "shared/noscript" %>

    Casa Volunteer Tracking removes
    the complexity of managing your
    CASA and tracking your volunteers.

    Ready to make yourself more efficient?

    The Casa Volunteer Tracking app removes the
    complexity from your day

    and lets you spend time helping those who need it.

    <%= image_tag("case-contact.svg", class: "app-images" , alt: "man reading a book to a girl" ) %>

    Case Contacts

    Volunteers can record case contacts and learning hours.

    <%= image_tag("court-report.svg", class: "app-images" , alt: "judge raising a gavel" ) %>

    Court Reports

    Volunteers, supervisors, and admins can generate a court report for any case with all recorded case contacts pre-filled.

    <%= image_tag("reimbursement.svg", class: "app-images" , alt: "car with two people" ) %>

    Reimbursements

    Volunteers can submit reimbursement requests and admins can generate reports.

    <%= image_tag("add-case.svg", class: "app-images" , alt: "file folders and file cabinet" ) %>

    Add Cases

    Supervisors and administrators can add cases and assign volunteers.

    <%= image_tag("spreadsheets.svg", class: "app-images" , alt: "computer screen with graphs and charts" ) %>

    Exportable Data

    All CASA data is easily exportable in CSV format.

    <%= image_tag("communicate.svg", class: "app-images" , alt: "woman holding a phone with a notification alert" ) %>

    Communication

    Easily communicate with volunteers by email, SMS or both.

    <%= image_tag("quote.svg", class: "quote-image" , style: "width: 300px" , alt: "Quote: No one is useless in this world who lightens the burdens of another - Charles Dickens" ) %>

    Testimonials

    The tracker has helped us streamline our processes, and we no longer have to worry about losing track of important documents.

    - Sarah B. | Program Manager

    Thanks to the tracker, we can now make quicker decisions and effectively monitor our cases, resulting in a more pleasant experience for our volunteers.

    - Nancy K. | Office Manager

    CASA Organizations Powered by Our App

    <% @casa_logos.each do |org| %>
    <%= image_tag org.logo, class: "org_logo" %>
    <%= org.display_name %>
    <% end %>

    Want to use the CASA Volunteer Tracking App?



    Have questions? Email us at casa@rubyforgood.org

    ================================================ FILE: app/views/supervisor_mailer/_active_volunteer_info.html.erb ================================================ Case <%= link_to casa_case.case_number, casa_case_url(casa_case) %>
    <% successful_contacts = recently_unassigned ? casa_case.decorate.successful_contacts_this_week_before(case_assignment.updated_at) : casa_case.decorate.successful_contacts_this_week %> <% unsuccessful_contacts = recently_unassigned ? casa_case.decorate.unsuccessful_contacts_this_week_before(case_assignment.updated_at) : casa_case.decorate.unsuccessful_contacts_this_week %> <% if successful_contacts + unsuccessful_contacts > 0 %> <%= "Number of successful case contacts made this week: #{successful_contacts}" %>
    <%= "Number of unsuccessful case contacts made this week: #{unsuccessful_contacts} " %>
    <% recent_contact = recently_unassigned ? casa_case.decorate.case_contacts_latest_before(case_assignment.updated_at) : casa_case.decorate.case_contacts_latest %> <%= "Most recent contact attempted:" %>
    <%= " - Date: #{I18n.l(recent_contact&.occurred_at, format: :full, default: nil)}" %>
    <%= " - Type: #{recent_contact&.decorate.contact_types}" %>
    <%= " - Duration: #{recent_contact&.duration_minutes}" %>
    <%= " - Contact Made: #{recent_contact&.contact_made}" %>
    <%= " - Medium Type: #{recent_contact&.medium_type}" %> <% recent_contact.contact_topic_answers.reject { _1.value.blank? }.each do |answer| %>
    - <%= "#{answer.contact_topic.question}" %><%= ": #{answer.value}" %> <% end %>
    <%= " - Notes: #{recent_contact&.notes}" %> <% else %> No contact attempts were logged for this week. <% end %> <% if recently_unassigned %>
    This case was unassigned from <%= volunteer_display_name %> on <%= case_assignment.updated_at.to_date.to_fs(:long_ordinal) %> <% if successful_contacts + unsuccessful_contacts > 0 %> The above activity only describes the part of the week when the case was still assigned to <% volunteer_display_name %> <% end %> <% end %> ================================================ FILE: app/views/supervisor_mailer/_active_volunteers.html.erb ================================================ <% active_ever_assigned.each do |volunteer| %> Summary for <%= link_to volunteer.display_name, edit_volunteer_url(volunteer) %> <% volunteer.case_assignments_with_cases.each do |case_assignment| %> <% recently_unassigned = case_assignment.decorate.unassigned_in_past_week? %> <% if case_assignment.active || recently_unassigned %> <% casa_case = case_assignment.casa_case %> <%= render("active_volunteer_info", casa_case: casa_case, recently_unassigned: recently_unassigned, case_assignment: case_assignment, volunteer_display_name: volunteer.display_name ) %> <% end %> <% end %>
    <% end %> ================================================ FILE: app/views/supervisor_mailer/_additional_notes.html.erb ================================================ Additional Notes:
    <% inactive_messages&.each do |message| %> <%= message %>
    <% end %> <% if inactive_messages&.none? %> There are no additional notes. <% end %> ================================================ FILE: app/views/supervisor_mailer/_no_recent_sign_in.html.erb ================================================ <% if inactive_volunteers.any? %> <%= "The following volunteers have not signed in or created case contacts in the last 30 days" %>
    <% inactive_volunteers.each do |volunteer| %> - <%= volunteer.display_name %>
    <% end %> <% end %> ================================================ FILE: app/views/supervisor_mailer/_pending_volunteers.html.erb ================================================ Pending Volunteers:
    <% if @supervisor.pending_volunteers.empty? %>
    There are no pending volunteers. <% end %> <% supervisor.pending_volunteers.each do |volunteer| %> <%= volunteer.display_name %> <% end %> ================================================ FILE: app/views/supervisor_mailer/_recently_unassigned_volunteers.html.erb ================================================ <% if supervisor.recently_unassigned_volunteers.any? %> <%= "The following volunteers have been unassigned from you:" %>
    <% supervisor.recently_unassigned_volunteers.each do |volunteer| %> - <%= volunteer.display_name %> <% unless volunteer.has_supervisor? %> (not assigned to a new supervisor) <% end %>
    <% end %> <% end %> ================================================ FILE: app/views/supervisor_mailer/_summary_header.html.erb ================================================ <%= supervisor_display_name %>, <% if has_active_volunteers %> You have no volunteers with assigned cases at the moment. When you do, you will see their status here. <% else %> Here's a summary of what happened with your volunteers this last week. <% end %> ================================================ FILE: app/views/supervisor_mailer/account_setup.html.erb ================================================
    A <%= @supervisor.casa_org.display_name %>’s County supervisor console account has been created for you. This console is for logging the time you spend and actions you take on your CASA case. You can log activity with your CASA youth, their family members, their foster family or placement, the DSS worker, your Case Supervisor and others associated with your CASA case (such as teachers and therapists).
    Your console account is associated with this email. If this is not the correct email to use, please stop here and contact your Administrator to change the email address. If you are ready to get started, please set your password. This is the first step to accessing your new supervisor account.
    " target="_blank" class="btn-primary" itemprop="url" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Set Your Password
    ================================================ FILE: app/views/supervisor_mailer/reimbursement_request_email.html.erb ================================================
    Hello <%= @supervisor.display_name %>,
    <%= @volunteer.display_name %> has submitted a reimbursement request, please follow up on the reimbursements page <%= link_to "using this link", reimbursements_url %>.
    ================================================ FILE: app/views/supervisor_mailer/weekly_digest.html.erb ================================================ <%= render("summary_header", has_active_volunteers: @supervisor.volunteers.length == 0, supervisor_display_name: @supervisor.display_name) %> <%= render("active_volunteers", supervisor: @supervisor, active_ever_assigned: @supervisor.volunteers_ever_assigned.where("supervisor_volunteers.is_active = ?", true).active) %> <%= render("recently_unassigned_volunteers", supervisor: @supervisor) %> <%= render("additional_notes", inactive_messages: @inactive_messages) %> <%= render("pending_volunteers", supervisor: @supervisor) %> <%= render("no_recent_sign_in", supervisor: @supervisor, inactive_volunteers: @inactive_volunteers) %>
    ================================================ FILE: app/views/supervisors/_manage_active.html.erb ================================================
    <% if user.active? %> Supervisor is <%= render BadgeComponent.new(text: "Active", type: :success, rounded: true) %>
    <% if current_user.casa_admin? %> <% if policy(user).deactivate? %> <%= link_to deactivate_supervisor_path(user), class: "btn-sm main-btn danger-btn-outline btn-hover", method: :patch, data: { confirm: "WARNING: Marking a supervisor inactive will make them unable to login. Are you sure you want to do this?" } do %> Deactivate Supervisor <% end %> <% end %> <% end %> <% else %>
    Supervisor was deactivated on: <%= user.decorate.formatted_updated_at %>
    <% if policy(user).activate? %> <%= link_to "Activate supervisor", activate_supervisor_path(user), method: :patch, class: "btn-sm main-btn danger-btn-outline btn-hover" %> <% end %> <% end %> <% if current_user.casa_admin? && user.invitation_accepted_at.nil? %> <%= link_to resend_invitation_supervisor_path(user), class: "btn-sm main-btn danger-btn-outline btn-hover", method: :patch do %> Resend Invitation <% end %> <% if current_user.casa_admin? %> <%= link_to change_to_admin_supervisor_path(user), class: "btn-sm main-btn danger-btn-outline btn-hover", method: :patch do %> Change to Admin <% end %> <% end %> <% end %>
    ================================================ FILE: app/views/supervisors/edit.html.erb ================================================

    Editing Supervisor

    <%= form_with(model: @supervisor, as: :supervisor, url: supervisor_path(@supervisor), method: :patch) do |form| %> <%= render "/shared/error_messages", resource: @supervisor %> <%= render "/shared/edit_form", resource: @supervisor, f: form %>

    <%= render "/shared/invite_login", resource: @supervisor %>

    <% if current_user.casa_admin? || current_user.supervisor? %>
    <%= form.label :monthly_learning_hours_report, "Receive Monthly Learning Hours Report" %> <%= form.check_box :monthly_learning_hours_report %>
    <% end %>

    <%= render "manage_active", user: @supervisor %>

    <% if policy(@supervisor).update_supervisor_email? || policy(@supervisor).update_supervisor_name? %>
    <%= button_tag( type: "submit", class: "btn-sm main-btn primary-btn btn-hover" ) do %> Submit <% end %>
    <% end %>

    <% end %>
    <% button_text = @all_volunteers_ever_assigned.nil? ? "Include unassigned" : "Hide unassigned" %> <% volunteer_title = @unassigned_volunteer_count == 0 ? "Assigned Volunteers" : "All Volunteers" %> <% content_for :table_title do %>

    <%= volunteer_title %>

    <% if @supervisor_has_unassigned_volunteers %> <%= button_to button_text, edit_supervisor_path(@supervisor), params: { include_unassigned: @all_volunteers_ever_assigned.nil? }, method: :get, class: "main-btn primary-btn-outline btn-hover my-3" %> <% end %> <% end %> <% content_for :table_body do %> <% (@all_volunteers_ever_assigned || @supervisor.volunteers).each do |volunteer| %> <%= link_to volunteer.display_name, edit_volunteer_path(volunteer) %> <%= volunteer.email %> <% if button_text == "Hide unassigned" %> <%= volunteer.has_supervisor? ? volunteer.supervisor.display_name : "No One" %> <% end %> <% if volunteer.supervised_by?(@supervisor) %>
    <%= button_to "Unassign", unassign_supervisor_volunteer_path(volunteer), method: :patch, class: "text-danger" %>
    <% else %> Unassigned <% end %> <% end %> <% end %> <%= render( "shared/manage_volunteers", assignable_obj: SupervisorVolunteer, assign_action: supervisor_volunteers_path(supervisor_id: @supervisor.id), available_volunteers: @available_volunteers, select_id: 'supervisor_volunteer_volunteer_id', select_name: 'supervisor_volunteer[volunteer_id]', show_assigned_volunteers: @supervisor_has_unassigned_volunteers || @supervisor.volunteers.any?, button_text: button_text ) %> ================================================ FILE: app/views/supervisors/index.html.erb ================================================

    Supervisors

    Legend for Supervisor List

    Number of Volunteers attempting contact (within 2 weeks)
    Number of Volunteers not attempting contact (within 2 weeks)
    Count of Transition Aged Youth

    <% @supervisors.each do |supervisor| %> <% end %>
    Supervisor Name
    Volunteer Info
    Actions
    <%= link_to(supervisor.display_name, edit_supervisor_path(supervisor)) %> <% no_attempt_volunteers = supervisor.no_attempt_for_two_weeks %> <% active_volunteers = (supervisor.active_volunteers - no_attempt_volunteers).nonzero? %> <% transition_volunteers = supervisor.volunteers_serving_transition_aged_youth %>
    <% if active_volunteers %>
    <%= active_volunteers %>
    <% end %> <% if no_attempt_volunteers.nonzero? %>
    <%= no_attempt_volunteers %>
    <% else %>
    <% end %>
    <% if !(active_volunteers || no_attempt_volunteers.nonzero?) %>
    No assigned volunteers
    <% else %>
    <%= transition_volunteers %>
    <% end %>
    <%= link_to edit_supervisor_path(supervisor) do %>
    <% end %>

    Volunteers without Supervisors

    <% if @available_volunteers.any? %>
    <% @available_volunteers.each do |volunteer| %> <% end %>
    Active volunteers not assigned to supervisors Assigned to Case(s)
    <%= link_to volunteer.display_name, edit_volunteer_path(volunteer) %> <% volunteer.casa_cases.map do |casa_case| %> <%= link_to(casa_case.case_number, casa_case_path(casa_case)) %>
    <% end %>
    <% else %> There are no active volunteers without supervisors to display here <% end %>

    Casa Cases without Court Dates

    <% @casa_cases.each do |casa_case| %> <% end %>
    Case Number Hearing Type Judge Status Transition Aged Youth Assigned To Actions
    <%= link_to(casa_case.case_number, casa_case) %> <%= casa_case.hearing_type_name %> <%= casa_case.judge_name %> <%= casa_case.decorate.status %> <%= casa_case.decorate.transition_aged_youth %> <% if casa_case.active? %> <% if current_user.volunteer? %> <%= safe_join(casa_case.assigned_volunteers.map { |vol| vol.display_name }, ", ") %> <% else %> <%= safe_join(casa_case.assigned_volunteers.map { |vol| link_to(vol.display_name, edit_volunteer_path(vol)) }, ", ") %> <% end %> <% else %> Case was deactivated on: <%= I18n.l(casa_case.updated_at, format: :standard, default: nil) %> <% end %> <%= link_to "Detail View", casa_case_path(casa_case) %> <%= link_to "Edit", edit_casa_case_path(casa_case), class: 'text-danger' %>
    ================================================ FILE: app/views/supervisors/new.html.erb ================================================

    Create New Supervisor

    <%= form_with model: @supervisor, local: true, url: supervisors_path do |form| %> <%= render "/shared/error_messages", resource: @supervisor %>
    <%= form.label :email, "Email" %> <%= form.text_field :email, class: "form-control" %>
    <%= form.label :display_name, "Display name" %> <%= form.text_field :display_name, class: "form-control" %>
    <%= form.label :phone_number, "Phone number" %> <%= form.telephone_field :phone_number, class: "form-control" %>
    <%= button_tag( type: "submit", class: "main-btn btn-sm primary-btn btn-hover mb-3" ) do %> Create Supervisor <% end %>
    <% end %>
    ================================================ FILE: app/views/user_mailer/password_changed_reminder.html.erb ================================================
    Hello <%= @user.try(:display_name) || @user.email %>
    Your CASA password has been changed.
    If you have any questions, please contact a (Name of relevant CASA) CASA administrator for assistance.
    ================================================ FILE: app/views/users/_edit_profile.erb ================================================
    CASA organization <%= resource&.casa_org&.name %>
    Added to system <%= resource&.decorate(context: {format: :edit_profile})&.formatted_created_at %>
    Email <%= resource&.email %>
    Invitation email sent <%= resource&.decorate(context: {format: :edit_profile})&.formatted_invitation_sent_at || "never" %>
    Last logged in <%= resource&.decorate(context: {format: :edit_profile})&.formatted_current_sign_in_at || "never" %>
    Invitation accepted <%= resource&.decorate(context: {format: :edit_profile})&.formatted_invitation_accepted_at || "never" %>
    Password reset last sent <%= resource&.decorate(context: {format: :edit_profile})&.formatted_reset_password_sent_at || "never" %>
    <% if resource.is_a?(Volunteer) %>
    Learning Hours This Year <%= resource.learning_hours_spent_in_one_year %>
    <% end %> ================================================ FILE: app/views/users/_languages.html.erb ================================================

    My Languages

    <% volunteer.languages.each do |lang| %> <% end %>

    Languages

    Actions

    English (default language)
    <%= lang.name %> <%= link_to "Delete", remove_language_users_path(language_id: lang.id), class: "btn btn-danger", method: :delete %>
    <%= form_with(model: volunteer, url: add_language_users_path) do |form| %>
    <%= form.label :languages, "Add Language" %>
    <%= form.select :languages, current_organization.languages.map { |lang| [lang.name, lang.id] }, {include_blank: true}, {name: :language_id} %>
    <%= button_tag "Add", type: :submit, class: "main-btn primary-btn btn-hover", id: "add-language-button" %>
    <% end %>
    ================================================ FILE: app/views/users/edit.html.erb ================================================

    Edit Profile

    <%= form_with(model: @user, scope: :user, url: users_path, method: :patch) do |form| %>
    <%= render "/shared/error_messages", resource: @user %>
    <%= form.label :display_name, "Display name" %> <%= form.text_field :display_name, class: "form-control" %>
    <%= form.label :phone_number, "Phone number" %> <%= form.text_field :phone_number, class: "form-control" %>
    <%= form.label :date_of_birth, "Date of birth" %> <%= form.date_field :date_of_birth, value: @user.date_of_birth, class: "form-control label-font-weight" %>
    <% if current_user.address %> <% address = current_user.address %> <% else %> <% address = Address.new %> <% end %>
    <%= form.fields_for :address, address do |f| %> <%= f.label :content, "Address" %> <%= f.text_field :content, class: "form-control" %> <% end %>
    <%= form.hidden_field :casa_org_id, value: current_user.casa_org_id %> <%= render "edit_profile", resource: current_user %>
    <%= form.button "Update Profile", type: "submit", class: "main-btn primary-btn btn-hover mb-3" %> <% if policy(CasaAdmin).see_deactivate_option? %> <% if @active_casa_admins.length > 1 %> <%= link_to "Deactivate", deactivate_casa_admin_path(current_user), method: :patch, class: "btn btn-outline-danger mb-3", data: {confirm: "WARNING: Marking an admin inactive will make them unable to login. Are you sure you want to do this?"} %> <% else %> <%= link_to "Deactivate", "#", class: "main-btn danger-btn-outline btn-hover mb-3", data: {confirm: "Contact your administrator at Ruby For Good to deactivate this account."} %> <% end %> <% end %>
    <% end %>

    <%= form_with(model: @user, scope: :user, url: {action: "update_password"}, method: :patch) do |f| %>
    <%= f.label :current_password, "Current Password" %>
    <%= f.password_field :current_password, autocomplete: "off", class: "form-control" %>
    <%= f.label :password, "New Password" %>
    <%= f.password_field :password, autocomplete: "off", class: "form-control password-new", minlength: User.password_length.min %>
    <%= f.label :password_confirmation, "New Password Confirmation" %>
    <%= f.password_field :password_confirmation, class: "form-control password-confirmation", minlength: User.password_length.min %>
    <%= f.submit "Update Password", class: "btn btn-danger submit-password" %>
    <% end %>

    <%= form_with(model: @user, scope: :user, url: {action: "update_email"}, method: :patch) do |f| %>
    <%= f.label :current_password, "Current Password" %>
    <%= f.password_field :current_password, autocomplete: "off", class: "form-control", id: "current_password_email", required: true %>
    <%= f.label :email, "New Email" %>
    <%= f.text_field :email, type: "email", class: "form-control email-new", autocomplete: "off", value: nil, required: true %>
    <%= f.submit "Update Email", class: "btn btn-danger submit-email" %>
    <% end %>
    <%= form_with(model: @user, scope: :user, url: users_path, method: :patch) do |form| %>

    Communication Preferences

    Tell us how you'd like to receive notifications.

    <%= form.check_box :receive_email_notifications, class: "toggle-email-notifications form-check-input" %> <%= form.label :receive_email_notifications, "Email Me", class: "form-check-label" %>
    <% if @user.casa_org.twilio_enabled? %>
    <%= form.check_box :receive_sms_notifications, class: "toggle-sms-notifications form-check-input" %> <%= form.label :receive_sms_notifications, "Text Me", class: "form-check-label" %>
    <%= form.collection_check_boxes("sms_notification_event_ids", SmsNotificationEvent.where(user_type: @user.type), :id, :name) do |event| %>
    <%= event.check_box(class: "form-check-input form-check-input", id: "toggle-sms-notification-event") %> <%= event.label(class: "form-check-label") %>
    <% end %>
    <% else %>
    <%= form.check_box :receive_sms_notifications, class: "toggle-sms-notifications form-check-input", disabled: true %> <%= form.label :receive_sms_notifications, "Enable Twilio For Text Messaging", class: "form-check-label" %>
    <% end %>
    <%= form.submit "Save Preferences", class: "main-btn primary-btn btn-hover mb-3 save-preference" %>
    <% end %>
    <% if current_user.volunteer? %> <%= render partial: "languages", locals: { volunteer: current_user } %> <% end %> ================================================ FILE: app/views/volunteer_mailer/account_setup.html.erb ================================================
    A <%= @user.casa_org.display_name %>’s County volunteer console account has been created for you. This console is for logging the time you spend and actions you take on your CASA case. You can log activity with your CASA youth, their family members, their foster family or placement, the DSS worker, your Case Supervisor and others associated with your CASA case (such as teachers and therapists).
    Your console account is associated with this email. If this is not the correct email to use, please stop here and contact your Case Supervisor to change the email address. If you are ready to get started, please set your password. This is the first step to accessing your new volunteer account.
    You can see a video tour on YouTube
    " target="_blank" class="btn-primary" itemprop="url" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Set Your Password
    ================================================ FILE: app/views/volunteer_mailer/case_contacts_reminder.html.erb ================================================
    Hello <%= @user.display_name %>,
    You are receiving this email as a reminder to input the case contacts which you have made. You can visit " target="_blank">this link to edit your case contacts.
    If you have any questions, please contact your most recent CASA supervisor for assistance.
    ================================================ FILE: app/views/volunteer_mailer/court_report_reminder.html.erb ================================================
    <%= @user.display_name %>,
    This is a reminder that your next court report is due on <%= @court_report_due_date %>. Please submit your court report to your supervisor no later than this date.
    You can generate a court report by clicking on "Generate Court Reports" in your volunteer portal. Log in at https://www.casavolunteertracking.org to get started.
    Please email your supervisor directly if you need further assistance.
    ================================================ FILE: app/views/volunteers/_form.html.erb ================================================ <%= form_with(model: @volunteer, url: volunteers_path, id: :new_volunteer) do |form| %> <%= render "/shared/error_messages", resource: volunteer %>
    <%= form.label :email, "Email" %> <%= form.text_field :email, placeholder: "Email", class: "form-control" %>
    <%= form.label :display_name, "Display name" %> <%= form.text_field :display_name, placeholder: "Display Name", class: "form-control" %>
    <%= form.label :phone_number, "Phone number" %> <%= form.telephone_field :phone_number, placeholder: "Phone Number", class: "form-control" %>
    <%= form.label :date_of_birth, "Date of birth" %> <%= form.date_field :date_of_birth, value: @volunteer.date_of_birth, class: "form-control label-font-weight" %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm" ) do %> Create Volunteer <% end %>
    <% end %> ================================================ FILE: app/views/volunteers/_manage_active.html.erb ================================================
    <% if user.active? %> Volunteer is <%= render BadgeComponent.new(text: "Active", type: :success) %>
    <% if policy(user).deactivate? %> <%= link_to deactivate_volunteer_path(user), method: :patch, class: "main-btn danger-btn-outline btn-hover btn-sm my-1", data: {confirm: "WARNING: Marking a volunteer inactive will make them unable to login. Are you sure you want to do this?"} do %> Deactivate volunteer <% end %> <% end %> <% else %>
    Volunteer was deactivated on: <%= user.decorate.formatted_updated_at %>
    <% if policy(user).activate? %> <%= link_to activate_volunteer_path(user), method: :patch, class: "main-btn success-btn-outline btn-hover btn-sm my-1" do %> Activate volunteer <% end %> <% end %> <% end %> <% if (current_user.supervisor? || current_user.casa_admin?) && user.invitation_accepted_at.nil? %> <%= link_to resend_invitation_volunteer_path(user), class: "main-btn danger-btn-outline btn-hover btn-sm my-1" do %> Resend Invitation (Email) <% end %> <% end %> <% if current_user.casa_admin? %> <%= link_to send_reactivation_alert_volunteer_path(user), id: "#{current_user.casa_org.twilio_enabled? ? "twilio_enabled" : "twilio_disabled"}", class: "main-btn danger-btn-outline btn-hover btn-sm my-1" do %> <%= current_user.casa_org.twilio_enabled? ? "Send Reactivation Alert (SMS)" : "Enable Twilio To Send Reactivation Alert (SMS)" %> <% end %> <% end %>
    ================================================ FILE: app/views/volunteers/_manage_cases.erb ================================================

    Manage Cases

    <% if @volunteer.case_assignments.any? %>

    Assigned Cases

    <% @volunteer.case_assignments_with_cases.each do |assignment| %> <% end %>
    Case Number Transition Aged Youth Assignment status Actions
    <%= link_to assignment.casa_case.case_number, casa_case_path(assignment.casa_case) %> <%= volunteer_badge(assignment.casa_case, current_user) %> <%= assignment.casa_case.decorate.transition_aged_youth %> <% if @volunteer.active? && assignment.casa_case.active? && assignment.active? %> Volunteer is <%= render BadgeComponent.new(text: "Active", type: :success) %> <% elsif @volunteer.active? && assignment.casa_case.active? && assignment.inactive? %> Volunteer is <%= render BadgeComponent.new(text: "Unassigned", type: :danger) %> <% elsif @volunteer.active? %> Case was deactivated on: <%= I18n.l(assignment.casa_case.updated_at, format: :standard, default: nil) %> <% else %> Deactivated <% end %> <% if @volunteer.active? && assignment.active && assignment.casa_case.active? %> <%- if Pundit.policy(current_user, @volunteer).unassign_case? %> <%= button_to unassign_case_assignment_path(assignment, volunteer_id: @volunteer.id), method: :patch, class: "main-btn danger-btn btn-hover btn-sm" do %> Unassign Case <% end %> <%- end %> <% else %> None <% end %>
    <% end %> <% if @volunteer.active? %>

    Assign a New Case

    <%= form_with(model: CaseAssignment.new, url: case_assignments_path(volunteer_id: @volunteer.id)) do |form| %>
    <%= form.select :casa_case_id, grouped_options_for_assigning_case(@volunteer), {}, {class: "form-control select2"} %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm" ) do %> Assign Case <% end %> <% end %> <% end %>
    ================================================ FILE: app/views/volunteers/_manage_supervisor.erb ================================================

    Manage Supervisor

    <% if @volunteer.has_supervisor? %>
    Current Supervisor: <%= link_to(@volunteer.supervisor.display_name, edit_supervisor_path(@volunteer.supervisor.id)) %>
    <%= button_to unassign_supervisor_volunteer_path(@volunteer), method: :patch, class: "main-btn danger-btn btn-hover btn-sm" do %> Unassign from Supervisor <% end %> <% else %>

    Assign a Supervisor

    <%= form_with(model: SupervisorVolunteer.new, url: supervisor_volunteers_path(volunteer_id: @volunteer.id)) do |form| %>
    <%= form.hidden_field :volunteer_id, :value => @volunteer.id %> <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm" ) do %> Assign Supervisor <% end %> <% end %> <% end %>
    ================================================ FILE: app/views/volunteers/_notes.html.erb ================================================

    Notes

    <% if @volunteer.notes != [] %>

    Notes About This Volunteer

    <% @volunteer.notes.each do |note| %> <% end %>
    Note Creator Date Actions
    <%= note.content %> <%= note.creator.display_name %> <%= l(note.created_at, format: :standard) %> <%= link_to edit_volunteer_note_path(@volunteer, note), class: "main-btn primary-btn btn-hover btn-sm" do %> Edit <% end %> <%= link_to volunteer_note_path(@volunteer, note), class: "main-btn danger-btn btn-hover btn-sm", method: :delete do %> Delete <% end %>
    <% end %>
    <%= form_with(model: @volunteer.notes.new, local: true, url: volunteer_notes_path(@volunteer), id: "volunteer-note-form") do |form| %>

    Create a New Note

    <%= form.text_area :content, :rows => 5, placeholder: "Enter a note regarding the volunteer. These notes are only visible to CASA administrators and supervisors.", class: "form-control" %>
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm", id: "note-submit" ) do %> Save Note <% end %>
    <% end %>
    ================================================ FILE: app/views/volunteers/_send_reminder_button.html.erb ================================================
    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm mr-3", id: "reminder_button", data_toggle: "tooltip", title: "Remind volunteer to input case contacts".to_s ) do %> Send Reminder <% end %>
    <%= check_box_tag 'with_cc', 1, false, class: 'form-check-input' %> <%= label_tag 'with_cc', 'Send CC to Supervisor and Admin', class: 'unbold form-check-label ml-2' %>
    ================================================ FILE: app/views/volunteers/_volunteer_reminder_form.erb ================================================ <%= form_with url: reminder_volunteer_path(volunteer), method: :patch, id: "cc-check" do %> <%= render "volunteers/send_reminder_button", volunteer: volunteer %> <% end %> ================================================ FILE: app/views/volunteers/edit.html.erb ================================================ <%= link_to 'Back', volunteers_path, class: "my-2" %>

    Editing Volunteer

    <%= render "volunteer_reminder_form", volunteer: @volunteer %>
    <%= link_to impersonate_volunteer_path(@volunteer), class: "main-btn primary-btn btn-hover casa-case-button btn-sm my-3" do %> Impersonate <% end %>
    <%= form_with(model: @volunteer, url: volunteer_path(@volunteer), method: :patch) do |form| %> <%= render "/shared/error_messages", resource: @volunteer %> <%= render "/shared/edit_form", resource: @volunteer, f: form %> <%= render "/shared/invite_login", resource: @volunteer %>

    <%= render "manage_active", user: @volunteer %>

    <%= button_tag( type: "submit", class: "main-btn primary-btn btn-hover btn-sm my-1" ) do %> Submit <% end %>
    <% end %>
    <%= render 'notes' %> <%= render 'manage_cases' %> <%= render 'manage_supervisor' %> ================================================ FILE: app/views/volunteers/index.html.erb ================================================

    Volunteers

    Filter by:

    <%= form_with(url: bulk_assignment_supervisor_volunteers_path, id: "form-bulk-assignment", data: { controller: "disable-form", "disable-form-unallowed-value": '["unselected"]', "disable-form-disabled-class": "deactive-btn", "disable-form-enabled-class": "btn-hover dark-btn" }) do |form| %>
    Name Email Supervisor Status Assigned To Transition Aged Youth Case Number(s) Last Attempted Contact Contacts Made in Past 60 Days Hours spent in last 30 days Extra Languages Actions
    <% end %>
    ================================================ FILE: app/views/volunteers/new.html.erb ================================================

    Create New Volunteer

    <%= render 'form', volunteer: @volunteer %>
    ================================================ FILE: app.json ================================================ { "name": "CASA", "addons": [ "papertrail:choklad", "scheduler:standard", { "plan": "heroku-postgresql", "options": { "version": "12" } } ], "buildpacks": [ { "url": "heroku/ruby" } ], "formation": { "web": { "quantity": 1, "size": "hobby" } }, "scripts": { "postdeploy": "bundle exec rake db:migrate db:seed" }, "stack": "heroku-20" } ================================================ FILE: babel.config.js ================================================ module.exports = { presets: ['@babel/preset-env'] } ================================================ FILE: bin/asset_bundling_scripts/build_js.js ================================================ #!/usr/bin/env node const CLIArgs = process.argv.slice(2) const isWatching = CLIArgs.includes('--watch') const esbuild = require('esbuild') const logger = require('./logger.js') const watchingConsoleLogger = [{ name: 'watching-console-logger', setup (build) { build.onEnd(result => { if (result.errors.length) { logger.error('watch build failed:') logger.error(` build failed with ${result.errors.length} errors`) for (const error of result.errors) { logger.error(' Error:') logger.error(JSON.stringify(error, null, 2)) } } else { logger.info('watch build succeeded:') logger.info(JSON.stringify(result, null, 2)) } }) } }] async function main () { const context = await esbuild.context({ entryPoints: ['app/javascript/application.js', 'app/javascript/all_casa_admin.js'], outdir: 'app/assets/builds', bundle: true, plugins: watchingConsoleLogger }) if (isWatching) { await context.watch() } else { await context.rebuild() await context.dispose() } } main().catch((e) => { console.error(e.message) process.exit(1) }) ================================================ FILE: bin/asset_bundling_scripts/logger.js ================================================ const defaultText = '\x1b[0m' // const bright = '\x1b[1m' // const dim = '\x1b[2m' // const underscore = '\x1b[4m' // const blink = '\x1b[5m' // const reverse = '\x1b[7m' // const hidden = '\x1b[8m' // const textBlack = '\x1b[30m' const textRed = '\x1b[31m' // const textGreen = '\x1b[32m' const textYellow = '\x1b[33m' // const textBlue = '\x1b[34m' // const textMagenta = '\x1b[35m' const textCyan = '\x1b[36m' // const textWhite = '\x1b[37m' // const highlightBlack = '\x1b[40m' // const highlightRed = '\x1b[41m' // const highlightGreen = '\x1b[42m' // const highlightYellow = '\x1b[43m' // const highlightBlue = '\x1b[44m' // const highlightMagenta = '\x1b[45m' // const highlightCyan = '\x1b[46m' // const highlightWhite = '\x1b[47m' module.exports = { error: function (message) { if (typeof message !== 'string') { throw new TypeError('Param message must be a string') } console.error(textRed + message + defaultText) }, info: function (message) { if (typeof message !== 'string') { throw new TypeError('Param message must be a string') } console.log(textCyan + message + defaultText) }, warn: function (message) { if (typeof message !== 'string') { throw new TypeError('Param message must be a string') } console.warn(textYellow + message + defaultText) } } ================================================ FILE: bin/brakeman ================================================ #!/usr/bin/env ruby require "rubygems" require "bundler/setup" ARGV.unshift("--ensure-latest") load Gem.bin_path("brakeman", "brakeman") ================================================ FILE: bin/bundle ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'bundle' is installed as part of a gem, and # this file is here to facilitate running it. # require 'rubygems' m = Module.new do module_function def invoked_as_script? File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) end def env_var_version ENV['BUNDLER_VERSION'] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs unless 'update'.start_with?(ARGV.first || ' ') return end # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN bundler_version = a end next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ bundler_version = Regexp.last_match(1) update_index = i end bundler_version end def gemfile gemfile = ENV['BUNDLE_GEMFILE'] return gemfile if gemfile && !gemfile.empty? File.expand_path('../Gemfile', __dir__) end def lockfile lockfile = case File.basename(gemfile) when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) else "#{gemfile}.lock" end File.expand_path(lockfile) end def lockfile_version return unless File.file?(lockfile) lockfile_contents = File.read(lockfile) unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ return end Regexp.last_match(1) end def bundler_version @bundler_version ||= env_var_version || cli_arg_version || lockfile_version end def bundler_requirement return "#{Gem::Requirement.default}.a" unless bundler_version bundler_gem_version = Gem::Version.new(bundler_version) requirement = bundler_gem_version.approximate_recommendation return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0') requirement += '.a' if bundler_gem_version.prerelease? requirement end def load_bundler! ENV['BUNDLE_GEMFILE'] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do gem 'bundler', bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do require 'bundler/version' end if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) return end warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" exit 42 end def activation_error_handling yield nil rescue StandardError, LoadError => e e end end m.load_bundler! load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? ================================================ FILE: bin/delayed_job ================================================ #!/usr/bin/env ruby require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) require 'delayed/command' Delayed::Command.new(ARGV).daemonize ================================================ FILE: bin/dev ================================================ #!/usr/bin/env bash if ! command -v foreman &> /dev/null then echo "Installing foreman..." gem install foreman fi foreman start -f Procfile.dev "$@" ================================================ FILE: bin/git_hooks/README.md ================================================ This folder contains helper scripts for [Git hooks](https://www.atlassian.com/git/tutorials/git-hooks). To create a hook, make a file inside the directory `.git/hooks/` with the name of the hook you want to set up. The file names will **not** include an extension like `.md` or `.txt`. For example, if you want to set up a pre-commit hook, the file should be called `pre-commit`, or if you want a pre-push hook, it should be called `pre-push`. Assuming you're on a unix based operating system, make sure the file is executable using `chmod +x `. Once you've created that file, put the appropriate she-bang on the first line: ```bash #!/bin/sh ``` followed by the script you want to run and any arguments or flags for that script. See [Example Hooks](#example-hooks) below for how you might want to set these up. You can read more about Git hooks [here](https://git-scm.com/docs/githooks). ## Hook Scripts ### `build-assets` Compiles js and css to be served by your local webserver Usage: `./build-assets` ### `lint` Lints files on the current branch Usage: `./lint ` + ``(optional) can be one of the the following - `--staged` lints the files staged for commit - `--unpushed` lints files changed by commits not yet pushed to origin - `--all` (default) lints all files in the repo ### `migrate-all` Runs all migrations if any are found to be down Usage: `./migrate-all` ### `update-dependences` Installs dependencies if any are missing Usage: `./update-dependencies` ### `update-branch` Updates the `main` and current branch by rebasing your commits on top of changes from the official casa repo This script assumes no commits were made directly to main Usage: `./update-branch ` + `` is the name of the [remote](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) pointing to the official casa repo ## Example Hooks ### pre-push #!/bin/sh ./bin/git_hooks/update-branch actual_casa ./bin/git_hooks/lint --unpushed ### post-checkout #!/bin/sh ./bin/git_hooks/update-dependencies ./bin/git_hooks/build-assets ### post-merge, post-rewrite #!/bin/sh ./bin/git_hooks/update-dependencies ./bin/git_hooks/build-assets # migrate-all has to come after update-dependencies because it relies on bundle ./bin/git_hooks/migrate-all ## Disabling Hook Scripts If you ever need to disable a certain script from running in a hook, simply comment it out using `#`. Example: #!/bin/sh ./bin/git_hooks/update-branch actual_casa #./bin/git_hooks/lint --unpushed # <-- This hook is now disabled ================================================ FILE: bin/git_hooks/build-assets ================================================ #!/bin/sh # Builds assets(js and css) # Usage: # ./build-assets repo_root="$(git rev-parse --show-toplevel)" . "$repo_root/bin/git_hooks/logger" log info "Building Assets" if ! [ -x "$(command -v npm)" ]; then log error "Command npm could not be found" exit 1 fi npm run build npm run build:css ================================================ FILE: bin/git_hooks/lint ================================================ #!/bin/sh # Lints files on the current branch # Usage: # lint # --staged # lints if the files staged for commit contain lintable files # --unpushed # lints files changed by commits not yet pushed to origin # --all (default) # lints all files in the repo repo_root="$(git rev-parse --show-toplevel)" . "$repo_root/bin/git_hooks/logger" log info "Attempting to lint" current_branch="$(git branch --show-current)" if [ $# -lt 1 ]; then diff_policy="--all" else diff_policy=$1 fi case $diff_policy in --all) changed_file_list="app/models/a.rb\nb.erb\nc.js" ;; --staged) changed_file_list=$(git diff --name-only --cached) ;; --unpushed) if [ -z "$(git ls-remote --heads origin ${current_branch})" ]; then changed_file_list=$(git diff --name-only HEAD~$(git cherry -v main ${current_branch} | wc -l) HEAD) else changed_file_list=$(git diff --name-only origin/${current_branch}..HEAD) fi ;; *) log error "unknown option $diff_policy" exit 1 ;; esac erb_changed_count=$(echo "$changed_file_list" | grep -c "\.erb$") factory_changed_count=$(echo "$changed_file_list" | grep -c -E "(app\/models|spec\/factories)\/.*\.rb$") js_changed_count=$(echo "$changed_file_list" | grep -c "\.js$") rb_changed_count=$(echo "$changed_file_list" | grep -c "\.rb$") lint_time=$(date +%s) if test $erb_changed_count -gt 0; then log info "Linting via erblint" cd $repo_root/app if ! [ -x "$(command -v bundle)" ]; then log error "Command bundle could not be found" exit 1 else if ! bundle exec erb_lint --lint-all --autocorrect ; then log error "ERB Lint linting failed, could not fix 1 or more issues\n See above output for more details" exit 1 fi fi fi if test $factory_changed_count -gt 0; then log info "Linting via factory:lint" cd $repo_root/app if ! [ -x "$(command -v bundle)" ]; then log error "Command bundle could not be found" exit 1 else if ! bundle exec rake factory_bot:lint; then log error "Factory bot linting failed, could not fix 1 or more issues\n See above output for more details" exit 1 fi fi fi if test $js_changed_count -gt 0; then log info "Linting javasript via standard" cd $repo_root/app if ! [ -x "$(command -v npm)" ]; then log error "Command npm could not be found" exit 1 else if ! npm run lint:fix ; then log error "npm linting failed, could not fix 1 or more issues\n See above output for more details" exit 1 fi fi fi if test $rb_changed_count -gt 0; then log info "Linting via standardrb" cd $repo_root if ! [ -x "$(command -v bundle)" ]; then log error "Command bundle could not be found" exit 1 else if ! bundle exec standardrb --fix ; then log error "Standard linting failed, could not fix 1 or more issues\n See above output for more details" exit 1 fi fi fi cd $repo_root for file in $(git diff --name-only); do last_modified_time=$(date -r $file +%s) if [ $last_modified_time -ge $lint_time ] ; then log warn "Some files were linted. Please preserve the changes before continuing.\n Run git diff to view linter changes." exit 1 fi done log success "No files were linted" ================================================ FILE: bin/git_hooks/logger ================================================ #!/bin/sh # Colorized output with logging levels # Set colors if available if test -t 1; then # if terminal ncolors=$(which tput > /dev/null && tput colors) # supports color if test -n "$ncolors" && test $ncolors -ge 8; then normal="$(tput sgr0)" red="$(tput setaf 1)" green="$(tput setaf 2)" yellow="$(tput setaf 3)" cyan="$(tput setaf 6)" fi fi # Colorized output # Param $1 string | The logging level: info, warning, or error # Param $2 string | The message to be logged log () { if [ $# -lt 2 ]; then echo "${red}ERROR: function log was run with insufficient parameters ${normal}" return fi case $1 in success) printf "${green}SUCCESS: $2 ${normal}\n" ;; info) printf "${cyan}INFO: $2 ${normal}\n" ;; warn) printf "${yellow}WARNING: $2 ${normal}\n" ;; error) printf "${red}ERROR: $2 ${normal}\n" ;; *) echo "${red}ERROR: Unrecognized log level: $1 ${normal}" ;; esac } ================================================ FILE: bin/git_hooks/migrate-all ================================================ #!/bin/sh # Runs migrations if any are found to be down # Usage: # ./migrate-all repo_root="$(git rev-parse --show-toplevel)" . "$repo_root/bin/git_hooks/logger" log info "Checking for down migrations" if ! [ -x "$(command -v bundle)" ]; then log error "Command bundle could not be found" exit 1 fi if bundle exec rails db:migrate:status | grep " down"; then log info "Down migrations found. Migrating..." bundle exec rails db:migrate else log success "No down migrations found" fi ================================================ FILE: bin/git_hooks/update-branch ================================================ #!/bin/sh # Updates the `main` and current branch by rebasing your commits on top of changes from the official casa repo # Usage: # update-branch # # The name of the remote pointing to the official casa repo repo_root="$(git rev-parse --show-toplevel)" . "$repo_root/bin/git_hooks/logger" log info "Attempting to update local repo" if [ $# -lt 1 ]; then log error "Missing required arg " exit 1 fi upstream_remote=$1 branch_to_update="$(git branch --show-current)" if test -z "$(git branch --list ${branch_to_update})"; then log error "Could not find branch $branch_to_update" exit 1 fi log info "Fetching updates from upsteam" git fetch $upstream_remote log info "Updating main" git checkout main git merge --ff-only $upstream_remote/main if test $branch_to_update != "main"; then log info "Updating $branch_to_update" git checkout $branch_to_update git rebase -r $upstream_remote/main fi ================================================ FILE: bin/git_hooks/update-dependencies ================================================ #!/bin/sh # Installs dependencies if any are missing # Usage: # update-dependencies # no arguments repo_root="$(git rev-parse --show-toplevel)" . "$repo_root/bin/git_hooks/logger" log info "Checking rails dependency status" if ! [ -x "$(command -v bundle)" ]; then log error "Command bundle could not be found" exit 1 fi if ! [ -x "$(command -v npm)" ]; then log error "Command npm could not be found" exit 1 fi if ! bundle check; then log info "Updating rails dependencies" bundle install else log success "Dependencies up to date" fi log info "Checking javascript dependency status" if [ $(git diff HEAD@{1}..HEAD@{0} -- "package-lock.json" | wc -l) -gt 0 ] || [ $(git diff HEAD@{1}..HEAD@{0} -- "package.json" | wc -l) -gt 0 ]; then log info "Updating JavaScript dependencies" npm install fi ================================================ FILE: bin/lint ================================================ #!/usr/bin/env bash bundle exec standardrb --fix --format progress bundle exec erb_lint --lint-all --autocorrect npm run lint:fix echo "Linting Factories" rails factory_bot:lint ================================================ FILE: bin/login ================================================ #!/usr/bin/env ruby # See HELP_TEXT below for description. require 'webdrivers' require 'capybara/dsl' class LoginExecutor include Capybara::DSL Capybara.default_driver = :selenium DEFAULT_LOGIN_URL = ENV['CASA_DEFAULT_LOGIN_URL'] || 'http://localhost:3000' ALL_CASA_ADMIN_LOGIN_URL = ENV['ALL_CASA_ADMIN_LOGIN_URL'] || 'http://localhost:3000/all_casa_admins/sign_in' User = Struct.new(:email, :url) USERS = [ User.new('volunteer1@example.com', DEFAULT_LOGIN_URL), User.new('supervisor1@example.com', DEFAULT_LOGIN_URL), User.new('casa_admin1@example.com', DEFAULT_LOGIN_URL), User.new('other_casa_admin@example.com', DEFAULT_LOGIN_URL), User.new('other.supervisor@example.com', DEFAULT_LOGIN_URL), User.new('other.volunteer@example.com', DEFAULT_LOGIN_URL), User.new('allcasaadmin@example.com', ALL_CASA_ADMIN_LOGIN_URL), ] HELP_TEXT = <<~HEREDOC Usage: bin/login [user_number] This script automates login for experimentation with the users added to the application when it is seeded in development mode. If executed without any arguments, it outputs the available users and accepts a numbered choice. It then logs in as that choice, at the URL appropriate for that user. It can also be executed with the user list element number passed as an argument, to bypass interactive mode. The browser window remains open as long as the script has not yet terminated (using Ctrl-C). You can either keep a terminal with this script open, or you can send it to the background with Ctrl-Z. If the latter, when you are finished using the browser, you can bring the script back to the foreground with `fg[Enter]`. HEREDOC def self.login self.new.call end def call if ARGV.first == '-h' puts HELP_TEXT exit 0 end user = ARGV.empty? ? get_user_from_input : get_user_from_arg puts "\nLogging in to #{user.url} as #{user.email}...\n\n" visit_and_log_in(user) print_post_open_message_and_wait end private # ----------------------------- all methods below are private ---------------- def visit_and_log_in(user) visit user.url fill_in "Email", with: user.email fill_in "Password", with: "12345678" click_on "Log in" end def get_user_from_arg arg = ARGV.first.strip user = USERS[arg.to_i - 1] unless user puts "\nInvalid option: #{arg}. Must be a number between 1 and #{USERS.size}" exit -1 end user end def get_user_from_input puts "With which user would you like to log in to CASA?\n\n" USERS.each_with_index do |user, index| puts "#{index + 1}) #{user.email}" end puts "\nInput a number, then [Enter]. An invalid entry will exit." choice = $stdin.gets.chomp.to_i exit unless (1..(USERS.size)).include?(choice) USERS[choice - 1] end def print_post_open_message_and_wait loop do puts <<~HEREDOC -------------------------------------------------------------------------------- Press Ctrl-C to exit and close browser. To move this script to the background so you can continue using your terminal: Press Ctrl-Z. When you are done, type fg[Enter] and then Ctrl-C. HEREDOC if ARGV.empty? puts "\nNext time you can pass the user number on the command line if you like.\n\n" end $stdin.gets end end end LoginExecutor.login ================================================ FILE: bin/npm ================================================ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do npm = ENV["PATH"].split(File::PATH_SEPARATOR). select { |dir| File.expand_path(dir) != __dir__ }. product(["npm", "npm.cmd", "npm.ps1"]). map { |dir, file| File.expand_path(file, dir) }. find { |file| File.executable?(file) } if npm command = ARGV.empty? ? ["install"] : ARGV exec npm, *command else $stderr.puts "npm executable was not detected in the system." $stderr.puts "Download npm by installing Node.js from https://nodejs.org/" exit 1 end end ================================================ FILE: bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: bin/rspec ================================================ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) rescue LoadError => e raise unless e.message.include?('spring') end require 'bundler/setup' load Gem.bin_path('rspec-core', 'rspec') ================================================ FILE: bin/setup ================================================ #!/usr/bin/env ruby require 'fileutils' # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args, exception: true) || abort("\n== Command #{args} failed ==") end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. expected_ruby_version = `cat .ruby-version`.chomp current_ruby_version = `ruby -v`.chomp unless current_ruby_version.include?(expected_ruby_version) puts "Ruby version must be #{expected_ruby_version}. You are on #{current_ruby_version}" exit end puts "\n== Installing dependencies ==" system! 'gem install foreman' system! 'gem install bundler --conservative' system!('bundle update --bundler --verbose') system!('bundle check') || system!('bundle install') # puts '\n== Copying sample files ==' # unless File.exist?('config/database.yml') # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end unless File.exist?('.env') puts "\n== Setup .env file from .env.example ==" system!('cp .env.example .env') end puts "\n== Preparing database ==" puts '⚠️ If you use docker to run postgres, make sure your database is running ⚠️' system! 'bin/rails db:reset' system! 'bin/rails db:test:prepare' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' puts "\n== Running post-deployment tasks ==" system! 'bin/rake after_party:run' puts "\n== Installing npm packages ==" system!('npm ci') || abort('install npm and try again') puts "\n== Building assets ==" system!('npm run build') system!('npm run build:css') puts "\n== Done ==" end ================================================ FILE: bin/spring ================================================ #!/usr/bin/env ruby if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) gem "bundler" require "bundler" # Load Spring without loading other gems in the Gemfile, for speed. Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path gem "spring", spring.version require "spring/binstub" rescue Gem::LoadError # Ignore when Spring is not installed. end end ================================================ FILE: bin/update ================================================ #!/usr/bin/env ruby require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') puts 'Updating npm' system('npm install') || abort("Install npm and try again") puts "\n== Updating database ==" system! 'bin/rails db:migrate' puts "\n== Running post-deployment tasks ==" system! 'bin/rake after_party:run' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' puts "\n== Building assets ==" system('npm run build') || abort("Failed to build assets. Ensure npm is installed and try again.") system('npm run build:css') || abort("Failed to build CSS assets. Ensure npm is installed and try again.") puts "\n== Done ==" end ================================================ FILE: cc-test-reporter ================================================ [File too large to display: 12.4 MB] ================================================ FILE: code-of-conduct.md ================================================ [View our Code of Conduct here!](./doc/code-of-conduct.md) ================================================ FILE: config/application.rb ================================================ require_relative "boot" require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Casa class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.2 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets generators tasks mailers]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") config.action_mailer.preview_paths << (defined?(Rails.root) ? Rails.root.join("lib/mailers/previews") : nil) config.eager_load_paths << Rails.root.join("app/lib/importers") config.assets.paths << Rails.root.join("app/assets/webfonts") config.active_storage.variant_processor = :mini_magick config.active_storage.content_types_to_serve_as_binary.delete("image/svg+xml") config.serve_static_assets = true # to use ViewComponent previews config.view_component.previews.paths << "#{Rails.root.join("spec/components/previews")}" end end ================================================ FILE: config/boot.rb ================================================ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. ================================================ FILE: config/brakeman.ignore ================================================ { "ignored_warnings": [ { "warning_type": "Dynamic Render Path", "warning_code": 15, "fingerprint": "82ef033042422190ef49507207d51ed6ccd9593483630925baf0bf6c5e65033e", "check_name": "Render", "message": "Render path contains parameter value", "file": "app/controllers/static_controller.rb", "line": 21, "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "code": "render(template => \"static/#{params[:name]}\", {})", "render_path": null, "location": { "type": "method", "class": "StaticController", "method": "page" }, "user_input": "params[:name]", "confidence": "Medium", "cwe_id": [ 22 ], "note": "" }, { "warning_type": "SQL Injection", "warning_code": 0, "fingerprint": "b75b292df5ec0c3d4d4f307a8ff2a18caecd456a9b3c9c62bb59d7cf3b67a562", "check_name": "SQL", "message": "Possible SQL injection", "file": "app/datatables/supervisor_datatable.rb", "line": 45, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "Arel.sql(\"COALESCE(users.display_name, users.email) #{order_direction}\")", "render_path": null, "location": { "type": "method", "class": "SupervisorDatatable", "method": "order_clause" }, "user_input": "order_direction", "confidence": "Medium", "cwe_id": [ 89 ], "note": "We have a filter for this variable here: app/datatables/application_datatable.rbapp/datatables/application_datatable.rb:72" } ], "updated": "2023-01-17 23:08:51 -0500", "brakeman_version": "5.4.0" } ================================================ FILE: config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: casa_production ================================================ FILE: config/credentials/development.key ================================================ f66bde702577ef605d3e69abc69798f8 ================================================ FILE: config/credentials/development.yml.enc ================================================ aewvdbZoQz8v7s3UlJ/+XOIrxpj1/nP2/dA7FkLGvTgmu8lZrnyecC19sDE6bcZN4XsnIqDomjSg/CL8TefHKXOsaoNNKmW8YPVfoH8AmlqXxvJduiZNuXlOcf7SR01E7E0r1VIdRga6g9KtOHBbgtc6hQyOs/2ajSxbD3gY5IFWnWNHIqMEWMUMy/PXtSSxUr+FdNCgdod9Rx0EEiecfEz1tMBP/V69dRwSrM5yfTeogkUPpOqReFisTbn9f0yolmNhhxo7nPoPzyeEcGHl4+maS1GHa6uYQ2n2d2t34FmhcDttI+rV7ITU9LmuwVcjgCE9fPxMUZ9bX2UBUEHialBZ8S+izXyBAKGTvbQw+/Wk9KNT98Tl3Gg=--BRmMgMTOgyAZUyw4--2OyLty/a3xH0OjlI0sf9Yw== ================================================ FILE: config/credentials/production.yml.enc ================================================ Y85/mpXDwuasGg7odbPklWamaevc8nlJJExz5YTnGBR/S5wCGTkkH7hwUBPINNTkYXpe3N1QN2ILq+NuGqTJhgtpy6Glwnk743UlxsFUChUQmSlccJfML1YQWKx3R1VSAFWjOoHiV9vnA0RcKf9tIQ5FWY5S8/IMn4SlpRbhU0zJfkHAb3Hbx5juTWmOWHh4BD+AmKYqu9lpVCCwQkitRJX/8AwLUBprbcaHm0nh9u4PL/Q/Fj0/UadHF2RCWDnpRxUAygqgMpCBxqZwpHzN3cRrATL3aDYFZt0CZzpkpnHBKQcuSs1K/oki06Jas+Z585iO00S6r4NRJtkPVkznFqu0DJ8JkUKriLDXOwbvcEHOHtCS0t2hCxYLPZvSg51qBsZbgS5No3+cfXPeGjIecq+h1c7zpg70PNieO6290IM/jpNYA9ZEt9nPXwUodAauMkvcocXlOxX4mOaYsL7fMiXhgu/JzqwK64maSXpCBaEEPcOwIkaKhTwn+w+iABnTYnhZeD2t3TMoFpzkI83w7cg9Mbetm1N9faX43dsYXRW93tFwSkJtF4b//CIxOyG9xFxruGNwnBqy8+IuoaSyBVF3WecnWjj+7TGSgmgl7FmiaUSjDDJkh27K8QDHbpevQM/Rc5U=--8GtVk0JHu3Dkz4jI--/FAbx4kRZsAKWLg9/+IWbw== ================================================ FILE: config/credentials/qa.key ================================================ f66bde702577ef605d3e69abc69798f8 ================================================ FILE: config/credentials/qa.yml.enc ================================================ kCD5s9jBDZZfXdKirYJD95LfwF3xeWi2E+OGS9x9/8EsI6WMzl+jNUjuBQt/fUdO5R5pCHXXPCYPzsGnDyTKJJczqszDXDsG2xS5eSImw9O7BGmNDh7W1sQ5kMmjZPUUu9GaCcAjmnvh0ImXM2X+Lo/5X3NeAZ7FllFmUd3CaBD5J3N3mbXlJgVGRylabaKABqrZqbRnZK5QDOcjwxYspsnu3m9M2qx5B7m2RnpKdytDpxbRtGSj34cZmH0C+rGOCQUm9rSR5wrQVmmJ5DdElYAnM7lo6bUTf2W5wbd51TsrEATPCHx1iK89ot/alUIjmHcoMlUstLnSX8VQzQeIluqa7a7o2IN/p++vknWELVlYROn67wiRTrLPXeDy+NkhiYkhQCBl3AIL8qs92tKX9xwvM75q5pGwsxQneGM5IjOSHUnEmw6j0f6B0R1oksOGt/xg9VP73ANP3zxqNTnbzRNoqO9wVVju0jJVt8KGoXVzdQx2qQSU5ZqUulJ7v88Xjro7Ew6mSY5gh4itzzsVIF7kAEJM20eh2WWxNEDMZyzvKDD34XWxOb8nks9MF0yCJzWBIog=--m9KJMwI1N3qSf0ry--d64JLH6L2BJNNIW2DdMR6w== ================================================ FILE: config/credentials/test.key ================================================ 5dfb1ed8adf6c5264039101150e400ec ================================================ FILE: config/credentials/test.yml.enc ================================================ zsvjeQzFV0vM7h3jsx7RUyA3WTQsEYWvEMreEvVbi4L0HN7sDNrP7kay2FZS5VEwrW5mJZdnu63BXa8fK/h1agvaaiOO9FhDXfyK+VT86TAfsLa1gsBK9mHjWSdXCJE9TZj9OjOtR3R/qHOI+2uPUnysh7Om4a0ckiu4Jwex3OcbgCYj2+G2JQtwHkWhlyBthGxLjuDDFfx+qxkJWkN7V9FhN0FkkPaflyj4FjR9BUf3/CB8pvHXqJ1lmxVScYsyhh50mc+CKyVptpqbLi9Jou3SiUePREX03ynV0KPR+7mT3FH9gCj4QyzzS1t3JOUfrgqeVFAzdV1TW01olinOyG2aMrZn1aA7GWfDeIr/GwnaPfUMmZNj4RQ=--KmPWCR7xHr6jUSx5--1L2S0bUzD2Bc+JFAHX7xKg== ================================================ FILE: config/database.yml ================================================ default: &default adapter: postgresql encoding: unicode username: <%= ENV.fetch("POSTGRES_USER") { } %> pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %> port: <%= ENV.fetch("POSTGRES_PORT", "5432") %> password: <%= ENV.fetch("POSTGRES_PASSWORD") { 'password' } %> development: <<: *default database: casa_development host: <%= ENV.fetch("DATABASE_HOST") { } %> test: <<: *default database: casa_test<%= ENV["TEST_ENV_NUMBER"] %> host: <%= ENV.fetch("DATABASE_HOST") { } %> production: <<: *default database: casa_production username: casa password: <%= ENV['CASA_DATABASE_PASSWORD'] %> ================================================ FILE: config/docker.env ================================================ DOCKER=true DATABASE_HOST=database POSTGRES_USER=postgres POSTGRES_HOST_AUTH_METHOD=trust ================================================ FILE: config/environment.rb ================================================ # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: config/environments/development.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.default_url_options = {host: "localhost", port: 3000} config.action_mailer.delivery_method = :letter_opener config.action_mailer.perform_deliveries = true config.action_mailer.perform_caching = false config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true # Suppress logger output for asset requests. config.assets.quiet = true config.assets.digest = false # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Prosopite N+1 query detection config.prosopite_enabled = true config.prosopite_min_n_queries = 5 # More lenient for development # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true config.hosts << ENV["DEV_HOSTS"] config.hosts << ".app.github.dev" if ENV["CODESPACES"] === "true" config.action_controller.forgery_protection_origin_check = false end # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = false end ================================================ FILE: config/environments/production.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. config.action_mailer.default_url_options = {host: ENV["DOMAIN"]} config.action_mailer.raise_delivery_errors = true config.action_mailer.show_previews = ENV["APP_ENVIRONMENT"] != "production" # Do not send emails in staging or qa config.action_mailer.perform_deliveries = ENV["APP_ENVIRONMENT"] == "production" config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: "smtp-relay.sendinblue.com", port: 587, user_name: ENV["SENDINBLUE_EMAIL"], password: ENV["SENDINBLUE_PASSWORD"], authentication: "login", enable_starttls_auto: true } # Code is not reloaded between requests. config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). config.require_master_key = true # Enable static file serving from the `/public` folder (turn off if using NGINX/Apache for it). config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. # config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :microsoft # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. # config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Log to STDOUT by default config.logger = ActiveSupport::Logger.new($stdout) .tap { |logger| logger.formatter = ::Logger::Formatter.new } .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [:request_id] # Info include generic and useful information about system operation, but avoids logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). If you # want to log everything, set the level to "debug". config.log_level = :info # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). config.active_job.queue_adapter = :delayed_job # production.rb # config.active_job.queue_name_prefix = "casa_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Don't log any deprecations. config.active_support.report_deprecations = false # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ # "example.com", # Allow requests from example.com # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end ================================================ FILE: config/environments/test.rb ================================================ require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. config.action_mailer.default_url_options = {host: "localhost", port: 3000} # for devise authentication # While tests run files are not watched, reloading is not necessary. # Turn false under Spring and add config.action_view.cache_template_loading = true. config.action_view.cache_template_loading = true # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? # cache classes on CI, but enable reloading for local work (bin/rspec) config.enable_reloading = ENV["CI"].blank? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = :none # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Rack Attack configuration # Set IP_BLOCKLIST for testing. Can't stub in spec since environment variable # gets read during application initialization. ENV["IP_BLOCKLIST"] = "4.5.6.7, 9.8.7.6,100.101.102.103" # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raises error for missing translations. config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = false # https://github.com/rails/rails/issues/48468 config.active_job.queue_adapter = :test config.secret_key_base = ENV["SECRET_KEY_BASE"] || "dummy_test_secret_key" end ================================================ FILE: config/initializers/after_party.rb ================================================ AfterParty.setup do |config| # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. require "after_party/active_record" end ================================================ FILE: config/initializers/all_casa_admin_access.rb ================================================ class CanAccessFlipperUI def self.matches?(request) session = request.env["warden"].raw_session.to_h session["warden.user.all_casa_admin.session"].present? end end ================================================ FILE: config/initializers/application_controller_renderer.rb ================================================ # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: config/initializers/authtrail.rb ================================================ # set to true for geocoding (and add the geocoder gem to your Gemfile) # we recommend configuring local geocoding as well # see https://github.com/ankane/authtrail#geocoding AuthTrail.geocode = false # add or modify data # AuthTrail.transform_method = lambda do |data, request| # data[:request_id] = request.request_id # end # exclude certain attempts from tracking # AuthTrail.exclude_method = lambda do |data| # data[:identity] == "capybara@example.org" # end ================================================ FILE: config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". # Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] ================================================ FILE: config/initializers/blueprinter.rb ================================================ require "oj" # you can skip this if OJ has already been required. Blueprinter.configure do |config| config.generator = Oj # default is JSON end ================================================ FILE: config/initializers/bugsnag.rb ================================================ if ENV["BUGSNAG_API_KEY"].present? Bugsnag.configure do |config| config.api_key = ENV["BUGSNAG_API_KEY"] config.ignore_classes << ActiveRecord::RecordNotFound config.release_stage = ENV["HEROKU_APP_NAME"] || ENV["APP_ENVIRONMENT"] callback = proc do |event| event.set_user(current_user&.id, current_user&.email) if defined?(current_user) end config.add_on_error(callback) end else Bugsnag.configuration.logger = ::Logger.new("/dev/null") end ================================================ FILE: config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap, inline scripts, and inline styles. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src style-src) # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end ================================================ FILE: config/initializers/cookies_serializer.rb ================================================ # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: config/initializers/cors.rb ================================================ # Be sure to restart your server when you modify this file. # Avoid CORS issues when API is called from the frontend app. # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests. # Read more: https://github.com/cyu/rack-cors Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "*" # make sure to change to domain name of frontend resource "/api/v1/*", headers: :any, methods: [:get, :post, :patch, :put, :delete, :options, :head] end end ================================================ FILE: config/initializers/date_formats.rb ================================================ Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") } Date::DATE_FORMATS[:slashes] = "%Y/%m/%d" ================================================ FILE: config/initializers/devise.rb ================================================ # frozen_string_literal: true # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. # Devise will use the `secret_key_base` as its `secret_key` # by default. You can change it below and use your own secret key. # config.secret_key = 'a42b150b56c158d6e064aa257c8434390c921811007f42c5aa16c7455b78e3777087c5a4bcf18bfb0886fdd9296dec34eaa02fc0b47bed58417347eba971ee2d' # Fixes rspec triggering Rails.application.secrets deprecation warning for Rails 7.1.0 upgrade config.secret_key = Rails.application.secret_key_base # ==> Controller configuration # Configure the parent class to the devise controllers. # config.parent_controller = 'DeviseController' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config.mailer_sender = "no-reply@#{ENV["DOMAIN"]}" # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' # Configure the parent class responsible to send e-mails. # config.parent_mailer = 'ActionMailer::Base' # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. require "devise/orm/active_record" # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is # just :email. You can configure it to use [:username, :subdomain], so for # authenticating a user, both parameters are required. Remember that those # parameters are used only when authenticating and not when retrieving from # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. # config.authentication_keys = [:email] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the # find_for_authentication method and considered in your model lookup. For instance, # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. # The same considerations mentioned for authentication_keys also apply to request_keys. # config.request_keys = [] # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. config.case_insensitive_keys = [:email] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. config.strip_whitespace_keys = [:email] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the # given strategies, for example, `config.params_authenticatable = [:database]` will # enable it only for database (email + password) authentication. # config.params_authenticatable = true # Tell if authentication through HTTP Auth is enabled. False by default. # It can be set to an array that will enable http authentication only for the # given strategies, for example, `config.http_authenticatable = [:database]` will # enable it only for database authentication. The supported strategies are: # :database = Support basic authentication with authentication key + password # config.http_authenticatable = false # If 401 status code should be returned for AJAX requests. True by default. # config.http_authenticatable_on_xhr = true # The realm used in Http Basic Authentication. 'Application' by default. # config.http_authentication_realm = 'Application' # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. # config.paranoid = true # By default Devise will store the user in session. You can skip storage for # particular strategies by setting this option. # Notice that if you are skipping storage for all authentication paths, you # may want to disable generating routes to Devise's sessions controller by # passing skip: :sessions to `devise_for` in your config/routes.rb config.skip_session_storage = [:http_auth] # By default, Devise cleans up the CSRF token on authentication to # avoid CSRF token fixation attacks. This means that, when using AJAX # requests for sign in and sign up, you need to get a new CSRF token # from the server. You can disable this option at your own risk. # config.clean_up_csrf_token_on_authentication = true # When false, Devise will not attempt to reload routes on eager load. # This can reduce the time taken to boot the app but if your application # requires the Devise mappings to be loaded during boot time the application # won't boot properly. # config.reload_routes = true # ==> Configuration for :database_authenticatable # For bcrypt, this is the cost for hashing the password and defaults to 11. If # using other algorithms, it sets how many times you want the password to be hashed. # # Limiting the stretches to just one in testing will increase the performance of # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use # a value less than 10 in other environments. Note that, for bcrypt (the default # algorithm), the cost increases exponentially with the number of stretches (e.g. # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). config.stretches = Rails.env.test? ? 1 : 11 # Set up a pepper to generate the hashed password. # config.pepper = 'cd6eaac91bb902fefeb87724dc41ea5023881a0b3044bbf4c23178147d2a8d883265a58105ab7c549a7bb5cf3a732f253ab4a17f26e44eefaeacb37f2ee13278' # Send a notification to the original email when the user's email is changed. config.send_email_changed_notification = true # Send a notification email when the user's password is changed. # config.send_password_change_notification = false # ==> Configuration for :invitable # The period the generated invitation token is valid. # After this period, the invited resource won't be able to accept the invitation. # When invite_for is 0 (the default), the invitation won't expire. # config.invite_for = 2.weeks # Number of invitations users can send. # - If invitation_limit is nil, there is no limit for invitations, users can # send unlimited invitations, invitation_limit column is not used. # - If invitation_limit is 0, users can't send invitations by default. # - If invitation_limit n > 0, users can send n invitations. # You can change invitation_limit column for some users so they can send more # or less invitations, even with global invitation_limit = 0 # Default: nil # config.invitation_limit = 5 # The key to be used to check existing users when sending an invitation # and the regexp used to test it when validate_on_invite is not set. # config.invite_key = { email: /\A[^@]+@[^@]+\z/ } # config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil } # Ensure that invited record is valid. # The invitation won't be sent if this check fails. # Default: false # config.validate_on_invite = true # Resend invitation if user with invited status is invited again # Default: true # config.resend_invitation = false # The class name of the inviting model. If this is nil, # the #invited_by association is declared to be polymorphic. # Default: nil # config.invited_by_class_name = 'User' # The foreign key to the inviting model (if invited_by_class_name is set) # Default: :invited_by_id # config.invited_by_foreign_key = :invited_by_id # The column name used for counter_cache column. If this is nil, # the #invited_by association is declared without counter_cache. # Default: nil # config.invited_by_counter_cache = :invitations_count # Auto-login after the user accepts the invite. If this is false, # the user will need to manually log in after accepting the invite. # Default: true # config.allow_insecure_sign_in_after_accept = false # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without # confirming their account. For instance, if set to 2.days, the user will be # able to access the website for two days without confirming their account, # access will be blocked just in the third day. # You can also set it to nil, which will allow the user to access the website # without confirming their account. # Default is 0.days, meaning the user cannot access the website without # confirming their account. # config.allow_unconfirmed_access_for = 2.days # A period that the user is allowed to confirm their account before their # token becomes invalid. For example, if set to 3.days, the user can confirm # their account within 3 days after the mail was sent, but on the fourth day # their account can't be confirmed with the token any more. # Default is nil, meaning there is no restriction on how long a user can take # before confirming their account. # config.confirm_within = 3.days # If true, requires any email changes to be confirmed (exactly the same way as # initial account confirmation) to be applied. Requires additional unconfirmed_email # db field (see migrations). Until confirmed, new email is stored in # unconfirmed_email column, and copied to email column on successful confirmation. config.reconfirmable = true # Defines which key will be used when confirming an account # config.confirmation_keys = [:email] # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks # Invalidates all the remember me tokens when the user signs out. # config.expire_all_remember_me_on_sign_out = true # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false # Options to be passed to the created cookie. For instance, you can set # secure: true in order to force SSL only cookies. # config.rememberable_options = {} # ==> Configuration for :validatable # Range for password length. config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly # to give user feedback and not to assert the e-mail validity. config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. config.timeout_in = 3.hours # ==> Configuration for :lockable # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. # :none = No lock strategy. You should handle locking by yourself. # config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account # config.unlock_keys = [:email] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. # config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. # config.maximum_attempts = 20 # Time interval to unlock the account if :time is enabled as unlock_strategy. # config.unlock_in = 1.hour # Warn on the last attempt before the account is locked. # config.last_attempt_warning = true # ==> Configuration for :recoverable # # Defines which key will be used when recovering the password for an account # config.reset_password_keys = [:email] # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. config.reset_password_within = 6.hours # When set to false, does not sign a user in automatically after their password is # reset. Defaults to true, so a user is signed in automatically after a reset. # config.sign_in_after_reset_password = true # ==> Configuration for :encryptable # Allow you to use another hashing or encryption algorithm besides bcrypt (default). # You can use :sha1, :sha512 or algorithms from others authentication tools as # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 # for default behavior) and :restful_authentication_sha1 (then you should set # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). # # Require the `devise-encryptable` gem when using anything other than bcrypt # config.encryptor = :sha512 # ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. config.scoped_views = true # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). config.default_scope = :user # Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. # config.sign_out_all_scopes = true # ==> Navigation configuration # Lists the formats that should be treated as navigational. Formats like # :html, should redirect to the sign in page when the user does not have # access, but formats like :xml or :json, should return 401. # # If you have any extra navigational formats, like :iphone or :mobile, you # should add them to the navigational formats lists. # # The "*/*" below is required to match Internet Explorer requests. config.navigational_formats = ["*/*", :html] # The default HTTP method used to sign out a resource. Default is :delete. config.sign_out_via = :get # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # # config.warden do |manager| # manager.intercept_401 = false # manager.default_strategies(scope: :user).unshift :some_external_strategy # end # ==> Mountable engine configurations # When using Devise inside an engine, let's call it `MyEngine`, and this engine # is mountable, there are some extra configurations to be taken into account. # The following options are available, assuming the engine is mounted as: # # mount MyEngine, at: '/my_engine' # # The router that invoked `devise_for`, in the example above, would be: # config.router_name = :my_engine # # When using OmniAuth, Devise cannot automatically set OmniAuth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' # ==> Turbolinks configuration # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: # # ActiveSupport.on_load(:devise_failure_app) do # include Turbolinks::Controller # end # ==> Configuration for :registerable # When set to false, does not sign a user in automatically after their password is # changed. Defaults to true, so a user is signed in automatically after changing a password. # config.sign_in_after_change_password = true end ================================================ FILE: config/initializers/extensions.rb ================================================ require "pdf_forms" require "ext/pdf_forms" ================================================ FILE: config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] ================================================ FILE: config/initializers/flipper.rb ================================================ ActiveSupport.on_load(:active_record) do require "flipper/adapters/active_record" Flipper.configure do |config| config.default do adapter = Flipper::Adapters::ActiveRecord.new Flipper.new(adapter) end end end ================================================ FILE: config/initializers/generators.rb ================================================ # Allows rails generators (scaffold/controller) to use custom policy generator. # Arguments for rails generators will be passed to the policy generator. # Options will be shown in the help text for the rails generators, # including the option to skip the policy generator (--skip-policy). module PolicyGenerator module ControllerGenerator extend ActiveSupport::Concern included do hook_for :policy, in: nil, default: true, type: :boolean do |generator| # use actions from controller invocation invoke generator, [name.singularize, *actions] end end end module ScaffoldControllerGenerator extend ActiveSupport::Concern included do hook_for :policy, in: nil, default: true, type: :boolean do |generator| # prevent attribute arguments (name:string) being confused with actions scaffold_actions = %w[index new create show edit update destroy] invoke generator, [name.singularize, *scaffold_actions] end end end end module ActiveModel class Railtie < Rails::Railtie generators do |app| Rails::Generators.configure! app.config.generators Rails::Generators::ControllerGenerator.include PolicyGenerator::ControllerGenerator Rails::Generators::ScaffoldControllerGenerator.include PolicyGenerator::ScaffoldControllerGenerator end end end ================================================ FILE: config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # 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| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "DSS" # TODO what is this for? end ================================================ FILE: config/initializers/lograge.rb ================================================ Rails.application.configure do config.lograge.enabled = true end ================================================ FILE: config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf Mime::Type.register "application/vnd.openxmlformats-officedocument.wordprocessingml.document", :docx ================================================ FILE: config/initializers/pagy.rb ================================================ require "pagy/extras/bootstrap" require "pagy/extras/size" Pagy::DEFAULT[:size] = [1, 3, 3, 1] Pagy::DEFAULT[:nav_class] = "pagy-bootstrap-nav" ================================================ FILE: config/initializers/permissions_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide HTTP permissions policy. For further # information see: https://developers.google.com/web/updates/2018/06/feature-policy # Rails.application.config.permissions_policy do |policy| # policy.camera :none # policy.gyroscope :none # policy.microphone :none # policy.usb :none # policy.fullscreen :self # policy.payment :self, "https://secure.example.com" # end ================================================ FILE: config/initializers/prosopite.rb ================================================ # frozen_string_literal: true # Rack middleware for development only — in test, scanning is handled by RSpec hooks if Rails.env.development? && Rails.configuration.respond_to?(:prosopite_enabled) && Rails.configuration.prosopite_enabled require "prosopite/middleware/rack" Rails.configuration.middleware.use(Prosopite::Middleware::Rack) end # Development configuration — test config lives in spec/support/prosopite.rb Rails.application.config.after_initialize do next unless Rails.env.development? Prosopite.enabled = Rails.configuration.respond_to?(:prosopite_enabled) && Rails.configuration.prosopite_enabled Prosopite.min_n_queries = Rails.configuration.respond_to?(:prosopite_min_n_queries) ? Rails.configuration.prosopite_min_n_queries : 2 Prosopite.rails_logger = true Prosopite.prosopite_logger = true end ================================================ FILE: config/initializers/rack_attack.rb ================================================ class Rack::Attack ### Configure Cache ### # If you don't want to use Rails.cache (Rack::Attack's default), then # configure it here. # # Note: The store is only used for throttling (not blocklisting and # safelisting). It must implement .increment and .write like # ActiveSupport::Cache::Store # Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new Rack::Attack.safelist("allow from localhost") do |req| # Requests are allowed if the return value is truthy req.ip == "127.0.0.1" || req.ip == "::1" end ### Throttle Spammy Clients ### # If any single client IP is making tons of requests, then they're # probably malicious or a poorly-configured scraper. Either way, they # don't deserve to hog all of the app server's CPU. Cut them off! # # Note: If you're serving assets through rack, those requests may be # counted by rack-attack and this throttle may be activated too # quickly. If so, enable the condition to exclude them from tracking. # Throttle all requests by IP (60rpm) # # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" throttle("req/ip", limit: 300, period: 5.minutes) do |req| req.ip unless req.path.start_with?("/packs") end ### Prevent Brute-Force Login Attacks ### # The most common brute-force login attack is a brute-force password # attack where an attacker simply tries a large number of emails and # passwords to see if any credentials match. # # Another common method of attack is to use a swarm of computers with # different IPs to try brute-forcing a password for a specific account. # Throttle POST requests to /xxxx/sign_in by IP address # # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" throttle("logins/ip", limit: 5, period: 20.seconds) do |req| if req.path =~ /sign_in/ && req.post? req.ip end end throttle("reg/ip", limit: 5, period: 20.seconds) do |req| req.ip if req.path.starts_with?("/api/v1") end # Throttle POST requests to /xxxx/sign_in by email param # # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}" # # Note: This creates a problem where a malicious user could intentionally # throttle logins for another user and force their login requests to be # denied, but that's not very common and shouldn't happen to you. (Knock # on wood!) throttle("logins/email", limit: 5, period: 20.seconds) do |req| if req.path =~ /sign_in/ && req.post? # return the email if present, nil otherwise req.params.dig("user", "email").presence || req.params.dig("all_casa_admin", "email").presence end end Rack::Attack.blocklist("fail2ban pentesters") do |req| # `filter` returns truthy value if request fails, or if it's from a # previously banned IP so the request is blocked Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 1.day) do # The count for the IP is incremented if the return value is truthy CGI.unescape(req.query_string) =~ %r{/etc/passwd} || req.path.match?(/etc\/passwd/) || req.path.match(/wp-admin/i) || req.path.match(/wp-login/i) || req.path.match(/php/i) || req.path.match(/sql/i) || req.path.match(/PMA\d+/i) || req.path.match(/serverstatus/i) || req.path.match(/config\/server/i) || req.path.match(/xmlrpc/i) || req.path.match(/a2billing/i) || req.path.match(/testproxy/i) || req.path.match(/shopdb/i) || req.path.match(/index.action/i) || req.path.match(/etc\/services/i) end end bad_ips = ENV["IP_BLOCKLIST"] if bad_ips.present? spammers = bad_ips.split(/\s*,\s*/) spammer_regexp = Regexp.union(spammers) blocklist("block bad ips") do |request| request.ip =~ spammer_regexp end end ### Custom Throttle Response ### # By default, Rack::Attack returns an HTTP 429 for throttled responses, # which is just fine. # # If you want to return 503 so that the attacker might be fooled into # believing that they've successfully broken your app (or you just want to # customize the response), then uncomment these lines. # self.throttled_response = lambda do |env| # [ 503, # status # {}, # headers # ['']] # body # end end ================================================ FILE: config/initializers/rswag_api.rb ================================================ Rswag::Api.configure do |c| # Specify a root folder where Swagger JSON files are located # This is used by the Swagger middleware to serve requests for API descriptions # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure # that it's configured to generate files in the same folder c.openapi_root = Rails.root.to_s + "/swagger" # Inject a lambda function to alter the returned Swagger prior to serialization # The function will have access to the rack env for the current request # For example, you could leverage this to dynamically assign the "host" property # # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } end ================================================ FILE: config/initializers/rswag_ui.rb ================================================ Rswag::Ui.configure do |c| # List the Swagger endpoints that you want to be documented through the # swagger-ui. The first parameter is the path (absolute or relative to the UI # host) to the corresponding endpoint and the second is a title that will be # displayed in the document selector. # NOTE: If you're using rspec-api to expose Swagger files # (under swagger_root) as JSON or YAML endpoints, then the list below should # correspond to the relative paths for those endpoints. c.openapi_endpoint "/api-docs/v1/swagger.yaml", "API V1 Docs" # Add Basic Auth in case your API is private # c.basic_auth_enabled = true # c.basic_auth_credentials 'username', 'password' end ================================================ FILE: config/initializers/sent_email_event.rb ================================================ ActiveSupport::Notifications.subscribe "process.action_mailer" do |*args| data = args.extract_options! next if data[:mailer] == "DebugPreviewMailer" user = data[:args][0] if user&.role != "All Casa Admin" SentEmail.create( casa_org_id: user&.casa_org_id, user_id: user&.id, sent_address: user&.email, mailer_type: data[:mailer], category: data[:action].to_s.humanize ) Rails.logger.info "#{data[:action]} email saved!" end end ================================================ FILE: config/initializers/strong_migrations.rb ================================================ # Mark existing migrations as safe StrongMigrations.start_after = 20220117181508 # Set timeouts for migrations # If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user StrongMigrations.lock_timeout = 10.seconds StrongMigrations.statement_timeout = 1.hour # Analyze tables after indexes are added # Outdated statistics can sometimes hurt performance StrongMigrations.auto_analyze = true # Set the version of the production database # so the right checks are run in development # StrongMigrations.target_version = 10 # Add custom checks # StrongMigrations.add_check do |method, args| # if method == :add_index && args[0].to_s == "users" # stop! "No more indexes on the users table" # end # end # Make some operations safe by default # See https://github.com/ankane/strong_migrations#safe-by-default # StrongMigrations.safe_by_default = true ================================================ FILE: config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: config/locales/devise.en.yml ================================================ # Additional translations at https://github.com/plataformatec/devise/wiki/I18n en: devise: confirmations: confirmed: "Your email address has been successfully confirmed." send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." failure: already_authenticated: "You are already signed in." inactive: "Your account is currently inactive. Please contact your supervisor for more details." invalid: "Invalid %{authentication_keys} or password." locked: "Your account is locked." last_attempt: "You have one more attempt before your account is locked." not_found_in_database: "Invalid %{authentication_keys} or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in before continuing." unconfirmed: "You have to confirm your email address before continuing." mailer: confirmation_instructions: subject: "Confirmation instructions" reset_password_instructions: subject: "Reset password instructions" unlock_instructions: subject: "Unlock instructions" email_changed: subject: "Email Changed" password_change: subject: "Password Changed" omniauth_callbacks: failure: "Could not authenticate you from %{kind} because \"%{reason}\"." success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." updated: "Your password has been changed successfully. You are now signed in." updated_not_active: "Your password has been changed successfully." registrations: destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." signed_up: "Welcome! You have signed up successfully." signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." updated: "Your account has been updated successfully." updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again" sessions: signed_in: "Signed in successfully." signed_out: "Signed out successfully." already_signed_out: "Signed out successfully." unlocks: send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: already_confirmed: "was already confirmed, please try signing in" confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" expired: "has expired, please request a new one" not_found: "not found" not_locked: "was not locked" not_saved: password: "%{count} error(s) prohibited this password change from being saved:" one: "1 error prohibited this %{resource} from being saved:" other: "%{count} errors prohibited this %{resource} from being saved:" ================================================ FILE: config/locales/devise_invitable.en.yml ================================================ en: devise: failure: invited: > "You have a pending invitation, accept it to finish creating your account." invitations: send_instructions: "An invitation email has been sent to %{email}." invitation_token_invalid: "The invitation token provided is not valid!" updated: "Your password was set successfully. You are now signed in." updated_not_active: "Your password was set successfully." no_invitations_remaining: "No invitations remaining" invitation_removed: "Your invitation was removed." mailer: invitation_instructions: subject: "CASA Console invitation instructions" time: formats: devise: mailer: invitation_instructions: accept_until_format: "%B %d, %Y %I:%M %p" ================================================ FILE: config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. --- en: activerecord: attributes: additional_expense: other_expenses_amount: Amount other_expenses_describe: Description case_contact: case_contact_contact_types: one: Contact Type other: Contact Types contact_types: one: Contact Type other: Contact Types contact_topic_answers: one: Discussion Topic other: Discussion Topics draft_case_ids: one: CASA Case other: CASA Cases occurred_at: Date case_contact/additional_expenses: other_expense_amount: Other Expense Amount other_expenses_describe: Other Expense Details case_contact/contact_topic_answers: contact_topic: Discussion Topic value: Discussion Notes errors: messages: cant_be_future: can't be in the future must_be_selected: must be selected must_be_true_or_false: must be true or false email_uniqueness: This email is already in use. If it does not appear on your roster, it may be associated with another CASA organization. Please use a different email address. time: formats: day_and_date: "%A, %b %d, %Y" standard: "%m-%d-%Y" contact_occurred_at: "%Y%m%d%H%M%s" full: "%B %-d, %Y" youth_date_of_birth: "%B %Y" short_date: "%-m/%d" long_date: "%m/%d/%y" edit_profile: "%B %d, %Y at %I:%M %p %Z" time_on_date: "%-I:%-M %p on %m-%e-%Y" date: formats: default: "%m/%d/%Y" full: "%B %-d, %Y" long: "%B %d, %Y" short: "%b %d" year_first: "%Y-%m-%d" imports: labels: casa_case: "CASA Case" volunteer: "Volunteer" supervisor: "Supervisor" ================================================ FILE: config/puma.rb ================================================ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to # to prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's # Global VM Lock (GVL) it has diminishing returns and will degrade the # response time (latency) of the application. # # The default is set to 3 threads as it's deemed a decent compromise between # throughput and latency for the average Rails application. # # Any libraries that use a connection pool or another resource pool should # be configured to provide at least as many connections as the number of # threads. This includes Active Record's `pool` parameter in `database.yml`. threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. port ENV.fetch("PORT", 3000) # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. pidfile ENV["PIDFILE"] if ENV["PIDFILE"] ================================================ FILE: config/routes.rb ================================================ # frozen_string_literal: true Rails.application.routes.draw do mount Rswag::Ui::Engine => "/api-docs" mount Rswag::Api::Engine => "/api-docs" devise_for :all_casa_admins, path: "all_casa_admins", controllers: {sessions: "all_casa_admins/sessions"} devise_for :users, controllers: {sessions: "users/sessions", passwords: "users/passwords", invitations: "users/invitations"} authenticate :all_casa_admins do mount PgHero::Engine, at: "pg_dashboard", constraints: lambda { |request| admin = request.env["warden"].user(:all_casa_admin) admin.present? && (admin.role == "All Casa Admin" || admin.casa_admin?) } end concern :with_datatable do post "datatable", on: :collection end authenticated :all_casa_admin do root to: "all_casa_admins/dashboard#show", as: :authenticated_all_casa_admin_root end authenticated :user do root to: "dashboard#show", as: :authenticated_user_root end devise_scope :user do root to: "static#index" end devise_scope :all_casa_admins do root to: "all_casa_admins/sessions#new", as: :unauthenticated_all_casa_root end resources :preference_sets, only: [] do collection do post "/table_state_update/:table_name", to: "preference_sets#table_state_update", as: :table_state_update get "/table_state/:table_name", to: "preference_sets#table_state", as: :table_state end end resources :health, only: %i[index] do collection do get :case_contacts_creation_times_in_last_week get :gc get :monthly_line_graph_data get :monthly_unique_users_graph_data end end get "/.well-known/assetlinks.json", to: "android_app_associations#index" resources :casa_cases, except: %i[destroy] do resource :emancipation, only: %i[show] do member do post "save" end end resource :fund_request, only: %i[new create] resources :court_dates, only: %i[create edit new show update destroy] resources :placements member do patch :deactivate patch :reactivate patch :copy_court_orders end end resources :casa_admins, except: %i[destroy show] do member do patch :deactivate patch :activate patch :resend_invitation post :send_reactivation_alert patch :change_to_supervisor end end get "case_contacts/leave", to: "case_contacts#leave", as: "leave_case_contacts_form" get "case_contacts/drafts", to: "case_contacts#drafts" # Feature flag for new case contact table design get "case_contacts/new_design", to: "case_contacts/case_contacts_new_design#index" post "case_contacts/new_design/datatable", to: "case_contacts/case_contacts_new_design#datatable", as: "datatable_case_contacts_new_design" resources :case_contacts, except: %i[create update show], concerns: %i[with_datatable] do member do post :restore end resources :form, controller: "case_contacts/form", only: %i[show update] resources :followups, only: %i[create], controller: "case_contacts/followups", shallow: true do patch :resolve, on: :member end end resources :contact_topic_answers, only: %i[create destroy] resources :reports, only: %i[index] get :export_emails, to: "reports#export_emails" resources :case_court_reports, only: %i[index show] do collection do post :generate end end resources :reimbursements, only: %i[index change_complete_status], concerns: %i[with_datatable] do patch :mark_as_complete, to: "reimbursements#change_complete_status" patch :mark_as_needs_review, to: "reimbursements#change_complete_status" end resources :imports, only: %i[index create] do collection do get :download_failed end end resources :case_contact_reports, only: %i[index] resources :mileage_reports, only: %i[index] resources :mileage_rates, only: %i[index new create edit update] resources :casa_org, only: %i[edit update] resources :contact_type_groups, only: %i[new create edit update] resources :contact_types, only: %i[new create edit update] resources :hearing_types, only: %i[new create edit update] do resources :checklist_items, only: %i[new create edit update destroy] end resources :emancipation_checklists, only: %i[index] resources :judges, only: %i[new create edit update] resources :notifications, only: [:index] do member do post "mark_as_read" end end resources :other_duties, only: %i[new create edit index update] resources :missing_data_reports, only: %i[index] resources :learning_hours_reports, only: %i[index] resources :learning_hour_types, only: %i[new create edit update] resources :learning_hour_topics, only: %i[new create edit update] resources :placement_types, only: %i[new create edit update] resources :contact_topics, except: %i[index show delete] do delete "soft_delete", on: :member end resources :followup_reports, only: :index resources :placement_reports, only: :index resources :banners, except: %i[show] do member do get :dismiss end end resources :bulk_court_dates, only: %i[new create] resources :case_groups, only: %i[index new edit create update destroy] resources :learning_hours, only: %i[index show new create edit update destroy] namespace :learning_hours do resources :volunteers, only: :show end resources :supervisors, except: %i[destroy show], concerns: %i[with_datatable] do member do patch :activate patch :deactivate patch :resend_invitation patch :change_to_admin end end resources :supervisor_volunteers, only: %i[create] do collection do post :bulk_assignment end member do patch :unassign end end resources :volunteers, except: %i[destroy], concerns: %i[with_datatable] do post :stop_impersonating, on: :collection member do patch :activate patch :deactivate get :resend_invitation get :send_reactivation_alert patch :reminder get :impersonate end resources :notes, only: %i[create edit update destroy] end resources :case_assignments, only: %i[create destroy] do member do get :unassign patch :unassign patch :show_hide_contacts patch :reimbursement end end resources :case_court_orders, only: %i[destroy] resources :additional_expenses, only: %i[create destroy] namespace :all_casa_admins do resources :casa_orgs, only: [:new, :create, :show] do resources :casa_admins, only: [:new, :create, :edit, :update] do member do patch :deactivate patch :activate end end end resources :patch_notes, only: %i[create destroy index update] end resources :all_casa_admins, only: [:new, :create] do collection do get :edit patch :update patch "update_password" end end resources :users, only: [] do collection do get :edit patch :update patch "update_password" patch "update_email" patch :add_language delete :remove_language end end resources :languages, only: %i[new create edit update] do delete :remove_from_volunteer end resources :custom_org_links, only: %i[new create edit update destroy] direct :help_admins_supervisors do "https://thunder-flower-8c2.notion.site/Casa-Volunteer-Tracking-App-HelpSite-3b95705e80c742ffa729ccce7beeabfa" end direct :help_volunteers do "https://thunder-flower-8c2.notion.site/Casa-Volunteer-Tracking-App-HelpSite-Volunteers-c24d9d2ef8b249bbbda8192191365039?pvs=4" end get "/error", to: "error#index" post "/error", to: "error#create" namespace :api do namespace :v1 do namespace :users do post "sign_in", to: "sessions#create" delete "sign_out", to: "sessions#destroy" end end end constraints CanAccessFlipperUI do mount Flipper::UI.app(Flipper) => "/flipper" end end ================================================ FILE: config/scout_apm.yml ================================================ # This configuration file is used for Scout APM. # https://scoutapm.com/docs/ruby/configuration common: &defaults # key: Your Organization key for Scout APM. Found on the settings screen. # - Default: none key: <%= ENV.fetch("SCOUT_APM_KEY", "local") %> # log_level: Verboseness of logs. # - Default: 'info' # - Valid Options: debug, info, warn, error # log_level: debug # name: Application name in APM Web UI # - Default: the application names comes from the Rails or Sinatra class name # name: # monitor: Enable Scout APM or not # - Default: none # - Valid Options: true, false monitor: true production: <<: *defaults errors_enabled: true development: <<: *defaults monitor: false dev_trace: true test: <<: *defaults monitor: false staging: <<: *defaults dev_trace: true ================================================ FILE: config/spring.rb ================================================ Spring.watch( ".ruby-version", ".rbenv-vars", "tmp/restart.txt", "tmp/caching-dev.txt" ) ================================================ FILE: config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage#{ENV['TEST_ENV_NUMBER']}") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) microsoft: service: AzureStorage storage_account_name: <%= ENV['STORAGE_ACCOUNT_NAME'] %> storage_access_key: <%= ENV['STORAGE_ACCESS_KEY'] %> container: <%= ENV['STORAGE_CONTAINER'] %> # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server ================================================ FILE: data/inputs_fdf.erb ================================================ %FDF-1.2 %âãÏÓ 1 0 obj < << /V (<%= FdfInputsService.clean(value) %>) /T (<%= name %>) >> <% end %> ] >> >> endobj trailer <> %%EOF ================================================ FILE: db/migrate/20200329050100_create_casa_cases.rb ================================================ class CreateCasaCases < ActiveRecord::Migration[6.0] def change create_table :casa_cases do |t| t.string :case_number t.boolean :teen_program_eligible t.timestamps end end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329062155_devise_create_users.rb ================================================ # frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable # t.integer :sign_in_count, default: 0, null: false # t.datetime :current_sign_in_at # t.datetime :last_sign_in_at # t.inet :current_sign_in_ip # t.inet :last_sign_in_ip ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true # add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329064203_add_role_to_user.rb ================================================ class AddRoleToUser < ActiveRecord::Migration[6.0] def change add_column :users, :role, :string, null: false, default: "volunteer" end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329071025_change_casa_case_teen_to_required.rb ================================================ class ChangeCasaCaseTeenToRequired < ActiveRecord::Migration[6.0] def change change_column :casa_cases, :teen_program_eligible, :boolean, null: false, default: false end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329071327_change_casa_case_number_to_required.rb ================================================ class ChangeCasaCaseNumberToRequired < ActiveRecord::Migration[6.0] def change change_column :casa_cases, :case_number, :string, null: false end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329071626_add_unique_index_on_case_case_number.rb ================================================ class AddUniqueIndexOnCaseCaseNumber < ActiveRecord::Migration[6.0] def change add_index :casa_cases, :case_number, unique: true end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329074655_create_supervisor_volunteers.rb ================================================ class CreateSupervisorVolunteers < ActiveRecord::Migration[6.0] def change create_table :supervisor_volunteers do |t| t.references :supervisor, foreign_key: {to_table: :users}, null: false t.references :volunteer, foreign_key: {to_table: :users}, null: false t.timestamps end end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329081206_create_case_assignments.rb ================================================ class CreateCaseAssignments < ActiveRecord::Migration[6.0] def change create_table :case_assignments do |t| t.references :casa_case, foreign_key: {to_table: :casa_cases}, null: false t.references :volunteer, foreign_key: {to_table: :users}, null: false t.boolean :is_active, null: false, default: true t.timestamps end end end # rubocop:enable Style/Documentation ================================================ FILE: db/migrate/20200329085225_create_versions.rb ================================================ # This migration creates the `versions` table, the only schema PT requires. # All other migrations PT provides are optional. class CreateVersions < ActiveRecord::Migration[6.0] # The largest text column available in all supported RDBMS is # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size # so that MySQL will use `longtext` instead of `text`. Otherwise, # when serializing very large objects, `text` might not be big enough. TEXT_BYTES = 1_073_741_823 def change create_table :versions do |t| t.string :item_type, {null: false} t.integer :item_id, null: false, limit: 8 t.string :event, null: false t.string :whodunnit t.text :object, limit: TEXT_BYTES # Known issue in MySQL: fractional second precision # ------------------------------------------------- # # MySQL timestamp columns do not support fractional seconds unless # defined with "fractional seconds precision". MySQL users should manually # add fractional seconds precision to this migration, specifically, to # the `created_at` column. # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html) # # MySQL users should also upgrade to at least rails 4.2, which is the first # version of ActiveRecord with support for fractional seconds in MySQL. # (https://github.com/rails/rails/pull/14359) # t.datetime :created_at end add_index :versions, %i[item_type item_id] end end ================================================ FILE: db/migrate/20200329095031_create_casa_orgs.rb ================================================ class CreateCasaOrgs < ActiveRecord::Migration[6.0] def change create_table :casa_orgs do |t| t.string :name, null: false t.timestamps end end end ================================================ FILE: db/migrate/20200329095154_add_casa_org_to_user.rb ================================================ class AddCasaOrgToUser < ActiveRecord::Migration[6.0] def change add_reference :users, :casa_org, foreign_key: true, null: false end end ================================================ FILE: db/migrate/20200329102102_devise_create_all_casa_admins.rb ================================================ # frozen_string_literal: true class DeviseCreateAllCasaAdmins < ActiveRecord::Migration[6.0] def change create_table :all_casa_admins do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable # t.integer :sign_in_count, default: 0, null: false # t.datetime :current_sign_in_at # t.datetime :last_sign_in_at # t.inet :current_sign_in_ip # t.inet :last_sign_in_ip ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.timestamps null: false end add_index :all_casa_admins, :email, unique: true add_index :all_casa_admins, :reset_password_token, unique: true # add_index :all_casa_admins, :confirmation_token, unique: true # add_index :all_casa_admins, :unlock_token, unique: true end end ================================================ FILE: db/migrate/20200329175337_create_case_contacts.rb ================================================ class CreateCaseContacts < ActiveRecord::Migration[6.0] def change create_table :case_contacts do |t| t.references :creator, foreign_key: {to_table: :users}, null: false t.references :casa_case, null: false, foreign_key: true t.string :contact_type, null: false t.string :other_type_text, null: true t.integer :duration_minutes, null: false t.datetime :occurred_at, null: false t.timestamps end end end ================================================ FILE: db/migrate/20200330231711_add_volunteer_reference_to_casa_cases.rb ================================================ class AddVolunteerReferenceToCasaCases < ActiveRecord::Migration[6.0] def change add_reference :casa_cases, :volunteer end end ================================================ FILE: db/migrate/20200405112910_remove_volunteer_id_from_casa_case.rb ================================================ class RemoveVolunteerIdFromCasaCase < ActiveRecord::Migration[6.0] def change remove_reference :casa_cases, :volunteer end end ================================================ FILE: db/migrate/20200420004403_add_medium_type_and_contact_made_to_case_contacts.rb ================================================ class AddMediumTypeAndContactMadeToCaseContacts < ActiveRecord::Migration[6.0] def change add_column :case_contacts, :contact_made, :boolean, default: false add_column :case_contacts, :medium_type, :string end end ================================================ FILE: db/migrate/20200422180727_replace_contact_type_with_contact_types_on_case_contact.rb ================================================ class ReplaceContactTypeWithContactTypesOnCaseContact < ActiveRecord::Migration[6.0] def change # NOTE: This is a destructive migration that we would normally avoid # if we were working on production data, but because there is # no production data we are comfortable being destructive and # losting whatever data is in the `contact_type` column. remove_column :case_contacts, :contact_type, :string add_column :case_contacts, :contact_types, :string, array: true # By default, indexes in postgresql are full-value indexes. # However, when you have fields that hold multiple values, such as enums # or jsonb, you want to rely on a full-text search index type. # gin indexes are a full-text index type that works well in this context. # You can read more at the official PostgreSQL docs: # https://www.postgresql.org/docs/current/textsearch-indexes.html add_index :case_contacts, :contact_types, using: :gin end end ================================================ FILE: db/migrate/20200423154018_change_teen_program_to_transition_aged_youth.rb ================================================ class ChangeTeenProgramToTransitionAgedYouth < ActiveRecord::Migration[6.0] def change rename_column :casa_cases, :teen_program_eligible, :transition_aged_youth end end ================================================ FILE: db/migrate/20200423204147_add_name_to_user.rb ================================================ class AddNameToUser < ActiveRecord::Migration[6.0] def change add_column :users, :display_name, :string, default: "" end end ================================================ FILE: db/migrate/20200525220759_add_driving_fields_to_case_contact.rb ================================================ class AddDrivingFieldsToCaseContact < ActiveRecord::Migration[6.0] def up add_column :case_contacts, :miles_driven, :integer, null: true add_column :case_contacts, :want_driving_reimbursement, :boolean, default: false execute <<-SQL ALTER TABLE case_contacts ADD CONSTRAINT want_driving_reimbursement_only_when_miles_driven CHECK ((miles_driven IS NOT NULL) OR (NOT want_driving_reimbursement)); SQL end def down execute <<-SQL ALTER TABLE case_contacts DROP CONSTRAINT want_driving_reimbursement_only_when_miles_driven SQL remove_column :case_contacts, :miles_driven, :integer remove_column :case_contacts, :want_driving_reimbursement, :boolean end end ================================================ FILE: db/migrate/20200726185103_devise_invitable_add_to_users.rb ================================================ class DeviseInvitableAddToUsers < ActiveRecord::Migration[6.0] def up change_table :users do |t| t.string :invitation_token t.datetime :invitation_created_at t.datetime :invitation_sent_at t.datetime :invitation_accepted_at t.integer :invitation_limit t.references :invited_by, polymorphic: true t.integer :invitations_count, default: 0 t.index :invitations_count t.index :invitation_token, unique: true # for invitable t.index :invited_by_id end end def down change_table :users do |t| t.remove_references :invited_by, polymorphic: true t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at end end end ================================================ FILE: db/migrate/20200729002247_add_casa_org_to_casa_case.rb ================================================ class AddCasaOrgToCasaCase < ActiveRecord::Migration[6.0] def change add_reference :casa_cases, :casa_org, null: false, foreign_key: true, index: true end end ================================================ FILE: db/migrate/20200801170524_add_type_to_user.rb ================================================ class User < ApplicationRecord self.table_name = "users" end class AddTypeToUser < ActiveRecord::Migration[6.0] def up add_column :users, :type, :string add_column :users, :active, :boolean, default: true # Set supervisor User.where(role: "supervisor").update_all(type: "Supervisor") # Set volunteers User.where(role: "volunteer").update_all(type: "Volunteer") User.where(role: "inactive").update_all(type: "Volunteer", active: false) # Set casa_admins User.where(role: "casa_admin").update_all(type: "CasaAdmin") end def down drop_column :users, :type end end ================================================ FILE: db/migrate/20200801192923_remove_role_from_user.rb ================================================ class RemoveRoleFromUser < ActiveRecord::Migration[6.0] def change remove_column :users, :role, :string, null: false, default: "volunteer" end end ================================================ FILE: db/migrate/20200818220659_add_is_active_to_supervisor_volunteers.rb ================================================ class AddIsActiveToSupervisorVolunteers < ActiveRecord::Migration[6.0] def change add_column :supervisor_volunteers, :is_active, :boolean, default: true end end ================================================ FILE: db/migrate/20200830011647_add_notes_to_case_contacts.rb ================================================ class AddNotesToCaseContacts < ActiveRecord::Migration[6.0] def change add_column :case_contacts, :notes, :string end end ================================================ FILE: db/migrate/20200905192934_remove_other_type_text_from_case_contacts.rb ================================================ class RemoveOtherTypeTextFromCaseContacts < ActiveRecord::Migration[6.0] def change remove_column :case_contacts, :other_type_text, :string end end ================================================ FILE: db/migrate/20200906145045_add_display_name_to_casa_orgs.rb ================================================ class AddDisplayNameToCasaOrgs < ActiveRecord::Migration[6.0] def change add_column :casa_orgs, :display_name, :string end end ================================================ FILE: db/migrate/20200906145725_add_address_to_casa_orgs.rb ================================================ class AddAddressToCasaOrgs < ActiveRecord::Migration[6.0] def change add_column :casa_orgs, :address, :string end end ================================================ FILE: db/migrate/20200906145830_add_footer_links_to_casa_orgs.rb ================================================ class AddFooterLinksToCasaOrgs < ActiveRecord::Migration[6.0] def change add_column :casa_orgs, :footer_links, :string, array: true, default: [] end end ================================================ FILE: db/migrate/20200906150641_create_casa_org_logos.rb ================================================ class CreateCasaOrgLogos < ActiveRecord::Migration[6.0] def change create_table :casa_org_logos do |t| t.references :casa_org, null: false, foreign_key: true t.string :url t.string :alt_text t.string :size t.timestamps end end end ================================================ FILE: db/migrate/20200906184455_add_banner_color_to_casa_org_logos.rb ================================================ class AddBannerColorToCasaOrgLogos < ActiveRecord::Migration[6.0] def change add_column :casa_org_logos, :banner_color, :string end end ================================================ FILE: db/migrate/20200907142411_create_task_records.rb ================================================ class CreateTaskRecords < ActiveRecord::Migration[6.0] def change create_table :task_records, id: false do |t| t.string :version, null: false end end end ================================================ FILE: db/migrate/20200917175655_add_default_value_to_case_contact_miles_driven.rb ================================================ class AddDefaultValueToCaseContactMilesDriven < ActiveRecord::Migration[6.0] def up change_column :case_contacts, :miles_driven, :integer, default: 0 end def down change_column :case_contacts, :miles_driven, :integer, default: nil end end ================================================ FILE: db/migrate/20200918115741_set_miles_driven_as_not_nullable.rb ================================================ class SetMilesDrivenAsNotNullable < ActiveRecord::Migration[6.0] def up change_column :case_contacts, :miles_driven, :integer, null: false end def down change_column :case_contacts, :miles_driven, :integer, null: true end end ================================================ FILE: db/migrate/20200922144730_add_birth_month_year_youth_to_casa_cases.rb ================================================ class AddBirthMonthYearYouthToCasaCases < ActiveRecord::Migration[6.0] def change add_column :casa_cases, :birth_month_year_youth, :datetime end end ================================================ FILE: db/migrate/20200922150754_create_contact_type.rb ================================================ class CreateContactType < ActiveRecord::Migration[6.0] def change create_table :contact_type_groups do |t| t.references :casa_org, null: false t.string :name, null: false t.timestamps end create_table :contact_types do |t| t.references :contact_type_group, null: false t.string :name, null: false t.timestamps end end end ================================================ FILE: db/migrate/20200922170308_link_case_contacts_to_contact_types.rb ================================================ class LinkCaseContactsToContactTypes < ActiveRecord::Migration[6.0] def change create_table :case_contact_contact_types do |t| t.references :case_contact, null: false t.references :contact_type, null: false t.timestamps end end end ================================================ FILE: db/migrate/20200924192310_create_casa_case_contact_type.rb ================================================ class CreateCasaCaseContactType < ActiveRecord::Migration[6.0] def change create_table :casa_case_contact_types do |t| t.references :contact_type, null: false t.references :casa_case, null: false t.timestamps end end end ================================================ FILE: db/migrate/20200925180941_add_active_to_contact_type_groups.rb ================================================ class AddActiveToContactTypeGroups < ActiveRecord::Migration[6.0] def change add_column :contact_type_groups, :active, :boolean, default: true end end ================================================ FILE: db/migrate/20200925181042_add_active_to_contact_types.rb ================================================ class AddActiveToContactTypes < ActiveRecord::Migration[6.0] def change add_column :contact_types, :active, :boolean, default: true end end ================================================ FILE: db/migrate/20200928233606_add_court_report_submitted_to_casa_cases.rb ================================================ class AddCourtReportSubmittedToCasaCases < ActiveRecord::Migration[6.0] def change add_column :casa_cases, :court_report_submitted, :boolean, default: false, null: false end end ================================================ FILE: db/migrate/20201002192636_add_court_date_to_casa_cases.rb ================================================ class AddCourtDateToCasaCases < ActiveRecord::Migration[6.0] def change add_column :casa_cases, :court_date, :datetime end end ================================================ FILE: db/migrate/20201004165322_add_court_report_due_date_to_casa_cases.rb ================================================ class AddCourtReportDueDateToCasaCases < ActiveRecord::Migration[6.0] def change add_column :casa_cases, :court_report_due_date, :datetime end end ================================================ FILE: db/migrate/20201005191326_create_hearing_types.rb ================================================ class CreateHearingTypes < ActiveRecord::Migration[6.0] def change create_table :hearing_types do |t| t.references :casa_org, null: false t.string :name, null: false t.boolean :active, null: false, default: true end end end ================================================ FILE: db/migrate/20201013171632_remove_contact_types_from_case_contacts.rb ================================================ class RemoveContactTypesFromCaseContacts < ActiveRecord::Migration[6.0] def change # case_contacts.contact_types has been deprecated in favor of case_contact.case_contact_contact_type remove_column :case_contacts, :contact_types, :string, array: true, default: [] end end ================================================ FILE: db/migrate/20201019120548_create_active_storage_tables.active_storage.rb ================================================ # This migration comes from active_storage (originally 20170806125915) class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change create_table :active_storage_blobs do |t| t.string :key, null: false t.string :filename, null: false t.string :content_type t.text :metadata t.bigint :byte_size, null: false t.string :checksum, null: false t.datetime :created_at, null: false t.index [:key], unique: true end create_table :active_storage_attachments do |t| t.string :name, null: false t.references :record, null: false, polymorphic: true, index: false t.references :blob, null: false t.datetime :created_at, null: false t.index [:record_type, :record_id, :name, :blob_id], name: "index_active_storage_attachments_uniqueness", unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end end ================================================ FILE: db/migrate/20201020095451_add_hearing_type_to_court_cases.rb ================================================ class AddHearingTypeToCourtCases < ActiveRecord::Migration[6.0] def change add_reference :casa_cases, :hearing_type, null: true end end ================================================ FILE: db/migrate/20201020220412_create_emancipation_categories.rb ================================================ class CreateEmancipationCategories < ActiveRecord::Migration[6.0] def change create_table :emancipation_categories do |t| t.string :name, null: false, index: {unique: true} t.boolean :mutually_exclusive, null: false t.timestamps end end end ================================================ FILE: db/migrate/20201021024459_create_emancipation_options.rb ================================================ class CreateEmancipationOptions < ActiveRecord::Migration[6.0] def change create_table :emancipation_options do |t| t.references :emancipation_category, null: false, foreign_key: true t.string :name, null: false t.index [:emancipation_category_id, :name], unique: true t.timestamps end end end ================================================ FILE: db/migrate/20201021034012_create_join_table_casa_cases_emancipaton_options.rb ================================================ class CreateJoinTableCasaCasesEmancipatonOptions < ActiveRecord::Migration[6.0] def change create_join_table :casa_cases, :emancipation_options do |t| # Manually naming the index here because the autogenerated name has an illegal length t.index [:casa_case_id, :emancipation_option_id], name: "index_cases_options_on_case_id_and_option_id", unique: true end end end ================================================ FILE: db/migrate/20201021143642_add_active_column_to_casa_cases_table.rb ================================================ class AddActiveColumnToCasaCasesTable < ActiveRecord::Migration[6.0] def change add_column :casa_cases, :active, :boolean, default: true, null: false end end ================================================ FILE: db/migrate/20201022034445_add_foreign_key_constraints_to_case_emancipation_join_table.rb ================================================ class AddForeignKeyConstraintsToCaseEmancipationJoinTable < ActiveRecord::Migration[6.0] def change add_foreign_key :casa_cases_emancipation_options, :casa_cases add_foreign_key :casa_cases_emancipation_options, :emancipation_options end end ================================================ FILE: db/migrate/20201023233638_create_judges.rb ================================================ class CreateJudges < ActiveRecord::Migration[6.0] def change create_table :judges do |t| t.references :casa_org, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20201023234325_add_active_to_judge.rb ================================================ class AddActiveToJudge < ActiveRecord::Migration[6.0] def change add_column :judges, :active, :boolean, default: true end end ================================================ FILE: db/migrate/20201024003821_add_name_to_judge.rb ================================================ class AddNameToJudge < ActiveRecord::Migration[6.0] def change add_column :judges, :name, :string end end ================================================ FILE: db/migrate/20201024113046_create_past_court_dates.rb ================================================ class CreatePastCourtDates < ActiveRecord::Migration[6.0] def change create_table :past_court_dates do |t| t.datetime :date, null: false t.references :casa_case, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20201025162142_add_judge_to_court_cases.rb ================================================ class AddJudgeToCourtCases < ActiveRecord::Migration[6.0] def change add_reference :casa_cases, :judge, null: true end end ================================================ FILE: db/migrate/20201108142333_add_court_report_status_to_casa_cases.rb ================================================ class AddCourtReportStatusToCasaCases < ActiveRecord::Migration[6.0] def change add_column :casa_cases, :court_report_submitted_at, :datetime add_column :casa_cases, :court_report_status, :integer, default: 0, not_null: true end end ================================================ FILE: db/migrate/20201120103041_remove_remember_created_at_from_users.rb ================================================ class RemoveRememberCreatedAtFromUsers < ActiveRecord::Migration[6.0] def change remove_column :users, :remember_created_at, :datetime end end ================================================ FILE: db/migrate/20201120103146_remove_remember_created_at_from_all_casa_admins.rb ================================================ class RemoveRememberCreatedAtFromAllCasaAdmins < ActiveRecord::Migration[6.0] def change remove_column :all_casa_admins, :remember_created_at, :datetime end end ================================================ FILE: db/migrate/20201120215756_remove_court_report_submitted_from_casa_cases.rb ================================================ class RemoveCourtReportSubmittedFromCasaCases < ActiveRecord::Migration[6.0] def change remove_column :casa_cases, :court_report_submitted, :boolean end end ================================================ FILE: db/migrate/20201123100716_remove_case_number_index_from_casa_cases.rb ================================================ class RemoveCaseNumberIndexFromCasaCases < ActiveRecord::Migration[6.0] def change remove_index :casa_cases, column: :case_number, unique: true end end ================================================ FILE: db/migrate/20201123112651_add_case_number_index_scoped_by_casa_org_to_casa_cases.rb ================================================ class AddCaseNumberIndexScopedByCasaOrgToCasaCases < ActiveRecord::Migration[6.0] def change add_index :casa_cases, [:case_number, :casa_org_id], unique: true end end ================================================ FILE: db/migrate/20201222125441_add_service_name_to_active_storage_blobs.active_storage.rb ================================================ # This migration comes from active_storage (originally 20190112182829) class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] def up unless column_exists?(:active_storage_blobs, :service_name) add_column :active_storage_blobs, :service_name, :string if (configured_service = ActiveStorage::Blob.service.name) ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) end change_column :active_storage_blobs, :service_name, :string, null: false end end def down remove_column :active_storage_blobs, :service_name end end ================================================ FILE: db/migrate/20201222125442_create_active_storage_variant_records.active_storage.rb ================================================ # This migration comes from active_storage (originally 20191206030411) class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] def change create_table :active_storage_variant_records do |t| t.belongs_to :blob, null: false, index: false t.string :variation_digest, null: false t.index %i[blob_id variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end end ================================================ FILE: db/migrate/20201226024029_create_casa_case_emancipation_categories.rb ================================================ class CreateCasaCaseEmancipationCategories < ActiveRecord::Migration[6.1] def change create_table :casa_case_emancipation_categories do |t| t.references :casa_case, null: false, foreign_key: true t.references :emancipation_category, null: false, foreign_key: true, index: {name: "index_case_emancipation_categories_on_emancipation_category_id"} t.timestamps end end end ================================================ FILE: db/migrate/20210105155534_create_followups.rb ================================================ class CreateFollowups < ActiveRecord::Migration[6.1] def change create_table :followups do |t| t.belongs_to :case_contact t.belongs_to :creator, foreign_key: {to_table: :users} t.integer :status, default: 0, not_null: true t.timestamps end end end ================================================ FILE: db/migrate/20210107181908_add_id_and_timestamps_to_case_emancipation_options_table.rb ================================================ class AddIdAndTimestampsToCaseEmancipationOptionsTable < ActiveRecord::Migration[6.1] def change add_column :casa_cases_emancipation_options, :id, :primary_key add_timestamps :casa_cases_emancipation_options, default: Time.zone.now change_column_default :casa_cases_emancipation_options, :created_at, nil change_column_default :casa_cases_emancipation_options, :updated_at, nil end end ================================================ FILE: db/migrate/20210109231411_drop_casa_org_logos_table.rb ================================================ class DropCasaOrgLogosTable < ActiveRecord::Migration[6.1] def up drop_table :casa_org_logos end def down fail ActiveRecord::IrreversibleMigration end end ================================================ FILE: db/migrate/20210117185614_create_notifications.rb ================================================ class CreateNotifications < ActiveRecord::Migration[6.1] def change create_table :notifications do |t| t.references :recipient, polymorphic: true, null: false t.string :type, null: false t.jsonb :params t.datetime :read_at t.timestamps end add_index :notifications, :read_at end end ================================================ FILE: db/migrate/20210223133248_create_case_court_mandates.rb ================================================ 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 ================================================ FILE: db/migrate/20210308195135_change_is_active_name.rb ================================================ class ChangeIsActiveName < ActiveRecord::Migration[6.1] def change rename_column :case_assignments, :is_active, :active end end ================================================ FILE: db/migrate/20210330182538_add_devise_trackable_columns_to_users.rb ================================================ class AddDeviseTrackableColumnsToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :sign_in_count, :integer, default: 0, null: false add_column :users, :current_sign_in_at, :datetime add_column :users, :last_sign_in_at, :datetime add_column :users, :current_sign_in_ip, :string add_column :users, :last_sign_in_ip, :string end end ================================================ FILE: db/migrate/20210401161710_add_deleted_at_to_case_contacts.rb ================================================ class AddDeletedAtToCaseContacts < ActiveRecord::Migration[6.1] def change add_column :case_contacts, :deleted_at, :datetime add_index :case_contacts, :deleted_at end end ================================================ FILE: db/migrate/20210401182359_add_implementation_status_to_edit_case_court_mandates.rb ================================================ class AddImplementationStatusToEditCaseCourtMandates < ActiveRecord::Migration[6.1] def change add_column :case_court_mandates, :implementation_status, :integer end end ================================================ FILE: db/migrate/20210502172706_add_devise_invitable_to_all_casa_admins.rb ================================================ class AddDeviseInvitableToAllCasaAdmins < ActiveRecord::Migration[6.1] def change add_column :all_casa_admins, :invitation_token, :string add_column :all_casa_admins, :invitation_created_at, :datetime add_column :all_casa_admins, :invitation_sent_at, :datetime add_column :all_casa_admins, :invitation_accepted_at, :datetime add_column :all_casa_admins, :invitation_limit, :integer add_column :all_casa_admins, :invited_by_id, :integer add_column :all_casa_admins, :invited_by_type, :string add_index :all_casa_admins, :invitation_token, unique: true end end ================================================ FILE: db/migrate/20210521151549_add_casa_case_details_to_past_court_dates.rb ================================================ class AddCasaCaseDetailsToPastCourtDates < ActiveRecord::Migration[6.1] def change add_reference :past_court_dates, :hearing_type, null: true add_reference :past_court_dates, :judge, null: true end end ================================================ FILE: db/migrate/20210521194321_add_past_court_date_to_case_court_mandates.rb ================================================ class AddPastCourtDateToCaseCourtMandates < ActiveRecord::Migration[6.1] def change add_reference :case_court_mandates, :past_court_date, null: true end end ================================================ FILE: db/migrate/20210526233058_create_sent_emails.rb ================================================ class CreateSentEmails < ActiveRecord::Migration[6.1] def change create_table :sent_emails do |t| t.belongs_to :user, index: true, foreign_key: true t.references :casa_org, null: false, foreign_key: true t.string :mailer_type t.string :category t.string :sent_address t.timestamps end end end ================================================ FILE: db/migrate/20210624125750_add_note_to_followups.rb ================================================ class AddNoteToFollowups < ActiveRecord::Migration[6.1] def change add_column :followups, :note, :text end end ================================================ FILE: db/migrate/20210913142024_set_display_name_as_not_nullable.rb ================================================ class SetDisplayNameAsNotNullable < ActiveRecord::Migration[6.1] def up change_column :users, :display_name, :string, null: false end def down change_column :users, :display_name, :string, null: true end end ================================================ FILE: db/migrate/20210925140028_add_slug_to_casa_orgs_and_casa_cases.rb ================================================ class AddSlugToCasaOrgsAndCasaCases < ActiveRecord::Migration[6.1] def change add_column :casa_cases, :slug, :string add_column :casa_orgs, :slug, :string add_index :casa_cases, :slug add_index :casa_orgs, :slug, unique: true end end ================================================ FILE: db/migrate/20211001204053_rename_court_mandates_to_court_orders.rb ================================================ class RenameCourtMandatesToCourtOrders < ActiveRecord::Migration[6.1] def change rename_table :case_court_mandates, :case_court_orders end end ================================================ FILE: db/migrate/20211007144114_add_show_driving_reimbursement_to_casa_orgs.rb ================================================ class AddShowDrivingReimbursementToCasaOrgs < ActiveRecord::Migration[6.1] def change add_column :casa_orgs, :show_driving_reimbursement, :boolean, default: true end end ================================================ FILE: db/migrate/20211008170357_add_text_to_case_court_orders.rb ================================================ class AddTextToCaseCourtOrders < ActiveRecord::Migration[6.1] def change add_column :case_court_orders, :text, :string end end ================================================ FILE: db/migrate/20211008170724_migrate_case_court_orders_mandate_text_to_text.rb ================================================ class MigrateCaseCourtOrdersMandateTextToText < ActiveRecord::Migration[6.1] def up execute "update case_court_orders set text=mandate_text" end end ================================================ FILE: db/migrate/20211008174527_remove_mandate_text_from_case_court_orders.rb ================================================ class RemoveMandateTextFromCaseCourtOrders < ActiveRecord::Migration[6.1] def change remove_column :case_court_orders, :mandate_text, :string end end ================================================ FILE: db/migrate/20211011195857_rename_past_court_date_to_court_date.rb ================================================ class RenamePastCourtDateToCourtDate < ActiveRecord::Migration[6.1] def change rename_table :past_court_dates, :court_dates rename_column :case_court_orders, :past_court_date_id, :court_date_id end end ================================================ FILE: db/migrate/20211012180102_change_casa_cases_court_date_to_reference.rb ================================================ class ChangeCasaCasesCourtDateToReference < ActiveRecord::Migration[6.1] def up CasaCase.find_each do |casa_case| CourtDate.create( date: casa_case.court_date, casa_case: casa_case, hearing_type: casa_case.hearing_type, judge: casa_case.judge, case_court_orders: casa_case.case_court_orders ) end end end ================================================ FILE: db/migrate/20211023165907_add_reimbursement_complete_to_case_contacts.rb ================================================ class AddReimbursementCompleteToCaseContacts < ActiveRecord::Migration[6.1] def change add_column :case_contacts, :reimbursement_complete, :boolean, default: false end end ================================================ FILE: db/migrate/20211024011923_create_mileage_rates.rb ================================================ class CreateMileageRates < ActiveRecord::Migration[6.1] def change create_table :mileage_rates do |t| t.decimal :amount t.date :effective_date t.boolean :is_active, default: true t.references :user, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20211024060815_create_preference_sets.rb ================================================ class CreatePreferenceSets < ActiveRecord::Migration[6.1] def change create_table :preference_sets do |t| t.references :user, index: true, foreign_key: true t.jsonb :case_volunteer_columns, null: false, default: "{}" t.timestamps end end end ================================================ FILE: db/migrate/20211025143709_create_healths.rb ================================================ class CreateHealths < ActiveRecord::Migration[6.1] def change create_table :healths do |t| t.timestamp :latest_deploy_time t.integer :singleton_guard t.timestamps end add_index(:healths, :singleton_guard, unique: true) end end ================================================ FILE: db/migrate/20211029032305_add_casa_org_to_mileage_rate.rb ================================================ class AddCasaOrgToMileageRate < ActiveRecord::Migration[6.1] def change # null false is dangerous if there are any in the db already! There aren't tho add_column :mileage_rates, :casa_org_id, :bigint, null: false add_index :mileage_rates, :casa_org_id end end ================================================ FILE: db/migrate/20211029033530_remove_user_required_from_mileage_rate.rb ================================================ class RemoveUserRequiredFromMileageRate < ActiveRecord::Migration[6.1] def change change_column_null :mileage_rates, :user_id, true end end ================================================ FILE: db/migrate/20211203181342_create_additional_expenses.rb ================================================ class CreateAdditionalExpenses < ActiveRecord::Migration[6.1] def change create_table :additional_expenses do |t| t.references :case_contact, null: false, foreign_key: true t.decimal "other_expense_amount" t.string "other_expenses_describe" t.timestamps end end end ================================================ FILE: db/migrate/20211230033457_create_delayed_jobs.rb ================================================ class CreateDelayedJobs < ActiveRecord::Migration[6.1] def self.up create_table :delayed_jobs do |table| table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. table.text :handler, null: false # YAML-encoded string of the object that will do work table.text :last_error # reason for last failure (See Note below) table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. table.datetime :locked_at # Set when a client is working on this object table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) table.string :locked_by # Who is working on this object (if locked) table.string :queue # The name of the queue this job is in table.timestamps null: true end add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" end def self.down drop_table :delayed_jobs end end ================================================ FILE: db/migrate/20220105030922_create_feature_flags.rb ================================================ class CreateFeatureFlags < ActiveRecord::Migration[6.1] def change create_table :feature_flags do |t| t.string :name, null: false t.boolean :enabled, null: false, default: false t.timestamps end end end ================================================ FILE: db/migrate/20220127055733_create_notes.rb ================================================ class CreateNotes < ActiveRecord::Migration[6.1] def change create_table :notes do |t| t.string :content t.bigint :creator_id t.references :notable, polymorphic: true t.timestamps end end end ================================================ FILE: db/migrate/20220223035901_remove_null_check_deprecated_field.rb ================================================ class RemoveNullCheckDeprecatedField < ActiveRecord::Migration[6.1] def change change_column_null :casa_cases, :transition_aged_youth, true end end ================================================ FILE: db/migrate/20220226040507_create_fund_request.rb ================================================ class CreateFundRequest < ActiveRecord::Migration[6.1] def change create_table :fund_requests do |t| t.text :submitter_email t.text :youth_name t.text :payment_amount t.text :deadline t.text :request_purpose t.text :payee_name t.text :requested_by_and_relationship t.text :other_funding_source_sought t.text :impact t.text :extra_information t.text :timestamps end end end ================================================ FILE: db/migrate/20220303183053_create_other_duty.rb ================================================ class CreateOtherDuty < ActiveRecord::Migration[6.1] def change create_table :other_duties do |t| t.bigint :creator_id, null: false t.string :creator_type t.datetime :occurred_at t.bigint :duration_minutes t.text :notes t.timestamps end end end ================================================ FILE: db/migrate/20220323145733_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb ================================================ # This migration comes from active_storage (originally 20211119233751) class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] def change change_column_null(:active_storage_blobs, :checksum, true) end end ================================================ FILE: db/migrate/20220324141758_create_learning_hours.rb ================================================ class CreateLearningHours < ActiveRecord::Migration[6.1] def change create_table :learning_hours do |t| t.references :user, null: false, foreign_key: true t.integer :learning_type, default: 5, not_null: true t.string :name, null: false t.integer :duration_minutes, null: false t.integer :duration_hours, null: false t.datetime :occurred_at, null: false t.timestamps end end end ================================================ FILE: db/migrate/20220402201247_add_phone_number_to_users.rb ================================================ class AddPhoneNumberToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :phone_number, :string, default: "" end end ================================================ FILE: db/migrate/20220406011016_add_sms_notification_preferences_to_users.rb ================================================ class AddSmsNotificationPreferencesToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :receive_sms_notifications, :boolean, null: false, default: false end end ================================================ FILE: db/migrate/20220406011144_add_email_notification_preferences_to_users.rb ================================================ class AddEmailNotificationPreferencesToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :receive_email_notifications, :boolean, default: true end end ================================================ FILE: db/migrate/20220409184741_add_fund_request.rb ================================================ class AddFundRequest < ActiveRecord::Migration[7.0] def change add_column :casa_orgs, :show_fund_request, :boolean, default: false end end ================================================ FILE: db/migrate/20220411180242_add_uniqueness_constraint_to_feature_flag_name.rb ================================================ class AddUniquenessConstraintToFeatureFlagName < ActiveRecord::Migration[7.0] disable_ddl_transaction! def change add_index :feature_flags, :name, unique: true, algorithm: :concurrently end end ================================================ FILE: db/migrate/20220509224425_add_columns_to_casa_orgs.rb ================================================ class AddColumnsToCasaOrgs < ActiveRecord::Migration[7.0] def change add_column :casa_orgs, :twilio_phone_number, :string add_column :casa_orgs, :twilio_account_sid, :string add_column :casa_orgs, :twilio_api_key_sid, :string add_column :casa_orgs, :twilio_api_key_secret, :string end end ================================================ FILE: db/migrate/20220513084954_add_date_in_care_to_casa_cases.rb ================================================ class AddDateInCareToCasaCases < ActiveRecord::Migration[7.0] def change add_column :casa_cases, :date_in_care, :datetime end end ================================================ FILE: db/migrate/20220513111133_delete_versions.rb ================================================ class DeleteVersions < ActiveRecord::Migration[7.0] def up drop_table :versions end # copied from db/migrate/20200329085225_create_versions.rb TEXT_BYTES = 1_073_741_823 def down create_table :versions do |t| t.string :item_type, {null: false} t.integer :item_id, null: false, limit: 8 t.string :event, null: false t.string :whodunnit t.text :object, limit: TEXT_BYTES # Known issue in MySQL: fractional second precision # ------------------------------------------------- # # MySQL timestamp columns do not support fractional seconds unless # defined with "fractional seconds precision". MySQL users should manually # add fractional seconds precision to this migration, specifically, to # the `created_at` column. # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html) # # MySQL users should also upgrade to at least rails 4.2, which is the first # version of ActiveRecord with support for fractional seconds in MySQL. # (https://github.com/rails/rails/pull/14359) # t.datetime :created_at end add_index :versions, %i[item_type item_id] end end ================================================ FILE: db/migrate/20220519210423_create_sms_notification_events.rb ================================================ class CreateSmsNotificationEvents < ActiveRecord::Migration[7.0] def change create_table :sms_notification_events do |t| t.string :name t.string :user_type t.timestamps end end end ================================================ FILE: db/migrate/20220519233803_create_user_sms_notification_events.rb ================================================ class CreateUserSmsNotificationEvents < ActiveRecord::Migration[7.0] def change create_table :user_sms_notification_events do |t| t.references :user, null: false, foreign_key: true t.references :sms_notification_event, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20220526011848_create_user_reminder_times.rb ================================================ class CreateUserReminderTimes < ActiveRecord::Migration[7.0] def change create_table :user_reminder_times do |t| t.belongs_to :user, null: false, foreign_key: true t.datetime :case_contact_types t.datetime :no_contact_made t.timestamps end end end ================================================ FILE: db/migrate/20220602215632_create_addresses.rb ================================================ class CreateAddresses < ActiveRecord::Migration[7.0] def change create_table :addresses do |t| t.string :content t.belongs_to :user, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20220607184910_add_hide_old_contacts_to_case_assignment.rb ================================================ class AddHideOldContactsToCaseAssignment < ActiveRecord::Migration[7.0] def change add_column :case_assignments, :hide_old_contacts, :boolean, default: false end end ================================================ FILE: db/migrate/20220610221701_create_checklist_items.rb ================================================ class CreateChecklistItems < ActiveRecord::Migration[7.0] def change create_table :checklist_items do |t| t.integer :hearing_type_id t.text :description, null: false t.string :category, null: false t.boolean :mandatory, default: false, null: false t.timestamps end add_index :checklist_items, :hearing_type_id end end ================================================ FILE: db/migrate/20220615015056_create_patch_note_types.rb ================================================ class CreatePatchNoteTypes < ActiveRecord::Migration[7.0] def change create_table :patch_note_types do |t| t.string :name, null: false t.index :name, unique: true t.timestamps end end end ================================================ FILE: db/migrate/20220616021404_add_checklist_updated_date_to_hearing_types.rb ================================================ class AddChecklistUpdatedDateToHearingTypes < ActiveRecord::Migration[7.0] def change add_column :hearing_types, :checklist_updated_date, :string, default: "None", null: false end end ================================================ FILE: db/migrate/20220618042137_create_patch_note_groups.rb ================================================ class CreatePatchNoteGroups < ActiveRecord::Migration[7.0] def change create_table :patch_note_groups do |t| t.string :value, null: false t.index :value, unique: true t.timestamps end end end ================================================ FILE: db/migrate/20220622022147_create_patch_notes.rb ================================================ class CreatePatchNotes < ActiveRecord::Migration[7.0] def change create_table :patch_notes do |t| t.text :note, null: false t.references :patch_note_type, null: false, foreign_key: true t.references :patch_note_group, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20220820231119_create_languages.rb ================================================ class CreateLanguages < ActiveRecord::Migration[7.0] def change create_table :languages do |t| t.string :name t.references :casa_org, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20220826130829_create_languages_users_join_table.rb ================================================ class CreateLanguagesUsersJoinTable < ActiveRecord::Migration[7.0] def change create_join_table :languages, :users do |t| t.index :language_id t.index :user_id end end end ================================================ FILE: db/migrate/20220924181447_remove_all_empty_languages.rb ================================================ class RemoveAllEmptyLanguages < ActiveRecord::Migration[7.0] def up safety_assured { execute "DELETE from languages WHERE name IS NULL or trim(name) = ''" } end end ================================================ FILE: db/migrate/20221002103627_add_user_foreign_key_to_other_duties.rb ================================================ class AddUserForeignKeyToOtherDuties < ActiveRecord::Migration[7.0] def change add_foreign_key :other_duties, :users, column: :creator_id, validate: false end end ================================================ FILE: db/migrate/20221002103754_validate_add_user_foreign_key_to_other_duties.rb ================================================ class ValidateAddUserForeignKeyToOtherDuties < ActiveRecord::Migration[7.0] def change validate_foreign_key :other_duties, :users end end ================================================ FILE: db/migrate/20221003202112_add_court_report_due_date_to_court_dates.rb ================================================ class AddCourtReportDueDateToCourtDates < ActiveRecord::Migration[7.0] def change add_column :court_dates, :court_report_due_date, :datetime, precision: nil end end ================================================ FILE: db/migrate/20221011044911_add_user_languages.rb ================================================ class AddUserLanguages < ActiveRecord::Migration[7.0] disable_ddl_transaction! def change create_table :user_languages do |t| t.references :user t.references :language t.timestamps end add_index :user_languages, [:language_id, :user_id], unique: true, algorithm: :concurrently end end ================================================ FILE: db/migrate/20221012203806_populate_user_languages_from_languages_users.rb ================================================ class PopulateUserLanguagesFromLanguagesUsers < ActiveRecord::Migration[7.0] def change query = Arel.sql("select language_id, user_id from languages_users") old_join_table_entries = ActiveRecord::Base.connection.execute(query).to_a old_join_table_entries.each do |entry| UserLanguage.create(user_id: entry["user_id"], language_id: entry["language_id"]) end end end ================================================ FILE: db/migrate/20230121174227_remove_court_data_from_casa_cases.rb ================================================ class RemoveCourtDataFromCasaCases < ActiveRecord::Migration[7.0] def change safety_assured { remove_column :casa_cases, :court_date } end end ================================================ FILE: db/migrate/20230220210146_add_phone_number_to_all_casa_admin_resource.rb ================================================ class AddPhoneNumberToAllCasaAdminResource < ActiveRecord::Migration[7.0] def change add_column :all_casa_admins, :phone_number, :string, default: "" end end ================================================ FILE: db/migrate/20230316152808_remove_phone_number_from_all_casa_admin_resource.rb ================================================ class RemovePhoneNumberFromAllCasaAdminResource < ActiveRecord::Migration[7.0] def change safety_assured { remove_column :all_casa_admins, :phone_number } end end ================================================ FILE: db/migrate/20230326225216_create_placement_types.rb ================================================ class CreatePlacementTypes < ActiveRecord::Migration[7.0] def change create_table :placement_types do |t| t.string :name, null: false t.references :casa_org, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20230326225230_create_placements.rb ================================================ class CreatePlacements < ActiveRecord::Migration[7.0] def change create_table :placements do |t| # Add table placement_type: name, casa_org_id. Add table placement: placement_id, casa_case_id, started_at, created_by_id (links to user table) t.datetime :placement_started_at, null: false t.references :placement_type, null: false, foreign_key: true t.references :creator, foreign_key: {to_table: :users}, null: false t.timestamps end end end ================================================ FILE: db/migrate/20230327154626_add_confirmable_to_users.rb ================================================ class AddConfirmableToUsers < ActiveRecord::Migration[7.0] disable_ddl_transaction! # Note: You can't use change, as User.update_all will fail in the down migration def up add_column :users, :confirmation_token, :string add_column :users, :confirmed_at, :datetime add_column :users, :confirmation_sent_at, :datetime add_column :users, :unconfirmed_email, :string # Only if using reconfirmable add_index :users, :confirmation_token, unique: true, algorithm: :concurrently # User.reset_column_information # Need for some types of updates, but not for update_all. # To avoid a short time window between running the migration and updating all existing # users as confirmed, do the following # User.update_all confirmed_at: DateTime.now # All existing user accounts should be able to log in after this. end def down remove_index :users, :confirmation_token remove_columns :users, :confirmation_token, :confirmed_at, :confirmation_sent_at remove_columns :users, :unconfirmed_email # Only if using reconfirmable end end ================================================ FILE: db/migrate/20230327155053_add_email_confirmation_and_old_emails_to_users.rb ================================================ class AddEmailConfirmationAndOldEmailsToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :old_emails, :string, array: true, default: [] add_column :users, :email_confirmation, :string end end ================================================ FILE: db/migrate/20230405202939_remove_email_confirmation_from_users.rb ================================================ class RemoveEmailConfirmationFromUsers < ActiveRecord::Migration[7.0] def change safety_assured { remove_column :users, :email_confirmation, :string } end end ================================================ FILE: db/migrate/20230412103356_add_casa_case_to_placements.rb ================================================ class AddCasaCaseToPlacements < ActiveRecord::Migration[7.0] disable_ddl_transaction! def change add_reference :placements, :casa_case, null: false, index: {algorithm: :concurrently} end end ================================================ FILE: db/migrate/20230420212437_add_table_columns_display_to_preference_set.rb ================================================ class AddTableColumnsDisplayToPreferenceSet < ActiveRecord::Migration[7.0] def change add_column :preference_sets, :table_state, :jsonb, default: {} end end ================================================ FILE: db/migrate/20230610153139_add_receive_reimbursement_email_to_users.rb ================================================ class AddReceiveReimbursementEmailToUsers < ActiveRecord::Migration[7.0] def change return if column_exists?(:users, :receive_reimbursement_email) add_column :users, :receive_reimbursement_email, :boolean, default: false end end ================================================ FILE: db/migrate/20230615155223_add_twilio_enabled_to_casa_orgs.rb ================================================ class AddTwilioEnabledToCasaOrgs < ActiveRecord::Migration[7.0] def change add_column :casa_orgs, :twilio_enabled, :boolean, default: false end end ================================================ FILE: db/migrate/20230621161252_add_additional_expenses_to_casa_orgs.rb ================================================ class AddAdditionalExpensesToCasaOrgs < ActiveRecord::Migration[7.0] def change add_column :casa_orgs, :additional_expenses_enabled, :boolean, default: false end end ================================================ FILE: db/migrate/20230627210040_add_allow_reimbursement_to_case_assignments.rb ================================================ class AddAllowReimbursementToCaseAssignments < ActiveRecord::Migration[7.0] def change add_column :case_assignments, :allow_reimbursement, :boolean, default: true end end ================================================ FILE: db/migrate/20230704123327_add_foreign_key_constraints_to_mileage_rates.rb ================================================ class AddForeignKeyConstraintsToMileageRates < ActiveRecord::Migration[7.0] def change add_foreign_key :mileage_rates, :casa_orgs, column: :casa_org_id, validate: false end end ================================================ FILE: db/migrate/20230710025852_add_token_to_users.rb ================================================ class AddTokenToUsers < ActiveRecord::Migration[7.0] def up add_column :users, :token, :string end def down remove_column :users, :token, :string end end ================================================ FILE: db/migrate/20230712080040_add_foreign_key_creator_id_to_note.rb ================================================ class AddForeignKeyCreatorIdToNote < ActiveRecord::Migration[7.0] def up add_foreign_key :notes, :users, column: :creator_id, validate: false end def down remove_foreign_key :notes, :creator_id end end ================================================ FILE: db/migrate/20230728135743_create_action_text_tables.action_text.rb ================================================ # This migration comes from action_text (originally 20180528164100) class CreateActionTextTables < ActiveRecord::Migration[6.0] def change # Use Active Record's configured type for primary and foreign keys primary_key_type, foreign_key_type = primary_and_foreign_key_types create_table :action_text_rich_texts, id: primary_key_type do |t| t.string :name, null: false t.text :body, size: :long t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type t.timestamps t.index [:record_type, :record_id, :name], name: "index_action_text_rich_texts_uniqueness", unique: true end end private def primary_and_foreign_key_types config = Rails.configuration.generators setting = config.options[config.orm][:primary_key_type] primary_key_type = setting || :primary_key foreign_key_type = setting || :bigint [primary_key_type, foreign_key_type] end end ================================================ FILE: db/migrate/20230728140249_create_banners.rb ================================================ class CreateBanners < ActiveRecord::Migration[7.0] def change create_table :banners do |t| t.references :casa_org, null: false, foreign_key: true t.references :user, null: false, foreign_key: true t.string :name t.boolean :active, default: false t.timestamps end end end ================================================ FILE: db/migrate/20230729143126_create_learning_hour_types.rb ================================================ class CreateLearningHourTypes < ActiveRecord::Migration[7.0] def change create_table :learning_hour_types do |t| t.references :casa_org, null: false, foreign_key: true t.string :name t.boolean :active, default: true t.integer :position, default: 1 t.timestamps end end end ================================================ FILE: db/migrate/20230729145310_add_reference_learning_hour_types.rb ================================================ class AddReferenceLearningHourTypes < ActiveRecord::Migration[7.0] disable_ddl_transaction! def change add_reference :learning_hours, :learning_hour_type, validate: false, index: {algorithm: :concurrently} end end ================================================ FILE: db/migrate/20230729145351_add_foreign_key_learning_hour_types.rb ================================================ class AddForeignKeyLearningHourTypes < ActiveRecord::Migration[7.0] def change add_foreign_key :learning_hours, :learning_hour_types, validate: false end end ================================================ FILE: db/migrate/20230729145419_validate_foreign_key_learning_hour_types.rb ================================================ class ValidateForeignKeyLearningHourTypes < ActiveRecord::Migration[7.0] def change validate_foreign_key :learning_hours, :learning_hour_types end end ================================================ FILE: db/migrate/20230729154529_create_case_groups.rb ================================================ class CreateCaseGroups < ActiveRecord::Migration[7.0] def change create_table :case_groups do |t| t.references :casa_org, null: false, foreign_key: true t.string :name t.timestamps end end end ================================================ FILE: db/migrate/20230729154545_create_case_group_memberships.rb ================================================ class CreateCaseGroupMemberships < ActiveRecord::Migration[7.0] def change create_table :case_group_memberships do |t| t.references :case_group, null: false, foreign_key: true t.references :casa_case, null: false, foreign_key: true t.timestamps end end end ================================================ FILE: db/migrate/20230729213608_remove_hearing_type_id_and_judge_id_from_casa_cases.rb ================================================ class RemoveHearingTypeIdAndJudgeIdFromCasaCases < ActiveRecord::Migration[7.0] def change safety_assured { remove_column :casa_cases, :hearing_type_id } safety_assured { remove_column :casa_cases, :judge_id } end end ================================================ FILE: db/migrate/20230730103110_remove_learning_type.rb ================================================ class RemoveLearningType < ActiveRecord::Migration[7.0] def change safety_assured { remove_column :learning_hours, :learning_type, :integer, default: 5, not_null: true } end end ================================================ FILE: db/migrate/20230809002819_drop_languages_users.rb ================================================ class DropLanguagesUsers < ActiveRecord::Migration[7.0] def up drop_table :languages_users end def down fail ActiveRecord::IrreversibleMigration end end ================================================ FILE: db/migrate/20230817144910_create_learning_hour_topics.rb ================================================ class CreateLearningHourTopics < ActiveRecord::Migration[7.0] def change create_table :learning_hour_topics do |t| t.string :name, null: false t.references :casa_org, null: false, foreign_key: true t.integer :position, default: 1 t.timestamps end end end ================================================ FILE: db/migrate/20230819124840_add_learning_hour_topic_id_to_learning_hour.rb ================================================ class AddLearningHourTopicIdToLearningHour < ActiveRecord::Migration[7.0] disable_ddl_transaction! def change add_reference :learning_hours, :learning_hour_topic, index: {algorithm: :concurrently} end end ================================================ FILE: db/migrate/20230819132316_add_learning_topic_active_to_casa_org.rb ================================================ class AddLearningTopicActiveToCasaOrg < ActiveRecord::Migration[7.0] def change add_column :casa_orgs, :learning_topic_active, :boolean, default: false end end ================================================ FILE: db/migrate/20230822152341_drop_jwt_denylist_table.rb ================================================ class DropJwtDenylistTable < ActiveRecord::Migration[7.0] def change drop_table :jwt_denylist, if_exists: true end end ================================================ FILE: db/migrate/20230902021531_add_monthly_learning_hours_report_to_user.rb ================================================ class AddMonthlyLearningHoursReportToUser < ActiveRecord::Migration[7.0] def change add_column :users, :monthly_learning_hours_report, :boolean, default: false, null: false end end ================================================ FILE: db/migrate/20230903182657_add_foreign_key_casa_case_to_placement.rb ================================================ class AddForeignKeyCasaCaseToPlacement < ActiveRecord::Migration[7.0] def change add_foreign_key :placements, :casa_cases, column: :casa_case_id, validate: false end end ================================================ FILE: db/migrate/20231102181027_add_birthdays_to_users.rb ================================================ class AddBirthdaysToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :date_of_birth, :datetime end end ================================================ FILE: db/migrate/20231125150721_status_for_case_contacts.rb ================================================ class StatusForCaseContacts < ActiveRecord::Migration[7.0] def change add_column :case_contacts, :status, :string, default: "started" add_column :case_contacts, :draft_case_ids, :integer, array: true, default: [] add_column :case_contacts, :volunteer_address, :string # We need these columns to be nil if the case_contact is in_progress (ie. DRAFT mode) change_column_null :case_contacts, :casa_case_id, true change_column_null :case_contacts, :duration_minutes, true change_column_null :case_contacts, :occurred_at, true end end ================================================ FILE: db/migrate/20240216013254_add_contact_topics.rb ================================================ class AddContactTopics < ActiveRecord::Migration[7.1] def change create_table :contact_topics do |t| t.references :casa_org, null: false, foreign_key: true t.boolean :active, null: false, default: true t.boolean :soft_delete, null: false, default: false t.text :details t.string :question t.timestamps end create_table :contact_topic_answers do |t| t.text :value t.references :case_contact, null: false, foreign_key: true t.references :contact_topic, null: false, foreign_key: true t.boolean :selected, null: false, default: false t.timestamps end end end ================================================ FILE: db/migrate/20240415160842_add_followupable_to_followups.rb ================================================ class AddFollowupableToFollowups < ActiveRecord::Migration[7.1] disable_ddl_transaction! # To handle Dangerous operation detected by #strong_migrations in converting followups to polymorphic we will: # 1. Create a new column # 2. Write to both columns # 3. Backfill data from the old column to the new column # 4. Move reads from the old column to the new column # 5. Stop writing to the old column # 6. Drop the old column # def change add_column :followups, :followupable_id, :bigint add_column :followups, :followupable_type, :string add_index :followups, [:followupable_type, :followupable_id], algorithm: :concurrently end end ================================================ FILE: db/migrate/20240507022441_create_login_activities.rb ================================================ class CreateLoginActivities < ActiveRecord::Migration[7.1] def change create_table :login_activities do |t| t.string :scope t.string :strategy t.string :identity, index: true t.boolean :success t.string :failure_reason t.references :user, polymorphic: true t.string :context t.string :ip, index: true t.text :user_agent t.text :referrer t.string :city t.string :region t.string :country t.float :latitude t.float :longitude t.datetime :created_at end end end ================================================ FILE: db/migrate/20240509104733_create_noticed_tables.noticed.rb ================================================ # This migration comes from noticed (originally 20231215190233) class CreateNoticedTables < ActiveRecord::Migration[6.1] def change primary_key_type, foreign_key_type = primary_and_foreign_key_types create_table :noticed_events, id: primary_key_type do |t| t.string :type t.belongs_to :record, polymorphic: true, type: foreign_key_type if t.respond_to?(:jsonb) t.jsonb :params else t.json :params end t.timestamps end create_table :noticed_notifications, id: primary_key_type do |t| t.string :type t.belongs_to :event, null: false, type: foreign_key_type t.belongs_to :recipient, polymorphic: true, null: false, type: foreign_key_type t.datetime :read_at t.datetime :seen_at t.timestamps end end private def primary_and_foreign_key_types config = Rails.configuration.generators setting = config.options[config.orm][:primary_key_type] primary_key_type = setting || :primary_key foreign_key_type = setting || :bigint [primary_key_type, foreign_key_type] end end ================================================ FILE: db/migrate/20240509104734_add_notifications_count_to_noticed_event.noticed.rb ================================================ # This migration comes from noticed (originally 20240129184740) class AddNotificationsCountToNoticedEvent < ActiveRecord::Migration[6.1] def change add_column :noticed_events, :notifications_count, :integer end end ================================================ FILE: db/migrate/20240531172823_add_expires_at_to_banner.rb ================================================ class AddExpiresAtToBanner < ActiveRecord::Migration[7.1] def change add_column :banners, :expires_at, :datetime, null: true end end ================================================ FILE: db/migrate/20240610071054_add_other_duties_enabled_to_casa_org.rb ================================================ class AddOtherDutiesEnabledToCasaOrg < ActiveRecord::Migration[7.1] def change add_column :casa_orgs, :other_duties_enabled, :boolean, default: true end end ================================================ FILE: db/migrate/20240621165358_create_flipper_tables.rb ================================================ class CreateFlipperTables < ActiveRecord::Migration[7.1] def up create_table :flipper_features do |t| t.string :key, null: false t.timestamps null: false end add_index :flipper_features, :key, unique: true create_table :flipper_gates do |t| t.string :feature_key, null: false t.string :key, null: false t.text :value t.timestamps null: false end add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: {value: 255} end def down drop_table :flipper_gates drop_table :flipper_features end end ================================================ FILE: db/migrate/20240622020203_drop_feature_flags.rb ================================================ class DropFeatureFlags < ActiveRecord::Migration[7.1] def up drop_table :feature_flags end def down create_table :feature_flags do |t| t.string :name, null: false t.boolean :enabled, null: false, default: false t.timestamps end add_index :feature_flags, :name, unique: true, algorithm: :concurrently end end ================================================ FILE: db/migrate/20240716194609_add_metadata_to_case_contacts.rb ================================================ class AddMetadataToCaseContacts < ActiveRecord::Migration[7.1] def change add_column :case_contacts, :metadata, :jsonb, default: {} end end ================================================ FILE: db/migrate/20241017050129_remove_contact_topic_answer_contact_topic_id_null_constraint.rb ================================================ class RemoveContactTopicAnswerContactTopicIdNullConstraint < ActiveRecord::Migration[7.2] def change change_column_null(:contact_topic_answers, :contact_topic_id, true) end end ================================================ FILE: db/migrate/20250207080433_remove_token_from_users.rb ================================================ class RemoveTokenFromUsers < ActiveRecord::Migration[7.2] def change safety_assured { remove_column :users, :token, :string } end end ================================================ FILE: db/migrate/20250207080511_create_api_credentials.rb ================================================ class CreateApiCredentials < ActiveRecord::Migration[7.2] def change create_table :api_credentials do |t| t.references :user, null: false, foreign_key: true t.string :api_token t.string :refresh_token t.datetime :token_expires_at, default: -> { "NOW() + INTERVAL '7 hours'" } t.datetime :refresh_token_expires_at, default: -> { "NOW() + INTERVAL '30 days'" } t.string :api_token_digest t.string :refresh_token_digest t.timestamps end add_index :api_credentials, :api_token_digest, unique: true, where: "api_token_digest IS NOT NULL" add_index :api_credentials, :refresh_token_digest, unique: true, where: "refresh_token_digest IS NOT NULL" end end ================================================ FILE: db/migrate/20250208160513_remove_plain_text_tokens_from_api_credentials.rb ================================================ class RemovePlainTextTokensFromApiCredentials < ActiveRecord::Migration[7.2] def change safety_assured { remove_column :api_credentials, :api_token, :string } safety_assured { remove_column :api_credentials, :refresh_token, :string } end end ================================================ FILE: db/migrate/20250331032424_validate_constraint_mileage_rates.rb ================================================ class ValidateConstraintMileageRates < ActiveRecord::Migration[7.2] def up ActiveRecord::Base.connection.execute(Arel.sql("ALTER TABLE mileage_rates VALIDATE CONSTRAINT fk_rails_3dad81992f;")) end def down # cannot un-validate a constraint end end ================================================ FILE: db/migrate/20250331033339_validate_constraint_notes.rb ================================================ class ValidateConstraintNotes < ActiveRecord::Migration[7.2] def up ActiveRecord::Base.connection.execute(Arel.sql("ALTER TABLE notes VALIDATE CONSTRAINT fk_rails_5d4a723a34;")) end def down # cannot un-validate a constraint end end ================================================ FILE: db/migrate/20250331033350_validate_constraint_placements.rb ================================================ class ValidateConstraintPlacements < ActiveRecord::Migration[7.2] def up ActiveRecord::Base.connection.execute(Arel.sql("ALTER TABLE placements VALIDATE CONSTRAINT fk_rails_65aeeb5669;")) end def down # cannot un-validate a constraint end end ================================================ FILE: db/migrate/20250331033418_remove_duplicate_indexindex_emancipation_options_on_emancipation_category_id.rb ================================================ class RemoveDuplicateIndexindexEmancipationOptionsOnEmancipationCategoryId < ActiveRecord::Migration[7.2] def change remove_index :emancipation_options, :emancipation_category_id end end ================================================ FILE: db/migrate/20250331033441_remove_duplicate_indexindex_user_languages_on_language_id.rb ================================================ class RemoveDuplicateIndexindexUserLanguagesOnLanguageId < ActiveRecord::Migration[7.2] def change remove_index :user_languages, :language_id end end ================================================ FILE: db/migrate/20250404200715_create_custom_org_links.rb ================================================ class CreateCustomOrgLinks < ActiveRecord::Migration[7.2] def change create_table :custom_org_links do |t| t.references :casa_org, null: false, foreign_key: true t.string :text, null: false t.string :url, null: false t.boolean :active, null: false, default: true t.timestamps end end end ================================================ FILE: db/migrate/20250507011754_remove_unused_indexes.rb ================================================ class RemoveUnusedIndexes < ActiveRecord::Migration[7.2] def change remove_index :case_contacts, name: "index_case_contacts_on_deleted_at", if_exists: true remove_index :noticed_notifications, name: "index_noticed_notifications_on_event_id", if_exists: true remove_index :contact_topic_answers, name: "index_contact_topic_answers_on_contact_topic_id", if_exists: true remove_index :login_activities, name: "index_login_activities_on_ip", if_exists: true remove_index :login_activities, name: "index_login_activities_on_identity", if_exists: true remove_index :delayed_jobs, name: "delayed_jobs_priority", if_exists: true remove_index :login_activities, name: "index_login_activities_on_user", if_exists: true remove_index :sent_emails, name: "index_sent_emails_on_user_id", if_exists: true remove_index :sent_emails, name: "index_sent_emails_on_casa_org_id", if_exists: true remove_index :noticed_events, name: "index_noticed_events_on_record", if_exists: true remove_index :notifications, name: "index_notifications_on_read_at", if_exists: true remove_index :notifications, name: "index_notifications_on_recipient", if_exists: true remove_index :api_credentials, name: "index_api_credentials_on_user_id", if_exists: true remove_index :court_dates, name: "index_court_dates_on_hearing_type_id", if_exists: true remove_index :court_dates, name: "index_court_dates_on_judge_id", if_exists: true remove_index :followups, name: "index_followups_on_followupable_type_and_followupable_id", if_exists: true remove_index :banners, name: "index_banners_on_casa_org_id", if_exists: true remove_index :banners, name: "index_banners_on_user_id", if_exists: true remove_index :casa_case_emancipation_categories, name: "index_case_emancipation_categories_on_emancipation_category_id", if_exists: true remove_index :case_group_memberships, name: "index_case_group_memberships_on_casa_case_id", if_exists: true remove_index :case_group_memberships, name: "index_case_group_memberships_on_case_group_id", if_exists: true remove_index :case_groups, name: "index_case_groups_on_casa_org_id", if_exists: true remove_index :contact_topics, name: "index_contact_topics_on_casa_org_id", if_exists: true remove_index :contact_type_groups, name: "index_contact_type_groups_on_casa_org_id", if_exists: true remove_index :followups, name: "index_followups_on_creator_id", if_exists: true remove_index :hearing_types, name: "index_hearing_types_on_casa_org_id", if_exists: true remove_index :judges, name: "index_judges_on_casa_org_id", if_exists: true remove_index :languages, name: "index_languages_on_casa_org_id", if_exists: true remove_index :learning_hour_topics, name: "index_learning_hour_topics_on_casa_org_id", if_exists: true remove_index :learning_hour_types, name: "index_learning_hour_types_on_casa_org_id", if_exists: true remove_index :learning_hours, name: "index_learning_hours_on_learning_hour_topic_id", if_exists: true remove_index :learning_hours, name: "index_learning_hours_on_learning_hour_type_id", if_exists: true remove_index :mileage_rates, name: "index_mileage_rates_on_casa_org_id", if_exists: true remove_index :mileage_rates, name: "index_mileage_rates_on_user_id", if_exists: true remove_index :notes, name: "index_notes_on_notable", if_exists: true remove_index :patch_notes, name: "index_patch_notes_on_patch_note_group_id", if_exists: true remove_index :patch_notes, name: "index_patch_notes_on_patch_note_type_id", if_exists: true remove_index :user_languages, name: "index_user_languages_on_user_id", if_exists: true remove_index :user_sms_notification_events, name: "index_user_sms_notification_events_on_sms_notification_event_id", if_exists: true remove_index :user_sms_notification_events, name: "index_user_sms_notification_events_on_user_id", if_exists: true remove_index :users, name: "index_users_on_invitations_count", if_exists: true remove_index :users, name: "index_users_on_invited_by_type_and_invited_by_id", if_exists: true remove_index :checklist_items, name: "index_checklist_items_on_hearing_type_id", if_exists: true remove_index :placement_types, name: "index_placement_types_on_casa_org_id", if_exists: true remove_index :placements, name: "index_placements_on_casa_case_id", if_exists: true remove_index :placements, name: "index_placements_on_creator_id", if_exists: true remove_index :placements, name: "index_placements_on_placement_type_id", if_exists: true remove_index :user_case_contact_types_reminders, name: "index_user_case_contact_types_reminders_on_user_id", if_exists: true end end ================================================ FILE: db/migrate/20250528092341_trim_whitespace_from_custom_org_links.rb ================================================ class TrimWhitespaceFromCustomOrgLinks < ActiveRecord::Migration[7.2] def up CustomOrgLink.find_each do |link| trimmed_text = link.text.strip link.update_columns(text: trimmed_text) if trimmed_text.present? rescue => e Rails.logger.error("Failed to update CustomOrgLink ##{link.id}: #{e.message}") end end def down Rails.logger.info("Rollback not implemented for TrimWhitespaceFromCustomOrgLinks as it is a data migration") end end ================================================ FILE: db/migrate/20250702142004_add_exclude_from_court_report_to_contact_topics.rb ================================================ class AddExcludeFromCourtReportToContactTopics < ActiveRecord::Migration[7.2] def change add_column :contact_topics, :exclude_from_court_report, :boolean, default: false, null: false end end ================================================ FILE: db/migrate/20260210233737_rename_casa_cases_emancipaton_options_to_casa_case_emancipaton_options.rb ================================================ class RenameCasaCasesEmancipatonOptionsToCasaCaseEmancipatonOptions < ActiveRecord::Migration[7.2] def change reversible do |dir| dir.up do create_table "casa_case_emancipation_options" do |t| t.bigint "casa_case_id", null: false t.bigint "emancipation_option_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["casa_case_id", "emancipation_option_id"], name: "index_case_options_on_case_id_and_option_id", unique: true end add_foreign_key "casa_case_emancipation_options", "casa_cases", validate: false add_foreign_key "casa_case_emancipation_options", "emancipation_options", validate: false safety_assured do execute <<-SQL INSERT INTO casa_case_emancipation_options (casa_case_id, emancipation_option_id, created_at, updated_at) SELECT casa_case_id, emancipation_option_id, created_at, updated_at FROM casa_cases_emancipation_options; SQL end end dir.down do remove_foreign_key "casa_case_emancipation_options", "casa_cases", validate: false remove_foreign_key "casa_case_emancipation_options", "emancipation_options", validate: false drop_table :casa_case_emancipation_options end end end end ================================================ FILE: db/migrate/20260211001655_rename_casa_cases_emancipaton_options_to_casa_case_emancipaton_options_follow_up.rb ================================================ class RenameCasaCasesEmancipatonOptionsToCasaCaseEmancipatonOptionsFollowUp < ActiveRecord::Migration[7.2] def up validate_foreign_key "casa_case_emancipation_options", "casa_cases" validate_foreign_key "casa_case_emancipation_options", "emancipation_options" drop_table :casa_cases_emancipation_options end def down fail ActiveRecord::IrreversibleMigration end end ================================================ FILE: db/migrate/20260414132818_add_deleted_at_to_contact_topic_answers.rb ================================================ class AddDeletedAtToContactTopicAnswers < ActiveRecord::Migration[7.2] def change add_column :contact_topic_answers, :deleted_at, :datetime end end ================================================ FILE: db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[7.2].define(version: 2026_04_14_132818) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "action_text_rich_texts", force: :cascade do |t| t.string "name", null: false t.text "body" t.string "record_type", null: false t.bigint "record_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end create_table "active_storage_blobs", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" t.text "metadata" t.bigint "byte_size", null: false t.string "checksum" t.datetime "created_at", precision: nil, null: false t.string "service_name", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end create_table "active_storage_variant_records", force: :cascade do |t| t.bigint "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end create_table "additional_expenses", force: :cascade do |t| t.bigint "case_contact_id", null: false t.decimal "other_expense_amount" t.string "other_expenses_describe" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["case_contact_id"], name: "index_additional_expenses_on_case_contact_id" end create_table "addresses", force: :cascade do |t| t.string "content" t.bigint "user_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id"], name: "index_addresses_on_user_id" end create_table "all_casa_admins", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at", precision: nil t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "invitation_token" t.datetime "invitation_created_at", precision: nil t.datetime "invitation_sent_at", precision: nil t.datetime "invitation_accepted_at", precision: nil t.integer "invitation_limit" t.integer "invited_by_id" t.string "invited_by_type" t.index ["email"], name: "index_all_casa_admins_on_email", unique: true t.index ["invitation_token"], name: "index_all_casa_admins_on_invitation_token", unique: true t.index ["reset_password_token"], name: "index_all_casa_admins_on_reset_password_token", unique: true end create_table "api_credentials", force: :cascade do |t| t.bigint "user_id", null: false t.datetime "token_expires_at", default: -> { "(now() + 'PT7H'::interval)" } t.datetime "refresh_token_expires_at", default: -> { "(now() + 'P30D'::interval)" } t.string "api_token_digest" t.string "refresh_token_digest" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["api_token_digest"], name: "index_api_credentials_on_api_token_digest", unique: true, where: "(api_token_digest IS NOT NULL)" t.index ["refresh_token_digest"], name: "index_api_credentials_on_refresh_token_digest", unique: true, where: "(refresh_token_digest IS NOT NULL)" end create_table "banners", force: :cascade do |t| t.bigint "casa_org_id", null: false t.bigint "user_id", null: false t.string "name" t.boolean "active", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "expires_at" end create_table "casa_case_contact_types", force: :cascade do |t| t.bigint "contact_type_id", null: false t.bigint "casa_case_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["casa_case_id"], name: "index_casa_case_contact_types_on_casa_case_id" t.index ["contact_type_id"], name: "index_casa_case_contact_types_on_contact_type_id" end create_table "casa_case_emancipation_categories", force: :cascade do |t| t.bigint "casa_case_id", null: false t.bigint "emancipation_category_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["casa_case_id"], name: "index_casa_case_emancipation_categories_on_casa_case_id" end create_table "casa_case_emancipation_options", force: :cascade do |t| t.bigint "casa_case_id", null: false t.bigint "emancipation_option_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["casa_case_id", "emancipation_option_id"], name: "index_case_options_on_case_id_and_option_id", unique: true end create_table "casa_cases", force: :cascade do |t| t.string "case_number", null: false t.boolean "transition_aged_youth", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "casa_org_id", null: false t.datetime "birth_month_year_youth", precision: nil t.datetime "court_report_due_date", precision: nil t.boolean "active", default: true, null: false t.datetime "court_report_submitted_at", precision: nil t.integer "court_report_status", default: 0 t.string "slug" t.datetime "date_in_care" t.index ["casa_org_id"], name: "index_casa_cases_on_casa_org_id" t.index ["case_number", "casa_org_id"], name: "index_casa_cases_on_case_number_and_casa_org_id", unique: true t.index ["slug"], name: "index_casa_cases_on_slug" end create_table "casa_orgs", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "display_name" t.string "address" t.string "footer_links", default: [], array: true t.string "slug" t.boolean "show_driving_reimbursement", default: true t.boolean "show_fund_request", default: false t.string "twilio_phone_number" t.string "twilio_account_sid" t.string "twilio_api_key_sid" t.string "twilio_api_key_secret" t.boolean "twilio_enabled", default: false t.boolean "additional_expenses_enabled", default: false t.boolean "learning_topic_active", default: false t.boolean "other_duties_enabled", default: true t.index ["slug"], name: "index_casa_orgs_on_slug", unique: true end create_table "case_assignments", force: :cascade do |t| t.bigint "casa_case_id", null: false t.bigint "volunteer_id", null: false t.boolean "active", default: true, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "hide_old_contacts", default: false t.boolean "allow_reimbursement", default: true t.index ["casa_case_id"], name: "index_case_assignments_on_casa_case_id" t.index ["volunteer_id"], name: "index_case_assignments_on_volunteer_id" end create_table "case_contact_contact_types", force: :cascade do |t| t.bigint "case_contact_id", null: false t.bigint "contact_type_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["case_contact_id"], name: "index_case_contact_contact_types_on_case_contact_id" t.index ["contact_type_id"], name: "index_case_contact_contact_types_on_contact_type_id" end create_table "case_contacts", force: :cascade do |t| t.bigint "creator_id", null: false t.bigint "casa_case_id" t.integer "duration_minutes" t.datetime "occurred_at", precision: nil t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "contact_made", default: false t.string "medium_type" t.integer "miles_driven", default: 0, null: false t.boolean "want_driving_reimbursement", default: false t.string "notes" t.datetime "deleted_at", precision: nil t.boolean "reimbursement_complete", default: false t.string "status", default: "started" t.integer "draft_case_ids", default: [], array: true t.string "volunteer_address" t.jsonb "metadata", default: {} t.index ["casa_case_id"], name: "index_case_contacts_on_casa_case_id" t.index ["creator_id"], name: "index_case_contacts_on_creator_id" 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_orders", force: :cascade do |t| t.bigint "casa_case_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "implementation_status" t.bigint "court_date_id" t.string "text" t.index ["casa_case_id"], name: "index_case_court_orders_on_casa_case_id" t.index ["court_date_id"], name: "index_case_court_orders_on_court_date_id" end create_table "case_group_memberships", force: :cascade do |t| t.bigint "case_group_id", null: false t.bigint "casa_case_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "case_groups", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "checklist_items", force: :cascade do |t| t.integer "hearing_type_id" t.text "description", null: false t.string "category", null: false t.boolean "mandatory", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "contact_topic_answers", force: :cascade do |t| t.text "value" t.bigint "case_contact_id", null: false t.bigint "contact_topic_id" t.boolean "selected", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "deleted_at" t.index ["case_contact_id"], name: "index_contact_topic_answers_on_case_contact_id" end create_table "contact_topics", force: :cascade do |t| t.bigint "casa_org_id", null: false t.boolean "active", default: true, null: false t.boolean "soft_delete", default: false, null: false t.text "details" t.string "question" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "exclude_from_court_report", default: false, null: false end create_table "contact_type_groups", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "active", default: true end create_table "contact_types", force: :cascade do |t| t.bigint "contact_type_group_id", null: false t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "active", default: true t.index ["contact_type_group_id"], name: "index_contact_types_on_contact_type_group_id" end create_table "court_dates", force: :cascade do |t| t.datetime "date", precision: nil, null: false t.bigint "casa_case_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "hearing_type_id" t.bigint "judge_id" t.datetime "court_report_due_date", precision: nil t.index ["casa_case_id"], name: "index_court_dates_on_casa_case_id" end create_table "custom_org_links", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "text", null: false t.string "url", null: false t.boolean "active", default: true, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["casa_org_id"], name: "index_custom_org_links_on_casa_org_id" end create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false t.text "handler", null: false t.text "last_error" t.datetime "run_at", precision: nil t.datetime "locked_at", precision: nil t.datetime "failed_at", precision: nil t.string "locked_by" t.string "queue" t.datetime "created_at" t.datetime "updated_at" end create_table "emancipation_categories", force: :cascade do |t| t.string "name", null: false t.boolean "mutually_exclusive", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["name"], name: "index_emancipation_categories_on_name", unique: true end create_table "emancipation_options", force: :cascade do |t| t.bigint "emancipation_category_id", null: false t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["emancipation_category_id", "name"], name: "index_emancipation_options_on_emancipation_category_id_and_name", unique: true end create_table "flipper_features", force: :cascade do |t| t.string "key", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["key"], name: "index_flipper_features_on_key", unique: true end create_table "flipper_gates", force: :cascade do |t| t.string "feature_key", null: false t.string "key", null: false t.text "value" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true end create_table "followups", force: :cascade do |t| t.bigint "case_contact_id" t.bigint "creator_id" t.integer "status", default: 0 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "note" t.bigint "followupable_id" t.string "followupable_type" t.index ["case_contact_id"], name: "index_followups_on_case_contact_id" end create_table "fund_requests", force: :cascade do |t| t.text "submitter_email" t.text "youth_name" t.text "payment_amount" t.text "deadline" t.text "request_purpose" t.text "payee_name" t.text "requested_by_and_relationship" t.text "other_funding_source_sought" t.text "impact" t.text "extra_information" t.text "timestamps" end create_table "healths", force: :cascade do |t| t.datetime "latest_deploy_time", precision: nil t.integer "singleton_guard" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["singleton_guard"], name: "index_healths_on_singleton_guard", unique: true end create_table "hearing_types", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "name", null: false t.boolean "active", default: true, null: false t.string "checklist_updated_date", default: "None", null: false end create_table "judges", force: :cascade do |t| t.bigint "casa_org_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "active", default: true t.string "name" end create_table "languages", force: :cascade do |t| t.string "name" t.bigint "casa_org_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "learning_hour_topics", force: :cascade do |t| t.string "name", null: false t.bigint "casa_org_id", null: false t.integer "position", default: 1 t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "learning_hour_types", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "name" t.boolean "active", default: true t.integer "position", default: 1 t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "learning_hours", force: :cascade do |t| t.bigint "user_id", null: false t.string "name", null: false t.integer "duration_minutes", null: false t.integer "duration_hours", null: false t.datetime "occurred_at", precision: nil, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "learning_hour_type_id" t.bigint "learning_hour_topic_id" t.index ["user_id"], name: "index_learning_hours_on_user_id" end create_table "login_activities", force: :cascade do |t| t.string "scope" t.string "strategy" t.string "identity" t.boolean "success" t.string "failure_reason" t.string "user_type" t.bigint "user_id" t.string "context" t.string "ip" t.text "user_agent" t.text "referrer" t.string "city" t.string "region" t.string "country" t.float "latitude" t.float "longitude" t.datetime "created_at" end create_table "mileage_rates", force: :cascade do |t| t.decimal "amount" t.date "effective_date" t.boolean "is_active", default: true t.bigint "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "casa_org_id", null: false end create_table "notes", force: :cascade do |t| t.string "content" t.bigint "creator_id" t.string "notable_type" t.bigint "notable_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "noticed_events", force: :cascade do |t| t.string "type" t.string "record_type" t.bigint "record_id" t.jsonb "params" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "notifications_count" end create_table "noticed_notifications", force: :cascade do |t| t.string "type" t.bigint "event_id", null: false t.string "recipient_type", null: false t.bigint "recipient_id", null: false t.datetime "read_at", precision: nil t.datetime "seen_at", precision: nil t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["recipient_type", "recipient_id"], name: "index_noticed_notifications_on_recipient" end create_table "notifications", force: :cascade do |t| t.string "recipient_type", null: false t.bigint "recipient_id", null: false t.string "type", null: false t.jsonb "params" t.datetime "read_at", precision: nil t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "other_duties", force: :cascade do |t| t.bigint "creator_id", null: false t.string "creator_type" t.datetime "occurred_at", precision: nil t.bigint "duration_minutes" t.text "notes" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "patch_note_groups", force: :cascade do |t| t.string "value", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["value"], name: "index_patch_note_groups_on_value", unique: true end create_table "patch_note_types", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["name"], name: "index_patch_note_types_on_name", unique: true end create_table "patch_notes", force: :cascade do |t| t.text "note", null: false t.bigint "patch_note_type_id", null: false t.bigint "patch_note_group_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "placement_types", force: :cascade do |t| t.string "name", null: false t.bigint "casa_org_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "placements", force: :cascade do |t| t.datetime "placement_started_at", null: false t.bigint "placement_type_id", null: false t.bigint "creator_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "casa_case_id", null: false end create_table "preference_sets", force: :cascade do |t| t.bigint "user_id" t.jsonb "case_volunteer_columns", default: "{}", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "table_state", default: {} t.index ["user_id"], name: "index_preference_sets_on_user_id" end create_table "sent_emails", force: :cascade do |t| t.bigint "user_id" t.bigint "casa_org_id", null: false t.string "mailer_type" t.string "category" t.string "sent_address" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "sms_notification_events", force: :cascade do |t| t.string "name" t.string "user_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "supervisor_volunteers", force: :cascade do |t| t.bigint "supervisor_id", null: false t.bigint "volunteer_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "is_active", default: true t.index ["supervisor_id"], name: "index_supervisor_volunteers_on_supervisor_id" t.index ["volunteer_id"], name: "index_supervisor_volunteers_on_volunteer_id" end create_table "task_records", id: false, force: :cascade do |t| t.string "version", null: false end create_table "user_languages", force: :cascade do |t| t.bigint "user_id" t.bigint "language_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["language_id", "user_id"], name: "index_user_languages_on_language_id_and_user_id", unique: true end create_table "user_reminder_times", force: :cascade do |t| t.bigint "user_id", null: false t.datetime "case_contact_types" t.datetime "no_contact_made" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id"], name: "index_user_reminder_times_on_user_id" end create_table "user_sms_notification_events", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "sms_notification_event_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at", precision: nil t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "casa_org_id", null: false t.string "display_name", default: "", null: false t.string "invitation_token" t.datetime "invitation_created_at", precision: nil t.datetime "invitation_sent_at", precision: nil t.datetime "invitation_accepted_at", precision: nil t.integer "invitation_limit" t.string "invited_by_type" t.bigint "invited_by_id" t.integer "invitations_count", default: 0 t.string "type" t.boolean "active", default: true t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at", precision: nil t.datetime "last_sign_in_at", precision: nil t.string "current_sign_in_ip" t.string "last_sign_in_ip" t.string "phone_number", default: "" t.boolean "receive_sms_notifications", default: false, null: false t.boolean "receive_email_notifications", default: true t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" t.string "old_emails", default: [], array: true t.boolean "receive_reimbursement_email", default: false t.boolean "monthly_learning_hours_report", default: false, null: false t.datetime "date_of_birth" t.index ["casa_org_id"], name: "index_users_on_casa_org_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true t.index ["invited_by_id"], name: "index_users_on_invited_by_id" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "additional_expenses", "case_contacts" add_foreign_key "addresses", "users" add_foreign_key "api_credentials", "users" add_foreign_key "banners", "casa_orgs" add_foreign_key "banners", "users" add_foreign_key "casa_case_emancipation_categories", "casa_cases" add_foreign_key "casa_case_emancipation_categories", "emancipation_categories" add_foreign_key "casa_case_emancipation_options", "casa_cases" add_foreign_key "casa_case_emancipation_options", "emancipation_options" add_foreign_key "casa_cases", "casa_orgs" add_foreign_key "case_assignments", "casa_cases" 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_orders", "casa_cases" add_foreign_key "case_group_memberships", "casa_cases" add_foreign_key "case_group_memberships", "case_groups" add_foreign_key "case_groups", "casa_orgs" add_foreign_key "contact_topic_answers", "case_contacts" add_foreign_key "contact_topic_answers", "contact_topics" add_foreign_key "contact_topics", "casa_orgs" add_foreign_key "court_dates", "casa_cases" add_foreign_key "custom_org_links", "casa_orgs" add_foreign_key "emancipation_options", "emancipation_categories" add_foreign_key "followups", "users", column: "creator_id" add_foreign_key "judges", "casa_orgs" add_foreign_key "languages", "casa_orgs" add_foreign_key "learning_hour_topics", "casa_orgs" add_foreign_key "learning_hour_types", "casa_orgs" add_foreign_key "learning_hours", "learning_hour_types" add_foreign_key "learning_hours", "users" add_foreign_key "mileage_rates", "casa_orgs" add_foreign_key "mileage_rates", "users" add_foreign_key "notes", "users", column: "creator_id" add_foreign_key "other_duties", "users", column: "creator_id" add_foreign_key "patch_notes", "patch_note_groups" add_foreign_key "patch_notes", "patch_note_types" add_foreign_key "placement_types", "casa_orgs" add_foreign_key "placements", "casa_cases" add_foreign_key "placements", "placement_types" add_foreign_key "placements", "users", column: "creator_id" add_foreign_key "preference_sets", "users" add_foreign_key "sent_emails", "casa_orgs" add_foreign_key "sent_emails", "users" add_foreign_key "supervisor_volunteers", "users", column: "supervisor_id" add_foreign_key "supervisor_volunteers", "users", column: "volunteer_id" add_foreign_key "user_reminder_times", "users" add_foreign_key "user_sms_notification_events", "sms_notification_events" add_foreign_key "user_sms_notification_events", "users" add_foreign_key "users", "casa_orgs" end ================================================ FILE: db/seeds/api_credential_data.rb ================================================ ApiCredential.destroy_all users = User.all users.each do |user| ApiCredential.create!(user: user, api_token_digest: Digest::SHA256.hexdigest(SecureRandom.hex(18)), refresh_token_digest: Digest::SHA256.hexdigest(SecureRandom.hex(18))) end ================================================ FILE: db/seeds/casa_org_populator_presets.rb ================================================ # Preset option sets for various sizes and for the Rails environments. # These do not include `org_name`, which can be provided separately by the caller to override default name. module CasaOrgPopulatorPresets module_function def minimal_dataset_options { case_count: 1, volunteer_count: 1, supervisor_count: 1, casa_admin_count: 1 } end def small_dataset_options { case_count: 8, volunteer_count: 5, supervisor_count: 2, casa_admin_count: 1 } end def medium_dataset_options { case_count: 75, volunteer_count: 50, supervisor_count: 4, casa_admin_count: 2 } end def large_dataset_options { case_count: 160, volunteer_count: 100, supervisor_count: 10, casa_admin_count: 3 } end def for_environment { "development" => CasaOrgPopulatorPresets.small_dataset_options, "qa" => CasaOrgPopulatorPresets.large_dataset_options, "staging" => CasaOrgPopulatorPresets.large_dataset_options, "test" => CasaOrgPopulatorPresets.small_dataset_options }[ENV["APP_ENVIRONMENT"] || Rails.env] end end ================================================ FILE: db/seeds/db_populator.rb ================================================ # Called by the seeding process to create data with a specified random number generator. # There is a 1 in 30 probability that a Volunteer will be inactive when created. # There is no instance of a volunteer who was previously assigned a case being inactivated. # Email addresses generated will be globally unique across all orgs. class DbPopulator SEED_PASSWORD = "12345678" WORD_LENGTH_TUNING = 10 LINE_BREAK_TUNING = 5 PREFIX_OPTIONS = ("A".ord.."Z".ord).to_a.map(&:chr) attr_reader :rng # Public Methods # Pass an instance of Random for Faker and Ruby `rand` and sample` calls. def initialize(random_instance, case_fourteen_years_old: false) @rng = random_instance @casa_org_counter = 0 @case_number_sequence = 1000 @case_fourteen_years_old = case_fourteen_years_old end def create_all_casa_admin(email) return if AllCasaAdmin.find_by(email: email) AllCasaAdmin.create!(email: email, password: SEED_PASSWORD, password_confirmation: SEED_PASSWORD) end def create_org(options) @casa_org_counter += 1 options.org_name ||= "CASA Organization ##{@casa_org_counter}" CasaOrg.find_or_create_by!(name: options.org_name) { |org| org.name = options.org_name org.display_name = options.org_name org.address = Faker::Address.full_address org.footer_links = [ ["https://example.org/contact/", "Contact Us"], ["https://example.org/subscribe-to-newsletter/", "Subscribe to newsletter"], ["https://www.example.org/give/givefrm.asp?CID=4450", "Donate"] ] org.logo.attach(io: File.open(CasaOrg::CASA_DEFAULT_LOGO), filename: CasaOrg::CASA_DEFAULT_LOGO.basename.to_s) } end # Create 2 judges for each casa_org. def create_judges(casa_org) 2.times { Judge.create(name: Faker::Name.name, casa_org: casa_org) } end # Creates 3 users, 1 each for [Volunteer, Supervisor, CasaAdmin]. # For org's after the first one created, adds an org number to the email address so that they will be globally unique def create_users(casa_org, options) # Generate email address; for orgs only after first org, and org number would be added, e.g.: # Org #1: volunteer1@example.com # Org #2: volunteer2-1@example.com email = ->(klass, n) do org_fragment = (@casa_org_counter > 1) ? "#{@casa_org_counter}-" : "" klass.name.underscore + org_fragment + n.to_s + "@example.com" end create_users_of_type = ->(klass, count) do (1..count).each do |n| current_email = email.call(klass, n) attributes = { casa_org: casa_org, email: current_email, password: SEED_PASSWORD, password_confirmation: SEED_PASSWORD, display_name: Faker::Name.name, phone_number: Faker::PhoneNumber.cell_phone_in_e164, active: true, confirmed_at: Time.now } # Approximately 1 out of 30 volunteers should be set to inactive. if klass == Volunteer && rng.rand(30) == 0 attributes[:active] = false end unless klass.find_by(email: current_email) klass.create!(attributes) end end end create_users_of_type.call(CasaAdmin, options.casa_admin_count) create_users_of_type.call(Supervisor, options.supervisor_count) create_users_of_type.call(Volunteer, options.volunteer_count) supervisors = Supervisor.all.to_a Volunteer.all.each { |v| v.supervisor = supervisors.sample(random: rng) } end # Create other duties (Volunteer only) # Increment other_duties_counter by 1 each time other duty is created # Print out statement that indicates number of other duties created def create_other_duties Volunteer.find_each do |v| 2.times { OtherDuty.create!( creator_id: v.id, creator_type: "Volunteer", occurred_at: Faker::Date.between(from: 2.days.ago, to: Date.today), duration_minutes: rand(5..180), notes: Faker::Lorem.sentence ) } end end def create_case_contacts(casa_case) draft_statuses = %w[started details notes expenses] case_contact_statuses = Array.new(random_zero_one_count, draft_statuses.sample(random: rng)) + Array.new(random_case_contact_count, "active") case_contact_statuses.shuffle(random: rng).each do |status| create_case_contact(casa_case, status:) end end def create_case_contact(casa_case, status: "active") params = base_case_contact_params(casa_case, status) status_modifications = { "started" => {want_driving_reimbursement: false, draft_case_ids: [], medium_type: nil, occurred_at: nil, duration_minutes: nil, notes: nil, miles_driven: 0}, "details" => {want_driving_reimbursement: false, notes: nil, miles_driven: 0}, "notes" => {want_driving_reimbursement: false, miles_driven: 0}, "expenses" => {want_driving_reimbursement: true} } params.merge!(status_modifications[status]) unless status == "active" CaseContact.create!(params) end def create_cases(casa_org, options) ContactTypePopulator.populate options.case_count.times do |index| case_number = generate_case_number court_date = generate_court_date court_report_submitted = index.even? new_casa_case = CasaCase.find_by(case_number: case_number) birth_month_year_youth = @case_fourteen_years_old ? ((Date.today - 18.year)..(Date.today - CasaCase::TRANSITION_AGE.year)).to_a.sample : ((Date.today - 18.year)..(Date.today - 1.year)).to_a.sample new_casa_case ||= CasaCase.find_or_create_by!( casa_org_id: casa_org.id, case_number: case_number, court_report_submitted_at: court_report_submitted ? Date.today : nil, court_report_status: court_report_submitted ? :submitted : :not_submitted, birth_month_year_youth: birth_month_year_youth, date_in_care: Date.today - (rand * 1500) ) new_court_date = CourtDate.find_or_create_by!( casa_case: new_casa_case, court_report_due_date: court_date + 1.month, date: court_date ) volunteer = new_casa_case.casa_org.volunteers.active.sample(random: rng) || new_casa_case.casa_org.volunteers.active.first || Volunteer.create!( casa_org: new_casa_case.casa_org, email: "#{SecureRandom.hex(10)}@example.com", password: SEED_PASSWORD, display_name: "active volunteer" ) CaseAssignment.find_or_create_by!(casa_case: new_casa_case, volunteer: volunteer) random_court_order_count.times do CaseCourtOrder.create!( casa_case_id: new_casa_case.id, court_date: new_court_date, text: order_choices.sample(random: rng), implementation_status: CaseCourtOrder::IMPLEMENTATION_STATUSES.values.sample(random: rng) ) end random_zero_one_count.times do |index| CourtDate.create!( casa_case_id: new_casa_case.id, date: Date.today + 5.weeks ) end random_past_court_date_count.times do |index| CourtDate.create!( casa_case_id: new_casa_case.id, date: Date.today - (index + 1).weeks ) end create_case_contacts(new_casa_case) # guarantee at least one case contact before and after most recent past court date if most_recent_past_court_date(new_casa_case.id) if !case_contact_before_last_court_date?(new_casa_case.id, most_recent_past_court_date(new_casa_case.id)) new_case_contact = create_case_contact(new_casa_case) new_case_contact.occurred_at = most_recent_past_court_date(new_casa_case.id) - 24.hours new_case_contact.save! end if !case_contact_after_last_court_date?(new_casa_case.id, most_recent_past_court_date(new_casa_case.id)) new_case_contact = create_case_contact(new_casa_case) new_case_contact.occurred_at = most_recent_past_court_date(new_casa_case.id) + 24.hours new_case_contact.save! end end # guarantee at least one transition aged youth case to "volunteer1" volunteer1 = Volunteer.find_by(email: "volunteer1@example.com") if volunteer1.casa_cases.where(birth_month_year_youth: ..CasaCase::TRANSITION_AGE.years.ago).blank? rand(1..3).times do birth_month_year_youth = ((Date.today - 18.year)..(Date.today - CasaCase::TRANSITION_AGE.year)).to_a.sample new_casa_case = volunteer1.casa_cases.find_or_create_by!( casa_org_id: volunteer1.casa_org.id, case_number: generate_case_number, court_report_submitted_at: court_report_submitted ? Date.today : nil, court_report_status: court_report_submitted ? :submitted : :not_submitted, birth_month_year_youth: birth_month_year_youth ) CourtDate.find_or_create_by!( casa_case: new_casa_case, court_report_due_date: court_date + 1.month, date: court_date ) end end end end def create_hearing_types(casa_org) active_hearing_type_names = [ "emergency hearing", "trial on the merits", "scheduling conference", "uncontested hearing", "pendente lite hearing", "pretrial conference" ] inactive_hearing_type_names = [ "deprecated hearing" ] active_hearing_type_names.each do |hearing_type_name| HearingType.find_or_create_by!( casa_org_id: casa_org.id, name: hearing_type_name, active: true ) end inactive_hearing_type_names.each do |hearing_type_name| HearingType.find_or_create_by!( casa_org_id: casa_org.id, name: hearing_type_name, active: false ) end end def create_checklist_items checklist_item_categories = [ "Education/Vocation", "Placement", "Category 3" ] checklist_item_descriptions = [ "checklist item description 1", "checklist item description 2", "checklist item description 3" ] mandatory_options = [true, false] half_of_the_hearing_types = HearingType.all.slice(0, HearingType.all.length / 2) half_of_the_hearing_types.each do |hearing_type| ChecklistItem.create( hearing_type_id: hearing_type.id, description: checklist_item_descriptions.sample, category: checklist_item_categories.sample, mandatory: mandatory_options.sample ) hearing_type.update_attribute(:checklist_updated_date, "Updated #{Time.new.strftime("%m/%d/%Y")}") end end def create_languages(casa_org) create_language("Spanish", casa_org) create_language("Vietnamese", casa_org) create_language("French", casa_org) create_language("Chinese Cantonese", casa_org) create_language("ASL", casa_org) create_language("Other", casa_org) end def create_language(name, casa_org) Language.find_or_create_by!(name: name, casa_org: casa_org) end def create_mileage_rates(casa_org) attempt_count = 5 i = 0 while i < attempt_count begin MileageRate.create!({ amount: Faker::Number.between(from: 0.0, to: 1.0).round(2), effective_date: Faker::Date.backward(days: 700), is_active: true, casa_org_id: casa_org.id }) rescue ActiveRecord::RecordInvalid attempt_count += 1 end i += 1 end end def create_learning_hour_types(casa_org) learning_types = %w[book movie webinar conference other] learning_types.each do |learning_type| learning_hour_type = casa_org.learning_hour_types.new(name: learning_type.capitalize) learning_hour_type.position = 99 if learning_type == "other" learning_hour_type.save end end def create_learning_hour_topics(casa_org) learning_topics = %w[cases reimbursements court_reports] learning_topics.each do |learning_topic| learning_hour_topic = casa_org.learning_hour_topics.new(name: learning_topic.humanize.capitalize) learning_hour_topic.save end end def create_learning_hours(casa_org) casa_org.volunteers.each do |user| [1, 2, 3].sample.times do learning_hour_topic = casa_org.learning_hour_topics.sample learning_hour_type = casa_org.learning_hour_types.sample # randomize between 30 to 180 minutes duration_minutes = (2..12).to_a.sample * 15 duration_hours = duration_minutes / 60 duration_minutes %= 60 occurred_at = Time.current - (1..7).to_a.sample.days LearningHour.create( user:, learning_hour_type:, name: "#{learning_hour_type.name} on #{learning_hour_topic.name}", duration_hours:, duration_minutes:, occurred_at:, learning_hour_topic: ) end end end private # ------------------------------------------------------------------------------------------------------- def most_recent_past_court_date(casa_case_id) CourtDate.where( "date < ? AND casa_case_id = ?", Date.today, casa_case_id ).order(date: :desc).first&.date end def case_contact_before_last_court_date?(casa_case_id, date) CaseContact.where( "occurred_at < ? AND casa_case_id = ?", date, casa_case_id ).any? end def case_contact_after_last_court_date?(case_case_id, date) CaseContact.where( "occurred_at > ? AND casa_case_id = ?", date, case_case_id ).any? end def order_choices [ "Limited guardianship of the children for medical and educational purposes to [name] shall be rescinded;", "The children shall remain children in need of assistance (cina), under the jurisdiction of the juvenile court, and shall remain committed to the department of health and human services/child welfare services, for continued placement on a trial home visit with [NAME]", "The youth shall continue to participate in educational tutoring, under the direction of the department;", "The youth shall continue to participate in family therapy with [name], under the direction of the department;", "The permanency plan for all the children of reunification is reaffirmed;", "Visitation between the youth and the father shall be unsupervised, minimum once weekly, in the community or at his home, and may include overnights when he has the appropriate space for the children to sleep, under the direction of the department;", "Youth shall continue to participate in individual therapy, under the direction of the department;", "The youth shall continue to maintain stable employment;", "The youth shall maintain appropriate housing while working towards obtaining housing that can accommodate all of the children being reunified, and make home available for inspection, under the direction of the department;", "The youth shall participate in case management services, under the direction of the department;", "The youth shall participate in mental health treatment and medication management, under the direction of the department;" ] end def transition_aged_youth?(birth_month_year_youth) (Date.today - birth_month_year_youth).days.in_years > CasaCase::TRANSITION_AGE end def base_case_contact_params(casa_case, status) { casa_case: casa_case, creator: casa_case.volunteers.sample(random: rng), duration_minutes: likely_contact_durations.sample(random: rng), occurred_at: rng.rand(0..6).months.ago, contact_types: ContactType.all.sample(2, random: rng), medium_type: CaseContact::CONTACT_MEDIUMS.sample(random: rng), miles_driven: rng.rand(5..40), want_driving_reimbursement: random_true_false, contact_made: random_true_false, notes: note_generator, status: status, draft_case_ids: [casa_case&.id] } end def random_case_contact_count @random_case_contact_counts ||= [0, 1, 2, 2, 2, 3, 3, 3, 11, 11, 11] @random_case_contact_counts.sample(random: rng) end def random_past_court_date_count @random_past_court_date_counts ||= [0, 2, 3, 4, 5] @random_past_court_date_counts.sample(random: rng) end def random_zero_one_count @random_zero_one_count ||= [0, 1] @random_zero_one_count.sample(random: rng) end def random_court_order_count @random_court_order_counts ||= [0, 3, 5, 10] @random_court_order_counts.sample(random: rng) end def likely_contact_durations @likely_contact_durations ||= [15, 30, 60, 75, 4 * 60, 6 * 60] end def note_generator paragraph_count = Random.rand(6) (0..paragraph_count).map { |index| Faker::Lorem.paragraph(sentence_count: 5, supplemental: true, random_sentences_to_add: 20) }.join("\n\n") end def generate_case_number # CINA-YY-XXXX years = ((DateTime.now.year - 20)..DateTime.now.year).to_a yy = years.sample(random: rng).to_s[2..3] @case_number_sequence += 1 "CINA-#{yy}-#{@case_number_sequence}" end def generate_court_date ((Date.today + 1.month)..(Date.today + 5.months)).to_a.sample end def random_true_false @true_false_array ||= [true, false] @true_false_array.sample(random: rng) end end ================================================ FILE: db/seeds/default_contact_topics.yml ================================================ - question: "Background information" details: |- a) When did the family first come into contact with the Department of Social Services or Department of Juvenile Justice – how many times? b) Tell the history of their involvement with the department and any facts about their life that could help determine the need for placement and/or services. c) Discuss the child’s history – behavior problems, educational history, medical history, psychological history (any hospitalizations, previous counseling, etc.) d) If child has been placed previously give a history of the child’s placements (placed with different parents, relatives, DSS, etc). - question: "Current situation" details: |- a) Where is the child placed? b) How is the child adjusting to the placement? c) Are there any issues or concerns about the placement? If so, describe these concerns and specify the actions being taken to address them. - question: "Education, vocation, or daycare" details: |- a) Where is the child placed for education (daycare, public school, non-public school, GED, Job Corps, etc)? b) How is the child adjusting to the educational placement? Are there any education-related concerns at this point? If yes, detail them and mention the steps taken to address them. c) Does the child have an IEP? If not, is there a need for one? d) Is the child employed? If not, are they looking for a job? e) Does the child have vocational/life skills? Are they attending life skill classes? f) Are there any other life skill needs? (Driver’s education, state ID, transportation assistance, etc.) g) What is the feedback from professionals providing these services about the child's progress? Include strengths and not just needs. - question: "Health and mental health" details: |- a) Is the child up to date with medical exams? b) Are there any other medical concerns? c) Is the child receiving therapy, medication monitoring, mentoring, or other services? If so, specify with whom these services are being received. - question: "Family and community connections" details: |- a) Is this child seeing parents, siblings, other relatives? If so, who is the child visiting, and how often? Does the child desire a different arrangement? b) Detail the steps parents have taken to address court orders. Address any barriers and highlight positive steps. - question: "Child’s strengths" details: |- a) Describe the child’s strengths, interests, and hobbies to provide a well-rounded perspective. ================================================ FILE: db/seeds/emancipation_data.rb ================================================ # Emancipation Checklist Form Data category_housing = EmancipationCategory.where(name: "Youth has housing.").first_or_create(mutually_exclusive: false) category_housing.add_option("With friend") category_housing.add_option("With relative") category_housing.add_option("With former foster parent") category_housing.add_option("Subsidized (e.g., FUP, Future Bridges, adult services)") category_housing.add_option("Independently (e.g., renting own apartment or room)") category_income = EmancipationCategory.where(name: "Youth has income to achieve self-sufficiency.").first_or_create(mutually_exclusive: false) category_income.add_option("Employment") category_income.add_option("Public benefits/TCA") category_income.add_option("SSI") category_income.add_option("SSDI") category_income.add_option("Inheritance/survivors benefits") EmancipationCategory.where(name: "Youth has completed a budget.").first_or_create(mutually_exclusive: false) category_employment = EmancipationCategory.where(name: "Youth is employed.").first_or_create(mutually_exclusive: true) category_employment.add_option("Part-time job") category_employment.add_option("Full-time job") category_employment.add_option("Apprenticeship or paid internship") category_employment.add_option("Self-employed") category_continuing_education = EmancipationCategory.where(name: "Youth is attending an educational or vocational program.").first_or_create(mutually_exclusive: true) category_continuing_education.add_option("High school") category_continuing_education.add_option("Post-secondary/college") category_continuing_education.add_option("Vocational") category_continuing_education.add_option("GED program") category_high_school_diploma = EmancipationCategory.where(name: "Youth has a high school diploma or equivalency.").first_or_create(mutually_exclusive: true) category_high_school_diploma.add_option("Traditional") category_high_school_diploma.add_option("Out of school program") category_high_school_diploma.add_option("GED") category_medical_insurance = EmancipationCategory.where(name: "Youth has medical insurance.").first_or_create(mutually_exclusive: false) category_medical_insurance.add_option("Has medical insurance card") category_medical_insurance.add_option("Knows his/her/their primary care entity") category_medical_insurance.add_option("Knows how to continue insurance coverage") category_medical_insurance.add_option("Knows that dental insurance ends at age 21") category_medical_insurance.add_option("Has plan for dental care") EmancipationCategory.where(name: "Youth can identify permanent family and/or adult connections.").first_or_create(mutually_exclusive: false) category_community = EmancipationCategory.where(name: "Youth is accessing community activities.").first_or_create(mutually_exclusive: false) category_community.add_option("Arts activities (e.g., singing, dancing, theater)") category_community.add_option("Religious affiliations (e.g., church, mosque)") category_community.add_option("Athletics/team sports") category_community.add_option("Other") category_documents = EmancipationCategory.where(name: "Youth has all identifying documents.").first_or_create(mutually_exclusive: false) category_documents.add_option("Birth certificate (original or certified copy)") category_documents.add_option("Social security card") category_documents.add_option("Learner's permit") category_documents.add_option("Driver's license") category_documents.add_option("Immigration documents") category_documents.add_option("State identification card") category_transportation = EmancipationCategory.where(name: "Youth has access to transportation.").first_or_create(mutually_exclusive: false) category_transportation.add_option("Vehicle") category_transportation.add_option("Public transportation") category_juvenile_criminal_cases = EmancipationCategory.where(name: "Youth has been or is involved in past or current juvenile cases.").first_or_create(mutually_exclusive: false) category_juvenile_criminal_cases.add_option("All juvenile issues have been resolved.") category_juvenile_criminal_cases.add_option("All eligible juvenile records have been expunged.") category_adult_criminal_cases = EmancipationCategory.where(name: "Youth has been or is involved in past or current adult criminal cases.").first_or_create(mutually_exclusive: false) category_adult_criminal_cases.add_option("All adult criminal cases have been resolved.") category_adult_criminal_cases.add_option("All eligible adult criminal records have been expunged.") EmancipationCategory.where(name: "Youth has been or is involved in civil or family cases.").first_or_create(mutually_exclusive: false) category_bank_account = EmancipationCategory.where(name: "Youth has a bank account in good standing.").first_or_create(mutually_exclusive: false) category_bank_account.add_option("Checking") category_bank_account.add_option("Savings") category_credit = EmancipationCategory.where(name: "Youth has obtained a copy of his/her/their credit report.").first_or_create(mutually_exclusive: false) category_credit.add_option("An adult has reviewed the credit report with Youth.") category_credit.add_option("Issues or concerns") EmancipationCategory.where(name: "Youth can identify his/her/their core values.").first_or_create(mutually_exclusive: false) EmancipationCategory.where(name: "Youth has completed the Ansell Casey Assessment.").first_or_create(mutually_exclusive: false) ================================================ FILE: db/seeds/emancipation_options_prune.rb ================================================ def get_category_by_name(category_name) EmancipationCategory.where(name: category_name)&.first end get_category_by_name("Youth has completed a budget.")&.delete_option("Completed budget") get_category_by_name("Youth is employed.")&.delete_option("Not employed") get_category_by_name("Youth is attending an educational or vocational program.")&.delete_option("Not attending") get_category_by_name("Youth has a high school diploma or equivalency.")&.delete_option("No") get_category_by_name("Youth can identify permanent family and/or adult connections.")&.delete_option("Has connections") get_category_by_name("Youth has been or is involved in civil or family cases.")&.delete_option("All civil or family cases have been resolved.") get_category_by_name("Youth can identify his/her/their core values.")&.delete_option("Identified values") get_category_by_name("Youth has completed the Ansell Casey Assessment.")&.delete_option("Threshold for self-sufficiency was met.") get_category_by_name("Youth has completed the Ansell Casey Assessment.")&.update_attribute(:name, "Youth has completed the Ansell Casey Assessment and threshold for self-sufficiency was met.") ================================================ FILE: db/seeds/patch_note_group_data.rb ================================================ # PatchNote Section Headers PatchNoteGroup.where(value: "CasaAdmin+Supervisor").first_or_create PatchNoteGroup.where(value: "CasaAdmin+Supervisor+Volunteer").first_or_create ================================================ FILE: db/seeds/patch_note_type_data.rb ================================================ # PatchNote Section Headers PatchNoteType.where(name: "Coming Up").first_or_create PatchNoteType.where(name: "Fixes").first_or_create PatchNoteType.where(name: "What's New?").first_or_create ================================================ FILE: db/seeds/placement_data.rb ================================================ casa_orgs = CasaOrg.all placement_types = [ "Reunification", "Custody/Guardianship by a relative", "Custody/Guardianship by a non-relative", "Adoption by relative", "Adoption by a non-relative", "APPLA" ] casa_orgs.each do |org| placement_types.each do |label| PlacementType.where(name: label, casa_org: org).first_or_create end end ================================================ FILE: db/seeds.rb ================================================ # This seed script populates the development DB with a data set whose size is dependent on the Rails environment. # You can control the randomness of the data provided by FAKER and the Rails libraries via the DB_SEEDS_RANDOM_SEED environment variable. # If you specify a number, that number will be used as the seed, so you can enforce consistent data across runs # with nondefault content. # If you specify the string 'random' (e.g. `export DB_SEEDS_RANDOM_SEED=random`), a random seed will be assigned for you. # If you don't specify anything, 0 will be used as the seed, ensuring consistent data across hosts and runs. require_relative "seeds/casa_org_populator_presets" require_relative "seeds/db_populator" require_relative "../lib/tasks/data_post_processors/case_contact_populator" require_relative "../lib/tasks/data_post_processors/contact_type_populator" require_relative "../lib/tasks/data_post_processors/sms_notification_event_populator" require_relative "../lib/tasks/data_post_processors/contact_topic_populator" class SeederMain attr_reader :db_populator, :rng def initialize random_seed = get_seed_specification @rng = Random.new(random_seed) # rng = random number generator @db_populator = DbPopulator.new(rng) Faker::Config.random = rng Faker::Config.locale = "en-US" # only allow US phone numbers end def seed log "NOTE: CASA seed does not delete anything anymore! Run rake db:seed:replant to delete everything and re-seed" log "Creating the objects in the database..." db_populator.create_all_casa_admin("allcasaadmin@example.com") db_populator.create_all_casa_admin("all_casa_admin1@example.com") db_populator.create_all_casa_admin("admin1@example.com") options1 = OpenStruct.new(CasaOrgPopulatorPresets.for_environment.merge({org_name: "Prince George CASA"})) org1 = db_populator.create_org(options1) create_org_related_data(db_populator, org1, options1) options2 = OpenStruct.new(CasaOrgPopulatorPresets.minimal_dataset_options) org2 = db_populator.create_org(options2) create_org_related_data(db_populator, org2, options2) SmsNotificationEventPopulator.populate 2.times do options3 = OpenStruct.new(CasaOrgPopulatorPresets.minimal_dataset_options) org3 = DbPopulator.new(rng, case_fourteen_years_old: true) .create_org(options3) create_org_related_data(db_populator, org3, options3) end post_process_data report_object_counts log "\nDone.\n\n" end private # ------------------------------------------------------------------------------------------------------- # Used for reporting record counts after completion: def active_record_classes @active_record_classes ||= [ AllCasaAdmin, CasaAdmin, CasaOrg, CasaCase, CaseContact, ContactTopic, ContactTopicAnswer, CaseCourtOrder, CaseAssignment, ChecklistItem, CourtDate, ContactType, ContactTypeGroup, HearingType, Judge, Language, LearningHourType, LearningHourTopic, MileageRate, OtherDuty, Supervisor, SupervisorVolunteer, User, LearningHour, Volunteer, PlacementType ] end def post_process_data ContactTypePopulator.populate CaseContactPopulator.populate ContactTopicPopulator.populate end def get_seed_specification seed_environment_value = ENV["DB_SEEDS_RANDOM_SEED"] if seed_environment_value.blank? seed = 0 log "\nENV['DB_SEEDS_RANDOM_SEED'] not set to 'random' or a number; setting seed to 0.\n\n" elsif seed_environment_value.casecmp("random") == 0 seed = Random.new_seed log "\n'random' specified in ENV['DB_SEEDS_RANDOM_SEED']; setting seed to randomly generated value #{seed}.\n\n" else seed = seed_environment_value.to_i log "\nUsing random seed #{seed} specified in ENV['DB_SEEDS_RANDOM_SEED'].\n\n" end seed end def report_object_counts log "\nRecords written to the DB:\n\nCount Class Name\n----- ----------\n\n" active_record_classes.each do |klass| log format("%5d %s", klass.count, klass.name) end log "\n\nVolunteers, Supervisors and CasaAdmins are types of Users" end def log(message) return if Rails.env.test? Rails.logger.debug { message } end def create_org_related_data(db_populator, casa_org, options) db_populator.create_users(casa_org, options) db_populator.create_cases(casa_org, options) db_populator.create_hearing_types(casa_org) db_populator.create_checklist_items db_populator.create_judges(casa_org) db_populator.create_languages(casa_org) db_populator.create_mileage_rates(casa_org) db_populator.create_learning_hour_types(casa_org) db_populator.create_learning_hour_topics(casa_org) db_populator.create_learning_hours(casa_org) db_populator.create_other_duties end end SeederMain.new.seed load(Rails.root.join("db/seeds/emancipation_data.rb")) begin load(Rails.root.join("db/seeds/emancipation_options_prune.rb")) rescue => e Rails.logger.error { "Caught error during db seed emancipation_options_prune, continuing. Message: #{e}" } end load(Rails.root.join("db/seeds/placement_data.rb")) load(Rails.root.join("db/seeds/api_credential_data.rb")) ================================================ FILE: doc/API.md ================================================ ### POST `/casa_cases.json` Creates a casa case ### Params: - **casa_case** Required. Contains all the object containing all the casa case params - **case_number**: "CINA-123-ABC", Required. A unique string to identify the casa case - **transition_aged_youth**: true, A boolean marking the case as transitioning or not. Currently in the process of deprecating this field - **birth_month_year_youth**: "2007-10-21", Required. A date in the format YYYY-MM-DD determining if the case as transitioning or not. - **casa_org_id**: 1, Required. The id of the casa org of the case. - **hearing_type_id**: 1, The id of the hearing type for the next court date. - **judge_id**: 1 The id of the case judge ### POST `/case_assignments.json` Creates a case_assignment - **Params:** - **casa_case_id**: 1, Required. The id of the casa case the volunteer is being assigned to. - **volunteer_id**: 1, Required. The id of the volunteer being assigned to the casa case. ### PATCH `/case_assignments/:id/unassign.json` Unassigns a case_assignment - **Params:** - **id**: 1, Required. The id of the case_assignment to be unassigned. ================================================ FILE: doc/CONTRIBUTING.md ================================================ # Contributing We ♥ contributors! By participating in this project, you agree to abide by the Ruby for Good [code of conduct](https://github.com/rubyforgood/code-of-conduct). If you have any questions about an issue, comment on the issue, open a new issue or ask in [the RubyForGood slack](https://join.slack.com/t/rubyforgood/shared_invite/zt-35218k86r-vlIiWqig54c9t~_LkGpQ7Q). CASA has a `#casa` channel in the Slack. Our channel in slack also contains a zoom link for office hours every day office hours are held. You won't be yelled at for giving your best effort. The worst that can happen is that you'll be politely asked to change something. We appreciate any sort of contributions, and don't want a wall of rules to get in the way of that. ## Contributing Steps ### Issues All work is organized by issues. [Find issues here.](https://github.com/rubyforgood/projects/9) If you would like to contribute, please ask for an issue to be assigned to you. If you would like to contribute something that is not represented by an issue, please make an issue and assign yourself. Only take multiple issues if they are related and you can solve all of them at the same time with the same pull request. ### Pull Requests If you are so inclined, you can open a draft PR as you continue to work on it. 1. Follow [the setup guide](https://github.com/rubyforgood/casa#installation) to get the project working locally. 1. We only accept pull requests with passing tests. To ensure your setup is working before you get started it's great to run the tests first. - To run the tests use: `bundle exec rspec` 1. Add a test for your change. If you are adding functionality or fixing a bug, you should add a test! 1. Run linters and fix any linting errors that come up. - From the repo root run: `./bin/git_hooks/lint` 1. Push to your branch/fork and submit a pull request. Include the issue number (ex. `Resolves #1`) in the PR description. This will ensure the issue gets closed automatically when the pull request gets merged. #### Pull Request Checks We will try to respond to your PR quickly. There are scripts that check the code to ensure the code is working. Most of them need to pass. You can see the scripts run at the bottom of your pull request webpage (learn more about the scripts [here](https://github.com/rubyforgood/casa/wiki/Pull-Request-Checks)). You should attempt to fix errors found in the automated testing. Pull requests are also manually reviewed. We may request changes after a manual review. Some qualities of good pull requests: - Small line diff count. Several small pull requests for a large issue are preferred over one big pull request. - Include tests that fail without your code, and pass with it. - For pull requests changing UI, make sure the UI matches the rest of the site. Some of our users aren't great with computers and we don't want to make them learn new things if we don't need to. - Update the documentation, for things like new rails/bash commands. Please include a guide if modifying the code in the future is difficult. For example [editing .docx templates](https://github.com/rubyforgood/casa/wiki/How-to-edit-docx-templates---word-document-court-report) is difficult because the documentation is hard to find and it requires microsoft word. - If your pull request involves user permissions, use [policy files](https://github.com/varvet/pundit#policies). - If your pull request has an erb file with complex rails logic inside of it, please use a [decorator](https://medium.com/@kosovacsedad/ruby-on-rails-decorator-design-pattern-b54a1afd03c8). ================================================ FILE: doc/DOCKER.md ================================================ # Development setup using Docker After you install Docker, please follow either the automatic setup or the manual setup. If you are new to Docker, it is recommended that you follow the manual setup. ## Installing Docker Install [Docker Community Edition](https://docs.docker.com/install/) if it is not already installed. ## Automatic Setup The automatic setup explained here relies on Bash scripts in the docker directory to execute the most basic and frequent tasks in Docker. There is substantially less typing to do under the automatic setup than under the manual setup. ### Initial setup 1. Clone the repository to your local machine: `git clone https://github.com/rubyforgood/casa.git` or create a fork in GitHub if you don't have permission to commit directly to this repo. 2. Change into the application directory: `cd casa` 3. Run `docker/build` to build the app, seed the database, run the local web server (in a detached state), run the test suite, and log the screen outputs of these processes in the log directory. The web application will be available at http://localhost:3000. 4. Run `docker/test` to run the test suite and log the screen output in the log directory. 5. If you reboot the machine, restart Docker, or stop any services, the tests and many other functions will not work. Please run `docker/server` to restart the app and allow the tests and other functions to work. ### Other Automated Scripts * Run `docker/seed` to reseed the database. * Run `docker/server` to restart the local web server (in a detached state). * Run `docker/nukec` to delete all of the Docker containers. * Run `docker/nuke` to delete all Docker containers, Docker networks, and Docker images. * Run `docker/console` to start the Rails Console. * Run `docker/sandbox` to start the Rails Sandbox. * Run `docker/brakeman` to run the Brakeman security tool, which checks for security vulnerabilities. * Use the `docker/run` script to run any command within the Rails Docker container. For example, entering `docker/run cat /etc/os-release` executes the command `cat /etc/os-release` within the Rails Docker container. ## Manual Setup The manual setup instructions walk you through building the images and starting the containers using Docker Compose commands directly. This setup method is particularly recommended if you are new to Docker. ### Initial setup The following commands should just be run for the initial setup only. Rebuilding the docker images is only necessary when upgrading, if there are changes to the Dockerfile, or if gems have been added or updated. 1. Clone the respository to your local machine: `git clone https://github.com/rubyforgood/casa.git` or create a fork in GitHub if you don't have permission to commit directly to this repo. 2. Change into the application directory: `cd casa` 3. Run `docker compose build` to build images for all services. 4. Run `docker compose run --rm web bundle install` to install ruby dependencies 5. Run `docker compose run --rm web rails db:reset` to create the dev and test databases, load the schema, and run the seeds file. 6. Run `docker compose run --rm web npm install` to install javascript dependencies 7. Run `docker compose run --rm web npm run build` to bundle javascript assets 8. Run `docker compose run --rm web npm run build:css` to bundle the css 9. Run `docker compose up` to start all the remaining services. Or use `docker compose up -d` to start containers in the background. 10. Run `docker compose ps` to view status of the containers. All should have state "Up". Check the [logs](#viewing-logs) if there are any containers that did not start. 11. The web application will be available at http://localhost:3000 ### For ongoing development: * Run `docker compose up -d` to start all services. * Run `docker compose ps` to view status of containers. * Run `docker compose stop` to stop all services. * Run `docker compose restart web` to restart the web server. * Run `docker compose rm ` to remove a stopped container. * Run `docker compose rm -f ` to force remove a stopped container. * Run `docker compose up -d --force-recreate` to start services with new containers. * Run `docker compose build web` to build a new image for the web service. After re-building an image, run `docker compose up -d --force-recreate web` to start a container running the new image. * Run `docker compose down -v` to stop and remove all containers, as well as volumes and networks. This command is helpful if you want to start with a clean slate. However, it will completely remove the database and you will need to go through the database setup steps again above. #### Running commands In order to run rake tasks, rails generators, bundle commands, etc., they need to be run inside the container: ``` $ docker compose exec web rails db:migrate ``` If you do not have the web container running, you can run a command in a one-off container: ``` $ docker compose run --rm web bundle install ``` However, when using a one-off container, make sure the image is up-to-date by running `docker compose build web` first. If you have been making gem updates to your container without rebuilding the image, then the one-off container will be out of date. #### Running webpack dev server To speed compiling of assets, run the webpack dev server in a separate terminal window: ``` $ docker compose exec web bin/webpack-dev-server ``` #### Viewing logs To view the logs, run: ``` $ docker compose logs -f ``` For example: ``` $ docker compose logs -f web ``` #### Accessing services ##### Postgres database ``` $ docker compose exec database psql -h database -Upostgres casa_development ``` ##### Rails console ``` $ docker compose exec web rails c ``` ### Testing Suite Run the testing suite from within the container: ``` $ docker compose exec web rspec spec -fd ``` For a shorter screen output from running the testing suite from within the container: ``` $ docker compose exec web rspec spec ``` System tests will generate a screenshot upon failure. The screenshots can be found in the local `tmp/screenshots` directory which maps to the `/usr/src/app/tmp/screenshots` directory inside the container. #### Watching tests run You can view the tests in real time by using a VNC client and temporarily switching to the `selenium_chrome_in_container` driver set in [spec/spec_helper.rb](https://github.com/rubyforgood/casa/blob/master/spec/spec_helper.rb). For example, you can change this: ``` if ENV["DOCKER"] driven_by :selenium_chrome_headless_in_container ``` to this: ``` if ENV["DOCKER"] # driven_by :selenium_chrome_headless_in_container ` driven_by :selenium_chrome_in_container ``` Mac OS comes with a built-in screen sharing application, "Screen Sharing". On Ubuntu-based Linux, the VNC client application "Vinagre" (aka "Remote Desktop Viewer") is commonly used, and can be installed with `sudo apt install vinagre`. You can open the VNC client application and configure it directly, but in both operating systems it's probably easier to click on [vnc://localhost:5900](vnc://localhost:5900) (or paste that into your browser's address bar) and let the browser launch the VNC client with the appropriate parameters for you. The VNC password is `secret`. Run the spec(s) from the command line and you can see the test running in the browser through the VNC client. ## Troubleshooting ### Nokogiri not found on some macs https://stackoverflow.com/questions/70963924/unable-to-load-nokogiri-in-docker-container-on-m1-mac ================================================ FILE: doc/LINUX_SETUP.md ================================================ # Linux Development Environment Installation The commands below can be run all at once by copying and pasting them all into a file and running the file as a script (e.g. `bash -x script_name`). If you copy and paste directly from this page to your command line, we recommend you do so one section (or even one line) at a time. ``` # Install Linux Packages sudo apt update # Check internet for updates sudo apt upgrade -y # Install updates sudo apt install -y git # In case you don't have it already sudo apt install -y libvips42 # Render images for your local web server sudo apt install -y libpq-dev # Helps compile C programs to be able to communicate with postgres # Optional sudo apt install -y curl # A command to help fetching and sending data to urls sudo apt install -y vim # A text editor accessible from the command line ``` ``` # Install Postgres # Add the postgres repo # Create the file repository configuration: sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' # Add the repo key to your keyring: wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | sudo tee /usr/share/keyrings/postgres-archive-keyring.gpg # This next step is done to limit the key to only the postgres repo # Otherwise the signing key is considered valid for all your enabled Debian repositories # Open /etc/apt/sources.list.d/pgdg.list with super user permissions so you are allowed to write to the file # Example using vim: # sudo vim /etc/apt/sources.list.d/pgdg.list # Paste "[signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg]" between "deb" and "http://apt.postgresql..." # Example: deb [signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main # Save the file # Update the package lists: sudo apt update # Install Postgres 12 sudo apt install -y postgresql-12 # Turn the server on sudo systemctl start postgresql@12-main # Add user to Postgres: sudo -u postgres psql -c "CREATE USER $USER WITH CREATEDB" # See https://www.postgresql.org/download/linux/ubuntu/ for more details ``` ``` # Install NVM and Node JS # you can use curl curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash # or wget wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash # Restart your terminal # List all available LTS versions nvm ls-remote | grep -i 'Latest LTS' # Install version from .nvmrc file: nvm install # Update npm npm i -g npm@latest ``` ``` # add node and node tools to the path nvm alias default lts/krypton ``` ``` # Install and configure rbenv sudo apt install libyaml-dev git clone https://github.com/rbenv/rbenv.git ~/.rbenv ~/.rbenv/bin/rbenv init # Restart your terminal # fetch list of ruby versions git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build rbenv install 4.0.2 ``` If you would like RVM instead of rbenv ``` # Install RVM (Part 1) gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB \curl -sSL https://get.rvm.io | bash . ./.bashrc rvm get head rvm install 4.0.2 rvm alias create ruby 4.0.2 rvm alias create default ruby-4.0.2 ``` ```# Download the Chrome browser (for RSpec testing): sudo curl -sS -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - sudo echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt -y update sudo apt -y install google-chrome-stable ``` ## Connecting to Github via ssh Connecting to Gihub via ssh prevents being required to login very often when using git commands. ### Creating an SSH Key Pair - Open Terminal. - Paste the text below, substituting in your GitHub email address. `ssh-keygen -t ed25519 -C "your_email@example.com"` - For all prompts simply press enter to set default values. #### Adding your SSH key to the ssh-agent - Run `eval "$(ssh-agent -s)"` in your terminal to start the ssh-agent in the background. It will use very few resources. - Run `ssh-add ~/.ssh/id_ed25519` to add your private key to the ssh agent. See [github's article](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) for more details/updates. ### Add your ssh key to your github account. [See github's guide](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) ### Test Your ssh Connection - Run `ssh -T git@github.com` - If you see `Are you sure you want to continue connecting (yes/no)?`, enter `yes` - If you see `Hi username! You've successfully authenticated, but GitHub does not provide shell access.`, the connection is set up correctly. ## Project Installation `cd` to the directory where you would like to install CASA Run this series of commands to install the project. ``` git clone git@github.com:rubyforgood/casa.git # Download a copy of the repository locally # The URI will be different when cloning a fork cd casa # Go into the folder containing casa bundle install # Install ruby dependencies bundle exec rails db:setup # Create your local test database bundle exec rails db:migrate # Update the database if it's out of date bundle exec rake after_party:run # Run post deployment tasks npm install # install javascript dependencies npm run build # compile javascript npm run build:css # compile css ``` [Back to the main readme for steps to test your installation.](https://github.com/rubyforgood/casa#running-the-app--verifying-installation) ================================================ FILE: doc/MAC_SETUP.md ================================================ # Install Needed Dependencies ## Homebrew If you haven't already, install the [homebrew](https://brew.sh/) package manager. ## Postgres Use homebrew to install and run postgresql: ```bash brew install postgresql ``` ```bash brew services start postgresql ``` If you have an older version of postgres, `brew postgresql-upgrade-database` For a more GUI focused postgres experience, try [Postgres.app](https://postgresapp.com/) an alternative to the CLI focused default postgres If you are having trouble connecting to your local postgres database using pgAdmin or another local tool, try the following configuration: ``` Host Name: localhost Port: 5432 Maintenance Database: postgres Username: you_mac_login_username (Can be found by calling whoami in a terminal) Password: password ``` ## Ruby ### Rbenv It is often useful to install Ruby with a ruby version manager. The version of Ruby that comes with Mac is not sufficient for this project. You can install [rbenv](https://github.com/rbenv/rbenv) with: ```bash brew install rbenv ruby-build ``` Then, setup rbenv: ```bash rbenv init ``` And finally, follow the setup instructions that are outputted to your terminal after running that. ### Actually installing Ruby Next, install the version of Ruby that this project uses. This can be found by checking the file in this repo, `.ruby-version`. To install the appropriate ruby version, run: ```bash rbenv install 4.0.2 ``` (Do not forget to switch 4.0.2 to the appropriate version) Finally, run: ```bash rbenv local 4.0.2 ``` (Do not forget to switch 4.0.2 to the appropriate version) ## Nodejs The Casa package frontend leverages several javascript packages managed through `npm`. ```bash brew install node ``` ## Chrome Many of the frontend tests are run using Google Chrome, so if you don't already have that installed you may wish to include it: ```bash brew install google-chrome ``` ## Project setup Install gem dependencies with: ```bash bundle install ``` Setup the database with: ```bash bin/rails db:setup ``` Install javascript dependencies with: ```bash npm install ``` Compile assets with: ```bash npm run build ``` and then: ```bash npm run build:css ``` And lastly, run the app with: ```bash bin/rails server ``` See the README for login information. ================================================ FILE: doc/NIX_SETUP.md ================================================ **Nix** If you have [Nix](https://nixos.org) installed you can use the [flake.nix](flake.nix) configuration file located at the root of the project to build and develop within an environment without needing to install `rvm`, `nodejs`, `postgresql` or other tools separately. The environment also uses the `gemset.nix` file to automatically download and install all the gems necessary to get the server up and running: 1. Install [Nix](https://zero-to-nix.com/concepts/nix-installer) 2. Add the following to `~/.config/nix/nix.conf` or `/etc/nix/nix.conf`: ``` experimental-features = nix-command flakes ``` 3. `cd` into casa 4. `nix-shell -p bundix --run "bundix -l"` to update the `gemset.nix` file 5. `nix develop` and wait for the packages to be downloaded and the environment to be built Then you can setup the database and run the server. This will run on Linux and macOS. ================================================ FILE: doc/SECURITY.md ================================================ # Security Policy ## Supported Versions Only the latest version [main branch](https://github.com/rubyforgood/casa) and currently deployed version of this project is in scope for security issues. Also, only the production environment is in scope, although it's ok and normal to test the staging environment. ## Reporting a Vulnerability Please report a vulnerability by emailing casa@rubyforgood.org You can also open a github issue (do NOT provide vulnerability details on github) to notify us that you need to report an issue. We will reply to all reported issues within a week and update at least every two days. We currently do not have any bug bounty program but we will be happy to list your name in our contributors list! :) ================================================ FILE: doc/WSL_SETUP.md ================================================ This guide will walk you through setting up the neccessary environment using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install) (Windows Subsystem for Linux), which will allow you to run Ubuntu on your Windows machine. You will need the following local tools installed: 1. WSL 2. Ruby 3. NodeJs (optional) 4. Postgres 5. Google Chrome ### WSL (Windows Subsystem for Linux) 1. **Install [WSL](https://docs.microsoft.com/en-us/windows/wsl/install)**. `wsl --install` The above command only works if WSL is not installed at all, if you run `wsl --install `and see the WSL help text, do `--install -d Ubuntu` 2. **Run Ubuntu on Windows** You can run Ubuntu on Windows [several different ways](https://docs.microsoft.com/en-us/windows/wsl/install#ways-to-run-multiple-linux-distributions-with-wsl), but we suggest using [Windows Terminal](https://docs.microsoft.com/en-us/windows/terminal/install). To open an Ubuntu tab in Terminal, click the downward arrow and choose 'Ubuntu'. The following commands should all be run in an Ubuntu window. ### Ruby Install a ruby version manager like [rbenv](https://github.com/rbenv/rbenv#installation) **Be sure to install the ruby version in `.ruby-version`. Right now that's Ruby 4.0.2.** Instructions for rbenv: 1. **Install rbenv** `sudo apt install rbenv` 2. **Set up rbenv in your shell** `rbenv init` 3. **Close your Terminal window and open a new one so your changes take effect.** 4. **Verify that rbenv is properly set up** `curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor | bash` 5. **[Install Ruby](https://github.com/rbenv/rbenv#installing-ruby-versions)** **Be sure to install the ruby version in `.ruby-version`. Right now that's Ruby 4.0.2.** `rbenv install 4.0.2` 6. **Set a Ruby version to finish installation and start** `rbenv global 4.0.2` OR `rbenv local 4.0.2` #### Troubleshooting If you are on Ubuntu in Windows Subsystem for Linux (WSL) and `rbenv install` indicates that the Ruby version is unavailable, you might be using Ubuntu's default install of `ruby-build`, which only comes with old installs of Ruby (ending before 2.6.) You should uninstall rvm and ruby-build's apt packages (`apt remove rvm ruby-build`) and install them with Git like this: - `git clone https://github.com/rbenv/rbenv.git ~/.rbenv` - `echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc` - `echo 'eval "$(rbenv init -)"' >> ~/.bashrc` - `exec $SHELL` - `git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build` You'll probably hit a problem where ruby-version reads `ruby-2.7.2` but the install available to you is called `2.7.2`. If you do, install [rbenv-alias](https://github.com/tpope/rbenv-aliases) and create an alias between the two. ### NodeJS The Casa package frontend leverages several javascript packages managed through `npm`, so if you are working on those elements you will want to have node, npm. 1. **(Recommended) [Install nvm](https://github.com/nvm-sh/nvm#installing-and-updating)** NVM is a node version manager. ### Postgres 1. **[Install PostgresSQL](https://docs.microsoft.com/en-us/windows/wsl/tutorials/wsl-database#install-postgresql) for WSL** `sudo apt install postgresql postgresql-contrib` - install `psql --version` - confirm installation and see version number 2. **Install libpq-dev library** `sudo apt-get install libpq-dev` 3. **Start your postgresql service** `sudo service postgresql start` ### Google Chrome Many of the frontend tests are run using Google Chrome, so if you don't already have that installed you may wish to install it. For some linux distributions, installing `chromium-browser` may be enough on WSL. However, some versions of Ubuntu may require the chromium snap to be installed in order to use chromium. If you receive errors about needing the chromium snap while running the test suite, you can install Chrome and chromedriver instead: 1. Download and Install Chrome on WSL Ubuntu - Update your packages: `sudo apt update` - Download Chrome: `wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb` - Install chrome from the downloaded file `sudo dpkg -i google-chrome-stable_current_amd64.deb` `sudo apt-get install -f` - Check that Chrome is installed correctly `google-chrome --version` 2. Install the appropriate Chromedriver - Depending on the version of google-chrome you installed, you will need a specific chromedriver. You can see which version you need on the [chromedriver dowload page](https://chromedriver.chromium.org/downloads). - For example, if `google-chrome --version` returns 105.x.xxxx.xxx you will want to download the version of chromedriver recommended on the page for version 105. - As of this writing, the download page says the following for Chrome version 105: > If you are using Chrome version 105, please download ChromeDriver 105.0.5195.52 - To download chromedriver, run the following command, replacing `{CHROMEDRIVER-VERSION}` with the version of chromedriver you need (e.g., 105.0.5195.52) `wget https://chromedriver.storage.googleapis.com/{CHROMEDRIVER-VERSION}/chromedriver_linux64.zip` - Next, unzip the file you downloaded `unzip chromedriver_linux64.zip` - Finally, move chromedriver to the correct location and enable it for use: `sudo mv chromedriver /usr/bin/chromedriver` `sudo chown root:root /usr/bin/chromedriver` `sudo chmod +x /usr/bin/chromedriver` 3. Run the test suite - Assuming the rest of the application is already set up, you can run the test suite to verify that you no longer receive error regarding chromium snap: `bin/rails spec` ### Casa & Rails Casa's install will also install the correct version of Rails. 1. **Download the project** **You should create a fork in GitHub if you don't have permission to directly commit to this repo. See our [contributing guide](https://github.com/rubyforgood/casa/blob/main/doc/CONTRIBUTING.md) for more detailed instructions.** `git clone ` - use your fork's address if you have one ie `git clone https://github.com/rubyforgood/casa.git` 2. **Installing Packages** `cd casa/` `bundle install` - install ruby dependencies. `npm install` - install javascript dependencies. 3. **Database Setup** Be sure your postgres service is running (`sudo service postgresql start`). Create a postgres user that matches your Ubuntu user: `sudo -u postgres createuser ` - create user `sudo -u postgres psql` - logs in as the postgres user `psql=# alter user with encrypted password '';` - add password `psql=# alter user CREATEDB;` - give permission to your user to create databases Set up the Casa DB `bin/rails db:setup` - sets up the db `bin/rails db:seed:replant` - generates test data (can be rerun to regenerate test data) 4. **Compile Assets** - `npm run build` compile javascript   `npm run build:dev` to auto recompile for when you edit js files - `npm run build:css` compile css   `npm run build:css:dev` to auto recompile for when you edit sass files ### Getting Started See [Running the App / Verifying Installation](https://github.com/rubyforgood/casa#running-the-app--verifying-installation). A good option for editing files in WSL is [Visual Studio Code Remote- WSL](https://code.visualstudio.com/docs/remote/wsl) ================================================ FILE: doc/architecture-decisions/0001-record-architecture-decisions.md ================================================ # 1. Record architecture decisions Date: 2020-04-04 ## Status Accepted ## Context We need to record the architectural decisions made on this project. ## Decision We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). ## Consequences See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). ================================================ FILE: doc/architecture-decisions/0002-disallow-ui-sign-ups.md ================================================ # 2. Disallow user sign-ups from the UI Date: 2020-04-04 ## Status Accepted ## Context We want it to be easy for people to join the organization, however we don't want random people signing up and spamming us. We want admin users to have control over who has accounts on the system. We don't have the capacity to handle this properly through the user interface right now. ## Decision We are going to disable Devise 'registerable' for the user model so that there will no longer be a public sign up option on the site. Creation of new accounts will be done on the backend. ## Consequences Admins have to do more work to sign up users, but this gives them more control over who can access the site. ================================================ FILE: doc/architecture-decisions/0003-multiple-user-tables.md ================================================ # 3. Having 2 user tables Date: 2020-04-05 ## Status Accepted ## Context This is planned to be a multi-tenant system. There will be multiple CASA orgs in the system, so every case, case_contact, volunteer, supervisor, casa_admin etc must have a casa_org_id, because no one is allowed to belong to multiple CASAs. Volunteer, supervisor, and casa_admin are all roles for a "User" db object. In addition to those existing roles, we want to create a new kind of user: all_casa_admin. We need to handle the case of super users who have access to multiple casa_orgs, so they would be difficult to handle in the existing User table--with null handling around their casa_org_id field. We have used the built-in Devise ability to have multiple user tables, as recommended to us by our Rails expert Betsy. This is to prevent needing null handling around casa_id for User records since all_casa_admin users will not have casa_id populated. Additionally, all_casa_admin users are currently intended to be allowed to create casa_admin users, but NOT to be able to see or edit any CASA data like volunteer assignments, cases, case_updates etc. ## Decision We are using two tables for users: "user" table for volunteers,supervisors, and casa_admin (all of which must have a casa_id). "all_casa_admin" for all_casa_admins, which will have no casa_id. ## Consequences The login behavior and dashboard page for all_casa_admin will need to be created and handled separately from the regular user login and dashboard ================================================ FILE: doc/architecture-decisions/0004-use-bootstrap.md ================================================ # 1. Use Bootstrap for styling Date: 2020-04-05 ## Status Proposed ## Context We would like to have an easy-to-use system for consistent styles that doesn't take much tinkering. We propose using the `bootstrap` gem. ## Decision Pending ## Consequences Some familiarity with [Bootstrap](https://getbootstrap.com/docs/4.4/getting-started/introduction/) will likely be necessary but we hope the medium-to-long term benefits will outweigh the cost to learn up front. ================================================ FILE: doc/architecture-decisions/0005-android-app-as-a-pwa.md ================================================ # 1. The android app is a [PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) running in a [TWA](https://developer.chrome.com/docs/android/trusted-web-activity/overview/) Date: 2021-07-07 ## Context Building a [progressive web app(PWA)](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) is a very quick way to convert a website into a mobile app for android. PWAs can support offline mode and push notifications. Our app runs in a [trusted web activity(TWA)](https://developer.chrome.com/docs/android/trusted-web-activity/overview/) which is very similar to having the web page load in a mobile browser. The trusted web activity offers browser like support for the PWA. ## Consequences More javascript support for [service workers](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers) to support offline mode. [Maintaining a key for app signing](https://github.com/rubyforgood/casa-android/wiki/How-to-manage-app-signing) ================================================ FILE: doc/architecture-decisions/0006-few-controller-tests.md ================================================ # 1. System tests are preferred over controller tests Date: 2021-07-14 "system" tests test both the erb and the controller at the same time. They are slower. They use capybara. Having some of these (one per rendered page) is very important because it is possible for a controller to define a variable `@a` and an erb to require a variable `@b` and the tests for the controller and erb to both pass separately, but for the page loading to fail. We need system tests to make sure that our codebase is working properly. In general, we don't write many controller tests because they tend to rely overly on mocking and are fully duplicitive with the system tests. ================================================ FILE: doc/architecture-decisions/0007-inline-css-for-email-views.md ================================================ # 1. Inline CSS for email views Date: 2021-08-18 CSS is inline for email views becuase other forms of CSS lack full support in some email platforms including gmail and outlook. See https://www.campaignmonitor.com/css/ to see the current status of support for forms of CSS such as `

    Sorry, you are not authorized to perform this action.

    ================================================ FILE: public/404.html ================================================ The page you were looking for doesn't exist (404)
    CASA/Volunteer Tracking

    The page you were looking for doesn't exist.

    You may have mistyped the address or the page may have moved.

    ================================================ FILE: public/406-unsupported-browser.html ================================================ Your browser is not supported (406)

    Your browser is not supported.

    Please upgrade your browser to continue.

    ================================================ FILE: public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

    Maybe you tried to change something you didn't have access to.

    If you are the application owner check the logs for more information.

    ================================================ FILE: public/500.html ================================================ We're sorry, but something went wrong (500)
    CASA/Volunteer Tracking

    We're sorry, but something went wrong.

    If you are the application owner check the logs for more information.

    ================================================ FILE: public/assets/css/lineicons.css ================================================ /*-------------------------------- LineIcons Web Font Author: lineicons.com -------------------------------- */ @font-face { font-family: "LineIcons"; src: url("../fonts/LineIcons.eot"); src: url("../fonts/LineIcons.eot") format("embedded-opentype"), url("../fonts/LineIcons.woff2") format("woff2"), url("../fonts/LineIcons.woff") format("woff"), url("../fonts/LineIcons.ttf") format("truetype"), url("../fonts/LineIcons.svg") format("svg"); font-weight: normal; font-style: normal; } /*------------------------ base class definition -------------------------*/ .lni { display: inline-block; font: normal normal normal 1em/1 "LineIcons"; color: inherit; flex-shrink: 0; speak: none; text-transform: none; line-height: 1; vertical-align: -0.125em; /* Better Font Rendering */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /*------------------------ change icon size -------------------------*/ /* relative units */ .lni-sm { font-size: 0.8em; } .lni-lg { font-size: 1.2em; } /* absolute units */ .lni-16 { font-size: 16px; } .lni-32 { font-size: 32px; } /*------------------------ spinning icons -------------------------*/ .lni-is-spinning { animation: lni-spin 1s infinite linear; } @keyframes lni-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /*------------------------ rotated/flipped icons -------------------------*/ .lni-rotate-90 { transform: rotate(90deg); } .lni-rotate-180 { transform: rotate(180deg); } .lni-rotate-270 { transform: rotate(270deg); } .lni-flip-y { transform: scaleY(-1); } .lni-flip-x { transform: scaleX(-1); } /*------------------------ icons -------------------------*/ .lni-500px::before { content: "\ea03"; } .lni-add-files::before { content: "\ea01"; } .lni-adobe::before { content: "\ea06"; } .lni-agenda::before { content: "\ea02"; } .lni-airbnb::before { content: "\ea07"; } .lni-alarm-clock::before { content: "\ea08"; } .lni-alarm::before { content: "\ea04"; } .lni-amazon-original::before { content: "\ea05"; } .lni-amazon-pay::before { content: "\ea09"; } .lni-amazon::before { content: "\ea0a"; } .lni-ambulance::before { content: "\ea0b"; } .lni-amex::before { content: "\ea0c"; } .lni-anchor::before { content: "\ea0d"; } .lni-android-original::before { content: "\ea0e"; } .lni-android::before { content: "\ea0f"; } .lni-angellist::before { content: "\ea10"; } .lni-angle-double-down::before { content: "\ea11"; } .lni-angle-double-left::before { content: "\ea12"; } .lni-angle-double-right::before { content: "\ea13"; } .lni-angle-double-up::before { content: "\ea14"; } .lni-angular::before { content: "\ea15"; } .lni-apartment::before { content: "\ea16"; } .lni-app-store::before { content: "\ea17"; } .lni-apple-music::before { content: "\ea18"; } .lni-apple-pay::before { content: "\ea19"; } .lni-apple::before { content: "\ea1a"; } .lni-archive::before { content: "\ea1f"; } .lni-arrow-down-circle::before { content: "\ea1b"; } .lni-arrow-down::before { content: "\ea1c"; } .lni-arrow-left-circle::before { content: "\ea1d"; } .lni-arrow-left::before { content: "\ea1e"; } .lni-arrow-right-circle::before { content: "\ea20"; } .lni-arrow-right::before { content: "\ea21"; } .lni-arrow-top-left::before { content: "\ea22"; } .lni-arrow-top-right::before { content: "\ea23"; } .lni-arrow-up-circle::before { content: "\ea24"; } .lni-arrow-up::before { content: "\ea25"; } .lni-arrows-horizontal::before { content: "\ea26"; } .lni-arrows-vertical::before { content: "\ea27"; } .lni-atlassian::before { content: "\ea28"; } .lni-aws::before { content: "\ea29"; } .lni-azure::before { content: "\ea2a"; } .lni-backward::before { content: "\ea2b"; } .lni-baloon::before { content: "\ea2c"; } .lni-ban::before { content: "\ea2d"; } .lni-bar-chart::before { content: "\ea2e"; } .lni-basketball::before { content: "\ea2f"; } .lni-behance-original::before { content: "\ea30"; } .lni-behance::before { content: "\ea31"; } .lni-bi-cycle::before { content: "\ea32"; } .lni-bitbucket::before { content: "\ea33"; } .lni-bitcoin::before { content: "\ea34"; } .lni-blackboard::before { content: "\ea35"; } .lni-blogger::before { content: "\ea36"; } .lni-bluetooth-original::before { content: "\ea37"; } .lni-bluetooth::before { content: "\ea38"; } .lni-bold::before { content: "\ea39"; } .lni-bolt-alt::before { content: "\ea3a"; } .lni-bolt::before { content: "\ea40"; } .lni-book::before { content: "\ea3b"; } .lni-bookmark-alt::before { content: "\ea3c"; } .lni-bookmark::before { content: "\ea3d"; } .lni-bootstrap::before { content: "\ea3e"; } .lni-bricks::before { content: "\ea3f"; } .lni-bridge::before { content: "\ea41"; } .lni-briefcase::before { content: "\ea42"; } .lni-brush-alt::before { content: "\ea43"; } .lni-brush::before { content: "\ea44"; } .lni-btc::before { content: "\ea45"; } .lni-bubble::before { content: "\ea46"; } .lni-bug::before { content: "\ea47"; } .lni-bulb::before { content: "\ea48"; } .lni-bullhorn::before { content: "\ea49"; } .lni-burger::before { content: "\ea4a"; } .lni-bus::before { content: "\ea4b"; } .lni-cake::before { content: "\ea4c"; } .lni-calculator::before { content: "\ea4d"; } .lni-calendar::before { content: "\ea4e"; } .lni-camera::before { content: "\ea4f"; } .lni-candy-cane::before { content: "\ea50"; } .lni-candy::before { content: "\ea51"; } .lni-capsule::before { content: "\ea52"; } .lni-car-alt::before { content: "\ea53"; } .lni-car::before { content: "\ea54"; } .lni-caravan::before { content: "\ea55"; } .lni-cart-full::before { content: "\ea56"; } .lni-cart::before { content: "\ea57"; } .lni-certificate::before { content: "\ea58"; } .lni-check-box::before { content: "\ea59"; } .lni-checkmark-circle::before { content: "\ea5a"; } .lni-checkmark::before { content: "\ea5b"; } .lni-chef-hat::before { content: "\ea5c"; } .lni-chevron-down-circle::before { content: "\ea5d"; } .lni-chevron-down::before { content: "\ea5e"; } .lni-chevron-left-circle::before { content: "\ea5f"; } .lni-chevron-left::before { content: "\ea60"; } .lni-chevron-right-circle::before { content: "\ea61"; } .lni-chevron-right::before { content: "\ea62"; } .lni-chevron-up-circle::before { content: "\ea63"; } .lni-chevron-up::before { content: "\ea64"; } .lni-chrome::before { content: "\ea65"; } .lni-chromecast::before { content: "\ea66"; } .lni-circle-minus::before { content: "\ea67"; } .lni-circle-plus::before { content: "\ea68"; } .lni-clipboard::before { content: "\ea69"; } .lni-close::before { content: "\ea6a"; } .lni-cloud-check::before { content: "\ea6b"; } .lni-cloud-download::before { content: "\ea6c"; } .lni-cloud-network::before { content: "\ea6d"; } .lni-cloud-sync::before { content: "\ea6e"; } .lni-cloud-upload::before { content: "\ea6f"; } .lni-cloud::before { content: "\ea70"; } .lni-cloudflare::before { content: "\ea71"; } .lni-cloudy-sun::before { content: "\ea72"; } .lni-code-alt::before { content: "\ea73"; } .lni-code::before { content: "\ea74"; } .lni-codepen::before { content: "\ea75"; } .lni-coffee-cup::before { content: "\ea76"; } .lni-cog::before { content: "\ea77"; } .lni-cogs::before { content: "\ea78"; } .lni-coin::before { content: "\ea79"; } .lni-comments-alt::before { content: "\ea7a"; } .lni-comments-reply::before { content: "\ea7b"; } .lni-comments::before { content: "\ea7c"; } .lni-compass::before { content: "\ea7d"; } .lni-connectdevelop::before { content: "\ea7e"; } .lni-construction-hammer::before { content: "\ea7f"; } .lni-construction::before { content: "\ea80"; } .lni-consulting::before { content: "\ea81"; } .lni-control-panel::before { content: "\ea82"; } .lni-cool::before { content: "\ea83"; } .lni-cpanel::before { content: "\ea84"; } .lni-creative-commons::before { content: "\ea85"; } .lni-credit-cards::before { content: "\ea86"; } .lni-crop::before { content: "\ea87"; } .lni-cross-circle::before { content: "\ea88"; } .lni-crown::before { content: "\ea89"; } .lni-css3::before { content: "\ea8a"; } .lni-cup::before { content: "\ea8b"; } .lni-customer::before { content: "\ea8c"; } .lni-cut::before { content: "\ea8d"; } .lni-dashboard::before { content: "\ea8e"; } .lni-database::before { content: "\ea8f"; } .lni-delivery::before { content: "\ea90"; } .lni-dev::before { content: "\ea91"; } .lni-diamond-alt::before { content: "\ea92"; } .lni-diamond::before { content: "\ea93"; } .lni-digitalocean::before { content: "\ea94"; } .lni-diners-club::before { content: "\ea95"; } .lni-dinner::before { content: "\ea96"; } .lni-direction-alt::before { content: "\ea97"; } .lni-direction-ltr::before { content: "\ea98"; } .lni-direction-rtl::before { content: "\ea99"; } .lni-direction::before { content: "\ea9a"; } .lni-discord::before { content: "\ea9b"; } .lni-discover::before { content: "\ea9c"; } .lni-display-alt::before { content: "\ea9d"; } .lni-display::before { content: "\ea9e"; } .lni-docker::before { content: "\ea9f"; } .lni-dollar::before { content: "\eaa0"; } .lni-domain::before { content: "\eaa1"; } .lni-download::before { content: "\eaa2"; } .lni-dribbble::before { content: "\eaa3"; } .lni-drop::before { content: "\eaa4"; } .lni-dropbox-original::before { content: "\eaa5"; } .lni-dropbox::before { content: "\eaa6"; } .lni-drupal-original::before { content: "\eaa7"; } .lni-drupal::before { content: "\eaa8"; } .lni-dumbbell::before { content: "\eaa9"; } .lni-edge::before { content: "\eaaa"; } .lni-empty-file::before { content: "\eaab"; } .lni-enter::before { content: "\eaac"; } .lni-envato::before { content: "\eaad"; } .lni-envelope::before { content: "\eaae"; } .lni-eraser::before { content: "\eaaf"; } .lni-euro::before { content: "\eab0"; } .lni-exit-down::before { content: "\eab1"; } .lni-exit-up::before { content: "\eab2"; } .lni-exit::before { content: "\eab3"; } .lni-eye::before { content: "\eab4"; } .lni-facebook-filled::before { content: "\eab5"; } .lni-facebook-messenger::before { content: "\eab6"; } .lni-facebook-original::before { content: "\eab7"; } .lni-facebook-oval::before { content: "\eab8"; } .lni-facebook::before { content: "\eab9"; } .lni-figma::before { content: "\eaba"; } .lni-files::before { content: "\eabb"; } .lni-firefox-original::before { content: "\eabc"; } .lni-firefox::before { content: "\eabd"; } .lni-fireworks::before { content: "\eabe"; } .lni-first-aid::before { content: "\eabf"; } .lni-flag-alt::before { content: "\eac0"; } .lni-flag::before { content: "\eac1"; } .lni-flags::before { content: "\eac2"; } .lni-flickr::before { content: "\eac3"; } .lni-flower::before { content: "\eac4"; } .lni-folder::before { content: "\eac5"; } .lni-forward::before { content: "\eac6"; } .lni-frame-expand::before { content: "\eac7"; } .lni-fresh-juice::before { content: "\eac8"; } .lni-friendly::before { content: "\eac9"; } .lni-full-screen::before { content: "\eaca"; } .lni-funnel::before { content: "\eacb"; } .lni-gallery::before { content: "\eacc"; } .lni-game::before { content: "\eacd"; } .lni-gatsby::before { content: "\eace"; } .lni-gift::before { content: "\eacf"; } .lni-git::before { content: "\ead0"; } .lni-github-original::before { content: "\ead1"; } .lni-github::before { content: "\ead2"; } .lni-goodreads::before { content: "\ead3"; } .lni-google-drive::before { content: "\ead4"; } .lni-google-pay::before { content: "\ead5"; } .lni-google-wallet::before { content: "\ead6"; } .lni-google::before { content: "\ead7"; } .lni-graduation::before { content: "\ead8"; } .lni-graph::before { content: "\ead9"; } .lni-grid-alt::before { content: "\eada"; } .lni-grid::before { content: "\eadb"; } .lni-grow::before { content: "\eadc"; } .lni-hacker-news::before { content: "\eadd"; } .lni-hammer::before { content: "\eade"; } .lni-hand::before { content: "\eadf"; } .lni-handshake::before { content: "\eae0"; } .lni-happy::before { content: "\eae1"; } .lni-harddrive::before { content: "\eae2"; } .lni-headphone-alt::before { content: "\eae3"; } .lni-headphone::before { content: "\eae4"; } .lni-heart-filled::before { content: "\eae5"; } .lni-heart-monitor::before { content: "\eae6"; } .lni-heart::before { content: "\eae7"; } .lni-helicopter::before { content: "\eae8"; } .lni-helmet::before { content: "\eae9"; } .lni-help::before { content: "\eaea"; } .lni-highlight-alt::before { content: "\eaeb"; } .lni-highlight::before { content: "\eaec"; } .lni-home::before { content: "\eaed"; } .lni-hospital::before { content: "\eaee"; } .lni-hourglass::before { content: "\eaef"; } .lni-html5::before { content: "\eaf0"; } .lni-image::before { content: "\eaf1"; } .lni-imdb::before { content: "\eaf2"; } .lni-inbox::before { content: "\eaf3"; } .lni-indent-decrease::before { content: "\eaf4"; } .lni-indent-increase::before { content: "\eaf5"; } .lni-infinite::before { content: "\eaf6"; } .lni-information::before { content: "\eaf7"; } .lni-instagram-filled::before { content: "\eaf8"; } .lni-instagram-original::before { content: "\eaf9"; } .lni-instagram::before { content: "\eafa"; } .lni-invention::before { content: "\eafb"; } .lni-invest-monitor::before { content: "\eafc"; } .lni-investment::before { content: "\eafd"; } .lni-island::before { content: "\eafe"; } .lni-italic::before { content: "\eaff"; } .lni-java::before { content: "\eb00"; } .lni-javascript::before { content: "\eb01"; } .lni-jcb::before { content: "\eb02"; } .lni-joomla-original::before { content: "\eb03"; } .lni-joomla::before { content: "\eb04"; } .lni-jsfiddle::before { content: "\eb05"; } .lni-juice::before { content: "\eb06"; } .lni-key::before { content: "\eb07"; } .lni-keyboard::before { content: "\eb08"; } .lni-keyword-research::before { content: "\eb09"; } .lni-laptop-phone::before { content: "\eb0a"; } .lni-laptop::before { content: "\eb0b"; } .lni-laravel::before { content: "\eb0c"; } .lni-layers::before { content: "\eb0d"; } .lni-layout::before { content: "\eb0e"; } .lni-leaf::before { content: "\eb0f"; } .lni-library::before { content: "\eb10"; } .lni-license::before { content: "\eb11"; } .lni-lifering::before { content: "\eb12"; } .lni-line-dashed::before { content: "\eb13"; } .lni-line-dotted::before { content: "\eb14"; } .lni-line-double::before { content: "\eb15"; } .lni-line-spacing::before { content: "\eb16"; } .lni-line::before { content: "\eb17"; } .lni-lineicons-alt::before { content: "\eb18"; } .lni-lineicons::before { content: "\eb19"; } .lni-link::before { content: "\eb1a"; } .lni-linkedin-original::before { content: "\eb1b"; } .lni-linkedin::before { content: "\eb1c"; } .lni-list::before { content: "\eb1d"; } .lni-lock-alt::before { content: "\eb1e"; } .lni-lock::before { content: "\eb1f"; } .lni-magento::before { content: "\eb20"; } .lni-magnet::before { content: "\eb21"; } .lni-magnifier::before { content: "\eb22"; } .lni-mailchimp::before { content: "\eb23"; } .lni-map-marker::before { content: "\eb24"; } .lni-map::before { content: "\eb25"; } .lni-markdown::before { content: "\eb26"; } .lni-mashroom::before { content: "\eb27"; } .lni-mastercard::before { content: "\eb28"; } .lni-medium::before { content: "\eb29"; } .lni-menu::before { content: "\eb2a"; } .lni-mic::before { content: "\eb2b"; } .lni-microphone::before { content: "\eb2c"; } .lni-microscope::before { content: "\eb2d"; } .lni-microsoft-edge::before { content: "\eb2e"; } .lni-microsoft::before { content: "\eb2f"; } .lni-minus::before { content: "\eb30"; } .lni-mobile::before { content: "\eb31"; } .lni-money-location::before { content: "\eb32"; } .lni-money-protection::before { content: "\eb33"; } .lni-more-alt::before { content: "\eb34"; } .lni-more::before { content: "\eb35"; } .lni-mouse::before { content: "\eb36"; } .lni-move::before { content: "\eb37"; } .lni-music::before { content: "\eb38"; } .lni-netlify::before { content: "\eb39"; } .lni-network::before { content: "\eb3a"; } .lni-night::before { content: "\eb3b"; } .lni-nodejs-alt::before { content: "\eb3c"; } .lni-nodejs::before { content: "\eb3d"; } .lni-notepad::before { content: "\eb3e"; } .lni-npm::before { content: "\eb3f"; } .lni-offer::before { content: "\eb40"; } .lni-opera::before { content: "\eb41"; } .lni-package::before { content: "\eb42"; } .lni-page-break::before { content: "\eb43"; } .lni-pagination::before { content: "\eb44"; } .lni-paint-bucket::before { content: "\eb45"; } .lni-paint-roller::before { content: "\eb46"; } .lni-pallet::before { content: "\eb47"; } .lni-paperclip::before { content: "\eb48"; } .lni-patreon::before { content: "\eb49"; } .lni-pause::before { content: "\eb4a"; } .lni-paypal-original::before { content: "\eb4b"; } .lni-paypal::before { content: "\eb4c"; } .lni-pencil-alt::before { content: "\eb4d"; } .lni-pencil::before { content: "\eb4e"; } .lni-phone-set::before { content: "\eb4f"; } .lni-phone::before { content: "\eb50"; } .lni-php::before { content: "\eb51"; } .lni-pie-chart::before { content: "\eb52"; } .lni-pilcrow::before { content: "\eb53"; } .lni-pin::before { content: "\eb54"; } .lni-pinterest::before { content: "\eb55"; } .lni-pizza::before { content: "\eb56"; } .lni-plane::before { content: "\eb57"; } .lni-play-store::before { content: "\eb58"; } .lni-play::before { content: "\eb59"; } .lni-playstation::before { content: "\eb5a"; } .lni-plug::before { content: "\eb5b"; } .lni-plus::before { content: "\eb5c"; } .lni-pointer-down::before { content: "\eb5d"; } .lni-pointer-left::before { content: "\eb5e"; } .lni-pointer-right::before { content: "\eb5f"; } .lni-pointer-top::before { content: "\eb60"; } .lni-pointer::before { content: "\eb61"; } .lni-popup::before { content: "\eb62"; } .lni-postcard::before { content: "\eb63"; } .lni-pound::before { content: "\eb64"; } .lni-power-switch::before { content: "\eb65"; } .lni-printer::before { content: "\eb66"; } .lni-producthunt::before { content: "\eb67"; } .lni-protection::before { content: "\eb68"; } .lni-pulse::before { content: "\eb69"; } .lni-pyramids::before { content: "\eb6a"; } .lni-python::before { content: "\eb6b"; } .lni-question-circle::before { content: "\eb6c"; } .lni-quora::before { content: "\eb6d"; } .lni-quotation::before { content: "\eb6e"; } .lni-radio-button::before { content: "\eb6f"; } .lni-rain::before { content: "\eb70"; } .lni-react::before { content: "\eb73"; } .lni-reddit::before { content: "\eb71"; } .lni-reload::before { content: "\eb72"; } .lni-remove-file::before { content: "\eb74"; } .lni-reply::before { content: "\eb75"; } .lni-restaurant::before { content: "\eb76"; } .lni-revenue::before { content: "\eb77"; } .lni-road::before { content: "\eb78"; } .lni-rocket::before { content: "\eb79"; } .lni-rss-feed::before { content: "\eb7a"; } .lni-ruler-alt::before { content: "\eb7b"; } .lni-ruler-pencil::before { content: "\eb7c"; } .lni-ruler::before { content: "\eb7d"; } .lni-rupee::before { content: "\eb7e"; } .lni-sad::before { content: "\eb7f"; } .lni-save::before { content: "\eb80"; } .lni-school-bench-alt::before { content: "\eb81"; } .lni-school-bench::before { content: "\eb82"; } .lni-scooter::before { content: "\eb83"; } .lni-scroll-down::before { content: "\eb84"; } .lni-search-alt::before { content: "\eb85"; } .lni-search::before { content: "\eb86"; } .lni-select::before { content: "\eb87"; } .lni-seo::before { content: "\eb88"; } .lni-service::before { content: "\eb89"; } .lni-share-alt-1::before { content: "\eb8a"; } .lni-share-alt::before { content: "\eb8b"; } .lni-share::before { content: "\eb8c"; } .lni-shield::before { content: "\eb8d"; } .lni-shift-left::before { content: "\eb8e"; } .lni-shift-right::before { content: "\eb8f"; } .lni-ship::before { content: "\eb90"; } .lni-shopify::before { content: "\eb91"; } .lni-shopping-basket::before { content: "\eb92"; } .lni-shortcode::before { content: "\eb93"; } .lni-shovel::before { content: "\eb94"; } .lni-shuffle::before { content: "\eb95"; } .lni-signal::before { content: "\eb96"; } .lni-sketch::before { content: "\eb97"; } .lni-skipping-rope::before { content: "\eb98"; } .lni-skype::before { content: "\eb99"; } .lni-slack-line::before { content: "\eb9a"; } .lni-slack::before { content: "\eb9b"; } .lni-slice::before { content: "\eb9c"; } .lni-slideshare::before { content: "\eb9d"; } .lni-slim::before { content: "\eb9e"; } .lni-smile::before { content: "\eb9f"; } .lni-snapchat::before { content: "\eba0"; } .lni-sort-alpha-asc::before { content: "\eba1"; } .lni-sort-amount-asc::before { content: "\eba2"; } .lni-sort-amount-dsc::before { content: "\eba3"; } .lni-soundcloud-original::before { content: "\eba4"; } .lni-soundcloud::before { content: "\eba5"; } .lni-speechless::before { content: "\eba6"; } .lni-spellcheck::before { content: "\eba7"; } .lni-spinner-arrow::before { content: "\eba8"; } .lni-spinner-solid::before { content: "\eba9"; } .lni-spinner::before { content: "\ebaa"; } .lni-spotify-original::before { content: "\ebab"; } .lni-spotify::before { content: "\ebac"; } .lni-spray::before { content: "\ebad"; } .lni-sprout::before { content: "\ebae"; } .lni-squarespace::before { content: "\ebaf"; } .lni-stackoverflow::before { content: "\ebb0"; } .lni-stamp::before { content: "\ebb1"; } .lni-star-empty::before { content: "\ebb2"; } .lni-star-filled::before { content: "\ebb3"; } .lni-star-half::before { content: "\ebb4"; } .lni-star::before { content: "\ebb5"; } .lni-stats-down::before { content: "\ebb6"; } .lni-stats-up::before { content: "\ebb7"; } .lni-steam::before { content: "\ebb8"; } .lni-sthethoscope::before { content: "\ebb9"; } .lni-stop::before { content: "\ebba"; } .lni-strikethrough::before { content: "\ebbb"; } .lni-stripe::before { content: "\ebbc"; } .lni-stumbleupon::before { content: "\ebbd"; } .lni-sun::before { content: "\ebbe"; } .lni-support::before { content: "\ebbf"; } .lni-surf-board::before { content: "\ebc0"; } .lni-suspect::before { content: "\ebc1"; } .lni-swift::before { content: "\ebc2"; } .lni-syringe::before { content: "\ebc3"; } .lni-tab::before { content: "\ebc4"; } .lni-tag::before { content: "\ebc5"; } .lni-target-customer::before { content: "\ebc6"; } .lni-target-revenue::before { content: "\ebc7"; } .lni-target::before { content: "\ebc8"; } .lni-taxi::before { content: "\ebc9"; } .lni-teabag::before { content: "\ebca"; } .lni-telegram-original::before { content: "\ebcb"; } .lni-telegram::before { content: "\ebcc"; } .lni-text-align-center::before { content: "\ebcd"; } .lni-text-align-justify::before { content: "\ebce"; } .lni-text-align-left::before { content: "\ebcf"; } .lni-text-align-right::before { content: "\ebd0"; } .lni-text-format-remove::before { content: "\ebd4"; } .lni-text-format::before { content: "\ebd1"; } .lni-thought::before { content: "\ebd2"; } .lni-thumbs-down::before { content: "\ebd3"; } .lni-thumbs-up::before { content: "\ebd5"; } .lni-thunder-alt::before { content: "\ebd6"; } .lni-thunder::before { content: "\ebd7"; } .lni-ticket-alt::before { content: "\ebd8"; } .lni-ticket::before { content: "\ebd9"; } .lni-tiktok::before { content: "\ebda"; } .lni-timer::before { content: "\ebdb"; } .lni-tounge::before { content: "\ebdc"; } .lni-train-alt::before { content: "\ebdd"; } .lni-train::before { content: "\ebde"; } .lni-trash-can::before { content: "\ebdf"; } .lni-travel::before { content: "\ebe0"; } .lni-tree::before { content: "\ebe1"; } .lni-trees::before { content: "\ebe2"; } .lni-trello::before { content: "\ebe3"; } .lni-trowel::before { content: "\ebe4"; } .lni-tshirt::before { content: "\ebe5"; } .lni-tumblr::before { content: "\ebe6"; } .lni-twitch::before { content: "\ebe7"; } .lni-twitter-filled::before { content: "\ebe8"; } .lni-twitter-original::before { content: "\ebe9"; } .lni-twitter::before { content: "\ebea"; } .lni-ubuntu::before { content: "\ebeb"; } .lni-underline::before { content: "\ebec"; } .lni-unlink::before { content: "\ebed"; } .lni-unlock::before { content: "\ebee"; } .lni-unsplash::before { content: "\ebef"; } .lni-upload::before { content: "\ebf0"; } .lni-user::before { content: "\ebf1"; } .lni-users::before { content: "\ebf6"; } .lni-ux::before { content: "\ebf2"; } .lni-vector::before { content: "\ebf3"; } .lni-video::before { content: "\ebf4"; } .lni-vimeo::before { content: "\ebf5"; } .lni-visa::before { content: "\ebf7"; } .lni-vk::before { content: "\ebf8"; } .lni-volume-high::before { content: "\ebf9"; } .lni-volume-low::before { content: "\ebfa"; } .lni-volume-medium::before { content: "\ebfb"; } .lni-volume-mute::before { content: "\ebfc"; } .lni-volume::before { content: "\ebfd"; } .lni-wallet::before { content: "\ebfe"; } .lni-warning::before { content: "\ebff"; } .lni-website-alt::before { content: "\ec00"; } .lni-website::before { content: "\ec01"; } .lni-wechat::before { content: "\ec02"; } .lni-weight::before { content: "\ec03"; } .lni-whatsapp::before { content: "\ec04"; } .lni-wheelbarrow::before { content: "\ec05"; } .lni-wheelchair::before { content: "\ec06"; } .lni-windows::before { content: "\ec07"; } .lni-wordpress-filled::before { content: "\ec08"; } .lni-wordpress::before { content: "\ec09"; } .lni-world-alt::before { content: "\ec0a"; } .lni-world::before { content: "\ec0c"; } .lni-write::before { content: "\ec0b"; } .lni-xbox::before { content: "\ec0d"; } .lni-yahoo::before { content: "\ec0e"; } .lni-ycombinator::before { content: "\ec0f"; } .lni-yen::before { content: "\ec10"; } .lni-youtube::before { content: "\ec13"; } .lni-zip::before { content: "\ec11"; } .lni-zoom-in::before { content: "\ec12"; } .lni-zoom-out::before { content: "\ec14"; } ================================================ FILE: public/assets/css/main.css ================================================ /*=========================== COMMON css ===========================*/ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"); html { scroll-behavior: smooth; } body { font-family: "Inter", sans-serif !important; font-weight: normal; font-style: normal; color: #5d657b; overflow-x: hidden; background: #f1f5f9; } * { margin: 0; padding: 0; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } a:focus, input:focus, textarea:focus, button:focus, .btn:focus, .btn.focus, .btn:not(:disabled):not(.disabled).active, .btn:not(:disabled):not(.disabled):active { text-decoration: none; outline: none; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } a:hover { color: #4a6cf7; } button, a { -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } a, a:focus, a:hover { text-decoration: none; } i, a { display: inline-block; } audio, canvas, iframe, img, svg, video { vertical-align: middle; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; } ul, ol { margin: 0px; padding: 0px; list-style-type: none; } p { font-size: 16px; font-weight: 400; line-height: 25px; margin: 0px; } .img-bg { background-position: center center; background-size: cover; background-repeat: no-repeat; width: 100%; height: 100%; } .para-width-500 { max-width: 500px; width: 100%; } @media (max-width: 767px) { .container { padding: 0 30px; } } /* ========== cart style ========== */ .card-style { background: #fff; box-sizing: border-box; padding: 25px 30px; position: relative; border: 1px solid #e2e8f0; box-shadow: 0px 10px 20px rgba(200, 208, 216, 0.3); border-radius: 10px; } @media (max-width: 767px) { .card-style { padding: 20px; } } .card-style .jvm-zoom-btn { position: absolute; display: inline-flex; justify-content: center; align-items: center; width: 30px; height: 30px; border: 1px solid rgba(0, 0, 0, 0.1); right: 30px; bottom: 30px; cursor: pointer; } .card-style .jvm-zoom-btn.jvm-zoomin { bottom: 70px; } .card-style .dropdown-toggle { border: none; background: none; } .card-style .dropdown-toggle::after { display: none; } .card-style .dropdown-menu { -webkit-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.07); -moz-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.07); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.07); } .card-style .dropdown-menu li:hover a { color: #4a6cf7 !important; } .card-style .dropdown-menu li a { display: block; font-size: 14px; } /* ======= Border Radius ========= */ .radius-4 { border-radius: 4px; } .radius-10 { border-radius: 10px; } .radius-30 { border-radius: 30px; } .radius-50 { border-radius: 50px; } .radius-full { border-radius: 50%; } .scroll-top { width: 45px; height: 45px; background: #4a6cf7; display: none; justify-content: center; align-items: center; font-size: 18px; color: #fff; border-radius: 5px; position: fixed; bottom: 30px; right: 30px; z-index: 9; cursor: pointer; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .scroll-top:hover { color: #fff; background: rgba(74, 108, 247, 0.8); } .form-control:focus { box-shadow: none; } .form-control.is-valid:focus, .was-validated .form-control:valid:focus, .form-control.is-invalid:focus, .was-validated .form-control:invalid:focus, .form-check-input.is-valid:focus, .was-validated .form-check-input:valid:focus, .form-check-input.is-invalid:focus, .was-validated .form-check-input:invalid:focus, .form-check-input:focus, .radio-style.radio-success .form-check-input:focus, .radio-style.radio-warning .form-check-input:focus, .radio-style.radio-danger .form-check-input:focus { box-shadow: none; } .hover-underline:hover { text-decoration: underline; } /* ============= typography css ============= */ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { color: #262d3f; margin: 0; } h1, .h1 { font-size: 32px; font-weight: 700; } h2, .h2 { font-size: 28px; font-weight: 600; } h3, .h3 { font-size: 24px; font-weight: 500; } h4, .h4 { font-size: 20px; font-weight: 600; } h5, .h5 { font-size: 16px; font-weight: 700; } h6, .h6 { font-size: 16px; font-weight: 600; } .text-bold { font-weight: 700; } .text-semi-bold { font-weight: 600; } .text-medium { font-weight: 500; } .text-regular { font-weight: 400; } .text-light { font-weight: 300; } .text-sm { font-size: 14px; line-height: 22px; } /* ========== breadcrumb ============ */ .breadcrumb-wrapper { display: flex; justify-content: flex-end; } @media (max-width: 767px) { .breadcrumb-wrapper { justify-content: flex-start; } } .breadcrumb-wrapper .breadcrumb li { font-size: 14px; color: #4a6cf7; } .breadcrumb-wrapper .breadcrumb li a { color: #5d657b; } .breadcrumb-wrapper .breadcrumb li a:hover { color: #4a6cf7; } /* ========== Buttons css ========== */ /* buttons base styles */ .main-btn { display: inline-block; text-align: center; white-space: nowrap; vertical-align: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 15px 45px; font-weight: 500; font-size: 14px; line-height: 24px; border-radius: 4px; cursor: pointer; z-index: 5; transition: all 0.4s ease-in-out; border: 1px solid transparent; overflow: hidden; } .main-btn:hover { color: inherit; } .btn-sm { padding: 10px 20px; font-weight: 400; } /* buttons hover effect */ .btn-hover { position: relative; overflow: hidden; } .btn-hover::after { content: ""; position: absolute; width: 0%; height: 0%; border-radius: 50%; background: rgba(255, 255, 255, 0.05); top: 50%; left: 50%; padding: 50%; z-index: -1; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; -webkit-transform: translate3d(-50%, -50%, 0) scale(0); -moz-transform: translate3d(-50%, -50%, 0) scale(0); -ms-transform: translate3d(-50%, -50%, 0) scale(0); -o-transform: translate3d(-50%, -50%, 0) scale(0); transform: translate3d(-50%, -50%, 0) scale(0); } .btn-hover:hover::after { -webkit-transform: translate3d(-50%, -50%, 0) scale(1.3); -moz-transform: translate3d(-50%, -50%, 0) scale(1.3); -ms-transform: translate3d(-50%, -50%, 0) scale(1.3); -o-transform: translate3d(-50%, -50%, 0) scale(1.3); transform: translate3d(-50%, -50%, 0) scale(1.3); } /* primary buttons */ .primary-btn { background: #4a6cf7; color: #fff; } .primary-btn:hover { color: #fff; } .primary-btn-outline { background: transparent; color: #4a6cf7; border-color: #4a6cf7; } .primary-btn-outline:hover { color: #fff; background: #4a6cf7; } /* secondary buttons */ .secondary-btn { background: #00c1f8; color: #fff; } .secondary-btn:hover { color: #fff; } .secondary-btn-outline { background: transparent; color: #00c1f8; border-color: #00c1f8; } .secondary-btn-outline:hover { color: #fff; background: #00c1f8; } /* success buttons */ .success-btn { background: #219653; color: #fff; } .success-btn:hover { color: #fff; } .success-btn-outline { background: transparent; color: #219653; border-color: #219653; } .success-btn-outline:hover { color: #fff; background: #219653; } /* danger buttons */ .danger-btn { background: #d50100; color: #fff; } .danger-btn:hover { color: #fff; } .danger-btn-outline { background: transparent; color: #d50100; border-color: #d50100; } .danger-btn-outline:hover { color: #fff; background: #d50100; } /* warning buttons */ .warning-btn { background: #f7c800; color: #fff; } .warning-btn:hover { color: #fff; } .warning-btn-outline { background: transparent; color: #f7c800; border-color: #f7c800; } .warning-btn-outline:hover { color: #fff; background: #f7c800; } /* info buttons */ .info-btn { background: #97ca31; color: #fff; } .info-btn:hover { color: #fff; } .info-btn-outline { background: transparent; color: #97ca31; border-color: #97ca31; } .info-btn-outline:hover { color: #fff; background: #97ca31; } /* dark buttons */ .dark-btn { background: #262d3f; color: #fff; } .dark-btn:hover { color: #fff; } .dark-btn-outline { background: transparent; color: #262d3f; border-color: #262d3f; } .dark-btn-outline:hover { color: #fff; background: #262d3f; } /* light buttons */ .light-btn { background: #efefef; color: #262d3f; } .light-btn:hover { color: #262d3f; } .light-btn-outline { background: transparent; color: #262d3f; border-color: #efefef; } .light-btn-outline:hover { color: #262d3f; background: #efefef; } /* active buttons */ .active-btn { background: #4a6cf7; color: #fff; } .active-btn:hover { color: #fff; } .active-btn-outline { background: transparent; color: #4a6cf7; border-color: #4a6cf7; } .active-btn-outline:hover { color: #fff; background: #4a6cf7; } /* deactive buttons */ .deactive-btn { background: #cbe1ff; color: #4a6cf7; } .deactive-btn:hover { color: #4a6cf7; } .deactive-btn-outline { background: transparent; color: #4a6cf7; border-color: #cbe1ff; } .deactive-btn-outline:hover { color: #4a6cf7; background: #cbe1ff; } /* ========= square-btn ========= */ .square-btn { border-radius: 0px; } /* ========= rounded-md ========= */ .rounded-md { border-radius: 10px; } /* ========= rounded-full ========= */ .rounded-full { border-radius: 30px; } /* ========== buttons group css ========= */ .buttons-group { display: flex; flex-wrap: wrap; margin: 0 -10px; } .buttons-group li { margin: 10px; } /* ====== Status Button ====== */ .status-btn { padding: 7px 15px; border-radius: 30px; font-size: 14px; font-weight: 400; } .status-btn.primary-btn { color: #fff; background: #4a6cf7; } .status-btn.active-btn { color: #4a6cf7; background: rgba(74, 108, 247, 0.1); } .status-btn.close-btn { color: #d50100; background: rgba(213, 1, 0, 0.1); } .status-btn.warning-btn { color: #f7c800; background: rgba(247, 200, 0, 0.1); } .status-btn.info-btn { color: #97ca31; background: rgba(151, 202, 49, 0.1); } .status-btn.success-btn { color: #219653; background: rgba(33, 150, 83, 0.1); } .status-btn.secondary-btn { color: #00c1f8; background: rgba(0, 193, 248, 0.1); } .status-btn.dark-btn { color: #262d3f; background: rgba(38, 45, 63, 0.1); } .status-btn.orange-btn { color: #f2994a; background: rgba(242, 153, 74, 0.1); } /* ============ alerts css ============ */ .alert-box { display: flex; position: relative; margin-bottom: 20px; } @media (max-width: 767px) { .alert-box { padding-left: 0px !important; } } .alert-box .left { max-width: 75px; width: 100%; height: 100%; border-radius: 4px; background: #d50100; position: absolute; left: 0; top: 0; display: flex; justify-content: center; align-items: center; } @media (max-width: 767px) { .alert-box .left { display: none; } } .alert-box .left h5 { -webkit-transform: rotate(-90deg); -moz-transform: rotate(-90deg); -ms-transform: rotate(-90deg); -o-transform: rotate(-90deg); transform: rotate(-90deg); color: #fff; } .alert-box .alert { margin-bottom: 0px; padding: 25px 40px; } @media (max-width: 767px) { .alert-box .alert { padding: 20px; } } /* Alert Primary */ .primary-alert .left { background: #4a6cf7; } .primary-alert .alert { color: #4a6cf7; border: 1px solid #4a6cf7; background: rgba(74, 108, 247, 0.2); width: 100%; } .primary-alert .alert .alert-heading { color: #4a6cf7; margin-bottom: 15px; } /* Alert Danger */ .danger-alert .left { background: #d50100; } .danger-alert .alert { color: #d50100; border: 1px solid #d50100; background: rgba(213, 1, 0, 0.2); width: 100%; } .danger-alert .alert .alert-heading { color: #d50100; margin-bottom: 15px; } /* Alert warning */ .warning-alert .left { background: #f7c800; } .warning-alert .alert { color: #f7c800; border: 1px solid #f7c800; background: rgba(247, 200, 0, 0.2); width: 100%; } .warning-alert .alert .alert-heading { color: #f7c800; margin-bottom: 15px; } /* Alert warning */ .warning-alert .left { background: #f7c800; } .warning-alert .alert { color: #f7c800; border: 1px solid #f7c800; background: rgba(247, 200, 0, 0.2); width: 100%; } .warning-alert .alert .alert-heading { color: #f7c800; margin-bottom: 15px; } /* Alert info */ .info-alert .left { background: #97ca31; } .info-alert .alert { color: #97ca31; border: 1px solid #97ca31; background: rgba(151, 202, 49, 0.2); width: 100%; } .info-alert .alert .alert-heading { color: #97ca31; margin-bottom: 15px; } /* Alert success */ .success-alert .left { background: #219653; } .success-alert .alert { color: #219653; border: 1px solid #219653; background: rgba(33, 150, 83, 0.2); width: 100%; } .success-alert .alert .alert-heading { color: #219653; margin-bottom: 15px; } /* Alert secondary */ .secondary-alert .left { background: #00c1f8; } .secondary-alert .alert { color: #00c1f8; border: 1px solid #00c1f8; background: rgba(0, 193, 248, 0.2); width: 100%; } .secondary-alert .alert .alert-heading { color: #00c1f8; margin-bottom: 15px; } /* Alert gray */ .gray-alert .left { background: #5d657b; } .gray-alert .alert { color: #5d657b; border: 1px solid #5d657b; background: rgba(93, 101, 123, 0.2); width: 100%; } .gray-alert .alert .alert-heading { color: #5d657b; margin-bottom: 15px; } /* Alert black */ .black-alert .left { background: #000; } .black-alert .alert { color: #000; border: 1px solid #000; background: rgba(0, 0, 0, 0.2); width: 100%; } .black-alert .alert .alert-heading { color: #000; margin-bottom: 15px; } /* Alert orange */ .orange-alert .left { background: #f2994a; } .orange-alert .alert { color: #f2994a; border: 1px solid #f2994a; background: rgba(242, 153, 74, 0.2); width: 100%; } .orange-alert .alert .alert-heading { color: #f2994a; margin-bottom: 15px; } /* ========== cards css =========== */ /* card-style-1 */ .card-style-1 { background: #fff; border: 1px solid #efefef; border-radius: 10px; padding: 25px 0; position: relative; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .card-style-1:hover { -webkit-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); -moz-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); } .card-style-1 .card-meta { display: flex; align-items: center; margin-bottom: 15px; padding: 0 30px; } @media (max-width: 767px) { .card-style-1 .card-meta { padding: 0 20px; } } .card-style-1 .card-meta .image { max-width: 40px; width: 100%; border-radius: 50%; overflow: hidden; margin-right: 12px; } .card-style-1 .card-meta .image img { width: 100%; } .card-style-1 .card-meta .text p { color: #262d3f; } .card-style-1 .card-meta .text p a { color: inherit; } .card-style-1 .card-meta .text p a:hover { color: #4a6cf7; } .card-style-1 .card-image { border-radius: 10px; margin-bottom: 25px; overflow: hidden; } .card-style-1 .card-image a { display: block; } .card-style-1 .card-image img { width: 100%; } .card-style-1 .card-content { padding: 0px 30px; } @media (max-width: 767px) { .card-style-1 .card-content { padding: 0px 20px; } } .card-style-1 .card-content h4 a { color: inherit; margin-bottom: 15px; display: block; } .card-style-1 .card-content h4 a:hover { color: #4a6cf7; } /* card-style-2 */ .card-style-2 { background: #fff; border: 1px solid #efefef; border-radius: 4px; padding: 20px; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .card-style-2:hover { -webkit-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); -moz-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); } .card-style-2 .card-image { border-radius: 4px; margin-bottom: 30px; overflow: hidden; } .card-style-2 .card-image a { display: block; } .card-style-2 .card-image img { width: 100%; } .card-style-2 .card-content { padding: 0px 10px; } @media (max-width: 767px) { .card-style-2 .card-content { padding: 0px; } } .card-style-2 .card-content h4 a { color: inherit; margin-bottom: 15px; display: block; } .card-style-2 .card-content h4 a:hover { color: #4a6cf7; } /* card-style-3 */ .card-style-3 { background: #fff; border: 1px solid #efefef; border-radius: 4px; padding: 25px 30px; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .card-style-3:hover { -webkit-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); -moz-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); } .card-style-3 .card-content h4 a { color: inherit; margin-bottom: 15px; display: block; } .card-style-3 .card-content h4 a:hover { color: #4a6cf7; } .card-style-3 .card-content a.read-more { font-weight: 500; color: #262d3f; margin-top: 20px; } .card-style-3 .card-content a.read-more:hover { color: #4a6cf7; letter-spacing: 2px; } /* ======= icon-card ======== */ .icon-card { display: flex; align-items: center; background: #fff; padding: 30px 20px; border: 1px solid #e2e8f0; box-shadow: 0px 10px 20px rgba(200, 208, 216, 0.3); border-radius: 10px; } .icon-card.icon-card-3 { display: block; padding: 0px; } .icon-card.icon-card-3 .card-content { display: flex; padding: 20px; padding-bottom: 0; } @media only screen and (min-width: 1200px) and (max-width: 1399px) { .icon-card h6 { font-size: 15px; } } @media only screen and (min-width: 1200px) and (max-width: 1399px) { .icon-card h3 { font-size: 20px; } } .icon-card.icon-card-2 { display: block; } .icon-card.icon-card-2 .progress { height: 7px; } .icon-card.icon-card-2 .progress .progress-bar { border-radius: 4px; } .icon-card .icon { max-width: 46px; width: 100%; height: 46px; border-radius: 10px; display: flex; justify-content: center; align-items: center; font-size: 24px; margin-right: 20px; background: rgba(74, 108, 247, 0.1); color: #4a6cf7; font-weight: 700; } @media only screen and (min-width: 1200px) and (max-width: 1399px) { .icon-card .icon { margin-right: 10px; } } .icon-card .icon.purple { background: rgba(155, 81, 224, 0.1); color: #9b51e0; } .icon-card .icon.success { background: rgba(33, 150, 83, 0.1); color: #219653; } .icon-card .icon.primary { background: rgba(74, 108, 247, 0.1); color: #4a6cf7; } .icon-card .icon.orange { background: rgba(242, 153, 74, 0.1); color: #f2994a; } .icon-card .icon.opacity-100.purple { background: #9b51e0; color: #fff; } .icon-card .icon.opacity-100.success { background: #219653; color: #fff; } .icon-card .icon.opacity-100.primary { background: #4a6cf7; color: #fff; } .icon-card .icon.opacity-100.orange { background: #f2994a; color: #fff; } .icon-card .icon.opacity-100.deep-blue { background: #345d9d; color: #fff; } /* =========== tables css =========== */ .table { border-collapse: inherit; border-spacing: 0px; } .table > :not(caption) > * > * { padding: 15px 0; border-bottom-color: #efefef; vertical-align: middle; } .table > :not(:last-child) > :last-child > * { border-bottom-color: #efefef; } .table tbody tr:first-child > * { padding-top: 20px; } .table tbody tr:last-child > * { border-bottom-color: transparent; padding-bottom: 0px; } .table th h6 { font-weight: 500; color: #262d3f; font-size: 14px; } .table td.min-width { padding: 5px; } @media (max-width: 767px) { .table td.min-width { min-width: 150px; } } .table td p { font-size: 14px; line-height: 1.5; color: #5d657b; } .table td p a { color: inherit; } .table td p a:hover { color: #4a6cf7; } .table .lead-info { min-width: 200px; } .table .lead-email { min-width: 150px; white-space: nowrap; } .table .lead-phone { min-width: 160px; } .table .lead-company { min-width: 180px; } .table .referrals-image { min-width: 150px; } .table .referrals-image .image { width: 55px; max-width: 100%; height: 55px; border-radius: 4px; overflow: hidden; } .table .referrals-image .image img { width: 100%; } .table .lead { display: flex; align-items: center; } .table .lead .lead-image { max-width: 50px; width: 100%; height: 50px; border-radius: 50%; overflow: hidden; margin-right: 15px; } .table .lead .lead-image img { width: 100%; } .table .lead .lead-text { width: 100%; } .table .employee-image { width: 50px; max-width: 100%; height: 50px; border-radius: 50%; overflow: hidden; margin-right: 15px; } .table .employee-image img { width: 100%; } .table .action { display: flex; align-items: center; } .table .action button { border: none; background: transparent; padding: 0px 6px; font-size: 18px; } .table .action button.edit:hover { color: #4a6cf7; } .table .action button::after { display: none; } .table .action .dropdown-menu { -webkit-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.07); -moz-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.07); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.07); } .table .action .dropdown-menu li:hover a { color: #4a6cf7 !important; } .table .action .dropdown-menu li a { display: block; } .top-selling-table tr th, .top-selling-table tr td { vertical-align: middle; padding: 10px 5px; } .top-selling-table tr .min-width { min-width: 80px; white-space: nowrap; } .top-selling-table .form-check-input[type="checkbox"] { margin-left: 5px; } .top-selling-table .product { display: flex; align-items: center; min-width: 150px; } .top-selling-table .product .image { border-radius: 4px; overflow: hidden; margin-right: 15px; max-width: 50px; width: 100%; height: 50px; } .top-selling-table .product .image img { width: 100%; } .top-selling-table .product p { width: 100%; } @media (max-width: 767px) { .referrals-table-card .title .right { width: 100%; } } @media only screen and (min-width: 550px) and (max-width: 767px) { .referrals-table-card .title .right { width: auto; } } .referrals-table-card .referrals-table td { padding: 10px; } /* ===== lead-table ===== */ .lead-table th, .lead-table td { padding: 10px 5px; } .lead-table .name { min-width: 120px; } .lead-table .email { min-width: 130px; } .lead-table .project { min-width: 150px; } .lead-table .status { min-width: 120px; text-align: center; } .lead-table .action { min-width: 60px; } .clients-table-card .table .employee-info { min-width: 150px; } .clients-table th, .clients-table td { padding: 5px; } .clients-table th.min-width, .clients-table td.min-width { min-width: 150px; } .clients-table .employee-image { margin-right: 0px; } /* =========== form elements css ========== */ /* ===== input style ===== */ .input-style-1 { position: relative; margin-bottom: 30px; } .input-style-1 label { font-size: 14px; font-weight: 500; color: #262d3f; display: block; margin-bottom: 10px; } .input-style-1 input, .input-style-1 textarea { width: 100%; background: rgba(239, 239, 239, 0.5); border: 1px solid #e5e5e5; border-radius: 4px; padding: 16px; color: #5d657b; resize: none; transition: all 0.3s; } .input-style-1 input:focus, .input-style-1 textarea:focus { border-color: #4a6cf7; background: #fff; } .input-style-1 input[type="date"], .input-style-1 input[type="time"], .input-style-1 textarea[type="date"], .input-style-1 textarea[type="time"] { background: transparent; } .input-style-2 { position: relative; margin-bottom: 30px; z-index: 1; } .input-style-2 label { font-size: 14px; font-weight: 500; color: #262d3f; display: block; margin-bottom: 10px; } .input-style-2 input, .input-style-2 textarea { width: 100%; background: rgba(239, 239, 239, 0.5); border: 1px solid #e5e5e5; border-radius: 4px; padding: 16px; color: #5d657b; resize: none; transition: all 0.3s; } .input-style-2 input:focus, .input-style-2 textarea:focus { border-color: #4a6cf7; background: #fff; } .input-style-2 input[type="date"], .input-style-2 input[type="time"], .input-style-2 textarea[type="date"], .input-style-2 textarea[type="time"] { background: transparent; } .input-style-2 input[type="date"]::-webkit-inner-spin-button, .input-style-2 input[type="date"]::-webkit-calendar-picker-indicator { opacity: 0; } .input-style-2 input[type="date"] ~ .icon { z-index: -1; } .input-style-2 .icon { position: absolute; right: 0; bottom: 0; padding: 17px; } .input-style-3 { position: relative; margin-bottom: 30px; } .input-style-3 label { font-size: 14px; font-weight: 500; color: #262d3f; display: block; margin-bottom: 10px; } .input-style-3 input, .input-style-3 textarea { width: 100%; background: rgba(239, 239, 239, 0.5); border: 1px solid #e5e5e5; border-radius: 4px; padding: 16px; padding-left: 45px; color: #5d657b; resize: none; transition: all 0.3s; } .input-style-3 input:focus, .input-style-3 textarea:focus { border-color: #4a6cf7; background: #fff; } .input-style-3 .icon { position: absolute; left: 0; top: 0; height: 100%; padding: 16px; } /* ========= select style ========== */ .select-style-1 { margin-bottom: 30px; } .select-style-1 label { font-size: 14px; font-weight: 500; color: #262d3f; display: block; margin-bottom: 10px; } .select-style-1 .select-position { position: relative; } .select-style-1 .select-position::after { border-bottom: 2px solid #5d657b; border-right: 2px solid #5d657b; content: ""; display: block; height: 10px; width: 10px; margin-top: -5px; pointer-events: none; position: absolute; right: 16px; top: 50%; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out; } .select-style-1 .select-position.select-sm::after { margin-top: -8px; } .select-style-1 .select-position.select-sm select { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } .select-style-1 .select-position select { width: 100%; background: transparent; border: 1px solid #e5e5e5; border-radius: 10px; padding: 16px; padding-right: 38px; color: #5d657b; appearance: none; -webkit-appearance: none; -moz-appearance: none; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .select-style-1 .select-position select:focus { border-color: #4a6cf7; outline: none; } .select-style-1 .select-position select.light-bg { background: rgba(239, 239, 239, 0.5); } .select-style-1 .select-position select.light-bg:focus { background: #fff; } .select-style-1 .select-position select.radius-30 { border-radius: 30px; } .select-style-2 { margin-bottom: 30px; } .select-style-2 .select-position { position: relative; } .select-style-2 .select-position.select-sm::after { margin-top: -8px; } .select-style-2 .select-position.select-sm::before { margin-top: 0; } .select-style-2 .select-position.select-sm select { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } .select-style-2 .select-position::before, .select-style-2 .select-position::after { content: ""; display: block; height: 8px; width: 8px; pointer-events: none; position: absolute; right: 16px; top: 50%; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out; } .select-style-2 .select-position::before { margin-top: 0px; border-bottom: 1px solid #5d657b; border-right: 1px solid #5d657b; } .select-style-2 .select-position::after { margin-top: -8px; border-top: 1px solid #5d657b; border-left: 1px solid #5d657b; } .select-style-2 .select-position select { width: 100%; background: transparent; border: 1px solid #e5e5e5; border-radius: 10px; padding: 16px; padding-right: 38px; color: #5d657b; appearance: none; -webkit-appearance: none; -moz-appearance: none; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .select-style-2 .select-position select:focus { border-color: #4a6cf7; outline: none; } .select-style-2 .select-position select.light-bg { background: rgba(239, 239, 239, 0.5); } .select-style-2 .select-position select.light-bg:focus { background: #fff; } .select-style-2 .select-position select.select-sm { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } .select-style-3 { margin-bottom: 30px; } .select-style-3 .select-position { position: relative; } .select-style-3 .select-position::after { border-bottom: 2px solid #5d657b; border-right: 2px solid #5d657b; content: ""; display: block; height: 10px; width: 10px; margin-top: -7px; pointer-events: none; position: absolute; right: 0px; top: 50%; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out; } .select-style-3 .select-position.select-sm::after { margin-top: -8px; } .select-style-3 .select-position.select-sm select { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } .select-style-3 .select-position select { width: 100%; background: transparent; border: transparent; border-radius: 10px; padding-right: 38px; color: #000; appearance: none; -webkit-appearance: none; -moz-appearance: none; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .select-style-3 .select-position select:focus { border-color: #4a6cf7; outline: none; } .select-style-3 .select-position select.light-bg { background: rgba(239, 239, 239, 0.5); } .toggle-switch { padding-left: 60px; min-height: 30px; } .toggle-switch .form-check-input { width: 50px; height: 28px; margin-left: -60px; cursor: pointer; } .toggle-switch label { margin-top: 6px; font-size: 14px; color: #262d3f; cursor: pointer; user-select: none; } .checkbox-style { padding-left: 40px; min-height: 28px; } .checkbox-style .form-check-input { width: 28px; height: 28px; border-radius: 4px; margin-left: -40px; cursor: pointer; } .checkbox-style .form-check-input:disabled { cursor: auto; } .checkbox-style .form-check-input:disabled ~ label { cursor: auto; } .checkbox-style label { margin-top: 6px; cursor: pointer; user-select: none; } .checkbox-style.checkbox-success .form-check-input:checked { background-color: #219653; border-color: #219653; } .checkbox-style.checkbox-warning .form-check-input:checked { background-color: #f7c800; border-color: #f7c800; } .checkbox-style.checkbox-danger .form-check-input:checked { background-color: #d50100; border-color: #d50100; } .radio-style { padding-left: 40px; min-height: 28px; } .radio-style .form-check-input { width: 28px; height: 28px; border-radius: 50%; margin-left: -40px; cursor: pointer; } .radio-style .form-check-input:disabled { cursor: auto; } .radio-style .form-check-input:disabled ~ label { cursor: auto; } .radio-style label { margin-top: 6px; cursor: pointer; user-select: none; } .radio-style.radio-success .form-check-input:checked { background-color: #219653; border-color: #219653; } .radio-style.radio-warning .form-check-input:checked { background-color: #f7c800; border-color: #f7c800; } .radio-style.radio-danger .form-check-input:checked { background-color: #d50100; border-color: #d50100; } @media (max-width: 767px) { .button-group .main-btn { width: 100%; } } .buy-sell-form .input-group { display: flex; } .buy-sell-form .input-group input { width: 60%; background: transparent; border: 1px solid #e2e8f0; border-radius: 4px; padding: 8px 16px; font-size: 14px; color: #5d657b; } .buy-sell-form .input-group input:focus { border-color: #4a6cf7; } .buy-sell-form .input-group .select-style-1 { width: 40%; } .buy-sell-form .input-group .select-style-1 .select-position::after { width: 8px; height: 8px; } .buy-sell-form .input-group select { border: 1px solid #e2e8f0; border-radius: 0px 4px 4px 0px; padding: 8px 16px; padding-right: 24px; font-size: 14px; color: #5d657b; } .buy-sell-form .buy-sell-btn .main-btn { display: block; width: 100%; font-weight: 500; } .buy-sell-form .buy-sell-btn .main-btn:hover { box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.1); } .buy-sell-form .buy-sell-btn .main-btn.success-btn { background: #08c18d; } .buy-sell-form .buy-sell-btn .main-btn.danger-btn { background: #eb5757; } .buy-sell-form .field-group-2 label { font-size: 12px; } .buy-sell-form .field-group-2 .input-group input { font-size: 12px; width: 70%; } .buy-sell-form .field-group-2 .input-group span { font-size: 12px; padding: 8px 16px; width: 30%; background: #e2e8f0; text-align: center; border-radius: 0px 4px 4px 0px; border: 1px solid #e2e8f0; } .buy-sell-form .input-group-2 label { font-size: 12px; color: #5d657b; margin-bottom: 8px; display: block; } .buy-sell-form .input-group-2 .select-position::after { width: 8px; height: 8px; } .buy-sell-form .input-group-2 select { padding: 8px 12px; font-size: 12px; color: #5d657b; border: 1px solid #e2e8f0; border-radius: 4px; width: 100%; } /* ============= notification css ============= */ .single-notification { display: flex; justify-content: space-between; align-items: flex-start; padding: 20px 0; border-bottom: 1px solid #efefef; } .single-notification.readed { opacity: 0.7; } .single-notification:first-child { padding-top: 0px; } .single-notification:last-child { padding-bottom: 0px; border-bottom: 0px; } .single-notification .checkbox { max-width: 50px; width: 100%; padding-top: 10px; } @media (max-width: 767px) { .single-notification .checkbox { display: none; } } .single-notification .checkbox input { background-color: #efefef; border-color: #e5e5e5; } .single-notification .checkbox input:checked { background-color: #4a6cf7; border-color: #4a6cf7; } .single-notification .notification { display: flex; width: 100%; } @media (max-width: 767px) { .single-notification .notification { flex-direction: column; } } .single-notification .notification .image { max-width: 50px; width: 100%; height: 50px; border-radius: 50%; overflow: hidden; color: #fff; display: flex; justify-content: center; align-items: center; font-weight: 600; margin-right: 15px; } @media (max-width: 767px) { .single-notification .notification .image { margin-bottom: 15px; } } .single-notification .notification .image img { width: 100%; } .single-notification .notification .content { display: block; max-width: 800px; } .single-notification .notification .content h6 { margin-bottom: 15px; } .single-notification .notification .content p { margin-bottom: 10px; } .single-notification .action { display: inline-flex; justify-content: flex-end; padding-top: 10px; } @media (max-width: 767px) { .single-notification .action { display: none; } } .single-notification .action button { border: none; background: transparent; color: #5d657b; margin-left: 20px; font-size: 18px; } .single-notification .action button.delete-btn:hover { color: #d50100; } .single-notification .action .dropdown-toggle::after { display: none; } /* ========== header css ========== */ .header { padding: 30px 0; background: #fff; } .header .header-left .menu-toggle-btn .main-btn { padding: 0px 15px; height: 46px; line-height: 46px; border-radius: 10px; } .header .header-left .header-search form { max-width: 270px; position: relative; } .header .header-left .header-search form input { width: 100%; border: 1px solid #efefef; background: rgba(239, 239, 239, 0.5); border-radius: 10px; height: 46px; padding-left: 44px; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .header .header-left .header-search form input:focus { border-color: #4a6cf7; background: #fff; } .header .header-left .header-search form button { position: absolute; border: none; background: transparent; left: 16px; top: 0; height: 46px; color: #5d657b; font-weight: 700; } .header .header-right { display: flex; justify-content: flex-end; } .header .header-right button { border: 1px solid #efefef; background: rgba(239, 239, 239, 0.5); border-radius: 10px; height: 46px; width: 46px; display: flex; justify-content: center; align-items: center; position: relative; } .header .header-right button::after { display: none; } .header .header-right button span { position: absolute; width: 20px; height: 20px; background: #4a6cf7; color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; top: -8px; right: -6px; font-size: 12px; font-weight: 500; } .header .header-right .dropdown-menu { width: 350px; border: 1px solid #efefef; padding: 10px 10px; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; top: 24px !important; right: 0; position: absolute; transform: translate3d(0px, 60px, 0px); border-radius: 10px; } .header .header-right .dropdown-menu li { padding: 3px 0px; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; border-bottom: 1px solid #efefef; position: relative; z-index: 2; } .header .header-right .dropdown-menu li:hover a { color: #4a6cf7; background: rgba(74, 108, 247, 0.05); } .header .header-right .dropdown-menu li:last-child { border-bottom: none; } .header .header-right .dropdown-menu li a, .header .header-right .dropdown-menu li span { padding: 8px 12px; display: flex; color: rgba(0, 0, 0, 0.7); border-radius: 6px; } .header .header-right .dropdown-menu li span { font-size: 14px; } .header .header-right .dropdown-menu li a .image { max-width: 35px; width: 100%; height: 35px; border-radius: 50%; overflow: hidden; margin-right: 12px; } .header .header-right .dropdown-menu li a .image img { width: 100%; } .header .header-right .dropdown-menu li a .content { width: 100%; } .header .header-right .dropdown-menu li a .content h6 { font-size: 14px; margin-bottom: 5px; font-weight: 600; line-height: 1; } .header .header-right .dropdown-menu li a .content p { font-size: 14px; color: rgba(0, 0, 0, 0.7); margin-bottom: 0px; line-height: 1.4; } .header .header-right .dropdown-menu li a .content span { font-size: 12px; color: rgba(0, 0, 0, 0.5); } .header .header-right .dropdown-box { position: relative; } .header .header-right .notification-box, .header .header-right .header-message-box { position: relative; } .header .header-right .notification-box .dropdown-menu.dropdown-menu-end { transform: translate3d(0px, 60px, 0px); } .header .header-right .header-message-box .dropdown-menu.dropdown-menu-end { transform: translate3d(0px, 60px, 0px); } .header .header-right .profile-box { display: flex; position: relative; } .header .header-right .profile-box button { width: auto; } .header .header-right .profile-box .dropdown-menu { width: 230px; } .header .header-right .profile-box .dropdown-menu.dropdown-menu-end { transform: translate3d(0px, 60px, 0px); } .header .header-right .profile-box .dropdown-menu li { border-bottom: none; } .header .header-right .profile-box .dropdown-menu li a { font-size: 14px; display: flex; align-items: center; } .header .header-right .profile-box .dropdown-menu li a i { margin-right: 15px; font-weight: 700; } .header .header-right .profile-box .profile-info { margin: 0 5px; } .header .header-right .profile-box .profile-info .info { display: flex; align-items: center; } .header .header-right .profile-box .profile-info .info .image { border: 2px solid #f9f9f9; -webkit-box-shadow: 0px 21px 25px rgba(218, 223, 227, 0.8); -moz-box-shadow: 0px 21px 25px rgba(218, 223, 227, 0.8); box-shadow: 0px 21px 25px rgba(218, 223, 227, 0.8); width: 46px; height: 46px; border-radius: 50%; margin-left: 16px; position: relative; } .header .header-right .profile-box .profile-info .info .image .status { width: 16px; height: 16px; border-radius: 50%; border: 2px solid #e5e5e5; background: #219653; position: absolute; bottom: 0; right: 0; top: auto; } .header .header-right .profile-box .profile-info .info .image img { width: 100%; border-radius: 50%; } /* ========== Dashboards css ================= */ @media (max-width: 767px) { #doughnutChart1 { height: 250px !important; } } .legend3 li { margin-right: 25px; } .legend3 li div { white-space: nowrap; } .legend3 li .bg-color { position: relative; margin-left: 12px; border-radius: 50%; } .legend3 li .bg-color::after { content: ""; position: absolute; width: 12px; height: 12px; border-radius: 50%; background: inherit; left: -12px; top: 5px; } .legend3 li .text { margin-left: 10px; } .legend3 li .text p { display: flex; align-items: center; width: 100%; } .todo-list-wrapper ul li.todo-list-item { position: relative; display: flex; align-items: center; justify-content: space-between; padding-left: 20px; margin-bottom: 25px; } .todo-list-wrapper ul li.todo-list-item:last-child { margin-bottom: 0px; } .todo-list-wrapper ul li.todo-list-item::before { content: ""; position: absolute; left: 0; top: 0; width: 4px; height: 100%; } @media (max-width: 767px) { .todo-list-wrapper ul li.todo-list-item { display: block; } .todo-list-wrapper ul li.todo-list-item .todo-status { margin-top: 20px; } } .todo-list-wrapper ul li.todo-list-item.success::before { background: #219653; } .todo-list-wrapper ul li.todo-list-item.primary::before { background: #4a6cf7; } .todo-list-wrapper ul li.todo-list-item.orange::before { background: #f2994a; } .todo-list-wrapper ul li.todo-list-item.danger::before { background: #d50100; } /* ============ signin css ============= */ .auth-row { background: #fff; border-radius: 4px; overflow: hidden; } .auth-cover-wrapper { display: flex; align-items: center; justify-content: center; padding: 45px; position: relative; z-index: 1; height: 100%; } @media (max-width: 767px) { .auth-cover-wrapper { padding: 30px 20px; } } .auth-cover-wrapper .auth-cover .title { text-align: cover; margin-bottom: 40px; } @media (max-width: 767px) { .auth-cover-wrapper .auth-cover .title h1 { font-size: 24px; } } .auth-cover-wrapper .auth-cover .cover-image { max-width: 100%; margin: auto; } .auth-cover-wrapper .auth-cover .cover-image img { width: 100%; } .auth-cover-wrapper .auth-cover .shape-image { position: absolute; z-index: -1; right: 0; bottom: 5%; } .signin-wrapper { background: #fff; padding: 60px; min-height: 600px; display: flex; align-items: center; justify-content: center; } @media only screen and (min-width: 992px) and (max-width: 1199px) { .signin-wrapper { padding: 40px; } } @media (max-width: 767px) { .signin-wrapper { padding: 30px; } } .signin-wrapper .form-wrapper { width: 100%; } .signin-wrapper .singin-option button { font-size: 16px; font-weight: 600; } @media only screen and (min-width: 1200px) and (max-width: 1399px) { .signin-wrapper .singin-option button { padding-left: 25px; padding-right: 25px; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { .signin-wrapper .singin-option button { padding-left: 30px; padding-right: 30px; } } @media (max-width: 767px) { .signin-wrapper .singin-option button { width: 100%; } } @media only screen and (min-width: 550px) and (max-width: 767px) { .signin-wrapper .singin-option button { width: auto; } } .signin-wrapper .singin-option a:hover { text-decoration: underline; } /* ============ signup css ============= */ .auth-row { background: #fff; border-radius: 4px; overflow: hidden; } .auth-cover-wrapper { display: flex; align-items: center; justify-content: center; padding: 45px; position: relative; z-index: 1; height: 100%; } @media (max-width: 767px) { .auth-cover-wrapper { padding: 30px 20px; } } .auth-cover-wrapper .auth-cover .title { text-align: cover; margin-bottom: 40px; } @media (max-width: 767px) { .auth-cover-wrapper .auth-cover .title h1 { font-size: 24px; } } .auth-cover-wrapper .auth-cover .cover-image { max-width: 100%; margin: auto; } .auth-cover-wrapper .auth-cover .cover-image img { width: 100%; } .auth-cover-wrapper .auth-cover .shape-image { position: absolute; z-index: -1; right: 0; bottom: 5%; } .signup-wrapper { background: #fff; padding: 60px; min-height: 600px; display: flex; align-items: center; justify-content: center; } @media only screen and (min-width: 992px) and (max-width: 1199px) { .signup-wrapper { padding: 40px; } } @media (max-width: 767px) { .signup-wrapper { padding: 30px; } } .signup-wrapper .form-wrapper { width: 100%; } .signup-wrapper .singup-option button { font-size: 16px; font-weight: 600; } @media only screen and (min-width: 1200px) and (max-width: 1399px) { .signup-wrapper .singup-option button { padding-left: 25px; padding-right: 25px; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { .signup-wrapper .singup-option button { padding-left: 30px; padding-right: 30px; } } @media (max-width: 767px) { .signup-wrapper .singup-option button { width: 100%; } } @media only screen and (min-width: 550px) and (max-width: 767px) { .signup-wrapper .singup-option button { width: auto; } } .signup-wrapper .singup-option a:hover { text-decoration: underline; } /* =========== settings css ============== */ .settings-card-1 .profile-info .profile-image { max-width: 75px; width: 100%; height: 75px; border-radius: 50%; margin-right: 20px; position: relative; z-index: 1; } .settings-card-1 .profile-info .profile-image img { width: 100%; border-radius: 50%; } .settings-card-1 .profile-info .profile-image .update-image { position: absolute; bottom: 0; right: 0; width: 30px; height: 30px; background: #efefef; border: 2px solid #fff; display: flex; justify-content: center; align-items: center; border-radius: 50%; cursor: pointer; z-index: 99; } .settings-card-1 .profile-info .profile-image .update-image:hover { opacity: 0.9; } .settings-card-1 .profile-info .profile-image .update-image input { opacity: 0; position: absolute; width: 100%; height: 100%; cursor: pointer; z-index: 99; } .settings-card-1 .profile-info .profile-image .update-image label { cursor: pointer; z-index: 99; } /* =========== Invoice Css ============= */ .invoice-card .invoice-header { display: flex; flex-wrap: wrap; justify-content: space-between; flex: 1; padding-bottom: 30px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } @media (max-width: 767px) { .invoice-card .invoice-header { flex-direction: column; } } .invoice-card .invoice-header .invoice-logo { width: 110px; height: 110px; border-radius: 50%; overflow: hidden; } @media (max-width: 767px) { .invoice-card .invoice-header .invoice-logo { order: -1; margin-bottom: 30px; } } .invoice-card .invoice-header .invoice-logo img { width: 100%; } @media (max-width: 767px) { .invoice-card .invoice-header .invoice-date { margin-top: 30px; } } .invoice-card .invoice-header .invoice-date p { font-size: 14px; font-weight: 400; margin-bottom: 10px; } .invoice-card .invoice-header .invoice-date p span { font-weight: 500; } .invoice-card .invoice-address { padding-top: 30px; display: flex; margin-bottom: 40px; } @media (max-width: 767px) { .invoice-card .invoice-address { display: block; } } .invoice-card .invoice-address .address-item { margin-right: 30px; min-width: 250px; } .invoice-card .invoice-address .address-item h5 { margin-bottom: 15px; } .invoice-card .invoice-address .address-item h1 { margin-bottom: 10px; font-size: 24px; } .invoice-card .invoice-address .address-item p { margin-bottom: 10px; } @media (max-width: 767px) { .invoice-card .invoice-action ul li { flex: 1; } } @media (max-width: 767px) { .invoice-card .invoice-action ul li a { width: 100%; } } .invoice-table th, .invoice-table td { padding: 10px 8px; } .invoice-table .service { min-width: 150px; } .invoice-table .desc { min-width: 150px; } .invoice-table .qty { min-width: 150px; } .invoice-table .amount { min-width: 100px; } /* ============== Icons Css ===========*/ .icons-wrapper .icons, .icons-wrapper ul { display: flex; flex-wrap: wrap; margin: 0 -10px; } .icons-wrapper .icons > div, .icons-wrapper .icons li, .icons-wrapper ul > div, .icons-wrapper ul li { display: flex; align-items: center; margin: 10px; flex-basis: 215px; } @media (max-width: 400px) { .icons-wrapper .icons > div, .icons-wrapper .icons li, .icons-wrapper ul > div, .icons-wrapper ul li { flex-basis: 100%; } } .icons-wrapper .icons > div i, .icons-wrapper .icons li i, .icons-wrapper ul > div i, .icons-wrapper ul li i { max-width: 45px; width: 100%; height: 45px; display: flex; align-items: center; justify-content: center; border: 1px solid #efefef; border-radius: 4px; background: transparent; color: #262d3f; font-size: 20px; margin-right: 10px; } .icons-wrapper .icons > div span, .icons-wrapper .icons li span, .icons-wrapper ul > div span, .icons-wrapper ul li span { color: #262d3f; user-select: all; } /* ============ Calendar Css ============= */ .calendar-card .fc { height: 450px; } .calendar-card .fc#calendar-full { height: 600px; } .calendar-card .fc table { border: none; } .calendar-card .fc .fc-toolbar-title { font-size: 16px; font-weight: 500; } .calendar-card .fc .fc-button { background: transparent; border: none; color: #5d657b; text-transform: capitalize; } .calendar-card .fc .fc-button:focus { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; color: #4a6cf7; } .calendar-card .fc th { text-align: left; border-bottom: 1px solid rgba(0, 0, 0, 0.1) !important; border-right: 0px; } .calendar-card .fc th a { color: #5d657b; font-weight: 400; } .calendar-card .fc .fc-day { border-width: 4px; background: #fff; } .calendar-card .fc .fc-day.fc-day-today .fc-daygrid-day-frame { background: rgba(74, 108, 247, 0.8); } .calendar-card .fc .fc-day.fc-day-today .fc-daygrid-day-frame a { color: #fff; } .calendar-card .fc .fc-day .fc-daygrid-day-frame { display: flex; flex-direction: column; align-items: flex-end; background: #f9f9f9; border-radius: 6px; } .calendar-card .fc .fc-day .fc-daygrid-day-frame a { color: #5d657b; } .calendar-card .fc-theme-standard td, .calendar-card .fc-theme-standard th { border-color: transparent; } /* =========== Sidebar css =========== */ .sidebar-nav-wrapper { background: #fff; width: 250px; padding: 20px 0px; height: 100vh; position: fixed; overflow-y: scroll; overflow-x: hidden; top: 0; left: 0; z-index: 99; box-shadow: 0px 0px 30px rgba(200, 208, 216, 0.3); -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; -webkit-transform: translateX(0); -moz-transform: translateX(0); -ms-transform: translateX(0); -o-transform: translateX(0); transform: translateX(0); } @media only screen and (min-width: 992px) and (max-width: 1199px), only screen and (min-width: 768px) and (max-width: 991px), (max-width: 767px) { .sidebar-nav-wrapper { -webkit-transform: translateX(-260px); -moz-transform: translateX(-260px); -ms-transform: translateX(-260px); -o-transform: translateX(-260px); transform: translateX(-260px); } } .sidebar-nav-wrapper.active { -webkit-transform: translateX(-260px); -moz-transform: translateX(-260px); -ms-transform: translateX(-260px); -o-transform: translateX(-260px); transform: translateX(-260px); } @media only screen and (min-width: 992px) and (max-width: 1199px), only screen and (min-width: 768px) and (max-width: 991px), (max-width: 767px) { .sidebar-nav-wrapper.active { -webkit-transform: translateX(0px); -moz-transform: translateX(0px); -ms-transform: translateX(0px); -o-transform: translateX(0px); transform: translateX(0px); } } .sidebar-nav-wrapper .navbar-logo { text-align: center; padding: 0 25px; margin-bottom: 30px; } .sidebar-nav-wrapper .sidebar-nav .divider { padding: 5px 25px; width: 100%; } .sidebar-nav-wrapper .sidebar-nav .divider hr { height: 1px; background: #e2e2e2; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item { position: relative; margin: 5px 0px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children > a { color: #262d3f; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children > a::before { opacity: 1; visibility: visible; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children > a::after { content: "\ea5e"; font: normal normal normal 1em/1 "LineIcons"; position: absolute; right: 25px; top: 16px; font-size: 12px; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; -webkit-transform: rotate(180deg); -moz-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg); } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children > a.collapsed { color: #5d657b; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children > a.collapsed::before { opacity: 0; visibility: hidden; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children > a.collapsed::after { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul { padding: 0px 15px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li { margin-bottom: 10px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li:last-child { margin-bottom: 0px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li a { font-size: 14px; font-weight: 400; border-radius: 6px; padding: 8px 15px; display: flex; align-items: center; border: 1px solid transparent; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li a.active, .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li a:hover { color: #4a6cf7; border-color: rgba(74, 108, 247, 0.15); background: rgba(74, 108, 247, 0.1); } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li a i { font-size: 16px; margin-right: 15px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li a span.text { display: flex; align-items: center; justify-content: space-between; width: 100%; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.nav-item-has-children ul li a span.pro-badge { background: #4a6cf7; color: #fff; padding: 1px 6px; border-radius: 4px; font-size: 10px; margin-left: 10px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item a { display: flex; align-items: center; color: #5d657b; font-size: 16px; font-weight: 500; width: 100%; position: relative; z-index: 1; padding: 10px 25px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item a::before { content: ""; position: absolute; left: 0; top: 0; height: 100%; width: 4px; background: #4a6cf7; border-radius: 0 3px 3px 0px; opacity: 0; visibility: hidden; -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item a span.text { display: flex; align-items: center; justify-content: space-between; width: 100%; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item a span.pro-badge { background: #4a6cf7; color: #fff; padding: 1px 6px; border-radius: 4px; font-size: 10px; margin-left: 10px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item a .icon { margin-right: 12px; font-size: 18px; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item a .icon svg { fill: currentColor; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.active > a, .sidebar-nav-wrapper .sidebar-nav ul .nav-item.active > a.collapsed, .sidebar-nav-wrapper .sidebar-nav ul .nav-item:hover > a, .sidebar-nav-wrapper .sidebar-nav ul .nav-item:hover > a.collapsed { color: #262d3f; } .sidebar-nav-wrapper .sidebar-nav ul .nav-item.active > a::before, .sidebar-nav-wrapper .sidebar-nav ul .nav-item.active > a.collapsed::before, .sidebar-nav-wrapper .sidebar-nav ul .nav-item:hover > a::before, .sidebar-nav-wrapper .sidebar-nav ul .nav-item:hover > a.collapsed::before { opacity: 1; visibility: visible; } .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); z-index: 11; -webkit-transform: translateX(-100%); -moz-transform: translateX(-100%); -ms-transform: translateX(-100%); -o-transform: translateX(-100%); transform: translateX(-100%); opacity: 0; visibility: hidden; } @media only screen and (min-width: 1400px), only screen and (min-width: 1200px) and (max-width: 1399px) { .overlay { display: none; } } @media only screen and (min-width: 992px) and (max-width: 1199px), only screen and (min-width: 768px) and (max-width: 991px), (max-width: 767px) { .overlay.active { opacity: 1; visibility: visible; -webkit-transform: translateX(0); -moz-transform: translateX(0); -ms-transform: translateX(0); -o-transform: translateX(0); transform: translateX(0); } } .main-wrapper { -webkit-transition: all 0.3s ease-out 0s; -moz-transition: all 0.3s ease-out 0s; -ms-transition: all 0.3s ease-out 0s; -o-transition: all 0.3s ease-out 0s; transition: all 0.3s ease-out 0s; margin-left: 250px; min-height: 100vh; padding-bottom: 85px; position: relative; } @media only screen and (min-width: 992px) and (max-width: 1199px), only screen and (min-width: 768px) and (max-width: 991px), (max-width: 767px) { .main-wrapper { margin-left: 0; } } @media (max-width: 767px) { .main-wrapper { padding-bottom: 110px; } } .main-wrapper.active { margin-left: 0; } .main-wrapper .container-fluid { padding-left: 40px; padding-right: 40px; } @media (max-width: 767px) { .main-wrapper .container-fluid { padding-left: 20px; padding-right: 20px; } } .main-wrapper .footer { padding: 25px 0; justify-items: flex-end; position: absolute; bottom: 0; width: 100%; } .main-wrapper .footer .copyright p a { color: inherit; } .main-wrapper .footer .copyright p a:hover { color: #4a6cf7; } @media (max-width: 767px) { .main-wrapper .footer .terms { margin-bottom: 10px; text-align: center; } } .main-wrapper .footer .terms a { color: #4a6cf7; } .main-wrapper .footer .terms a:hover { color: #4a6cf7; } .promo-box { box-shadow: 0px 10px 20px rgba(200, 208, 216, 0.3); padding: 24px 16px; text-align: center; max-width: 210px; margin: 0 auto; margin-top: 32px; border-radius: 4px; } .promo-box h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; } .promo-box p { font-size: 12px; line-height: 1.5; margin-bottom: 16px; } .promo-box .main-btn { padding: 12px; width: 100%; } /* ========== DEFAULT CSS ======== */ /* ======= Margin Top ======= */ .mt-5 { margin-top: 5px; } .mt-10 { margin-top: 10px; } .mt-15 { margin-top: 15px; } .mt-20 { margin-top: 20px; } .mt-25 { margin-top: 25px; } .mt-30 { margin-top: 30px; } .mt-35 { margin-top: 35px; } .mt-40 { margin-top: 40px; } .mt-45 { margin-top: 45px; } .mt-50 { margin-top: 50px; } .mt-55 { margin-top: 55px; } .mt-60 { margin-top: 60px; } .mt-65 { margin-top: 65px; } .mt-70 { margin-top: 70px; } .mt-75 { margin-top: 75px; } .mt-80 { margin-top: 80px; } .mt-85 { margin-top: 85px; } .mt-90 { margin-top: 90px; } .mt-95 { margin-top: 95px; } .mt-100 { margin-top: 100px; } .mt-105 { margin-top: 105px; } .mt-110 { margin-top: 110px; } .mt-115 { margin-top: 115px; } .mt-120 { margin-top: 120px; } .mt-125 { margin-top: 125px; } .mt-130 { margin-top: 130px; } .mt-135 { margin-top: 135px; } .mt-140 { margin-top: 140px; } .mt-145 { margin-top: 145px; } .mt-150 { margin-top: 150px; } .mt-155 { margin-top: 155px; } .mt-160 { margin-top: 160px; } .mt-165 { margin-top: 165px; } .mt-170 { margin-top: 170px; } .mt-175 { margin-top: 175px; } .mt-180 { margin-top: 180px; } .mt-185 { margin-top: 185px; } .mt-190 { margin-top: 190px; } .mt-195 { margin-top: 195px; } .mt-200 { margin-top: 200px; } .mt-205 { margin-top: 205px; } .mt-210 { margin-top: 210px; } .mt-215 { margin-top: 215px; } .mt-220 { margin-top: 220px; } .mt-225 { margin-top: 225px; } /* ======= Margin Bottom ======= */ .mb-5 { margin-bottom: 5px; } .mb-10 { margin-bottom: 10px; } .mb-15 { margin-bottom: 15px; } .mb-20 { margin-bottom: 20px; } .mb-25 { margin-bottom: 25px; } .mb-30 { margin-bottom: 30px; } .mb-35 { margin-bottom: 35px; } .mb-40 { margin-bottom: 40px; } .mb-45 { margin-bottom: 45px; } .mb-50 { margin-bottom: 50px; } .mb-55 { margin-bottom: 55px; } .mb-60 { margin-bottom: 60px; } .mb-65 { margin-bottom: 65px; } .mb-70 { margin-bottom: 70px; } .mb-75 { margin-bottom: 75px; } .mb-80 { margin-bottom: 80px; } .mb-85 { margin-bottom: 85px; } .mb-90 { margin-bottom: 90px; } .mb-95 { margin-bottom: 95px; } .mb-100 { margin-bottom: 100px; } .mb-105 { margin-bottom: 105px; } .mb-110 { margin-bottom: 110px; } .mb-115 { margin-bottom: 115px; } .mb-120 { margin-bottom: 120px; } .mb-125 { margin-bottom: 125px; } .mb-130 { margin-bottom: 130px; } .mb-135 { margin-bottom: 135px; } .mb-140 { margin-bottom: 140px; } .mb-145 { margin-bottom: 145px; } .mb-150 { margin-bottom: 150px; } .mb-155 { margin-bottom: 155px; } .mb-160 { margin-bottom: 160px; } .mb-165 { margin-bottom: 165px; } .mb-170 { margin-bottom: 170px; } .mb-175 { margin-bottom: 175px; } .mb-180 { margin-bottom: 180px; } .mb-185 { margin-bottom: 185px; } .mb-190 { margin-bottom: 190px; } .mb-195 { margin-bottom: 195px; } .mb-200 { margin-bottom: 200px; } .mb-205 { margin-bottom: 205px; } .mb-210 { margin-bottom: 210px; } .mb-215 { margin-bottom: 215px; } .mb-220 { margin-bottom: 220px; } .mb-225 { margin-bottom: 225px; } /* ======= Margin Left ======= */ .ml-5 { margin-left: 5px; } .ml-10 { margin-left: 10px; } .ml-15 { margin-left: 15px; } .ml-20 { margin-left: 20px; } .ml-25 { margin-left: 25px; } .ml-30 { margin-left: 30px; } .ml-35 { margin-left: 35px; } .ml-40 { margin-left: 40px; } .ml-45 { margin-left: 45px; } .ml-50 { margin-left: 50px; } .ml-55 { margin-left: 55px; } .ml-60 { margin-left: 60px; } .ml-65 { margin-left: 65px; } .ml-70 { margin-left: 70px; } .ml-75 { margin-left: 75px; } .ml-80 { margin-left: 80px; } .ml-85 { margin-left: 85px; } .ml-90 { margin-left: 90px; } .ml-95 { margin-left: 95px; } .ml-100 { margin-left: 100px; } .ml-105 { margin-left: 105px; } .ml-110 { margin-left: 110px; } .ml-115 { margin-left: 115px; } .ml-120 { margin-left: 120px; } .ml-125 { margin-left: 125px; } .ml-130 { margin-left: 130px; } .ml-135 { margin-left: 135px; } .ml-140 { margin-left: 140px; } .ml-145 { margin-left: 145px; } .ml-150 { margin-left: 150px; } .ml-155 { margin-left: 155px; } .ml-160 { margin-left: 160px; } .ml-165 { margin-left: 165px; } .ml-170 { margin-left: 170px; } .ml-175 { margin-left: 175px; } .ml-180 { margin-left: 180px; } .ml-185 { margin-left: 185px; } .ml-190 { margin-left: 190px; } .ml-195 { margin-left: 195px; } .ml-200 { margin-left: 200px; } .ml-205 { margin-left: 205px; } .ml-210 { margin-left: 210px; } .ml-215 { margin-left: 215px; } .ml-220 { margin-left: 220px; } .ml-225 { margin-left: 225px; } /* ======= Margin Right ======= */ .mr-5 { margin-right: 5px; } .mr-10 { margin-right: 10px; } .mr-15 { margin-right: 15px; } .mr-20 { margin-right: 20px; } .mr-25 { margin-right: 25px; } .mr-30 { margin-right: 30px; } .mr-35 { margin-right: 35px; } .mr-40 { margin-right: 40px; } .mr-45 { margin-right: 45px; } .mr-50 { margin-right: 50px; } .mr-55 { margin-right: 55px; } .mr-60 { margin-right: 60px; } .mr-65 { margin-right: 65px; } .mr-70 { margin-right: 70px; } .mr-75 { margin-right: 75px; } .mr-80 { margin-right: 80px; } .mr-85 { margin-right: 85px; } .mr-90 { margin-right: 90px; } .mr-95 { margin-right: 95px; } .mr-100 { margin-right: 100px; } .mr-105 { margin-right: 105px; } .mr-110 { margin-right: 110px; } .mr-115 { margin-right: 115px; } .mr-120 { margin-right: 120px; } .mr-125 { margin-right: 125px; } .mr-130 { margin-right: 130px; } .mr-135 { margin-right: 135px; } .mr-140 { margin-right: 140px; } .mr-145 { margin-right: 145px; } .mr-150 { margin-right: 150px; } .mr-155 { margin-right: 155px; } .mr-160 { margin-right: 160px; } .mr-165 { margin-right: 165px; } .mr-170 { margin-right: 170px; } .mr-175 { margin-right: 175px; } .mr-180 { margin-right: 180px; } .mr-185 { margin-right: 185px; } .mr-190 { margin-right: 190px; } .mr-195 { margin-right: 195px; } .mr-200 { margin-right: 200px; } .mr-205 { margin-right: 205px; } .mr-210 { margin-right: 210px; } .mr-215 { margin-right: 215px; } .mr-220 { margin-right: 220px; } .mr-225 { margin-right: 225px; } /* ======= Padding Top ======= */ .pt-5 { padding-top: 5px; } .pt-10 { padding-top: 10px; } .pt-15 { padding-top: 15px; } .pt-20 { padding-top: 20px; } .pt-25 { padding-top: 25px; } .pt-30 { padding-top: 30px; } .pt-35 { padding-top: 35px; } .pt-40 { padding-top: 40px; } .pt-45 { padding-top: 45px; } .pt-50 { padding-top: 50px; } .pt-55 { padding-top: 55px; } .pt-60 { padding-top: 60px; } .pt-65 { padding-top: 65px; } .pt-70 { padding-top: 70px; } .pt-75 { padding-top: 75px; } .pt-80 { padding-top: 80px; } .pt-85 { padding-top: 85px; } .pt-90 { padding-top: 90px; } .pt-95 { padding-top: 95px; } .pt-100 { padding-top: 100px; } .pt-105 { padding-top: 105px; } .pt-110 { padding-top: 110px; } .pt-115 { padding-top: 115px; } .pt-120 { padding-top: 120px; } .pt-125 { padding-top: 125px; } .pt-130 { padding-top: 130px; } .pt-135 { padding-top: 135px; } .pt-140 { padding-top: 140px; } .pt-145 { padding-top: 145px; } .pt-150 { padding-top: 150px; } .pt-155 { padding-top: 155px; } .pt-160 { padding-top: 160px; } .pt-165 { padding-top: 165px; } .pt-170 { padding-top: 170px; } .pt-175 { padding-top: 175px; } .pt-180 { padding-top: 180px; } .pt-185 { padding-top: 185px; } .pt-190 { padding-top: 190px; } .pt-195 { padding-top: 195px; } .pt-200 { padding-top: 200px; } .pt-205 { padding-top: 205px; } .pt-210 { padding-top: 210px; } .pt-215 { padding-top: 215px; } .pt-220 { padding-top: 220px; } .pt-225 { padding-top: 225px; } /* ======= Padding Bottom ======= */ .pb-5 { padding-bottom: 5px; } .pb-10 { padding-bottom: 10px; } .pb-15 { padding-bottom: 15px; } .pb-20 { padding-bottom: 20px; } .pb-25 { padding-bottom: 25px; } .pb-30 { padding-bottom: 30px; } .pb-35 { padding-bottom: 35px; } .pb-40 { padding-bottom: 40px; } .pb-45 { padding-bottom: 45px; } .pb-50 { padding-bottom: 50px; } .pb-55 { padding-bottom: 55px; } .pb-60 { padding-bottom: 60px; } .pb-65 { padding-bottom: 65px; } .pb-70 { padding-bottom: 70px; } .pb-75 { padding-bottom: 75px; } .pb-80 { padding-bottom: 80px; } .pb-85 { padding-bottom: 85px; } .pb-90 { padding-bottom: 90px; } .pb-95 { padding-bottom: 95px; } .pb-100 { padding-bottom: 100px; } .pb-105 { padding-bottom: 105px; } .pb-110 { padding-bottom: 110px; } .pb-115 { padding-bottom: 115px; } .pb-120 { padding-bottom: 120px; } .pb-125 { padding-bottom: 125px; } .pb-130 { padding-bottom: 130px; } .pb-135 { padding-bottom: 135px; } .pb-140 { padding-bottom: 140px; } .pb-145 { padding-bottom: 145px; } .pb-150 { padding-bottom: 150px; } .pb-155 { padding-bottom: 155px; } .pb-160 { padding-bottom: 160px; } .pb-165 { padding-bottom: 165px; } .pb-170 { padding-bottom: 170px; } .pb-175 { padding-bottom: 175px; } .pb-180 { padding-bottom: 180px; } .pb-185 { padding-bottom: 185px; } .pb-190 { padding-bottom: 190px; } .pb-195 { padding-bottom: 195px; } .pb-200 { padding-bottom: 200px; } .pb-205 { padding-bottom: 205px; } .pb-210 { padding-bottom: 210px; } .pb-215 { padding-bottom: 215px; } .pb-220 { padding-bottom: 220px; } .pb-225 { padding-bottom: 225px; } /* ======= Padding Left ======= */ .pl-5 { padding-left: 5px; } .pl-10 { padding-left: 10px; } .pl-15 { padding-left: 15px; } .pl-20 { padding-left: 20px; } .pl-25 { padding-left: 25px; } .pl-30 { padding-left: 30px; } .pl-35 { padding-left: 35px; } .pl-40 { padding-left: 40px; } .pl-45 { padding-left: 45px; } .pl-50 { padding-left: 50px; } .pl-55 { padding-left: 55px; } .pl-60 { padding-left: 60px; } .pl-65 { padding-left: 65px; } .pl-70 { padding-left: 70px; } .pl-75 { padding-left: 75px; } .pl-80 { padding-left: 80px; } .pl-85 { padding-left: 85px; } .pl-90 { padding-left: 90px; } .pl-95 { padding-left: 95px; } .pl-100 { padding-left: 100px; } .pl-105 { padding-left: 105px; } .pl-110 { padding-left: 110px; } .pl-115 { padding-left: 115px; } .pl-120 { padding-left: 120px; } .pl-125 { padding-left: 125px; } .pl-130 { padding-left: 130px; } .pl-135 { padding-left: 135px; } .pl-140 { padding-left: 140px; } .pl-145 { padding-left: 145px; } .pl-150 { padding-left: 150px; } .pl-155 { padding-left: 155px; } .pl-160 { padding-left: 160px; } .pl-165 { padding-left: 165px; } .pl-170 { padding-left: 170px; } .pl-175 { padding-left: 175px; } .pl-180 { padding-left: 180px; } .pl-185 { padding-left: 185px; } .pl-190 { padding-left: 190px; } .pl-195 { padding-left: 195px; } .pl-200 { padding-left: 200px; } .pl-205 { padding-left: 205px; } .pl-210 { padding-left: 210px; } .pl-215 { padding-left: 215px; } .pl-220 { padding-left: 220px; } .pl-225 { padding-left: 225px; } /* ======= Padding Right ======= */ .pr-5 { padding-right: 5px; } .pr-10 { padding-right: 10px; } .pr-15 { padding-right: 15px; } .pr-20 { padding-right: 20px; } .pr-25 { padding-right: 25px; } .pr-30 { padding-right: 30px; } .pr-35 { padding-right: 35px; } .pr-40 { padding-right: 40px; } .pr-45 { padding-right: 45px; } .pr-50 { padding-right: 50px; } .pr-55 { padding-right: 55px; } .pr-60 { padding-right: 60px; } .pr-65 { padding-right: 65px; } .pr-70 { padding-right: 70px; } .pr-75 { padding-right: 75px; } .pr-80 { padding-right: 80px; } .pr-85 { padding-right: 85px; } .pr-90 { padding-right: 90px; } .pr-95 { padding-right: 95px; } .pr-100 { padding-right: 100px; } .pr-105 { padding-right: 105px; } .pr-110 { padding-right: 110px; } .pr-115 { padding-right: 115px; } .pr-120 { padding-right: 120px; } .pr-125 { padding-right: 125px; } .pr-130 { padding-right: 130px; } .pr-135 { padding-right: 135px; } .pr-140 { padding-right: 140px; } .pr-145 { padding-right: 145px; } .pr-150 { padding-right: 150px; } .pr-155 { padding-right: 155px; } .pr-160 { padding-right: 160px; } .pr-165 { padding-right: 165px; } .pr-170 { padding-right: 170px; } .pr-175 { padding-right: 175px; } .pr-180 { padding-right: 180px; } .pr-185 { padding-right: 185px; } .pr-190 { padding-right: 190px; } .pr-195 { padding-right: 195px; } .pr-200 { padding-right: 200px; } .pr-205 { padding-right: 205px; } .pr-210 { padding-right: 210px; } .pr-215 { padding-right: 215px; } .pr-220 { padding-right: 220px; } .pr-225 { padding-right: 225px; } /* ======= bg-primary shades ========= */ .bg-primary-100 { background: rgba(74, 108, 247, 0.1); } .bg-primary-200 { background: rgba(74, 108, 247, 0.2); } .bg-primary-300 { background: rgba(74, 108, 247, 0.3); } .bg-primary-400 { background: rgba(74, 108, 247, 0.4); } .bg-primary-500 { background: rgba(74, 108, 247, 0.5); } .bg-primary-600 { background: rgba(74, 108, 247, 0.6); } .bg-primary-700 { background: rgba(74, 108, 247, 0.7); } .bg-primary-800 { background: rgba(74, 108, 247, 0.8); } .bg-primary-900 { background: rgba(74, 108, 247, 0.9); } /* ======= bg-secondary shades ========= */ .bg-secondary-100 { background: rgba(0, 193, 248, 0.1); } .bg-secondary-200 { background: rgba(0, 193, 248, 0.2); } .bg-secondary-300 { background: rgba(0, 193, 248, 0.3); } .bg-secondary-400 { background: rgba(0, 193, 248, 0.4); } .bg-secondary-500 { background: rgba(0, 193, 248, 0.5); } .bg-secondary-600 { background: rgba(0, 193, 248, 0.6); } .bg-secondary-700 { background: rgba(0, 193, 248, 0.7); } .bg-secondary-800 { background: rgba(0, 193, 248, 0.8); } .bg-secondary-900 { background: rgba(0, 193, 248, 0.9); } /* ======= bg-success shades ========= */ .bg-success-100 { background: rgba(33, 150, 83, 0.1); } .bg-success-200 { background: rgba(33, 150, 83, 0.2); } .bg-success-300 { background: rgba(33, 150, 83, 0.3); } .bg-success-400 { background: rgba(33, 150, 83, 0.4); } .bg-success-500 { background: rgba(33, 150, 83, 0.5); } .bg-success-600 { background: rgba(33, 150, 83, 0.6); } .bg-success-700 { background: rgba(33, 150, 83, 0.7); } .bg-success-800 { background: rgba(33, 150, 83, 0.8); } .bg-success-900 { background: rgba(33, 150, 83, 0.9); } /* ======= bg-danger shades ========= */ .bg-danger-100 { background: rgba(213, 1, 0, 0.1); } .bg-danger-200 { background: rgba(213, 1, 0, 0.2); } .bg-danger-300 { background: rgba(213, 1, 0, 0.3); } .bg-danger-400 { background: rgba(213, 1, 0, 0.4); } .bg-danger-500 { background: rgba(213, 1, 0, 0.5); } .bg-danger-600 { background: rgba(213, 1, 0, 0.6); } .bg-danger-700 { background: rgba(213, 1, 0, 0.7); } .bg-danger-800 { background: rgba(213, 1, 0, 0.8); } .bg-danger-900 { background: rgba(213, 1, 0, 0.9); } /* ======= bg-warning shades ========= */ .bg-warning-100 { background: rgba(247, 200, 0, 0.1); } .bg-warning-200 { background: rgba(247, 200, 0, 0.2); } .bg-warning-300 { background: rgba(247, 200, 0, 0.3); } .bg-warning-400 { background: rgba(247, 200, 0, 0.4); } .bg-warning-500 { background: rgba(247, 200, 0, 0.5); } .bg-warning-600 { background: rgba(247, 200, 0, 0.6); } .bg-warning-700 { background: rgba(247, 200, 0, 0.7); } .bg-warning-800 { background: rgba(247, 200, 0, 0.8); } .bg-warning-900 { background: rgba(247, 200, 0, 0.9); } /* ======= bg-info shades ========= */ .bg-info-100 { background: rgba(151, 202, 49, 0.1); } .bg-info-200 { background: rgba(151, 202, 49, 0.2); } .bg-info-300 { background: rgba(151, 202, 49, 0.3); } .bg-info-400 { background: rgba(151, 202, 49, 0.4); } .bg-info-500 { background: rgba(151, 202, 49, 0.5); } .bg-info-600 { background: rgba(151, 202, 49, 0.6); } .bg-info-700 { background: rgba(151, 202, 49, 0.7); } .bg-info-800 { background: rgba(151, 202, 49, 0.8); } .bg-info-900 { background: rgba(151, 202, 49, 0.9); } /* ======= bg-dark shades ========= */ .bg-dark-100 { background: rgba(38, 45, 63, 0.1); } .bg-dark-200 { background: rgba(38, 45, 63, 0.2); } .bg-dark-300 { background: rgba(38, 45, 63, 0.3); } .bg-dark-400 { background: rgba(38, 45, 63, 0.4); } .bg-dark-500 { background: rgba(38, 45, 63, 0.5); } .bg-dark-600 { background: rgba(38, 45, 63, 0.6); } .bg-dark-700 { background: rgba(38, 45, 63, 0.7); } .bg-dark-800 { background: rgba(38, 45, 63, 0.8); } .bg-dark-900 { background: rgba(38, 45, 63, 0.9); } /* ======= bg-purple shades ========= */ .bg-purple-100 { background: rgba(155, 81, 224, 0.1); } .bg-purple-200 { background: rgba(155, 81, 224, 0.2); } .bg-purple-300 { background: rgba(155, 81, 224, 0.3); } .bg-purple-400 { background: rgba(155, 81, 224, 0.4); } .bg-purple-500 { background: rgba(155, 81, 224, 0.5); } .bg-purple-600 { background: rgba(155, 81, 224, 0.6); } .bg-purple-700 { background: rgba(155, 81, 224, 0.7); } .bg-purple-800 { background: rgba(155, 81, 224, 0.8); } .bg-purple-900 { background: rgba(155, 81, 224, 0.9); } /* ======= bg-orange shades ========= */ .bg-orange-100 { background: rgba(242, 153, 74, 0.1); } .bg-orange-200 { background: rgba(242, 153, 74, 0.2); } .bg-orange-300 { background: rgba(242, 153, 74, 0.3); } .bg-orange-400 { background: rgba(242, 153, 74, 0.4); } .bg-orange-500 { background: rgba(242, 153, 74, 0.5); } .bg-orange-600 { background: rgba(242, 153, 74, 0.6); } .bg-orange-700 { background: rgba(242, 153, 74, 0.7); } .bg-orange-800 { background: rgba(242, 153, 74, 0.8); } .bg-orange-900 { background: rgba(242, 153, 74, 0.9); } /* ======== Background Colors ========== */ .primary-bg { background-color: #4a6cf7; } .secondary-bg { background-color: #00c1f8; } .success-bg { background-color: #219653; } .danger-bg { background-color: #d50100; } .warning-bg { background-color: #f7c800; } .info-bg { background-color: #97ca31; } .dark-bg { background-color: #262d3f; } .light-bg { background-color: #efefef; } .active-bg { background-color: #4a6cf7; } .deactive-bg { background-color: #cbe1ff; } .deactive-bg { background-color: #cbe1ff; } .gray-bg { background-color: #5d657b; } .purple-bg { background-color: #9b51e0; } .orange-bg { background-color: #f2994a; } .deep-blue-bg { background-color: #345d9d; } /* ======== Text Colors ========== */ .text-primary { color: #4a6cf7 !important; } .text-secondary { color: #00c1f8 !important; } .text-success { color: #219653 !important; } .text-danger { color: #d50100 !important; } .text-warning { color: #f7c800 !important; } .text-info { color: #97ca31 !important; } .text-dark { color: #262d3f !important; } .text-light { color: #efefef !important; } .text-active { color: #4a6cf7 !important; } .text-deactive { color: #cbe1ff !important; } .text-deactive { color: #cbe1ff !important; } .text-gray { color: #5d657b !important; } .text-orange { color: #f2994a !important; } /* ========= Font Weight =========== */ .fw-300 { font-weight: 300; } .fw-400 { font-weight: 400; } .fw-500 { font-weight: 500; } .fw-600 { font-weight: 600; } .fw-700 { font-weight: 700; } .fw-800 { font-weight: 800; } .fw-900 { font-weight: 900; } /* ====== Supervisor Stats ====== */ .supervisor-stat { display: block; padding: 7px 15px; border-radius: 30px; font-size: 14px; font-weight: 400; } ================================================ FILE: public/assets/scss/_common.scss ================================================ /*=========================== COMMON css ===========================*/ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"); html { scroll-behavior: smooth; } body { font-family: $font !important; font-weight: normal; font-style: normal; color: $gray; overflow-x: hidden; background: #f1f5f9; } * { margin: 0; padding: 0; @include box-sizing(border-box); } a:focus, input:focus, textarea:focus, button:focus, .btn:focus, .btn.focus, .btn:not(:disabled):not(.disabled).active, .btn:not(:disabled):not(.disabled):active { text-decoration: none; outline: none; @include box-shadow(none); } a:hover { color: $primary; } button, a { @include transition(0.3s); } a, a:focus, a:hover { text-decoration: none; } i, span, a { display: inline-block; } audio, canvas, iframe, img, svg, video { vertical-align: middle; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; } ul, ol { margin: 0px; padding: 0px; list-style-type: none; } p { font-size: 16px; font-weight: 400; line-height: 25px; margin: 0px; } .img-bg { background-position: center center; background-size: cover; background-repeat: no-repeat; width: 100%; height: 100%; } .para-width-500 { max-width: 500px; width: 100%; } .container { @media #{$xs} { padding: 0 30px; } } /* ========== cart style ========== */ .card-style { background: $white; box-sizing: border-box; padding: 25px 30px; position: relative; border: 1px solid #e2e8f0; box-shadow: 0px 10px 20px rgba(200, 208, 216, 0.3); border-radius: 10px; @media #{$xs} { padding: 20px; } .jvm-zoom-btn { position: absolute; display: inline-flex; justify-content: center; align-items: center; width: 30px; height: 30px; border: 1px solid $black-10; right: 30px; bottom: 30px; cursor: pointer; &.jvm-zoomin { bottom: 70px; } } .dropdown-toggle { border: none; background: none; &::after { display: none; } } .dropdown-menu { @include box-shadow(0px 0px 5px rgba(0, 0, 0, 0.07)); li { &:hover { a { color: $primary !important; } } a { display: block; font-size: 14px; } } } } /* ======= Border Radius ========= */ .radius-4 { border-radius: 4px; } .radius-10 { border-radius: 10px; } .radius-30 { border-radius: 30px; } .radius-50 { border-radius: 50px; } .radius-full { border-radius: 50%; } // scroll-top .scroll-top { width: 45px; height: 45px; background: $primary; display: none; justify-content: center; align-items: center; font-size: 18px; color: $white; border-radius: 5px; position: fixed; bottom: 30px; right: 30px; z-index: 9; cursor: pointer; @include transition(0.3s); &:hover { color: $white; background: rgba($primary, 0.8); } } .form-control { &:focus { box-shadow: none; } } .form-control.is-valid:focus, .was-validated .form-control:valid:focus, .form-control.is-invalid:focus, .was-validated .form-control:invalid:focus, .form-check-input.is-valid:focus, .was-validated .form-check-input:valid:focus, .form-check-input.is-invalid:focus, .was-validated .form-check-input:invalid:focus, .form-check-input:focus, .radio-style.radio-success .form-check-input:focus, .radio-style.radio-warning .form-check-input:focus, .radio-style.radio-danger .form-check-input:focus { box-shadow: none; } .hover-underline:hover { text-decoration: underline; } ================================================ FILE: public/assets/scss/_default.scss ================================================ /* ========== DEFAULT CSS ======== */ /* ======= Margin Top ======= */ @for $i from 1 through 45 { .mt-#{5 * $i} { margin-top: 5px * $i; } } /* ======= Margin Bottom ======= */ @for $i from 1 through 45 { .mb-#{5 * $i} { margin-bottom: 5px * $i; } } /* ======= Margin Left ======= */ @for $i from 1 through 45 { .ml-#{5 * $i} { margin-left: 5px * $i; } } /* ======= Margin Right ======= */ @for $i from 1 through 45 { .mr-#{5 * $i} { margin-right: 5px * $i; } } /* ======= Padding Top ======= */ @for $i from 1 through 45 { .pt-#{5 * $i} { padding-top: 5px * $i; } } /* ======= Padding Bottom ======= */ @for $i from 1 through 45 { .pb-#{5 * $i} { padding-bottom: 5px * $i; } } /* ======= Padding Left ======= */ @for $i from 1 through 45 { .pl-#{5 * $i} { padding-left: 5px * $i; } } /* ======= Padding Right ======= */ @for $i from 1 through 45 { .pr-#{5 * $i} { padding-right: 5px * $i; } } /* ======= bg-primary shades ========= */ @for $i from 1 through 9 { .bg-primary-#{100 * $i} { background: rgba($primary, 0.1 * $i); } } /* ======= bg-secondary shades ========= */ @for $i from 1 through 9 { .bg-secondary-#{100 * $i} { background: rgba($secondary, 0.1 * $i); } } /* ======= bg-success shades ========= */ @for $i from 1 through 9 { .bg-success-#{100 * $i} { background: rgba($success, 0.1 * $i); } } /* ======= bg-danger shades ========= */ @for $i from 1 through 9 { .bg-danger-#{100 * $i} { background: rgba($danger, 0.1 * $i); } } /* ======= bg-warning shades ========= */ @for $i from 1 through 9 { .bg-warning-#{100 * $i} { background: rgba($warning, 0.1 * $i); } } /* ======= bg-info shades ========= */ @for $i from 1 through 9 { .bg-info-#{100 * $i} { background: rgba($info, 0.1 * $i); } } /* ======= bg-dark shades ========= */ @for $i from 1 through 9 { .bg-dark-#{100 * $i} { background: rgba($dark, 0.1 * $i); } } /* ======= bg-purple shades ========= */ @for $i from 1 through 9 { .bg-purple-#{100 * $i} { background: rgba($purple, 0.1 * $i); } } /* ======= bg-orange shades ========= */ @for $i from 1 through 9 { .bg-orange-#{100 * $i} { background: rgba($orange, 0.1 * $i); } } /* ======== Background Colors ========== */ .primary-bg { background-color: $primary; } .secondary-bg { background-color: $secondary; } .success-bg { background-color: $success; } .danger-bg { background-color: $danger; } .warning-bg { background-color: $warning; } .info-bg { background-color: $info; } .dark-bg { background-color: $dark; } .light-bg { background-color: $light; } .active-bg { background-color: $active; } .deactive-bg { background-color: $deactive; } .deactive-bg { background-color: $deactive; } .gray-bg { background-color: $gray; } .purple-bg { background-color: $purple; } .orange-bg { background-color: $orange; } .deep-blue-bg { background-color: $deep-blue; } /* ======== Text Colors ========== */ .text-primary { color: $primary !important; } .text-secondary { color: $secondary !important; } .text-success { color: $success !important; } .text-danger { color: $danger !important; } .text-warning { color: $warning !important; } .text-info { color: $info !important; } .text-dark { color: $dark !important; } .text-light { color: $light !important; } .text-active { color: $active !important; } .text-deactive { color: $deactive !important; } .text-deactive { color: $deactive !important; } .text-gray { color: $gray !important; } .text-orange { color: $orange !important; } /* ========= Font Weight =========== */ .fw-300 { font-weight: 300; } .fw-400 { font-weight: 400; } .fw-500 { font-weight: 500; } .fw-600 { font-weight: 600; } .fw-700 { font-weight: 700; } .fw-800 { font-weight: 800; } .fw-900 { font-weight: 900; } ================================================ FILE: public/assets/scss/_mixin.scss ================================================ @mixin transition($time) { -webkit-transition: all $time ease-out 0s; -moz-transition: all $time ease-out 0s; -ms-transition: all $time ease-out 0s; -o-transition: all $time ease-out 0s; transition: all $time ease-out 0s; } @mixin transform($value) { -webkit-transform: $value; -moz-transform: $value; -ms-transform: $value; -o-transform: $value; transform: $value; } @mixin user-select($value) { -webkit-user-select: $value; -moz-user-select: $value; -ms-user-select: $value; user-select: $value; } @mixin box-sizing($value) { -webkit-box-sizing: $value; -moz-box-sizing: $value; box-sizing: $value; } @mixin animation($value) { -webkit-animation: $value; -moz-animation: $value; -o-animation: $value; animation: $value; } @mixin animation-delay($value) { -webkit-animation-delay: $value; -moz-animation-delay: $value; -o-animation-delay: $value; animation-delay: $value; } @mixin box-shadow($value) { -webkit-box-shadow: $value; -moz-box-shadow: $value; box-shadow: $value; } // Placeholder Mixins @mixin placeholder { &::placeholder { @content; } &::-moz-placeholder { @content; } &::-moz-placeholder { @content; } &::-webkit-input-placeholder { @content; } } @mixin flex-center { display: flex; justify-content: center; align-items: center; } ================================================ FILE: public/assets/scss/_sidebar.scss ================================================ /* =========== Sidebar css =========== */ .sidebar-nav-wrapper { background: $white; width: 250px; padding: 20px 0px; height: 100vh; position: fixed; overflow-y: scroll; overflow-x: hidden; top: 0; left: 0; z-index: 99; box-shadow: 0px 0px 30px rgba(200, 208, 216, 0.3); @include transition(0.3s); @include transform(translateX(0)); @media #{$lg, $md, $xs} { @include transform(translateX(-260px)); } &.active { @include transform(translateX(-260px)); @media #{$lg, $md, $xs} { @include transform(translateX(0px)); } } .navbar-logo { text-align: center; padding: 0 25px; margin-bottom: 30px; } .sidebar-nav { .divider { padding: 5px 25px; width: 100%; hr { height: 1px; background: #e2e2e2; } } ul { .nav-item { position: relative; margin: 5px 0px; &.nav-item-has-children { & > a { color: $dark; &::before { opacity: 1; visibility: visible; } &::after { content: "\ea5e"; font: normal normal normal 1em/1 "LineIcons"; position: absolute; right: 25px; top: 16px; font-size: 12px; @include transition(0.3s); @include transform(rotate(180deg)); } &.collapsed { color: $gray; &::before { opacity: 0; visibility: hidden; } &::after { @include transform(rotate(0deg)); } } } ul { padding: 0px 15px; li { margin-bottom: 10px; &:last-child { margin-bottom: 0px; } a { font-size: 14px; font-weight: 400; border-radius: 6px; padding: 8px 15px; display: flex; align-items: center; border: 1px solid transparent; &.active, &:hover { color: $primary; border-color: rgba($primary, 0.15); background: rgba($primary, 0.1); } i { font-size: 16px; margin-right: 15px; } span.text { display: flex; align-items: center; justify-content: space-between; width: 100%; } span.pro-badge { background: $primary; color: $white; padding: 1px 6px; border-radius: 4px; font-size: 10px; margin-left: 10px; } } } } } a { display: flex; align-items: center; color: $gray; font-size: 16px; font-weight: 500; width: 100%; position: relative; z-index: 1; padding: 10px 25px; &::before { content: ""; position: absolute; left: 0; top: 0; height: 100%; width: 4px; background: $primary; border-radius: 0 3px 3px 0px; opacity: 0; visibility: hidden; @include transition(0.3s); } span.text { display: flex; align-items: center; justify-content: space-between; width: 100%; } span.pro-badge { background: $primary; color: $white; padding: 1px 6px; border-radius: 4px; font-size: 10px; margin-left: 10px; } .icon { margin-right: 12px; font-size: 18px; svg { fill: currentColor; } } } &.active, &:hover { & > a, & > a.collapsed { color: $dark; &::before { opacity: 1; visibility: visible; } } } } } } } .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba($black, 0.3); z-index: 11; @include transform(translateX(-100%)); opacity: 0; visibility: hidden; @media #{$desktop, $laptop} { display: none; } &.active { @media #{$lg, $md, $xs} { opacity: 1; visibility: visible; @include transform(translateX(0)); } } } .main-wrapper { @include transition(0.3s); margin-left: 250px; min-height: 100vh; padding-bottom: 85px; position: relative; @media #{$lg, $md, $xs} { margin-left: 0; } @media #{$xs} { padding-bottom: 110px; } &.active { margin-left: 0; } .container-fluid { padding-left: 40px; padding-right: 40px; @media #{$xs} { padding-left: 20px; padding-right: 20px; } } .footer { padding: 25px 0; justify-items: flex-end; position: absolute; bottom: 0; width: 100%; .copyright { p { a { color: inherit; &:hover { color: $primary; } } } } .terms { @media #{$xs} { margin-bottom: 10px; text-align: center; } a { color: $gray; &:hover { color: $primary; } } } } } .promo-box { box-shadow: 0px 10px 20px rgba(200, 208, 216, 0.3); padding: 24px 16px; text-align: center; max-width: 210px; margin: 0 auto; margin-top: 32px; border-radius: 4px; h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; } p { font-size: 12px; line-height: 1.5; margin-bottom: 16px; } .main-btn { padding: 12px; width: 100%; } } ================================================ FILE: public/assets/scss/_variables.scss ================================================ // Font Family $font: "Inter", sans-serif; // ======== white colors variants $white: #fff; $white-90: rgba($white, 0.9); $white-80: rgba($white, 0.8); $white-70: rgba($white, 0.7); $white-60: rgba($white, 0.6); $white-50: rgba($white, 0.5); $white-40: rgba($white, 0.4); $white-30: rgba($white, 0.3); $white-20: rgba($white, 0.2); $white-10: rgba($white, 0.1); // ========== black colors variants $black: #000; $black-90: rgba($black, 0.9); $black-80: rgba($black, 0.8); $black-70: rgba($black, 0.7); $black-60: rgba($black, 0.6); $black-50: rgba($black, 0.5); $black-40: rgba($black, 0.4); $black-30: rgba($black, 0.3); $black-20: rgba($black, 0.2); $black-10: rgba($black, 0.1); // ======== dark color $dark: #262d3f; $dark-2: #2f3546; $dark-3: #090e34; // ======== gray color $gray: #5d657b; $gray-2: #c2cbd6; // ======== primary color $primary: #4a6cf7; // ======== secondary color $secondary: #00c1f8; // ======== success color $success: #219653; // ======== danger color $danger: #d50100; // ======== warning color $warning: #f7c800; // ======== info color $info: #97ca31; // ======== purple color $purple: #9b51e0; // ======== light color $light: #efefef; $light-2: #e5e5e5; // ======== active color $active: #4a6cf7; // ======== deactive color $deactive: #cbe1ff; // ======== orange color $orange: #f2994a; // ======== deep-blue color $deep-blue: #345d9d; // ======== Shadow $shadow-one: 0px 5px 20px rgba(0, 0, 0, 0.1); // ======== Responsive Variables $desktop: "only screen and (min-width: 1400px)"; $laptop: "only screen and (min-width: 1200px) and (max-width: 1399px)"; $lg: "only screen and (min-width: 992px) and (max-width: 1199px)"; $md: "only screen and (min-width: 768px) and (max-width: 991px)"; $xs: "(max-width: 767px)"; $sm: "only screen and (min-width: 550px) and (max-width: 767px)"; ================================================ FILE: public/assets/scss/alerts/_alerts.scss ================================================ /* ============ alerts css ============ */ .alert-box { display: flex; position: relative; margin-bottom: 20px; @media #{$xs} { padding-left: 0px !important; } .left { max-width: 75px; width: 100%; height: 100%; border-radius: 4px; background: $danger; position: absolute; left: 0; top: 0; @include flex-center; @media #{$xs} { display: none; } h5 { @include transform(rotate(-90deg)); color: $white; } } .alert { margin-bottom: 0px; padding: 25px 40px; @media #{$xs} { padding: 20px; } } } /* Alert Primary */ .primary-alert { .left { background: $primary; } .alert { color: $primary; border: 1px solid $primary; background: rgba($primary, 0.2); width: 100%; .alert-heading { color: $primary; margin-bottom: 15px; } } } /* Alert Danger */ .danger-alert { .left { background: $danger; } .alert { color: $danger; border: 1px solid $danger; background: rgba($danger, 0.2); width: 100%; .alert-heading { color: $danger; margin-bottom: 15px; } } } /* Alert warning */ .warning-alert { .left { background: $warning; } .alert { color: $warning; border: 1px solid $warning; background: rgba($warning, 0.2); width: 100%; .alert-heading { color: $warning; margin-bottom: 15px; } } } /* Alert warning */ .warning-alert { .left { background: $warning; } .alert { color: $warning; border: 1px solid $warning; background: rgba($warning, 0.2); width: 100%; .alert-heading { color: $warning; margin-bottom: 15px; } } } /* Alert info */ .info-alert { .left { background: $info; } .alert { color: $info; border: 1px solid $info; background: rgba($info, 0.2); width: 100%; .alert-heading { color: $info; margin-bottom: 15px; } } } /* Alert success */ .success-alert { .left { background: $success; } .alert { color: $success; border: 1px solid $success; background: rgba($success, 0.2); width: 100%; .alert-heading { color: $success; margin-bottom: 15px; } } } /* Alert secondary */ .secondary-alert { .left { background: $secondary; } .alert { color: $secondary; border: 1px solid $secondary; background: rgba($secondary, 0.2); width: 100%; .alert-heading { color: $secondary; margin-bottom: 15px; } } } /* Alert gray */ .gray-alert { .left { background: $gray; } .alert { color: $gray; border: 1px solid $gray; background: rgba($gray, 0.2); width: 100%; .alert-heading { color: $gray; margin-bottom: 15px; } } } /* Alert black */ .black-alert { .left { background: $black; } .alert { color: $black; border: 1px solid $black; background: rgba($black, 0.2); width: 100%; .alert-heading { color: $black; margin-bottom: 15px; } } } /* Alert orange */ .orange-alert { .left { background: $orange; } .alert { color: $orange; border: 1px solid $orange; background: rgba($orange, 0.2); width: 100%; .alert-heading { color: $orange; margin-bottom: 15px; } } } ================================================ FILE: public/assets/scss/auth/_signin.scss ================================================ /* ============ signin css ============= */ .auth-row { background: $white; border-radius: 4px; overflow: hidden; } .auth-cover-wrapper { display: flex; align-items: center; justify-content: center; padding: 45px; position: relative; z-index: 1; height: 100%; @media #{$xs} { padding: 30px 20px; } .auth-cover { .title { text-align: cover; margin-bottom: 40px; @media #{$xs} { h1 { font-size: 24px; } } } .cover-image { max-width: 100%; margin: auto; img { width: 100%; } } .shape-image { position: absolute; z-index: -1; right: 0; bottom: 5%; } } } .signin-wrapper { background: $white; padding: 60px; min-height: 600px; display: flex; align-items: center; justify-content: center; @media #{$lg} { padding: 40px; } @media #{$xs} { padding: 30px; } .form-wrapper { width: 100%; } .singin-option { button { font-size: 16px; font-weight: 600; @media #{$laptop} { padding-left: 25px; padding-right: 25px; } @media #{$lg} { padding-left: 30px; padding-right: 30px; } @media #{$xs} { width: 100%; } @media #{$sm} { width: auto; } } a { &:hover { text-decoration: underline; } } } } ================================================ FILE: public/assets/scss/auth/_signup.scss ================================================ /* ============ signup css ============= */ .auth-row { background: $white; border-radius: 4px; overflow: hidden; } .auth-cover-wrapper { display: flex; align-items: center; justify-content: center; padding: 45px; position: relative; z-index: 1; height: 100%; @media #{$xs} { padding: 30px 20px; } .auth-cover { .title { text-align: cover; margin-bottom: 40px; @media #{$xs} { h1 { font-size: 24px; } } } .cover-image { max-width: 100%; margin: auto; img { width: 100%; } } .shape-image { position: absolute; z-index: -1; right: 0; bottom: 5%; } } } .signup-wrapper { background: $white; padding: 60px; min-height: 600px; display: flex; align-items: center; justify-content: center; @media #{$lg} { padding: 40px; } @media #{$xs} { padding: 30px; } .form-wrapper { width: 100%; } .singup-option { button { font-size: 16px; font-weight: 600; @media #{$laptop} { padding-left: 25px; padding-right: 25px; } @media #{$lg} { padding-left: 30px; padding-right: 30px; } @media #{$xs} { width: 100%; } @media #{$sm} { width: auto; } } a { &:hover { text-decoration: underline; } } } } ================================================ FILE: public/assets/scss/buttons/_buttons.scss ================================================ /* ========== Buttons css ========== */ /* buttons base styles */ .main-btn { display: inline-block; text-align: center; white-space: nowrap; vertical-align: middle; @include user-select(none); padding: 15px 45px; font-weight: 500; font-size: 14px; line-height: 24px; border-radius: 4px; cursor: pointer; z-index: 5; transition: all 0.4s ease-in-out; border: 1px solid transparent; overflow: hidden; &:hover { color: inherit; } } .btn-sm { padding: 10px 20px; font-weight: 400; } /* buttons hover effect */ .btn-hover { position: relative; overflow: hidden; &::after { content: ""; position: absolute; width: 0%; height: 0%; border-radius: 50%; background: rgba(255, 255, 255, 0.05); top: 50%; left: 50%; padding: 50%; z-index: -1; @include transition(0.3s); @include transform(translate3d(-50%, -50%, 0) scale(0)); } &:hover { &::after { @include transform(translate3d(-50%, -50%, 0) scale(1.3)); } } } /* primary buttons */ .primary-btn { background: $primary; color: $white; &:hover { color: $white; } } .primary-btn-outline { background: transparent; color: $primary; border-color: $primary; &:hover { color: $white; background: $primary; } } /* secondary buttons */ .secondary-btn { background: $secondary; color: $white; &:hover { color: $white; } } .secondary-btn-outline { background: transparent; color: $secondary; border-color: $secondary; &:hover { color: $white; background: $secondary; } } /* success buttons */ .success-btn { background: $success; color: $white; &:hover { color: $white; } } .success-btn-outline { background: transparent; color: $success; border-color: $success; &:hover { color: $white; background: $success; } } /* danger buttons */ .danger-btn { background: $danger; color: $white; &:hover { color: $white; } } .danger-btn-outline { background: transparent; color: $danger; border-color: $danger; &:hover { color: $white; background: $danger; } } /* warning buttons */ .warning-btn { background: $warning; color: $white; &:hover { color: $white; } } .warning-btn-outline { background: transparent; color: $warning; border-color: $warning; &:hover { color: $white; background: $warning; } } /* info buttons */ .info-btn { background: $info; color: $white; &:hover { color: $white; } } .info-btn-outline { background: transparent; color: $info; border-color: $info; &:hover { color: $white; background: $info; } } /* dark buttons */ .dark-btn { background: $dark; color: $white; &:hover { color: $white; } } .dark-btn-outline { background: transparent; color: $dark; border-color: $dark; &:hover { color: $white; background: $dark; } } /* light buttons */ .light-btn { background: $light; color: $dark; &:hover { color: $dark; } } .light-btn-outline { background: transparent; color: $dark; border-color: $light; &:hover { color: $dark; background: $light; } } /* active buttons */ .active-btn { background: $active; color: $white; &:hover { color: $white; } } .active-btn-outline { background: transparent; color: $active; border-color: $active; &:hover { color: $white; background: $active; } } /* deactive buttons */ .deactive-btn { background: $deactive; color: $active; &:hover { color: $active; } } .deactive-btn-outline { background: transparent; color: $active; border-color: $deactive; &:hover { color: $active; background: $deactive; } } /* ========= square-btn ========= */ .square-btn { border-radius: 0px; } /* ========= rounded-md ========= */ .rounded-md { border-radius: 10px; } /* ========= rounded-full ========= */ .rounded-full { border-radius: 30px; } /* ========== buttons group css ========= */ .buttons-group { display: flex; flex-wrap: wrap; margin: 0 -10px; li { margin: 10px; } } /* ====== Status Button ====== */ .status-btn { padding: 7px 15px; border-radius: 30px; font-size: 14px; font-weight: 400; &.primary-btn { color: $white; background: rgba($primary, 1); } &.active-btn { color: $primary; background: rgba($primary, 0.1); } &.close-btn { color: $danger; background: rgba($danger, 0.1); } &.warning-btn { color: $warning; background: rgba($warning, 0.1); } &.info-btn { color: $info; background: rgba($info, 0.1); } &.success-btn { color: $success; background: rgba($success, 0.1); } &.secondary-btn { color: $secondary; background: rgba($secondary, 0.1); } &.dark-btn { color: $dark; background: rgba($dark, 0.1); } &.orange-btn { color: $orange; background: rgba($orange, 0.1); } } ================================================ FILE: public/assets/scss/calendar/_calendar.scss ================================================ /* ============ Calendar Css ============= */ .calendar-card { .fc { height: 450px; &#calendar-full { height: 600px; } table { border: none; } .fc-toolbar-title { font-size: 16px; font-weight: 500; } .fc-button { background: transparent; border: none; color: $gray; text-transform: capitalize; &:focus { @include box-shadow(none); color: $primary; } } th { text-align: left; border-bottom: 1px solid $black-10 !important; border-right: 0px; a { color: $gray; font-weight: 400; } } .fc-day { border-width: 4px; background: $white; &.fc-day-today { .fc-daygrid-day-frame { background: rgba($primary, 0.8); a { color: $white; } } } .fc-daygrid-day-frame { display: flex; flex-direction: column; align-items: flex-end; background: #f9f9f9; border-radius: 6px; // padding: 5px; a { color: $gray; } } } } .fc-theme-standard td, .fc-theme-standard th { border-color: transparent; } } ================================================ FILE: public/assets/scss/cards/_cards.scss ================================================ /* ========== cards css =========== */ /* card-style-1 */ .card-style-1 { background: $white; border: 1px solid $light; border-radius: 10px; padding: 25px 0; position: relative; @include transition(0.3s); &:hover { @include box-shadow(0px 0px 5px rgba(0, 0, 0, 0.1)); } .card-meta { display: flex; align-items: center; margin-bottom: 15px; padding: 0 30px; @media #{$xs} { padding: 0 20px; } .image { max-width: 40px; width: 100%; border-radius: 50%; overflow: hidden; margin-right: 12px; img { width: 100%; } } .text { p { color: $dark; a { color: inherit; &:hover { color: $primary; } } } } } .card-image { border-radius: 10px; margin-bottom: 25px; overflow: hidden; a { display: block; } img { width: 100%; } } .card-content { padding: 0px 30px; @media #{$xs} { padding: 0px 20px; } h4 { a { color: inherit; margin-bottom: 15px; display: block; &:hover { color: $primary; } } } } } /* card-style-2 */ .card-style-2 { background: $white; border: 1px solid $light; border-radius: 4px; padding: 20px; @include transition(0.3s); &:hover { @include box-shadow(0px 0px 5px rgba(0, 0, 0, 0.1)); } .card-image { border-radius: 4px; margin-bottom: 30px; overflow: hidden; a { display: block; } img { width: 100%; } } .card-content { padding: 0px 10px; @media #{$xs} { padding: 0px; } h4 { a { color: inherit; margin-bottom: 15px; display: block; &:hover { color: $primary; } } } } } /* card-style-3 */ .card-style-3 { background: $white; border: 1px solid $light; border-radius: 4px; padding: 25px 30px; @include transition(0.3s); &:hover { @include box-shadow(0px 0px 5px rgba(0, 0, 0, 0.1)); } .card-content { h4 { a { color: inherit; margin-bottom: 15px; display: block; &:hover { color: $primary; } } } a.read-more { font-weight: 500; color: $dark; margin-top: 20px; &:hover { color: $primary; letter-spacing: 2px; } } } } /* ======= icon-card ======== */ .icon-card { display: flex; align-items: center; background: $white; padding: 30px 20px; border: 1px solid #e2e8f0; box-shadow: 0px 10px 20px rgba(200, 208, 216, 0.3); border-radius: 10px; &.icon-card-3 { display: block; padding: 0px; .card-content { display: flex; padding: 20px; padding-bottom: 0; } } h6 { @media #{$laptop} { font-size: 15px; } } h3 { @media #{$laptop} { font-size: 20px; } } &.icon-card-2 { display: block; .progress { height: 7px; .progress-bar { border-radius: 4px; } } } .icon { max-width: 46px; width: 100%; height: 46px; border-radius: 10px; @include flex-center; font-size: 24px; margin-right: 20px; background: rgba($primary, 0.1); color: $primary; font-weight: 700; @media #{$laptop} { margin-right: 10px; } &.purple { background: rgba($purple, 0.1); color: $purple; } &.success { background: rgba($success, 0.1); color: $success; } &.primary { background: rgba($primary, 0.1); color: $primary; } &.orange { background: rgba($orange, 0.1); color: $orange; } &.opacity-100 { &.purple { background: $purple; color: $white; } &.success { background: $success; color: $white; } &.primary { background: $primary; color: $white; } &.orange { background: $orange; color: $white; } &.deep-blue { background: $deep-blue; color: $white; } } } } ================================================ FILE: public/assets/scss/dashboards/_dashboards.scss ================================================ /* ========== Dashboards css ================= */ #doughnutChart1 { @media #{$xs} { height: 250px !important; } } .legend3 { li { margin-right: 25px; div { white-space: nowrap; } .bg-color { position: relative; margin-left: 12px; border-radius: 50%; &::after { content: ""; position: absolute; width: 12px; height: 12px; border-radius: 50%; background: inherit; left: -12px; top: 5px; } } .text { margin-left: 10px; p { display: flex; align-items: center; width: 100%; } } } } .todo-list-wrapper { ul { li.todo-list-item { position: relative; display: flex; align-items: center; justify-content: space-between; padding-left: 20px; margin-bottom: 25px; &:last-child { margin-bottom: 0px; } &::before { content: ""; position: absolute; left: 0; top: 0; width: 4px; height: 100%; } @media #{$xs} { display: block; .todo-status { margin-top: 20px; } } &.success { &::before { background: $success; } } &.primary { &::before { background: $primary; } } &.orange { &::before { background: $orange; } } &.danger { &::before { background: $danger; } } } } } ================================================ FILE: public/assets/scss/forms/_form-elements.scss ================================================ /* =========== form elements css ========== */ /* ===== input style ===== */ .input-style-1 { position: relative; margin-bottom: 30px; label { font-size: 14px; font-weight: 500; color: $dark; display: block; margin-bottom: 10px; } input, textarea { width: 100%; background: rgba($light, 0.5); border: 1px solid $light-2; border-radius: 4px; padding: 16px; color: $gray; resize: none; transition: all 0.3s; &:focus { border-color: $primary; background: $white; } &[type="date"], &[type="time"] { background: transparent; } } } .input-style-2 { position: relative; margin-bottom: 30px; z-index: 1; label { font-size: 14px; font-weight: 500; color: $dark; display: block; margin-bottom: 10px; } input, textarea { width: 100%; background: rgba($light, 0.5); border: 1px solid $light-2; border-radius: 4px; padding: 16px; color: $gray; resize: none; transition: all 0.3s; &:focus { border-color: $primary; background: $white; } &[type="date"], &[type="time"] { background: transparent; } } input[type="date"]::-webkit-inner-spin-button, input[type="date"]::-webkit-calendar-picker-indicator { opacity: 0; } input[type="date"] ~ .icon { z-index: -1; } .icon { position: absolute; right: 0; bottom: 0; padding: 17px; } } .input-style-3 { position: relative; margin-bottom: 30px; label { font-size: 14px; font-weight: 500; color: $dark; display: block; margin-bottom: 10px; } input, textarea { width: 100%; background: rgba($light, 0.5); border: 1px solid $light-2; border-radius: 4px; padding: 16px; padding-left: 45px; color: $gray; resize: none; transition: all 0.3s; &:focus { border-color: $primary; background: $white; } } .icon { position: absolute; left: 0; top: 0; height: 100%; padding: 16px; } } /* ========= select style ========== */ .select-style-1 { margin-bottom: 30px; label { font-size: 14px; font-weight: 500; color: $dark; display: block; margin-bottom: 10px; } .select-position { position: relative; &::after { border-bottom: 2px solid $gray; border-right: 2px solid $gray; content: ""; display: block; height: 10px; width: 10px; margin-top: -5px; pointer-events: none; position: absolute; right: 16px; top: 50%; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out; } &.select-sm { &::after { margin-top: -8px; } select { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } } select { width: 100%; background: transparent; border: 1px solid $light-2; border-radius: 10px; padding: 16px; padding-right: 38px; color: $gray; appearance: none; -webkit-appearance: none; -moz-appearance: none; @include transition(0.3s); &:focus { border-color: $primary; outline: none; } &.light-bg { background: rgba($light, 0.5); } &.light-bg:focus { background: $white; } &.radius-30 { border-radius: 30px; } } } } .select-style-2 { margin-bottom: 30px; .select-position { position: relative; &.select-sm { &::after { margin-top: -8px; } &::before { margin-top: 0; } select { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } } &::before, &::after { content: ""; display: block; height: 8px; width: 8px; pointer-events: none; position: absolute; right: 16px; top: 50%; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out; } &::before { margin-top: 0px; border-bottom: 1px solid $gray; border-right: 1px solid $gray; } &::after { margin-top: -8px; border-top: 1px solid $gray; border-left: 1px solid $gray; } select { width: 100%; background: transparent; border: 1px solid $light-2; border-radius: 10px; padding: 16px; padding-right: 38px; color: $gray; appearance: none; -webkit-appearance: none; -moz-appearance: none; @include transition(0.3s); &:focus { border-color: $primary; outline: none; } &.light-bg { background: rgba($light, 0.5); } &.light-bg:focus { background: $white; } &.select-sm { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } } } } .select-style-3 { margin-bottom: 30px; .select-position { position: relative; &::after { border-bottom: 2px solid $gray; border-right: 2px solid $gray; content: ""; display: block; height: 10px; width: 10px; margin-top: -7px; pointer-events: none; position: absolute; right: 0px; top: 50%; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out; } &.select-sm { &::after { margin-top: -8px; } select { padding-top: 8px; padding-bottom: 8px; font-size: 14px; } } select { width: 100%; background: transparent; border: transparent; border-radius: 10px; padding-right: 38px; color: $black; appearance: none; -webkit-appearance: none; -moz-appearance: none; @include transition(0.3s); &:focus { border-color: $primary; outline: none; } &.light-bg { background: rgba($light, 0.5); } } } } .toggle-switch { padding-left: 60px; min-height: 30px; .form-check-input { width: 50px; height: 28px; margin-left: -60px; cursor: pointer; } label { margin-top: 6px; font-size: 14px; color: $dark; cursor: pointer; user-select: none; } } //* ===== checkbox style */ .checkbox-style { padding-left: 40px; min-height: 28px; .form-check-input { width: 28px; height: 28px; border-radius: 4px; margin-left: -40px; cursor: pointer; &:disabled { cursor: auto; } } .form-check-input:disabled ~ label { cursor: auto; } label { margin-top: 6px; cursor: pointer; user-select: none; } &.checkbox-success { .form-check-input { &:checked { background-color: $success; border-color: $success; } } } &.checkbox-warning { .form-check-input { &:checked { background-color: $warning; border-color: $warning; } } } &.checkbox-danger { .form-check-input { &:checked { background-color: $danger; border-color: $danger; } } } } //* ===== radio style */ .radio-style { padding-left: 40px; min-height: 28px; .form-check-input { width: 28px; height: 28px; border-radius: 50%; margin-left: -40px; cursor: pointer; &:disabled { cursor: auto; } } .form-check-input:disabled ~ label { cursor: auto; } label { margin-top: 6px; cursor: pointer; user-select: none; } &.radio-success { .form-check-input { &:checked { background-color: $success; border-color: $success; } } } &.radio-warning { .form-check-input { &:checked { background-color: $warning; border-color: $warning; } } } &.radio-danger { .form-check-input { &:checked { background-color: $danger; border-color: $danger; } } } } .button-group { @media #{$xs} { .main-btn { width: 100%; } } } .buy-sell-form { .input-group { display: flex; input { width: 60%; background: transparent; border: 1px solid #e2e8f0; border-radius: 4px; padding: 8px 16px; font-size: 14px; color: $gray; &:focus { border-color: $primary; } } .select-style-1 { width: 40%; .select-position { &::after { width: 8px; height: 8px; } } } select { border: 1px solid #e2e8f0; border-radius: 0px 4px 4px 0px; padding: 8px 16px; padding-right: 24px; font-size: 14px; color: $gray; } } .buy-sell-btn { .main-btn { display: block; width: 100%; font-weight: 500; &:hover { box-shadow: $shadow-one; } &.success-btn { background: #08c18d; } &.danger-btn { background: #eb5757; } } } .field-group-2 { label { font-size: 12px; } .input-group { input { font-size: 12px; width: 70%; } span { font-size: 12px; padding: 8px 16px; width: 30%; background: #e2e8f0; text-align: center; border-radius: 0px 4px 4px 0px; border: 1px solid #e2e8f0; } } } .input-group-2 { label { font-size: 12px; color: $gray; margin-bottom: 8px; display: block; } .select-position { &::after { width: 8px; height: 8px; } } select { padding: 8px 12px; font-size: 12px; color: $gray; border: 1px solid #e2e8f0; border-radius: 4px; width: 100%; } } } ================================================ FILE: public/assets/scss/header/_header.scss ================================================ /* ========== header css ========== */ .header { padding: 30px 0; background: $white; .header-left { .menu-toggle-btn { .main-btn { padding: 0px 15px; height: 46px; line-height: 46px; border-radius: 10px; } } .header-search { form { max-width: 270px; position: relative; input { width: 100%; border: 1px solid $light; background: rgba($light, 0.5); border-radius: 10px; height: 46px; padding-left: 44px; @include transition(0.3s); &:focus { border-color: $primary; background: $white; } } button { position: absolute; border: none; background: transparent; left: 16px; top: 0; height: 46px; color: $gray; font-weight: 700; } } } } .header-right { display: flex; justify-content: flex-end; button { border: 1px solid $light; background: rgba($light, 0.5); border-radius: 10px; height: 46px; width: 46px; @include flex-center; position: relative; &::after { display: none; } span { position: absolute; width: 20px; height: 20px; background: $primary; color: $white; border-radius: 50%; @include flex-center; top: -8px; right: -6px; font-size: 12px; font-weight: 500; } } .dropdown-menu { width: 350px; border: 1px solid $light; padding: 10px 10px; @include transition(0.3s); top: 24px !important; right: 0; position: absolute; transform: translate3d(0px, 60px, 0px); border-radius: 10px; li { padding: 3px 0px; @include transition(0.3s); border-bottom: 1px solid $light; position: relative; z-index: 2; &:hover { a { color: $primary; background: rgba($primary, 0.05); } } &:last-child { border-bottom: none; } a { padding: 8px 12px; display: flex; color: $black-70; border-radius: 6px; .image { max-width: 35px; width: 100%; height: 35px; border-radius: 50%; overflow: hidden; margin-right: 12px; img { width: 100%; } } .content { width: 100%; h6 { font-size: 14px; margin-bottom: 5px; font-weight: 600; line-height: 1; } p { font-size: 14px; color: $black-70; margin-bottom: 0px; line-height: 1.4; } span { font-size: 12px; color: $black-50; } } } } } .dropdown-box { position: relative; } .notification-box, .header-message-box { position: relative; } .notification-box { .dropdown-menu { &.dropdown-menu-end { transform: translate3d(0px, 60px, 0px); } } } .header-message-box { .dropdown-menu { &.dropdown-menu-end { transform: translate3d(0px, 60px, 0px); } } } .profile-box { display: flex; position: relative; button { width: auto; } .dropdown-menu { width: 230px; &.dropdown-menu-end { transform: translate3d(0px, 60px, 0px); } li { border-bottom: none; a { font-size: 14px; display: flex; align-items: center; i { margin-right: 15px; font-weight: 700; } } } } .profile-info { margin: 0 5px; .info { display: flex; align-items: center; .image { border: 2px solid #f9f9f9; @include box-shadow(0px 21px 25px rgba(218, 223, 227, 0.8)); width: 46px; height: 46px; border-radius: 50%; margin-left: 16px; position: relative; .status { width: 16px; height: 16px; border-radius: 50%; border: 2px solid $light-2; background: $success; position: absolute; bottom: 0; right: 0; top: auto; } img { width: 100%; border-radius: 50%; } } } } } } } ================================================ FILE: public/assets/scss/icons/_icons.scss ================================================ /* ============== Icons Css ===========*/ .icons-wrapper { .icons, ul { display: flex; flex-wrap: wrap; margin: 0 -10px; & > div, li { display: flex; align-items: center; margin: 10px; flex-basis: 215px; @media (max-width: 400px) { flex-basis: 100%; } i { max-width: 45px; width: 100%; height: 45px; display: flex; align-items: center; justify-content: center; border: 1px solid #efefef; border-radius: 4px; background: transparent; color: $dark; font-size: 20px; margin-right: 10px; } span { color: $dark; user-select: all; } } } } ================================================ FILE: public/assets/scss/invoice/_invoice.scss ================================================ /* =========== Invoice Css ============= */ .invoice-card { .invoice-header { display: flex; flex-wrap: wrap; justify-content: space-between; flex: 1; padding-bottom: 30px; border-bottom: 1px solid rgba($black, 0.1); @media #{$xs} { flex-direction: column; } .invoice-logo { width: 110px; height: 110px; border-radius: 50%; overflow: hidden; @media #{$xs} { order: -1; margin-bottom: 30px; } img { width: 100%; } } .invoice-date { @media #{$xs} { margin-top: 30px; } p { font-size: 14px; font-weight: 400; margin-bottom: 10px; span { font-weight: 500; } } } } .invoice-address { padding-top: 30px; display: flex; margin-bottom: 40px; @media #{$xs} { display: block; } .address-item { margin-right: 30px; min-width: 250px; h5 { margin-bottom: 15px; } h1 { margin-bottom: 10px; font-size: 24px; } p { margin-bottom: 10px; } } } .invoice-action { ul { li { @media #{$xs} { flex: 1; } a { @media #{$xs} { width: 100%; } } } } } } .invoice-table { th, td { padding: 10px 8px; } .service { min-width: 150px; } .desc { min-width: 150px; } .qty { min-width: 150px; } .amount { min-width: 100px; } } ================================================ FILE: public/assets/scss/main.scss ================================================ @import "variables"; @import "mixin"; @import "common"; @import "./typography/typography"; @import "./buttons/buttons"; @import "./alerts/alerts"; @import "./cards/cards"; @import "./tables/tables"; @import "./forms/form-elements"; @import "./notification/notification"; @import "./header/header"; @import "./dashboards/dashboards"; // ======= auth css @import "./auth/signin"; @import "./auth/signup"; @import "./settings/settings"; @import "./invoice/invoice"; @import "./icons/icons"; @import "./calendar/calendar"; @import "sidebar"; @import "default"; ================================================ FILE: public/assets/scss/notification/_notification.scss ================================================ /* ============= notification css ============= */ .single-notification { display: flex; justify-content: space-between; align-items: flex-start; padding: 20px 0; border-bottom: 1px solid $light; &.readed { opacity: 0.7; } &:first-child { padding-top: 0px; } &:last-child { padding-bottom: 0px; border-bottom: 0px; } .checkbox { max-width: 50px; width: 100%; padding-top: 10px; @media #{$xs} { display: none; } input { background-color: $light; border-color: $light-2; &:checked { background-color: $primary; border-color: $primary; } } } .notification { display: flex; width: 100%; @media #{$xs} { flex-direction: column; } .image { max-width: 50px; width: 100%; height: 50px; border-radius: 50%; overflow: hidden; color: $white; @include flex-center; font-weight: 600; margin-right: 15px; @media #{$xs} { margin-bottom: 15px; } img { width: 100%; } } .content { display: block; max-width: 800px; h6 { margin-bottom: 15px; } p { margin-bottom: 10px; } } } .action { display: inline-flex; justify-content: flex-end; padding-top: 10px; @media #{$xs} { display: none; } button { border: none; background: transparent; color: $gray; margin-left: 20px; font-size: 18px; &.delete-btn { &:hover { color: $danger; } } } .dropdown-toggle::after { display: none; } } } ================================================ FILE: public/assets/scss/settings/_settings.scss ================================================ /* =========== settings css ============== */ .settings-card-1 { .profile-info { .profile-image { max-width: 75px; width: 100%; height: 75px; border-radius: 50%; margin-right: 20px; position: relative; z-index: 1; img { width: 100%; border-radius: 50%; } .update-image { position: absolute; bottom: 0; right: 0; width: 30px; height: 30px; background: $light; border: 2px solid $white; @include flex-center; border-radius: 50%; cursor: pointer; z-index: 99; &:hover { opacity: 0.9; } input { opacity: 0; position: absolute; width: 100%; height: 100%; cursor: pointer; z-index: 99; } label { cursor: pointer; z-index: 99; } } } } } ================================================ FILE: public/assets/scss/tables/_tables.scss ================================================ /* =========== tables css =========== */ .table { border-collapse: inherit; border-spacing: 0px; & > :not(caption) > * > * { padding: 15px 0; border-bottom-color: $light; vertical-align: middle; } & > :not(:last-child) > :last-child > * { border-bottom-color: $light; } tbody { tr { &:first-child > * { padding-top: 20px; } &:last-child > * { border-bottom-color: transparent; padding-bottom: 0px; } } } th { h6 { font-weight: 500; color: $dark; font-size: 14px; } } td { &.min-width { padding: 5px; @media #{$xs} { min-width: 150px; } } p { font-size: 14px; line-height: 1.5; color: $gray; a { color: inherit; &:hover { color: $primary; } } } } .lead-info { min-width: 200px; } .lead-email { min-width: 150px; white-space: nowrap; } .lead-phone { min-width: 160px; } .lead-company { min-width: 180px; } .referrals-image { min-width: 150px; .image { width: 55px; max-width: 100%; height: 55px; border-radius: 4px; overflow: hidden; img { width: 100%; } } } .lead { display: flex; align-items: center; .lead-image { max-width: 50px; width: 100%; height: 50px; border-radius: 50%; overflow: hidden; margin-right: 15px; img { width: 100%; } } .lead-text { width: 100%; } } .employee-image { width: 50px; max-width: 100%; height: 50px; border-radius: 50%; overflow: hidden; margin-right: 15px; img { width: 100%; } } .action { display: flex; align-items: center; button { border: none; background: transparent; padding: 0px 6px; font-size: 18px; &.edit { &:hover { color: $primary; } } &::after { display: none; } } .dropdown-menu { @include box-shadow(0px 0px 5px rgba(0, 0, 0, 0.07)); li { &:hover { a { color: $primary !important; } } a { display: block; } } } } } // ===== Top selling table .top-selling-table { tr { th, td { vertical-align: middle; padding: 10px 5px; } .min-width { min-width: 80px; white-space: nowrap; } } .form-check-input[type="checkbox"] { margin-left: 5px; } .product { display: flex; align-items: center; min-width: 150px; .image { border-radius: 4px; overflow: hidden; margin-right: 15px; max-width: 50px; width: 100%; height: 50px; img { width: 100%; } } p { width: 100%; } } } // ===== referrals-table .referrals-table-card { .title { @media #{$xs} { .right { width: 100%; } } @media #{$sm} { .right { width: auto; } } } .referrals-table { td { padding: 10px; } } } /* ===== lead-table ===== */ .lead-table { th, td { padding: 10px 5px; } .name { min-width: 120px; } .email { min-width: 130px; } .project { min-width: 150px; } .status { min-width: 120px; text-align: center; } .action { min-width: 60px; } } // ======== Clients Table .clients-table-card { .table { .employee-info { min-width: 150px; } } } .clients-table { th, td { padding: 5px; &.min-width { min-width: 150px; } } .employee-image { margin-right: 0px; } } ================================================ FILE: public/assets/scss/typography/_typography.scss ================================================ /* ============= typography css ============= */ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { color: $dark; margin: 0; } h1, .h1 { font-size: 32px; font-weight: 700; } h2, .h2 { font-size: 28px; font-weight: 600; } h3, .h3 { font-size: 24px; font-weight: 500; } h4, .h4 { font-size: 20px; font-weight: 600; } h5, .h5 { font-size: 16px; font-weight: 700; } h6, .h6 { font-size: 16px; font-weight: 600; } .text-bold { font-weight: 700; } .text-semi-bold { font-weight: 600; } .text-medium { font-weight: 500; } .text-regular { font-weight: 400; } .text-light { font-weight: 300; } .text-sm { font-size: 14px; line-height: 22px; } /* ========== breadcrumb ============ */ .breadcrumb-wrapper { display: flex; justify-content: flex-end; @media #{$xs} { justify-content: flex-start; } .breadcrumb { li { font-size: 14px; color: $primary; a { color: $gray; &:hover { color: $primary; } } } } } ================================================ FILE: public/casa_cases.csv ================================================ case_number,case_assignment,birth_month_year_youth,next_court_date CINA-01-4347,volunteer1@example.net,March 2010,September 16 2022 CINA-01-4348,"volunteer2@example.net, volunteer3@example.net",September 2015,Jan 6 2023 ================================================ FILE: public/robots.txt ================================================ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file User-agent: * Disallow: / ================================================ FILE: public/sms-terms-conditions.html ================================================ SMS Terms & Conditions

    SMS Terms & Conditions

    Last updated: 05/21/2022

    The CASA mobile message service (the "Service") is operated by "Court Appointed Special Advocate (CASA)/Prince George's County" ("CASA", "PGCasa", "we", or "us"). Your use of the Service constitutes your agreement to these terms and conditions ("Mobile Terms"). We may modify or cancel the Service or any of its features without notice. To the extent permitted by applicable law, we may also modify these Mobile Terms at any time and your continued use of the Service following the effective date of any such changes shall constitute your acceptance of such changes.

    By consenting to CASA's SMS/text messaging service, you agree to receive recurring SMS/text messages from and on behalf of CASA through your wireless provider to the mobile number you provided, even if your mobile number is registered on any state or federal Do Not Call list. Text messages may be sent using an automatic telephone dialing system or other technology. Service-related messages may include updates, alerts, and information (e.g., event reminders, account alerts, password resets, etc.).

    You understand that you do not have to sign up for this program, and your consent is not a condition of any purchase with CASA. Your participation in this program is completely voluntary. We do not charge for the Service, but you are responsible for all charges and fees associated with text messaging imposed by your wireless provider. Message frequency varies. Message and data rates may apply. Check your mobile plan and contact your wireless provider for details. You are solely responsible for all charges related to SMS/text messages, including charges from your wireless provider.

    You may opt-in/out of the Service at any time under your CASA profile page (https://casavolunteertracking.org/). No further messages will be sent to your mobile device unless initiated by you. If you have subscribed to other CASA mobile message programs and wish to cancel, except where applicable law requires otherwise, you will need to opt-out separately from those programs by following the instructions provided in their respective mobile terms. For Service support or assistance, email casa@rubyforgood.org.

    We may change any short-code or telephone number we use to operate the Service at any time and will notify you of these changes. You acknowledge that any messages you send to a short-code or telephone number we have changed may not be received and we will not be responsible for honoring requests made in such messages. The wireless carriers supported by the Service are not liable for delayed or undelivered messages. You agree to provide us with a valid mobile number. If you get a new mobile number, you will need to update your profile with the new number.

    To the extent permitted by applicable law, you agree that we will not be liable for failed, delayed, or misdirected delivery of any information sent through the Service, any errors in such information, and/or any action you may or may not take in reliance on the information or Service.


    ================================================ FILE: public/supervisors.csv ================================================ email,display_name,supervisor_volunteers,phone_number supervisor1@example.net,Supervisor One,volunteer1@example.net,11111111111 supervisor2@example.net,Supervisor Two,"volunteer2@example.net, volunteer3@example.net",11111111111 ================================================ FILE: public/volunteers.csv ================================================ display_name,email,phone_number Volunteer One,volunteer1@example.net,11234567890 Volunteer Two,volunteer2@example.net,11234567891 Volunteer Three,volunteer3@example.net,11234567892 ================================================ FILE: scripts/generate_github_issues_for_missing_spec.rb ================================================ # for every xit test Dir.glob("spec/**/*spec.rb").each do |filename| File.open(filename, "r").readlines.select { |line| line.include?("xit \"") }.each do |xit_line| line_number = $. clean_test_name = xit_line.gsub("xit ", "").gsub(" do\n", "").delete('"').delete("\n").strip # clean_test_name = xit_line.gsub('xit', '').gsub('\"do.*', '').gsub('"', '').gsub("\n", '').strip title = "Fix or remove xit-ignored test in #{filename}:#{line_number} '#{clean_test_name}'" `gh issue create --title "#{title}" --body "#{title}"` end end nil ================================================ FILE: scripts/import_casa_case_date_of_birth.rb ================================================ # This will be run by hand with real prod data in prod console, # because we don't want to check in the data # and we don't want to make Sarah from PG CASA do hundreds of these by hand in the UI # because import didn't have DOB when PG CASA onboarded. # data = """ # 1/21/2000,,,,CINA 11-1234, # 2/22/2000,,,,TPR 12-1234, # 3/23/2000,,,,CINA 13-1234, # """ def update_casa_case_birth_month_year_youth(casa_case, new_date) casa_case.update!(birth_month_year_youth: Date.new(new_date.year, new_date.month, 1)) end def dates_match(casa_case, new_date) casa_case.birth_month_year_youth.year == new_date.year && casa_case.birth_month_year_youth.month == new_date.month end def update_casa_case_dates_of_birth(data, case_not_found, already_has_nonmatching_date, no_edit_made, updated_casa_cases) casa_org = CasaOrg.find_by(name: "Prince George CASA") return "Prince George CASA not found" unless casa_org data.split("\n").map(&:strip).reject(&:empty?).each do |row| chunks = row.split(",").compact d1 = chunks[0] p d1 d2 = Date.strptime(d1, "%m/%d/%Y") # https://ruby-doc.org/stdlib-2.4.1/libdoc/date/rdoc/Date.html#method-i-strftime p d2 case_number = chunks.last cc = CasaCase.find_by(case_number: case_number, casa_org_id: casa_org.id) process_casa_case_date(cc, import_date, case_number, already_has_nonmatching_date, no_edit_made, updated_casa_cases, case_not_found) end end def process_casa_case_date(cc, import_date, case_number, already_has_nonmatching_date, no_edit_made, updated_casa_cases, case_not_found) if cc&.birth_month_year_youth if !dates_match(cc, d2) already_has_nonmatching_date << {case_number: case_number, prev_date: cc.birth_month_year_youth, import_date: d2} else no_edit_made << cc.case_number end elsif cc update_casa_case_birth_month_year_youth(cc, d2) updated_casa_cases << cc.case_number else case_not_found << case_number end {not_found: case_not_found, nonmatching: already_has_nonmatching_date, no_edit_made: no_edit_made, updated_casa_cases: updated_casa_cases} end # data = """ # 1/21/2000,,,,CINA 11-1234, # 2/22/2000,,,,TPR 12-1234, # 3/23/2000,,,,CINA 13-1234, # """ # case_not_found = [] # already_has_nonmatching_date = [] # no_edit_made = [] # updated_casa_cases = [] # r1 = update_casa_case_dates_of_birth(data, case_not_found, already_has_nonmatching_date, no_edit_made, updated_casa_cases) # r1 # puts CasaCase.all.pluck(:case_number, :birth_month_year_youth).map {|i| i.join(", ")}.sort ================================================ FILE: spec/.prosopite_ignore ================================================ # Directories excluded from Prosopite N+1 raise (will still log). # Remove paths as you fix the underlying N+1s. # # See PROSOPITE_TODO.md for the full list of known issues. spec/models spec/services spec/lib spec/system spec/requests spec/controllers spec/views spec/decorators spec/policies spec/datatables spec/helpers spec/seeds spec/mailers spec/presenters spec/config spec/notifications ================================================ FILE: spec/blueprints/api/v1/session_blueprint_spec.rb ================================================ require "rails_helper" RSpec.describe Api::V1::SessionBlueprint do # TODO: Add tests for Api::V1::SessionBlueprint pending "add some tests for Api::V1::SessionBlueprint" end ================================================ FILE: spec/callbacks/case_contact_metadata_callback_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactMetadataCallback do let(:past) { Date.new(2020, 1, 1).in_time_zone } let(:parsed_past) { past.as_json } let(:present) { Date.new(2024, 1, 1).in_time_zone } let(:parsed_present) { present.as_json } before { travel_to present } # NOTE: as you might notice these tests are omitting quite a few cases # ex: notes => details, expenses => notes. I don't think it is worth dealing # with metadata surrounding cases that are not processed in the correct order. # A user would not be able to replicate this behavior. # describe "after_commit" do it "sets started metadata when case contact is created" do cc = create(:case_contact) expect(cc.metadata.dig("status", "active")).to eq(parsed_present) end context "case contact is in started status" do let(:case_contact) { create(:case_contact, :started, created_at: past) } context "updates to started status" do before { case_contact.update(status: "started") } it "does not update the metadata" do expect(case_contact.metadata.dig("status", "started")).to eq(parsed_past) end end context "updates to details status" do before { case_contact.update(status: "details") } it { expect(case_contact.metadata.dig("status", "details")).to eq(parsed_present) } end context "updates to notes status" do before { case_contact.update(status: "notes") } it { expect(case_contact.metadata.dig("status", "notes")).to eq(parsed_present) } end context "updates to expenses status" do before { case_contact.update(status: "expenses") } it { expect(case_contact.metadata.dig("status", "expenses")).to eq(parsed_present) } end context "updates to active status" do before { case_contact.update(status: "active") } it { expect(case_contact.metadata.dig("status", "active")).to eq(parsed_present) } end end context "case contact is in details status" do let(:case_contact) { create(:case_contact, :details, created_at: past) } context "updates to details status" do before { case_contact.update(status: "details") } it "does not update the metadata" do expect(case_contact.metadata.dig("status", "details")).to eq(parsed_past) end end context "updates to notes status" do before { case_contact.update(status: "notes") } it { expect(case_contact.metadata.dig("status", "notes")).to eq(parsed_present) } end context "updates to expenses status" do before { case_contact.update(status: "expenses") } it { expect(case_contact.metadata.dig("status", "expenses")).to eq(parsed_present) } end context "updates to active status" do before { case_contact.update(status: "active") } it { expect(case_contact.metadata.dig("status", "active")).to eq(parsed_present) } end end context "case contact is in notes status" do let(:case_contact) { create(:case_contact, :notes, created_at: past) } context "updates to notes status" do before { case_contact.update(status: "notes") } it { expect(case_contact.metadata.dig("status", "notes")).to eq(parsed_past) } end context "updates to expenses status" do before { case_contact.update(status: "expenses") } it { expect(case_contact.metadata.dig("status", "expenses")).to eq(parsed_present) } end context "updates to active status" do before { case_contact.update(status: "active") } it { expect(case_contact.metadata.dig("status", "active")).to eq(parsed_present) } end end context "case contact is in expenses status" do let!(:case_contact) { create(:case_contact, :expenses, created_at: past) } context "updates to expenses status" do before { case_contact.update(status: "expenses") } it "does not update the metadata" do expect(case_contact.metadata.dig("status", "expenses")).to eq(parsed_past) end end context "updates to active status" do before { case_contact.update(status: "active") } it { expect(case_contact.metadata.dig("status", "active")).to eq(parsed_present) } end end context "case contact is in active status" do let(:case_contact) { create(:case_contact, created_at: past) } context "updates to active status" do before { case_contact.update(status: "active") } it "does not update the metadata" do expect(case_contact.metadata.dig("status", "active")).to eq(parsed_past) end end end end end ================================================ FILE: spec/channels/application_cable/channel_spec.rb ================================================ require "rails_helper" RSpec.describe "Channel", type: :channel do # TODO: Add tests for Channel pending "add some tests for Channel" end ================================================ FILE: spec/channels/application_cable/connection_spec.rb ================================================ require "rails_helper" RSpec.describe "Connection", type: :channel do # TODO: Add tests for Connection pending "add some tests for Connection" end ================================================ FILE: spec/components/badge_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe BadgeComponent, type: :component do it "renders a success badge with only required parameters" do component = described_class.new(text: "success", type: :success) render_inline(component) expect(page).to have_css("span.bg-success", text: "success") expect(page).to have_css(".text-uppercase") expect(page).not_to have_css(".text-dark") expect(page).not_to have_css(".rounded-pill") expect(page).to have_css(".my-1") end it "renders a danger badge changing default parameters" do component = described_class.new(text: "danger", type: :danger, rounded: true, margin: false) render_inline(component) expect(page).to have_css("span.bg-danger", text: "danger") expect(page).to have_css(".text-uppercase") expect(page).not_to have_css(".text-dark") expect(page).to have_css(".rounded-pill") expect(page).not_to have_css(".my-1") end it "renders the dark text badges" do dark_text_badges = ["warning", "light"] dark_text_badges.each do |badge| component = described_class.new(text: badge, type: badge) render_inline(component) expect(page).to have_css("span.bg-#{badge}", text: badge) expect(page).to have_css(".text-dark") end end end ================================================ FILE: spec/components/dropdown_menu_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe DropdownMenuComponent, type: :component do it "renders the dropdown menu with an icon and label" do render_inline(DropdownMenuComponent.new(menu_title: "Example", icon_name: "example-icon")) { "Example Content" } expect(page).to have_css("div.dropdown") expect(page).to have_css("button.btn.btn-secondary.dropdown-toggle span", text: "Example") expect(page).to have_css("button.btn.btn-secondary.dropdown-toggle i.lni.mr-10.lni-example-icon") expect(page).to have_css(".dropdown-menu", text: "Example Content") end it "renders the dropdown menu with a hidden label" do render_inline(DropdownMenuComponent.new(menu_title: "Example", icon_name: "example-icon", hide_label: true)) { "Example Content" } expect(page).to have_css("div.dropdown") expect(page).to have_css("button.btn.btn-secondary.dropdown-toggle span.sr-only", text: "Example") expect(page).to have_css("button.btn.btn-secondary.dropdown-toggle i.lni.mr-10.lni-example-icon") expect(page).to have_css(".dropdown-menu", text: "Example Content") end it "renders the dropdown menu with only a label and content" do render_inline(DropdownMenuComponent.new(menu_title: "Example Title")) { "Example Item" } expect(page).to have_css("div.dropdown") expect(page).to have_css("button.btn.btn-secondary.dropdown-toggle svg") expect(page).to have_css("svg title", text: "Example Title") expect(page).to have_css(".dropdown-menu", text: "Example Item") end it "doesn't render anything if no content provided" do render_inline(DropdownMenuComponent.new(menu_title: nil)) expect(page).not_to have_css("div.dropdown") end it "renders the dropdown menu with additional classes" do render_inline(DropdownMenuComponent.new(menu_title: "Example", klass: "example-class")) { "Example Content" } expect(page).to have_css("div.dropdown.example-class") end it "doesn't render if render_check is false" do render_inline(DropdownMenuComponent.new(menu_title: "Example", render_check: false)) expect(page).not_to have_css("div.dropdown") end end ================================================ FILE: spec/components/form/hour_minute_duration_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Form::HourMinuteDurationComponent, type: :component do let(:case_contact) { build(:case_contact) } let(:form_builder) { ActionView::Helpers::FormBuilder.new(:object, double("object"), ActionView::Base.new(ActionView::LookupContext.new("app/views"), {}, ActionController::Base.new), {}) } it "has initial values set by the component" do minute_value = "1112" hour_value = 256 component = described_class.new(form: form_builder, hour_value: hour_value, minute_value: minute_value) render_inline(component) expect(page.find_css("input[type=number][value=#{minute_value}]").length).to eq(1) expect(page.find_css("input[type=number][value=#{hour_value}]").length).to eq(1) end it "throws errors for incorrect parameters" do expect { described_class.new(form: form_builder, hour_value: "Not a number", minute_value: 10) }.to raise_error(ArgumentError) expect { described_class.new(form: form_builder, hour_value: 10, minute_value: -10) }.to raise_error(RangeError) expect { described_class.new(form: form_builder, hour_value: 10, minute_value: "-10") }.to raise_error(RangeError) expect { described_class.new(form: form_builder, hour_value: false, minute_value: "10") }.to raise_error(TypeError) end end ================================================ FILE: spec/components/form/multiple_select/item_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Form::MultipleSelect::ItemComponent, type: :component do pending "add some examples to (or delete) #{__FILE__}" # it "renders something useful" do # expect( # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html # ).to include( # "Hello, components!" # ) # end end ================================================ FILE: spec/components/form/multiple_select_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Form::MultipleSelectComponent, type: :component do pending "add some examples to (or delete) #{__FILE__}" # it "renders something useful" do # expect( # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html # ).to include( # "Hello, components!" # ) # end end ================================================ FILE: spec/components/local_time_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe LocalTimeComponent, type: :component do it "formats the date using strftime" do component = described_class.new(format: "%b %d, %Y", unix_timestamp: 1693825843, time_zone: ActiveSupport::TimeZone.new("Eastern Time (US & Canada)")) render_inline(component) expect(page).to have_text("Sep 04, 2023") end it "uses the time zone passed to it to format the time" do component = described_class.new(format: "%l:%M %p %Z", unix_timestamp: 1693825843, time_zone: ActiveSupport::TimeZone.new("Eastern Time (US & Canada)")) render_inline(component) expect(page).to have_text("7:10 AM EDT") component = described_class.new(format: "%l:%M %p %Z", unix_timestamp: 1693825843, time_zone: ActiveSupport::TimeZone.new("Central Time (US & Canada)")) render_inline(component) expect(page).to have_text("6:10 AM CDT") component = described_class.new(format: "%l:%M %p %Z", unix_timestamp: 1693825843, time_zone: ActiveSupport::TimeZone.new("Mountain Time (US & Canada)")) render_inline(component) expect(page).to have_text("5:10 AM MDT") component = described_class.new(format: "%l:%M %p %Z", unix_timestamp: 1693825843, time_zone: ActiveSupport::TimeZone.new("Pacific Time (US & Canada)")) render_inline(component) expect(page).to have_text("4:10 AM PDT") end it "has an unambigous detailed date as the title of the element" do component = described_class.new(format: "%l:%M %p %Z", unix_timestamp: 1693825843, time_zone: ActiveSupport::TimeZone.new("Central Time (US & Canada)")) render_inline(component) expect(page.find_css("span").attr("title").value).to have_text("Sep 04, 2023, 6:10 AM CDT") end end ================================================ FILE: spec/components/modal/body_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Modal::BodyComponent, type: :component do it "renders the body with text" do render_inline(Modal::BodyComponent.new(text: "Example Body", klass: "example-class")) expect(page).to have_css("div.modal-body.example-class") expect(page).to have_css("div.modal-body p", text: "Example Body") end it "renders the body with multiple paragraphs" do render_inline(Modal::BodyComponent.new(text: ["Paragraph 1", "Paragraph 2"])) expect(page).to have_css("div.modal-body p", text: "Paragraph 1") expect(page).to have_css("div.modal-body p", text: "Paragraph 2") end it "renders the body with content" do render_inline(Modal::BodyComponent.new) do "Content Override" end expect(page).to have_css("div.modal-body", text: "Content Override") end it "renders the body with content and overrides text" do render_inline(Modal::BodyComponent.new(text: "Example Body")) do "Content Override" end expect(page).to have_css("div.modal-body", text: "Content Override") expect(page).not_to have_css("div.modal-body", text: "Example Body") end it "does not render if text and content missing" do render_inline(Modal::BodyComponent.new) expect(page).not_to have_css("div.modal-body") end it "doesn't render if render_check is false" do render_inline(Modal::BodyComponent.new(text: "Example Body", render_check: false)) expect(page).not_to have_css("div.modal-body") end end ================================================ FILE: spec/components/modal/footer_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Modal::FooterComponent, type: :component do it "renders the footer with content" do render_inline(Modal::FooterComponent.new(klass: "example-class")) do "Footer Content" end expect(page).to have_css("div.modal-footer.example-class") expect(page).to have_css("div.modal-footer button.btn.btn-secondary", text: "Close") expect(page).to have_text("Footer Content") end it "does not render the footer if content missing" do render_inline(Modal::FooterComponent.new) expect(page).not_to have_css("div.modal-footer") end it "doesn't render if render_check is false" do render_inline(Modal::FooterComponent.new(render_check: false)) do "Footer Content" end expect(page).not_to have_css("div.modal-footer") end end ================================================ FILE: spec/components/modal/group_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Modal::GroupComponent, type: :component do before do @component = Modal::GroupComponent.new(id: "exampleModal", klass: "example-class") end it "renders the modal group with header, body, and footer" do @component.with_header(id: "header-id") { "Example Header" } @component.with_body { "Example Body" } @component.with_footer { "Example Footer" } render_inline(@component) expect(page).to have_css("div.modal.fade.example-class#exampleModal") expect(page).to have_css("div.modal-dialog.modal-dialog-centered") expect(page).to have_css("div.modal-content") expect(page).to have_text("Example Header") expect(page).to have_text("Example Body") expect(page).to have_text("Example Footer") end it "renders the modal group with only a header" do @component.with_header(id: "header-id") { "Example Header" } render_inline(@component) expect(page).to have_css("div.modal.fade.example-class#exampleModal") expect(page).to have_css("div.modal-dialog.modal-dialog-centered") expect(page).to have_css("div.modal-content") expect(page).to have_text("Example Header") expect(page).not_to have_css("div.modal-body") expect(page).not_to have_css("div.modal-footer") end it "renders the modal group with only a body" do @component.with_body { "Example Body" } render_inline(@component) expect(page).to have_css("div.modal.fade.example-class#exampleModal") expect(page).to have_css("div.modal-dialog.modal-dialog-centered") expect(page).to have_css("div.modal-content") expect(page).to have_text("Example Body") expect(page).not_to have_css("div.modal-header") expect(page).not_to have_css("div.modal-footer") end it "doesn't render anything if no content provided" do render_inline(@component) expect(page).not_to have_css("div.modal.fade.example-class#exampleModal") expect(page).not_to have_css("div.modal-dialog.modal-dialog-centered") expect(page).not_to have_css("div.modal-content") end it "doesn't render if render_check is false" do @component = Modal::GroupComponent.new(id: "exampleModal", klass: "example-class", render_check: false) @component.with_header(id: "header-id") { "Example Header" } @component.with_body { "Example Body" } @component.with_footer { "Example Footer" } render_inline(@component) expect(page).not_to have_css("div.modal.fade.example-class#exampleModal") expect(page).not_to have_css("div.modal-dialog.modal-dialog-centered") expect(page).not_to have_css("div.modal-content") end end ================================================ FILE: spec/components/modal/header_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Modal::HeaderComponent, type: :component do it "renders the header with text and icon" do render_inline(Modal::HeaderComponent.new(text: "Example Header", id: "modalHeader", icon: "example-icon", klass: "example-class")) expect(page).to have_css("div.modal-header.example-class") expect(page).to have_css("div.modal-header h1#modalHeader-label.modal-title.fs-5") expect(page).to have_css("div.modal-header h1#modalHeader-label i.lni.mr-10.lni-example-icon") expect(page).to have_text("Example Header") expect(page).to have_css("div.modal-header button.btn-close") end it "renders the header with only text" do render_inline(Modal::HeaderComponent.new(text: "Example Header", id: "modalHeader")) expect(page).to have_css("div.modal-header") expect(page).to have_css("div.modal-header h1#modalHeader-label.modal-title.fs-5") expect(page).not_to have_css("div.modal-header i") expect(page).to have_text("Example Header") expect(page).to have_css("div.modal-header button.btn-close") end it "renders the header with content" do render_inline(Modal::HeaderComponent.new(id: "modalHeader")) do "Header Content" end expect(page).to have_css("div.modal-header") expect(page).to have_text("Header Content") expect(page).to have_css("div.modal-header button.btn-close") end it "content overrides text" do render_inline(Modal::HeaderComponent.new(id: "modalHeader", text: "Missing")) do "Header Content" end expect(page).to have_css("div.modal-header") expect(page).to have_text("Header Content") expect(page).not_to have_text("Missing") expect(page).to have_css("div.modal-header button.btn-close") end it "doesn't render anything if both text and content are absent" do render_inline(Modal::HeaderComponent.new(id: "modalHeader")) expect(page).not_to have_css("div.modal-header") end it "doesn't render if render_check is false" do render_inline(Modal::HeaderComponent.new(text: "Example Header", id: "modalHeader", render_check: false)) expect(page).not_to have_css("div.modal-header") end end ================================================ FILE: spec/components/modal/open_button_component_spec.rb ================================================ # frozen_string_literal: true # spec/components/modal/open_button_component_spec.rb require "rails_helper" RSpec.describe Modal::OpenButtonComponent, type: :component do it "renders the button with text and icon" do render_inline(Modal::OpenButtonComponent.new(target: "myModal", text: "Example Text", icon: "example-icon", klass: "example-class")) expect(page).to have_css("button[type='button'][class='example-class'][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).to have_css("button i.lni.mr-10.lni-example-icon") expect(page).to have_text("Example Text") end it "renders the button with only text" do render_inline(Modal::OpenButtonComponent.new(target: "myModal", text: "Example Text")) expect(page).to have_css("button[type='button'][class=''][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).not_to have_css("button i") expect(page).to have_text("Example Text") end it "renders the button with content" do render_inline(Modal::OpenButtonComponent.new(target: "myModal")) do "Example Text" end expect(page).to have_css("button[type='button'][class=''][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).not_to have_css("button i") expect(page).to have_text("Example Text") end it "content overrides text" do render_inline(Modal::OpenButtonComponent.new(target: "myModal", text: "Overwritten")) do "Example Text" end expect(page).to have_css("button[type='button'][class=''][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).not_to have_css("button i") expect(page).to have_text("Example Text") expect(page).not_to have_text("Overwritten") end it "doesn't render anything if both text and content are absent" do render_inline(Modal::OpenButtonComponent.new(target: "myModal")) expect(page).not_to have_css("button") end it "doesn't render if render_check is false" do render_inline(Modal::OpenButtonComponent.new(target: "myModal", text: "Example Text", render_check: false)) expect(page).not_to have_css("button") end end ================================================ FILE: spec/components/modal/open_link_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Modal::OpenLinkComponent, type: :component do it "renders the link with text and icon" do render_inline(Modal::OpenLinkComponent.new(target: "myModal", text: "Example Text", icon: "example-icon", klass: "example-class")) expect(page).to have_css("a[href='#'][role='button'][class='btn example-class'][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).to have_css("a i.lni.mr-10.lni-example-icon") expect(page).to have_text("Example Text") end it "renders the link with only text" do render_inline(Modal::OpenLinkComponent.new(target: "myModal", text: "Example Text")) expect(page).to have_css("a[href='#'][role='button'][class='btn '][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).not_to have_css("a i") expect(page).to have_text("Example Text") end it "renders the link with content" do render_inline(Modal::OpenLinkComponent.new(target: "myModal")) do "Example Text" end expect(page).to have_css("a[href='#'][role='button'][class='btn '][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).not_to have_css("a i") expect(page).to have_text("Example Text") end it "content overrides text" do render_inline(Modal::OpenLinkComponent.new(target: "myModal", text: "Override")) do "Example Text" end expect(page).to have_css("a[href='#'][role='button'][class='btn '][data-bs-toggle='modal'][data-bs-target='#myModal']") expect(page).not_to have_css("a i") expect(page).to have_text("Example Text") expect(page).not_to have_text("Override") end it "doesn't render anything if both text and content are absent" do render_inline(Modal::OpenLinkComponent.new(target: "myModal")) expect(page).not_to have_css("a") end it "doesn't render if render_check is false" do render_inline(Modal::OpenLinkComponent.new(target: "myModal", text: "Example Text", render_check: false)) expect(page).not_to have_css("a") end end ================================================ FILE: spec/components/notification_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe NotificationComponent, type: :component do let(:user) { create(:user, display_name: "John Doe") } let(:casa_case) { create(:casa_case, case_number: "CINA-1234") } let(:followup_with_note) { create(:notification, :followup_with_note, created_by: user) } let(:followup_no_note) { create(:notification, :followup_without_note, created_by: user) } let(:followup_read) { create(:notification, :followup_read, created_by: user) } let(:emancipation_checklist_reminder) { create(:notification, :emancipation_checklist_reminder, casa_case: casa_case) } let(:youth_birthday) { create(:notification, :youth_birthday, casa_case: casa_case) } it "renders a followup with note" do component = described_class.new(notification: followup_with_note) render_inline(component) expect(page).to have_text("New followup") expect(page).to have_text("Note: ") expect(page).to have_text("#{user.display_name} has flagged a Case Contact that needs follow up.") end it "renders a followup without a note" do component = described_class.new(notification: followup_no_note) render_inline(component) expect(page).to have_text("New followup") expect(page).not_to have_text("Note: ") expect(page).to have_text("#{user.display_name} has flagged a Case Contact that needs follow up. Click to see more.") end it "renders read followups with the correct styles" do component = described_class.new(notification: followup_read) render_inline(component) expect(page).to have_css("a.bg-light.text-muted") expect(page).not_to have_css("i.fas.fa-bell") end it "renders an emancipation checklist reminder" do component = described_class.new(notification: emancipation_checklist_reminder) render_inline(component) expect(page).to have_text("Emancipation Checklist Reminder") expect(page).to have_text("Your case #{casa_case.case_number} is a transition aged youth. We want to make sure that along the way, we’re preparing our youth for emancipation. Make sure to check the emancipation checklist.") end it "renders a youth birthday notification" do component = described_class.new(notification: youth_birthday) render_inline(component) expect(page).to have_text("Youth Birthday") expect(page).to have_text("Your youth, case number: #{casa_case.case_number} has a birthday next month.") end end ================================================ FILE: spec/components/previews/truncated_text_component_preview.rb ================================================ class TruncatedTextComponentPreview < ViewComponent::Preview def default render(TruncatedTextComponent.new( Faker::Lorem.paragraph(sentence_count: 3), label: "Some Label" )) end end ================================================ FILE: spec/components/sidebar/group_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Sidebar::GroupComponent, type: :component do before do @component = described_class.new(title: "Group Actions", icon: "list") end it "renders component when rendered links are added" do @component.with_link(title: "Generate Court Reports", icon: "paperclip", path: "/case_court_reports") @component.with_link(title: "Reimbursement Queue", icon: "money-location", path: "/reimbursements") render_inline(@component) expect(page).to have_css "li[class='nav-item nav-item-has-children group-item']" expect(page).to have_css "a[class='group-actions collapsed']" expect(page).to have_css "a[data-bs-target='#ddmenu_group-actions']" expect(page).to have_css "a[aria-controls='ddmenu_group-actions']" expect(page).to have_css "i[class='lni mr-10 lni-list']" expect(page).to have_css "span[data-sidebar-target='linkTitle']", text: "Group Actions" expect(page).to have_css "ul[id='ddmenu_group-actions']" end it "renders links" do @component.with_link(title: "Generate Court Reports", icon: "paperclip", path: "/case_court_reports") @component.with_link(title: "Reimbursement Queue", icon: "money-location", path: "/reimbursements") render_inline(@component) expect(page).to have_css "span[data-sidebar-target='linkTitle']", text: "Generate Court Reports" expect(page).to have_css "span[data-sidebar-target='linkTitle']", text: "Reimbursement Queue" end it "does not render component if no links are added" do render_inline(@component) expect(page).not_to have_css "li[class='nav-item nav-item-has-children group-item']" end it "does not render component if all links are not rendered" do @component.with_link(title: "Generate Court Reports", icon: "paperclip", path: "/case_court_reports", render_check: false) render_inline(@component) end end ================================================ FILE: spec/components/sidebar/link_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe Sidebar::LinkComponent, type: :component do context "component render" do it "is by default" do render_inline(described_class.new(title: "Supervisors", path: "/supervisors", icon: "network")) expect(page).to have_css "span[data-sidebar-target='linkTitle']", text: "Supervisors" expect(page).to have_css "a[href='/supervisors']" expect(page).to have_css "i[class='lni mr-10 lni-network']" end it "doesn't happen if render_check is false" do render_inline(described_class.new(title: "Supervisors", path: "/supervisors", icon: "network", render_check: false)) expect(page).not_to have_css "span[data-sidebar-target='linkTitle']", text: "Supervisors" end end context "icon render" do it "doesn't happen if icon not set" do render_inline(described_class.new(title: "Supervisors", path: "/supervisors")) expect(page).not_to have_css "i" end end context "active class" do it "is rendered if request path matches link's path" do with_request_url "/supervisors" do render_inline(described_class.new(title: "Supervisors", path: "/supervisors", icon: "network")) expect(page).to have_css "li[class='nav-item active']" end end it "is not rendered if request path doesn't match" do with_request_url "/volunteers" do render_inline(described_class.new(title: "Supervisors", path: "/supervisors", icon: "network")) expect(page).to have_css "li[class='nav-item ']" expect(page).to have_no_content "li[class='nav-item active']" end end end end ================================================ FILE: spec/components/truncated_text_component_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe TruncatedTextComponent, type: :component do let(:text) { "This is some sample text." } let(:label) { "Label" } context "when text and a label is provided" do it "renders the component with the provided text" do render_inline(TruncatedTextComponent.new(text, label: label)) expect(page).to have_css(".truncation-container") expect(page).to have_css("div.line-clamp-1", text: text) expect(page).to have_css("span.text-bold", text: label) expect(page).to have_css('a[data-truncated-text-target="moreButton"]', text: "[read more]") expect(page).to have_css('a[data-truncated-text-target="hideButton"]', text: "[hide]") end end context "when text is provided but a label is not" do it "renders the component with the provided content" do render_inline(TruncatedTextComponent.new(text)) expect(page).to have_css(".truncation-container") expect(page).to have_css("div.line-clamp-1", text: text) expect(page).not_to have_css("span.text-bold", text: label) expect(page).to have_css('a[data-truncated-text-target="moreButton"]', text: "[read more]") expect(page).to have_css('a[data-truncated-text-target="hideButton"]', text: "[hide]") end end end ================================================ FILE: spec/config/initializers/rack_attack_spec.rb ================================================ require "rails_helper" RSpec.describe Rack::Attack do include Rack::Test::Methods # https://makandracards.com/makandra/46189-how-to-rails-cache-for-individual-rspec-tests # memory store is per process and therefore no conflicts in parallel tests let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } let(:header) { {"REMOTE_ADDR" => remote_ip} } let(:params) { {} } let(:limit) { 5 } let(:cache) { Rails.cache } before do Rack::Attack.enabled = true ActionController::Base.perform_caching = true allow(Rails).to receive(:cache).and_return(memory_store) Rails.cache.clear freeze_time end after do ActionController::Base.perform_caching = false end def app Rails.application end describe "throttle excessive requests by single IP address" do shared_examples "correctly throttles" do it "changes the request status to 429 if greater than limit" do (limit * 2).times do |i| post path, params, header expect(last_response.status).not_to eq 429 if i < limit expect(last_response.status).to eq(429) if i >= limit end end end it_behaves_like "correctly throttles" do let(:path) { "/users/sign_in" } let(:remote_ip) { "111.200.300.123" } end it_behaves_like "correctly throttles" do let(:path) { "/all_casa_admins/sign_in" } let(:remote_ip) { "111.200.300.456" } end end describe "localhost is not throttled" do let(:remote_ip) { "127.0.0.1" } let(:path) { "/users/sign_in" } it "does not change the request status to 429" do (limit * 2).times do |i| post path, params, header expect(last_response.status).not_to eq(429) if i > limit end end end describe "throttle excessive requests for email login by variety of IP addresses" do shared_examples "correctly throttles" do it "changes the request status to 429 when greater than limit" do (limit * 2).times do |i| header = {"REMOTE_ADDR" => "#{remote_ip}#{i}"} post path, params, header expect(last_response.status).not_to eq 429 if i < limit expect(last_response.status).to eq(429) if i >= limit end end end it_behaves_like "correctly throttles" do let(:user) { create(:user, email: "foo@example.com") } let(:remote_ip) { "189.23.45.1" } let(:path) { "/users/sign_in" } let(:params) { { user: { email: user.email, password: "badpassword" } } } end it_behaves_like "correctly throttles" do let(:user) { create(:all_casa_admin, email: "bar@example.com") } let(:remote_ip) { "199.23.45.1" } let(:path) { "/all_casa_admins/sign_in" } let(:first_block) { "223" } let(:params) { { all_casa_admin: { email: user.email, password: "badpassword" } } } end end context "blocklist" do let(:path) { "/users/sign_in" } context "good ip" do let(:remote_ip) { "101.202.103.104" } it "is not blocked" do post path, params, header expect(last_response.status).not_to eq(403) end end context "bad ips" do # IP_BLOCKLIST environment variable set in config/environments/test.rb shared_examples "blocks request" do it "changes the request status to 403" do post path, params, header expect(last_response.status).to eq(403) end end it_behaves_like "blocks request" do let(:remote_ip) { "4.5.6.7" } end it_behaves_like "blocks request" do let(:remote_ip) { "9.8.7.6" } end it_behaves_like "blocks request" do let(:remote_ip) { "100.101.102.103" } end end end describe "fail2ban" do shared_examples "bans successfully" do it "changes the request status to 403" do head path, params, header expect(last_response.status).to eq(403) end end context "phpmyadmin" do it_behaves_like "bans successfully" do let(:remote_ip) { "1.2.33.4" } let(:path) { "/phpMyAdmin/" } end end context "phpmyadmin4" do it_behaves_like "bans successfully" do let(:remote_ip) { "55.66.77.88" } let(:path) { "/phpMyAdmin4/" } end end context "sql/phpmy-admin" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.66.77.99" } let(:path) { "/sql/phpmy-admin/" } end end context "db/phpmyadmin-32" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.96.77.99" } let(:path) { "/db/phpMyAdmin-3/" } end end context "sqlmanager" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.95.77.99" } let(:path) { "/mysql/mysqlmanager/" } end end context "PMA year" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.94.77.99" } let(:path) { "/PMA2014" } end end context "mysql" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.93.77.99" } let(:path) { "/mysql/dbadmin/" } end end context "config/server" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.92.77.99" } let(:path) { "/config/server" } end end context "config/server" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.91.77.99" } let(:path) { "/_ServerStatus" } end end context "etc/services" do it_behaves_like "bans successfully" do let(:remote_ip) { "44.89.77.99" } let(:path) { "/etc/services" } end end end end ================================================ FILE: spec/controllers/README.md ================================================ Tests for controllers should be in spec/requests ================================================ FILE: spec/controllers/application_controller_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe ApplicationController, type: :controller do let(:volunteer) { create(:volunteer) } controller do def index render plain: "hello there..." end # input => array of urls # output => hash of valid short urls {id => short url/nil} def handle_short_url(url_list) super end def store_referring_location super end def not_authorized_error raise Pundit::NotAuthorizedError end def unknown_organization raise Organizational::UnknownOrganization end end before do # authorize user # sign in as an admin allow(controller).to receive(:authenticate_user!).and_return(true) allow(controller).to receive(:current_user).and_return(volunteer) @short_io_stub = WebMockHelper.short_io_stub_sms @short_io_error_stub = WebMockHelper.short_io_error_stub end describe "#index" do it "does not store URL path for POST" do path = "/index" session_key = "user_return_to" routes.draw { post "index" => "anonymous#index" } post :index expect(session[session_key]).not_to eq path expect(session[session_key]).to be_nil end end describe "handle_short_url" do it "returns a hash of shortened urls" do input_list = ["www.clubpenguin.com", "www.miniclip.com"] output_hash = controller.handle_short_url(input_list) expect(output_hash[0]).to eq("https://42ni.short.gy/jzTwdF") expect(output_hash[1]).to eq("https://42ni.short.gy/jzTwdF") expect(output_hash.length).to eq(2) expect(@short_io_stub).to have_been_requested.times(2) end it "returns a hash with a mix of valid/invalid short urls" do input_list = ["www.clubpenguin.com", "www.badrequest.com", "www.miniclip.com"] output_hash = controller.handle_short_url(input_list) expect(output_hash[1]).to eq(nil) expect(output_hash.length).to eq(3) expect(@short_io_stub).to have_been_requested.times(3) expect(@short_io_error_stub).to have_been_requested.times(1) end end describe "Raise error:" do it "redirects to root_url if rescued Pundit::NotAuthorizedError" do routes.draw { get :not_authorized_error, to: "anonymous#not_authorized_error" } get :not_authorized_error expect(response).to redirect_to(root_url) end it "redirects to root_url if rescued Organizational::UnknownOrganization" do routes.draw { get :unknown_organization, to: "anonymous#unknown_organization" } get :unknown_organization expect(response).to redirect_to(root_url) end end describe "After signin path" do it "is equal to initial path" do routes.draw { get :index, to: "anonymous#index" } get :index path = controller.after_sign_in_path_for(volunteer) expect(path).to eq("/index") end end describe "After signout path" do it "is equal to new_all_casa_admin_session_path" do path = controller.after_sign_out_path_for(:all_casa_admin) expect(path).to eq(new_all_casa_admin_session_path) end it "is equal to root_path" do path = controller.after_sign_out_path_for(volunteer) expect(path).to eq(root_path) end end describe "sms acct creation notice" do it "sms status is blank" do expect(controller.send(:sms_acct_creation_notice, "admin", "blank")).to eq("New admin created successfully.") end it "sms status is error" do expect(controller.send(:sms_acct_creation_notice, "admin", "error")).to eq("New admin created successfully. SMS not sent. Error: .") end it "sms status is sent" do expect(controller.send(:sms_acct_creation_notice, "admin", "sent")).to eq("New admin created successfully. SMS has been sent!") end end describe "#store_referring_location" do it "stores referring location in session if referer is present and not sign in page" do request.env["HTTP_REFERER"] = "http://example.com" controller.store_referring_location expect(session[:return_to]).to eq("http://example.com") end it "does not store referring location if referer is sign in page" do request.env["HTTP_REFERER"] = "http://example.com/users/sign_in" controller.store_referring_location expect(session[:return_to]).to be_nil end it "does not store referring location if referer is not present" do request.env["HTTP_REFERER"] = nil controller.store_referring_location expect(session[:return_to]).to be_nil end end end ================================================ FILE: spec/controllers/concerns/accessible_spec.rb ================================================ require "rails_helper" class MockController < ApplicationController before_action :reset_session, only: :no_session_action include Accessible def action render plain: "controller action test..." end def no_session_action render plain: "controller no session action test..." end end RSpec.describe MockController, type: :controller do let(:admin) { create(:casa_admin) } let(:volunteer) { create(:volunteer) } context "Authenticated user" do around do |example| Rails.application.routes.draw do get :action, to: "mock#action" get :no_session_action, to: "mock#no_session_action" # required routes to make Accessible concern work get :mock_admin, to: "admin#mock", as: :authenticated_all_casa_admin_root get :mock_user, to: "user#mock", as: :authenticated_user_root end example.run Rails.application.reload_routes! end it "redirects to authenticated casa admin root path" do allow(controller).to receive(:authenticate_user!).and_return(true) allow(controller).to receive(:current_all_casa_admin).and_return(admin) get :action expect(response).to redirect_to authenticated_all_casa_admin_root_path end it "redirects to authenticated user root path" do allow(controller).to receive(:authenticate_user!).and_return(true) allow(controller).to receive(:current_user).and_return(volunteer) get :no_session_action expect(response).to redirect_to authenticated_user_root_path end end end ================================================ FILE: spec/controllers/concerns/court_date_params_spec.rb ================================================ require "rails_helper" RSpec.describe CourtDateParams, type: :controller do let(:host) do Class.new do include CourtDateParams attr_accessor :params def initialize(params) @params = params end end end it "exists and defines private API" do expect(described_class).to be_a(Module) expect(host.private_instance_methods) .to include(:sanitized_court_date_params, :court_date_params) end describe "#sanitized_court_date_params" do let(:casa_case) { create(:casa_case) } let(:controller) { host.new(params) } context "when case_court_orders_attributes contains blank entries" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", case_court_orders_attributes: { "0" => {text: "Valid order", implementation_status: "not_implemented"}, "1" => {text: "", implementation_status: ""}, "2" => {text: "Another valid order", implementation_status: "partially_implemented"} } } ) end it "removes entries where both text and implementation_status are blank" do result = controller.send(:sanitized_court_date_params, casa_case) expect(result[:case_court_orders_attributes].keys).to contain_exactly("0", "2") end it "sets casa_case_id for remaining entries" do result = controller.send(:sanitized_court_date_params, casa_case) expect(result[:case_court_orders_attributes]["0"][:casa_case_id]).to eq(casa_case.id) expect(result[:case_court_orders_attributes]["2"][:casa_case_id]).to eq(casa_case.id) end end context "when case_court_orders_attributes has text but blank implementation_status" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", case_court_orders_attributes: { "0" => {text: "Order with text only", implementation_status: ""} } } ) end it "keeps the entry" do result = controller.send(:sanitized_court_date_params, casa_case) expect(result[:case_court_orders_attributes].keys).to include("0") end end context "when case_court_orders_attributes has implementation_status but blank text" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", case_court_orders_attributes: { "0" => {text: "", implementation_status: "implemented"} } } ) end it "keeps the entry" do result = controller.send(:sanitized_court_date_params, casa_case) expect(result[:case_court_orders_attributes].keys).to include("0") end end context "when case_court_orders_attributes is nil" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15" } ) end it "does not raise an error" do expect { controller.send(:sanitized_court_date_params, casa_case) }.not_to raise_error end end context "when case_court_orders_attributes is present" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", case_court_orders_attributes: { "0" => {text: "Test order", implementation_status: "not_implemented"} } } ) end it "returns the court_date parameter" do result = controller.send(:sanitized_court_date_params, casa_case) expect(result[:date]).to eq("2025-10-15") end end end describe "#court_date_params" do let(:casa_case) { create(:casa_case) } let(:controller) { host.new(params) } context "with all permitted attributes" do let(:hearing_type) { create(:hearing_type, casa_org: casa_case.casa_org) } let(:judge) { create(:judge, casa_org: casa_case.casa_org) } let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", hearing_type_id: hearing_type.id, judge_id: judge.id, court_report_due_date: "2025-10-10", case_court_orders_attributes: { "0" => { text: "Test order", implementation_status: "not_implemented", id: "123", casa_case_id: casa_case.id, _destroy: "false" } } } ) end it "permits all allowed attributes" do result = controller.send(:court_date_params, casa_case) expect(result.permitted?).to be true expect(result[:date]).to eq("2025-10-15") expect(result[:hearing_type_id]).to eq(hearing_type.id) expect(result[:judge_id]).to eq(judge.id) expect(result[:court_report_due_date]).to eq("2025-10-10") end it "permits nested case_court_orders_attributes" do result = controller.send(:court_date_params, casa_case) order_attrs = result[:case_court_orders_attributes]["0"] expect(order_attrs[:text]).to eq("Test order") expect(order_attrs[:implementation_status]).to eq("not_implemented") expect(order_attrs[:id]).to eq("123") expect(order_attrs[:casa_case_id]).to eq(casa_case.id) expect(order_attrs[:_destroy]).to eq("false") end end context "with unpermitted attributes" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", unauthorized_field: "should not be permitted", case_court_orders_attributes: { "0" => { text: "Test order", implementation_status: "not_implemented", unauthorized_nested_field: "should not be permitted" } } } ) end it "filters out unpermitted attributes" do result = controller.send(:court_date_params, casa_case) expect(result.to_h.keys).not_to include("unauthorized_field") end it "filters out unpermitted nested attributes" do result = controller.send(:court_date_params, casa_case) order_attrs = result[:case_court_orders_attributes]["0"] expect(order_attrs.to_h.keys).not_to include("unauthorized_nested_field") end end context "when sanitized_court_date_params removes blank orders" do let(:params) do ActionController::Parameters.new( court_date: { date: "2025-10-15", case_court_orders_attributes: { "0" => {text: "Valid order", implementation_status: "not_implemented"}, "1" => {text: "", implementation_status: ""} } } ) end it "only includes non-blank orders in the result" do result = controller.send(:court_date_params, casa_case) expect(result[:case_court_orders_attributes].keys).to eq(["0"]) end end end end ================================================ FILE: spec/controllers/concerns/loads_case_contacts_spec.rb ================================================ require "rails_helper" RSpec.describe LoadsCaseContacts do let(:host) do Class.new do include LoadsCaseContacts end end it "exists and defines private API" do expect(described_class).to be_a(Module) expect(host.private_instance_methods) .to include(:load_case_contacts, :current_organization_groups, :all_case_contacts) end describe "integration with Flipper flags", type: :request do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let!(:casa_case) { create(:casa_case, casa_org: organization) } let!(:case_contact) { create(:case_contact, :active, casa_case: casa_case) } before { sign_in admin } context "when new_case_contact_table flag is enabled" do before do allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(true) end it "loads case contacts successfully through the new design controller" do get case_contacts_new_design_path expect(response).to have_http_status(:success) expect(assigns(:filtered_case_contacts)).to be_present end end context "when new_case_contact_table flag is disabled" do before do allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(false) end it "does not load case contacts and redirects instead" do get case_contacts_new_design_path expect(response).to redirect_to(case_contacts_path) expect(assigns(:filtered_case_contacts)).to be_nil end end end end ================================================ FILE: spec/controllers/concerns/organizational_spec.rb ================================================ require "rails_helper" class MockController < ApplicationController include Organizational end RSpec.describe MockController, type: :controller do it "raises a UnknownOrganization error" do expect { controller.require_organization! }.to raise_error(Organizational::UnknownOrganization) end it "does not raise a UnknownOrganization error" do current_user = create(:volunteer) allow(controller).to receive(:current_user).and_return(current_user) expect { controller.require_organization! }.not_to raise_error end it "returns the user's current organization" do current_user = create(:volunteer) allow(controller).to receive(:current_user).and_return(current_user) expect(controller.current_organization).to eq(current_user.casa_org) end context "when admin" do it "returns the current user role" do current_user = create(:all_casa_admin) allow(controller).to receive(:current_user).and_return(current_user) expect(controller.current_role).to eq(current_user.role) end end context "when admin" do it "returns the current user role" do current_user = create(:all_casa_admin) allow(controller).to receive(:current_user).and_return(current_user) expect(controller.current_role).to eq(current_user.role) end end context "when supervisor" do it "returns the current user role" do current_user = create(:supervisor) allow(controller).to receive(:current_user).and_return(current_user) expect(controller.current_role).to eq(current_user.role) end end context "when volunteer" do it "returns the current user role" do current_user = create(:volunteer) allow(controller).to receive(:current_user).and_return(current_user) expect(controller.current_role).to eq(current_user.role) end end end ================================================ FILE: spec/controllers/concerns/users/time_zone_spec.rb ================================================ require "rails_helper" class MockController < ApplicationController include Users::TimeZone end RSpec.describe MockController, type: :controller do let(:browser_time_zone) { "America/Los_Angeles" } let(:default_time_zone) { "Eastern Time (US & Canada)" } let(:time_date) { Time.zone.now } before do allow(controller).to receive(:cookies).and_return(browser_time_zone: browser_time_zone) end describe "#browser_time_zone" do it "returns the matching time zone" do browser_tz = ActiveSupport::TimeZone.find_tzinfo(browser_time_zone) matching_zone = ActiveSupport::TimeZone.all.find { |zone| zone.tzinfo == browser_tz } expect(controller.browser_time_zone).to eq(matching_zone || Time.zone) end context "when browser_time_zone cookie is not set" do before do allow(controller).to receive(:cookies).and_return({}) end it "returns the default time zone" do expect(controller.browser_time_zone).to eq(Time.zone) end end context "when browser_time_zone cookie contains an invalid value" do before do allow(controller).to receive(:cookies).and_return(browser_time_zone: "Invalid/Timezone") end it "returns the default time zone" do expect(controller.browser_time_zone).to eq(Time.zone) end end end describe "#to_user_timezone" do it "takes a time_date and converts it to user's time zone" do expected = controller.to_user_timezone(time_date) returned = time_date.in_time_zone(browser_time_zone) expect(expected.zone).to eq(returned.zone) expect(expected.day).to eq(returned.day) expect(expected.hour).to eq(returned.hour) end context "when invalid param is sent" do it "returns the empty string for nil param" do expect(controller.to_user_timezone(nil)).to eq("") end it "returns empty string if empty string param provided" do expect(controller.to_user_timezone("")).to eq("") end it "returns nil for invalid date string" do expect(controller.to_user_timezone("invalid-date")).to eq(nil) end end end describe "#user_timezone" do context "when browser time zone has an invalid value" do before do allow(controller).to receive(:cookies).and_return(browser_time_zone: "Invalid/Timezone") end it "returns the default time zone" do expect(controller.user_timezone).to eq(default_time_zone) end end context "when browser time zone is not set" do before do allow(controller).to receive(:cookies).and_return({}) end it "returns the default time zone" do expect(controller.user_timezone).to eq(default_time_zone) end end end end ================================================ FILE: spec/controllers/emancipations_controller_spec.rb ================================================ require "rails_helper" RSpec.describe EmancipationsController, type: :controller do let(:organization) { create(:casa_org) } let(:other_org) { create(:casa_org) } let(:user) { create(:supervisor, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization, birth_month_year_youth: 20.years.ago) } let(:non_transition_case) { create(:casa_case, :pre_transition, casa_org: organization) } let(:emancipation_category) { create(:emancipation_category) } let(:emancipation_option) { create(:emancipation_option, emancipation_category: emancipation_category) } before { sign_in user } describe "GET #show" do context "when authenticated and authorized" do it "returns http success" do get :show, params: {casa_case_id: casa_case.friendly_id} expect(response).to have_http_status(:success) end it "assigns @current_case" do get :show, params: {casa_case_id: casa_case.friendly_id} expect(assigns(:current_case)).to eq(casa_case) end it "assigns @emancipation_form_data with all categories" do get :show, params: {casa_case_id: casa_case.friendly_id} expect(assigns(:emancipation_form_data)).to match_array(EmancipationCategory.all) end end context "when case does not exist" do it "raises a record not found error" do expect { get :show, params: {casa_case_id: "nonexistent-case"} }.to raise_error(ActiveRecord::RecordNotFound) end end context "when user belongs to a different org" do let(:user) { create(:supervisor, casa_org: other_org) } it "redirects to root with an authorization notice" do get :show, params: {casa_case_id: casa_case.friendly_id} expect(response).to redirect_to(root_url) expect(flash[:notice]).to match(/not authorized/) end end context "docx format" do it "sends a docx file with the correct filename" do get :show, params: {casa_case_id: casa_case.friendly_id}, format: :docx expect(response.headers["Content-Disposition"]).to include( "#{casa_case.case_number} Emancipation Checklist.docx" ) end end end describe "POST #save" do def post_save(action, check_item_id, case_id: casa_case.friendly_id) post :save, params: { casa_case_id: case_id, check_item_action: action, check_item_id: check_item_id }, format: :json end # Authorization context "when user belongs to a different org" do let(:user) { create(:supervisor, casa_org: other_org) } it "returns unauthorized with a json error message" do post_save("add_category", emancipation_category.id) expect(response).to have_http_status(:unauthorized) expect(json_response["error"]).to match(/not authorized/) end end # Case not found context "when casa_case_id does not match any case" do it "returns 404 with a descriptive error" do post_save("add_category", emancipation_category.id, case_id: "nonexistent-id") expect(response).to have_http_status(:not_found) expect(json_response["error"]).to match(/Could not find case/) end end # Non-transitioning case context "when the case is not in transition age" do it "returns bad_request" do post_save("add_category", emancipation_category.id, case_id: non_transition_case.friendly_id) expect(response).to have_http_status(:bad_request) expect(json_response["error"]).to match(/not marked as transitioning/) end end # Unsupported action context "when check_item_action is not supported" do it "returns bad_request with unsupported action message" do post_save("unsupported_action", emancipation_category.id) expect(response).to have_http_status(:bad_request) expect(json_response["error"]).to match(/not a supported action/) end end # ADD_CATEGORY context "with action: add_category" do it "adds the category to the case and returns success" do expect { post_save("add_category", emancipation_category.id) }.to change { casa_case.emancipation_categories.count }.by(1) expect(response).to have_http_status(:ok) expect(json_response).to eq("success") end it "returns bad_request when category is already associated" do casa_case.add_emancipation_category(emancipation_category.id) post_save("add_category", emancipation_category.id) expect(response).to have_http_status(:bad_request) expect(json_response["error"]).to match(/already exists/) end it "returns bad_request when category id does not exist" do post_save("add_category", -1) expect(response).to have_http_status(:bad_request) end end # ADD_OPTION context "with action: add_option" do it "adds the option to the case and returns success" do expect { post_save("add_option", emancipation_option.id) }.to change { casa_case.emancipation_options.count }.by(1) expect(response).to have_http_status(:ok) expect(json_response).to eq("success") end it "returns bad_request when option is already associated" do casa_case.add_emancipation_option(emancipation_option.id) post_save("add_option", emancipation_option.id) expect(response).to have_http_status(:bad_request) expect(json_response["error"]).to match(/already exists/) end it "returns bad_request when option id does not exist" do post_save("add_option", -1) expect(response).to have_http_status(:bad_request) end end # DELETE_CATEGORY context "with action: delete_category" do before do casa_case.add_emancipation_category(emancipation_category.id) casa_case.add_emancipation_option(emancipation_option.id) end it "removes the category and its associated options from the case" do post_save("delete_category", emancipation_category.id) expect(response).to have_http_status(:ok) expect(json_response).to eq("success") expect(casa_case.reload.emancipation_categories).not_to include(emancipation_category) expect(casa_case.reload.emancipation_options).not_to include(emancipation_option) end it "returns bad_request when category is not associated with the case" do other_category = create(:emancipation_category) post_save("delete_category", other_category.id) expect(response).to have_http_status(:bad_request) expect(json_response["error"]).to match(/does not exist/) end end # DELETE_OPTION context "with action: delete_option" do before { casa_case.add_emancipation_option(emancipation_option.id) } it "removes the option from the case and returns success" do expect { post_save("delete_option", emancipation_option.id) }.to change { casa_case.emancipation_options.count }.by(-1) expect(response).to have_http_status(:ok) expect(json_response).to eq("success") end it "returns bad_request when option is not associated with the case" do other_option = create(:emancipation_option, emancipation_category: emancipation_category) post_save("delete_option", other_option.id) expect(response).to have_http_status(:bad_request) expect(json_response["error"]).to match(/does not exist/) end end # SET_OPTION context "with action: set_option" do let(:other_option) { create(:emancipation_option, emancipation_category: emancipation_category) } before { casa_case.add_emancipation_option(other_option.id) } it "replaces the existing option in the same category with the new one" do post_save("set_option", emancipation_option.id) expect(response).to have_http_status(:ok) expect(json_response).to eq("success") expect(casa_case.reload.emancipation_options).to include(emancipation_option) expect(casa_case.reload.emancipation_options).not_to include(other_option) end it "returns bad_request when option id does not exist" do post_save("set_option", -1) expect(response).to have_http_status(:bad_request) end end end # JSON error handler for unauthorized access from save describe "#not_authorized" do let(:user) { create(:supervisor, casa_org: other_org) } it "renders a json unauthorized error when called from save" do post :save, params: { casa_case_id: casa_case.friendly_id, check_item_action: "add_category", check_item_id: emancipation_category.id }, format: :json expect(response).to have_http_status(:unauthorized) expect(json_response["error"]).to match(/not authorized/) end end # Helper to parse JSON responses def json_response JSON.parse(response.body) end end ================================================ FILE: spec/controllers/learning_hours/volunteers_controller_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHours::VolunteersController, type: :controller do # TODO: Add tests for LearningHours::VolunteersController pending "add some tests for LearningHours::VolunteersController" end ================================================ FILE: spec/controllers/users/sessions_controller_spec.rb ================================================ require "rails_helper" RSpec.describe Users::SessionsController, type: :controller do # TODO: Add tests for Users::SessionsController pending "add some tests for Users::SessionsController" end ================================================ FILE: spec/datatables/application_datatable_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationDatatable do # TODO: Add tests for ApplicationDatatable pending "add some tests for ApplicationDatatable" end ================================================ FILE: spec/datatables/case_contact_datatable_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe CaseContactDatatable do let(:organization) { create(:casa_org) } let(:supervisor) { create(:supervisor, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:contact_type) { create(:contact_type, casa_org: organization) } let(:params) do { draw: "1", start: "0", length: "10", search: {value: search_term}, order: {"0" => {column: order_column, dir: order_direction}}, columns: { "0" => {name: "occurred_at", orderable: "true"}, "1" => {name: "contact_made", orderable: "true"}, "2" => {name: "medium_type", orderable: "true"}, "3" => {name: "duration_minutes", orderable: "true"} } } end let(:search_term) { "" } let(:order_column) { "0" } let(:order_direction) { "desc" } let(:base_relation) { organization.case_contacts } let(:current_user) { create(:casa_admin, casa_org: organization) } subject(:datatable) { described_class.new(base_relation, params, current_user) } describe "#data" do let!(:case_contact) do create(:case_contact, casa_case: casa_case, creator: volunteer, occurred_at: 2.days.ago, contact_made: true, medium_type: "in-person", duration_minutes: 60, notes: "Test notes") end let!(:contact_topic) { create(:contact_topic, casa_org: organization) } before do case_contact.contact_types << contact_type create(:contact_topic_answer, case_contact: case_contact, contact_topic: contact_topic) end it "returns an array of case contact data" do expect(datatable.as_json[:data]).to be_an(Array) end it "includes case contact attributes" do contact_data = datatable.as_json[:data].first expect(contact_data[:id]).to eq(case_contact.id.to_s) expect(contact_data[:contact_made]).to eq("true") expect(contact_data[:medium_type]).to eq("In Person") expect(contact_data[:duration_minutes]).to eq("60") end it "includes formatted occurred_at date" do contact_data = datatable.as_json[:data].first expected_date = I18n.l(case_contact.occurred_at, format: :full, default: nil) expect(contact_data[:occurred_at]).to eq(expected_date) end it "includes casa_case data" do contact_data = datatable.as_json[:data].first expect(contact_data[:casa_case][:id]).to eq(casa_case.id.to_s) expect(contact_data[:casa_case][:case_number]).to eq(casa_case.case_number) end it "includes contact_types as comma-separated string" do contact_data = datatable.as_json[:data].first expect(contact_data[:contact_types]).to include(contact_type.name) end it "includes creator data" do contact_data = datatable.as_json[:data].first expect(contact_data[:creator][:id]).to eq(volunteer.id.to_s) expect(contact_data[:creator][:display_name]).to eq(volunteer.display_name) expect(contact_data[:creator][:email]).to eq(volunteer.email) expect(contact_data[:creator][:role]).to eq("Volunteer") end it "includes contact_topics as pipe-separated string" do contact_data = datatable.as_json[:data].first expect(contact_data[:contact_topics]).to include(contact_topic.question) end it "includes is_draft status" do contact_data = datatable.as_json[:data].first expect(contact_data[:is_draft]).to eq((!case_contact.active?).to_s) end context "with action metadata" do it "includes all expected action metadata keys" do contact_data = datatable.as_json[:data].first expect(contact_data).to include(:can_edit, :can_destroy, :edit_path, :followup_id, :has_followup) end it "includes edit_path for the contact" do contact_data = datatable.as_json[:data].first expected_path = Rails.application.routes.url_helpers.edit_case_contact_path(case_contact) expect(contact_data[:edit_path]).to eq(expected_path) end it "includes followup_id as empty string when no requested followup exists" do contact_data = datatable.as_json[:data].first expect(contact_data[:followup_id]).to eq("") end it "includes followup_id when a requested followup exists" do followup = create(:followup, case_contact: case_contact, status: "requested") contact_data = datatable.as_json[:data].first expect(contact_data[:followup_id]).to eq(followup.id.to_s) end it "does not include followup_id for a resolved followup" do create(:followup, case_contact: case_contact, status: "resolved") contact_data = datatable.as_json[:data].first expect(contact_data[:followup_id]).to eq("") end end context "with permission flags" do context "when current_user is an admin" do it "sets can_edit to true" do expect(datatable.as_json[:data].first[:can_edit]).to eq("true") end it "sets can_destroy to true" do expect(datatable.as_json[:data].first[:can_destroy]).to eq("true") end end context "when current_user is the volunteer who created the contact" do let(:current_user) { volunteer } it "sets can_edit to true" do expect(datatable.as_json[:data].first[:can_edit]).to eq("true") end it "sets can_destroy to false for an active contact" do expect(datatable.as_json[:data].first[:can_destroy]).to eq("false") end end context "when current_user is the volunteer who created a draft contact" do let(:current_user) { volunteer } let!(:case_contact) do create(:case_contact, casa_case: casa_case, creator: volunteer, status: "started") end it "sets can_destroy to true for their own draft" do expect(datatable.as_json[:data].first[:can_destroy]).to eq("true") end end end context "when case_contact has no casa_case (draft)" do let!(:draft_contact) do build(:case_contact, casa_case: nil, creator: volunteer, occurred_at: 1.day.ago).tap do |cc| cc.save(validate: false) end end it "handles nil casa_case gracefully" do draft_data = datatable.as_json[:data].find { |d| d[:id] == draft_contact.id.to_s } # The sanitize method converts nil to empty string expect(draft_data[:casa_case][:id]).to eq("") expect(draft_data[:casa_case][:case_number]).to eq("") end end context "with followups" do it "sets has_followup to true when requested followup exists" do create(:followup, case_contact: case_contact, status: "requested") contact_data = datatable.as_json[:data].first expect(contact_data[:has_followup]).to eq("true") end it "sets has_followup to false when no requested followup exists" do contact_data = datatable.as_json[:data].first expect(contact_data[:has_followup]).to eq("false") end it "sets has_followup to false when followup is resolved" do create(:followup, case_contact: case_contact, status: "resolved") contact_data = datatable.as_json[:data].first expect(contact_data[:has_followup]).to eq("false") end end end describe "search functionality" do let!(:john_contact) do create(:case_contact, casa_case: casa_case, creator: create(:volunteer, display_name: "John Doe", email: "john@example.com"), notes: "Meeting with youth") end let!(:jane_contact) do create(:case_contact, casa_case: create(:casa_case, casa_org: organization, case_number: "CASA-2024-001"), creator: create(:volunteer, display_name: "Jane Smith", email: "jane@example.com"), notes: "Phone call") end let!(:family_contact_type) { create(:contact_type, name: "Family", casa_org: organization) } let!(:school_contact_type) { create(:contact_type, name: "School", casa_org: organization) } before do john_contact.contact_types << family_contact_type jane_contact.contact_types << school_contact_type end context "searching by creator display_name" do let(:search_term) { "John" } it "returns matching case contacts" do expect(datatable.as_json[:data].pluck(:id)).to include(john_contact.id.to_s) expect(datatable.as_json[:data].pluck(:id)).not_to include(jane_contact.id.to_s) end end context "searching by creator email" do let(:search_term) { "jane@example.com" } it "returns matching case contacts" do expect(datatable.as_json[:data].pluck(:id)).to include(jane_contact.id.to_s) expect(datatable.as_json[:data].pluck(:id)).not_to include(john_contact.id.to_s) end end context "searching by case number" do let(:search_term) { "2024-001" } it "returns matching case contacts" do expect(datatable.as_json[:data].pluck(:id)).to include(jane_contact.id.to_s) expect(datatable.as_json[:data].pluck(:id)).not_to include(john_contact.id.to_s) end end context "searching by notes" do let(:search_term) { "Meeting" } it "returns matching case contacts" do expect(datatable.as_json[:data].pluck(:id)).to include(john_contact.id.to_s) expect(datatable.as_json[:data].pluck(:id)).not_to include(jane_contact.id.to_s) end end context "searching by contact_type name" do let(:search_term) { "Family" } it "returns matching case contacts" do expect(datatable.as_json[:data].pluck(:id)).to include(john_contact.id.to_s) expect(datatable.as_json[:data].pluck(:id)).not_to include(jane_contact.id.to_s) end end context "with case-insensitive search" do let(:search_term) { "JOHN" } it "returns matching case contacts regardless of case" do expect(datatable.as_json[:data].pluck(:id)).to include(john_contact.id.to_s) end end context "with partial search term" do let(:search_term) { "Smi" } it "returns matching case contacts with partial match" do expect(datatable.as_json[:data].pluck(:id)).to include(jane_contact.id.to_s) end end context "with blank search term" do let(:search_term) { "" } it "returns all case contacts" do expect(datatable.as_json[:data].pluck(:id)).to include(john_contact.id.to_s, jane_contact.id.to_s) end end context "with no matching results" do let(:search_term) { "NonexistentName" } it "returns empty array" do expect(datatable.as_json[:data]).to be_empty end end end describe "ordering" do let!(:old_contact) do create(:case_contact, casa_case: casa_case, creator: volunteer, occurred_at: 5.days.ago, contact_made: false, medium_type: "text/email", duration_minutes: 30) end let!(:recent_contact) do create(:case_contact, casa_case: casa_case, creator: volunteer, occurred_at: 1.day.ago, contact_made: true, medium_type: "in-person", duration_minutes: 90) end context "ordering by occurred_at" do let(:order_column) { "0" } context "descending" do let(:order_direction) { "desc" } it "orders contacts by occurred_at descending" do ids = datatable.as_json[:data].pluck(:id) expect(ids).to eq([recent_contact.id.to_s, old_contact.id.to_s]) end end context "ascending" do let(:order_direction) { "asc" } it "orders contacts by occurred_at ascending" do ids = datatable.as_json[:data].pluck(:id) expect(ids).to eq([old_contact.id.to_s, recent_contact.id.to_s]) end end end context "ordering by contact_made" do let(:order_column) { "1" } let(:order_direction) { "desc" } it "orders contacts by contact_made" do ids = datatable.as_json[:data].pluck(:id) expect(ids.first).to eq(recent_contact.id.to_s) end end context "ordering by medium_type" do let(:order_column) { "2" } let(:order_direction) { "asc" } it "orders contacts by medium_type" do ids = datatable.as_json[:data].pluck(:id) expect(ids.first).to eq(recent_contact.id.to_s) end end context "ordering by duration_minutes" do let(:order_column) { "3" } let(:order_direction) { "desc" } it "orders contacts by duration_minutes" do ids = datatable.as_json[:data].pluck(:id) expect(ids).to eq([recent_contact.id.to_s, old_contact.id.to_s]) end end end describe "pagination" do let!(:contacts) do 25.times.map do |i| create(:case_contact, casa_case: casa_case, creator: volunteer, occurred_at: i.days.ago) end end context "first page" do let(:params) do super().merge(start: "0", length: "10") end it "returns first 10 records" do expect(datatable.as_json[:data].length).to eq(10) end it "returns correct recordsTotal" do expect(datatable.as_json[:recordsTotal]).to eq(25) end it "returns correct recordsFiltered" do expect(datatable.as_json[:recordsFiltered]).to eq(25) end end context "second page" do let(:params) do super().merge(start: "10", length: "10") end it "returns next 10 records" do expect(datatable.as_json[:data].length).to eq(10) end end context "last page with partial results" do let(:params) do super().merge(start: "20", length: "10") end it "returns remaining 5 records" do expect(datatable.as_json[:data].length).to eq(5) end end context "with search filtering" do let!(:searchable_contact) do create(:case_contact, casa_case: casa_case, creator: create(:volunteer, display_name: "UniqueSearchName", casa_org: organization), occurred_at: 1.day.ago) end let(:search_term) { "UniqueSearchName" } it "paginates filtered results" do expect(datatable.as_json[:data].length).to eq(1) expect(datatable.as_json[:recordsFiltered]).to eq(1) expect(datatable.as_json[:recordsTotal]).to eq(26) end end end describe "#as_json" do let!(:case_contact) do create(:case_contact, casa_case: casa_case, creator: volunteer) end it "returns hash with data, recordsFiltered, and recordsTotal" do json = datatable.as_json expect(json).to have_key(:data) expect(json).to have_key(:recordsFiltered) expect(json).to have_key(:recordsTotal) end it "sanitizes HTML in data" do contact_with_html = create(:case_contact, casa_case: casa_case, creator: volunteer, notes: "") json = datatable.as_json contact_data = json[:data].find { |d| d[:id] == contact_with_html.id.to_s } # Note: The sanitize method in ApplicationDatatable should escape HTML expect(contact_data).to be_present end end describe "N+1 queries" do let!(:contacts) do 3.times.map do create(:case_contact, casa_case: create(:casa_case, casa_org: organization), creator: create(:volunteer, casa_org: organization)) end end it "does not trigger N+1 queries for casa_org when computing per-row policy permissions" do casa_org_queries = [] subscription = ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload| casa_org_queries << payload[:sql] if payload[:sql].match?(/SELECT.*casa_orgs/) end datatable.as_json ActiveSupport::Notifications.unsubscribe(subscription) # With proper eager loading, casa_orgs is fetched in at most 2 batch queries # (one for :casa_org through casa_case, one for :creator_casa_org through creator). # An N+1 would fire 1 query per record (3+ here). expect(casa_org_queries.length).to be <= 2 end end describe "associations loading" do let!(:contacts) do 10.times.map do |i| contact = create(:case_contact, casa_case: create(:casa_case, casa_org: organization), creator: create(:volunteer, casa_org: organization), occurred_at: i.days.ago) contact_type = create(:contact_type, casa_org: organization) contact.contact_types << contact_type contact_topic = create(:contact_topic, casa_org: organization) create(:contact_topic_answer, case_contact: contact, contact_topic: contact_topic) contact end end it "loads all associations efficiently with includes" do # This test verifies that the datatable returns data successfully # with proper includes to prevent N+1 queries json = datatable.as_json expect(json[:data].length).to eq(10) expect(json[:data].first).to have_key(:contact_types) expect(json[:data].first).to have_key(:contact_topics) expect(json[:data].first).to have_key(:creator) expect(json[:data].first).to have_key(:casa_case) end end end ================================================ FILE: spec/datatables/reimbursement_datatable_spec.rb ================================================ require "rails_helper" RSpec.describe "ReimbursementDatatable" do let(:org) { CasaOrg.first } let(:case_contacts) { CaseContact.joins(:casa_case) } let(:instance) { described_class.new(case_contacts, params) } let(:json_result) { instance.as_json } let(:first_result) { json_result[:data].first } let(:order_by) { nil } let(:order_direction) { nil } let(:page) { 1 } let(:per_page) { 10 } let(:params) do datatable_params( order_by: order_by, order_direction: order_direction, page: page, per_page: per_page ) end # Requires the following to be defined: # - `sorted_case_contacts` = array of reimbursement records ordered in the expected way RSpec.shared_examples_for "a sorted results set" do it "orders ascending by default" do expect(first_result[:id]).to eq(sorted_case_contacts.first.id.to_s) end describe "explicit ascending order" do let(:order_direction) { "ASC" } it "orders correctly" do expect(first_result[:id]).to eq(sorted_case_contacts.first.id.to_s) end end describe "descending order" do let(:order_direction) { "DESC" } it "orders correctly" do expect(first_result[:id]).to eq(sorted_case_contacts.last.id.to_s) end end end describe "the data shape" do let(:first_contact) { case_contacts.first } before do create(:case_contact, casa_case: create(:casa_case)) end describe ":casa_case" do subject(:casa_case) { first_result[:casa_case] } it { is_expected.to include(id: first_contact.casa_case.id.to_s) } it { is_expected.to include(case_number: first_contact.casa_case.case_number.to_s) } end describe ":volunteer" do subject(:volunteer) { first_result[:volunteer] } it { is_expected.to include(id: first_contact.creator.id.to_s) } it { is_expected.to include(display_name: first_contact.creator.display_name.to_s) } it { is_expected.to include(email: first_contact.creator.email.to_s) } it { is_expected.to include(address: first_contact.creator.address.to_s) } end describe ":contact_types" do subject(:contact_types) { first_result[:contact_types] } let(:expected_contact_types) do first_contact.contact_types.map do |ct| { name: ct.name, group_name: ct.contact_type_group.name } end end it { is_expected.to eq(expected_contact_types) } end describe ":occurred_at" do subject(:occurred_at) { first_result[:occurred_at] } it { is_expected.to eq(first_contact.occurred_at.to_s) } end describe ":miles_driven" do subject(:miles_driven) { first_result[:miles_driven] } it { is_expected.to eq(first_contact.miles_driven.to_s) } end describe ":complete" do subject(:complete) { first_result[:complete] } it { is_expected.to eq(first_contact.reimbursement_complete.to_s) } end describe ":mark_as_complete_path" do subject(:mark_as_complete_path) { first_result[:mark_as_complete_path] } it { is_expected.to eq("/reimbursements/#{first_contact.id}/mark_as_complete") } end end describe "multiple record handling" do before do possible_miles_driven_values = (0..100).to_a.shuffle possible_occurred_at_offsets = (0..100).to_a.shuffle 5.times.collect do casa_case = create(:casa_case) 3.times.collect do create( :case_contact, casa_case: casa_case, occurred_at: Time.new - possible_occurred_at_offsets.pop, miles_driven: possible_miles_driven_values.pop ) end.reverse end.flatten end it "has the correct recordsFiltered" do expect(json_result[:recordsFiltered]).to eq(15) end it "has the correct recordsTotal" do expect(json_result[:recordsTotal]).to eq(15) end it "yields the correct number of records" do expect(json_result[:data].size).to eq 10 end describe "order by creator display name" do let(:order_by) { "display_name" } let(:sorted_case_contacts) do case_contacts.sort_by { |case_contact| case_contact.creator.display_name } end it_behaves_like "a sorted results set" end describe "order by created at" do let(:order_by) { "occurred_at" } let(:sorted_case_contacts) do case_contacts.sort_by { |case_contact| case_contact.occurred_at } end it_behaves_like "a sorted results set" end describe "order by miles driven" do let(:order_by) { "miles_driven" } let(:sorted_case_contacts) do case_contacts.sort_by { |case_contact| case_contact.miles_driven } end it_behaves_like "a sorted results set" end describe "order by case number" do let(:order_by) { "case_number" } let(:sorted_case_contacts) { case_contacts.sort_by { |case_contact| case_contact.casa_case.case_number } } let(:first_case_number) { first_result[:casa_case][:case_number] } let(:lowest_case_number) { sorted_case_contacts.first.casa_case.case_number } it "orders ascending by default" do expect(first_case_number).to eq(lowest_case_number) end describe "explicit ascending order" do let(:order_direction) { "ASC" } it "orders correctly" do expect(first_case_number).to eq(lowest_case_number) end end describe "descending order" do let(:order_direction) { "DESC" } let(:highest_case_number) { sorted_case_contacts.last.casa_case.case_number } it "orders correctly" do expect(first_case_number).to eq(highest_case_number) end end end end end ================================================ FILE: spec/datatables/supervisor_datatable_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorDatatable do subject { described_class.new(org.supervisors, params).as_json } let(:org) { create(:casa_org) } let(:order_by) { "display_name" } let(:order_direction) { "asc" } let(:params) { datatable_params(order_by: nil, additional_filters: additional_filters) } describe "filter" do let!(:active_supervisor) { create(:supervisor, casa_org: org, active: true) } let!(:inactive_supervisor) { create(:supervisor, casa_org: org, active: false) } describe "active" do context "when active" do let(:additional_filters) { {active: %w[true]} } it "brings only active supervisors", :aggregate_failures do expect(subject[:recordsTotal]).to eq(2) expect(subject[:recordsFiltered]).to eq(1) expect(subject[:data].pluck(:display_name)).to include(CGI.escapeHTML(active_supervisor.display_name)) expect(subject[:data].pluck(:display_name)).not_to include(CGI.escapeHTML(inactive_supervisor.display_name)) end end context "when inactive" do let(:additional_filters) { {active: %w[false]} } it "brings only inactive supervisors", :aggregate_failures do expect(subject[:recordsTotal]).to eq(2) expect(subject[:recordsFiltered]).to eq(1) expect(subject[:data].pluck(:display_name)).to include(CGI.escapeHTML(inactive_supervisor.display_name)) expect(subject[:data].pluck(:display_name)).not_to include(CGI.escapeHTML(active_supervisor.display_name)) end end context "when both" do let(:additional_filters) { {active: %w[false true]} } let!(:inactive_supervisor) { create(:supervisor, casa_org: org, active: true, display_name: "Neil O'Reilly") } it "brings only all supervisors", :aggregate_failures do expect(subject[:recordsTotal]).to eq(2) expect(subject[:recordsFiltered]).to eq(2) expect(subject[:data].pluck(:display_name)).to include(CGI.escapeHTML(active_supervisor.display_name)) expect(subject[:data].pluck(:display_name)).to include(CGI.escapeHTML(inactive_supervisor.display_name)) end end context "when no selection" do let(:additional_filters) { {active: []} } it "brings nothing", :aggregate_failures do expect(subject[:recordsTotal]).to eq(2) expect(subject[:recordsFiltered]).to eq(0) end end end end end ================================================ FILE: spec/datatables/volunteer_datatable_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerDatatable do let(:org) { CasaOrg.first } let(:supervisors) { Supervisor.all } let(:assigned_volunteers) { Volunteer.joins(:supervisor) } let(:subject) { described_class.new(org.volunteers, params).as_json } let(:additional_filters) do { active: %w[false true], supervisor: supervisors.pluck(:id), transition_aged_youth: %w[false true] } end let(:order_by) { "display_name" } let(:order_direction) { "asc" } let(:page) { 1 } let(:per_page) { 10 } let(:search_term) { nil } let(:params) do datatable_params( additional_filters: additional_filters, order_by: order_by, order_direction: order_direction, page: page, per_page: per_page, search_term: search_term ) end describe ":has_transition_aged_youth_cases" do let(:volunteer_has_transition_aged_youth) { subject[:data].first[:has_transition_aged_youth_cases] } let(:non_transitional_birth) { 10.years.ago } let(:transitional_birth) { 16.years.ago } let(:org) { build :casa_org } let(:active_assignment) { true } context "with a volunteer with a case assignment" do let(:casa_case) { build :casa_case, casa_org: org, birth_month_year_youth: youth_month_year } let(:supervisor) { create :supervisor, casa_org: org } let(:volunteer) { create :volunteer, casa_org: org, supervisor: supervisor } before do create :case_assignment, volunteer: volunteer, casa_case: casa_case, active: active_assignment end context "which has a non-transition aged case" do let(:youth_month_year) { non_transitional_birth } it "is 'false'" do expect(volunteer_has_transition_aged_youth).to eq "false" end end context "which had (but no longer has) a transition aged case" do let(:youth_month_year) { transitional_birth } let(:active_assignment) { false } it "is 'false'" do expect(volunteer_has_transition_aged_youth).to eq "false" end end context "which has a transition aged case" do let(:youth_month_year) { transitional_birth } it "is 'true'" do expect(volunteer_has_transition_aged_youth).to eq "true" end end end end context "with supervisors who have volunteers who have cases" do before :all do DatabaseCleaner.strategy = :transaction DatabaseCleaner.start org = build(:casa_org) supervisors = create_list :supervisor, 3, casa_org: org supervisors.each do |supervisor| volunteers = create_list :volunteer, 2, casa_org: org, supervisor: supervisor volunteers.each_with_index do |volunteer, idx| volunteer.casa_cases << build(:casa_case, casa_org: org, birth_month_year_youth: (idx == 1) ? 10.years.ago : 16.years.ago) end end create_list :volunteer, 2, casa_org: org end after :all do DatabaseCleaner.clean end describe "order by" do let(:values) { subject[:data] } let(:check_attr_equality) do lambda { |model, idx| expect(values[idx][:id]).to eq model.id.to_s } end let(:check_asc_order) do lambda { sorted_models.each_with_index(&check_attr_equality) } end let(:check_desc_order) do lambda { sorted_models.reverse.each_with_index(&check_attr_equality) } end describe "display_name" do let(:order_by) { "display_name" } let(:sorted_models) { assigned_volunteers.order :display_name } context "when ascending" do it "is successful" do check_asc_order.call end end context "when descending" do let(:order_direction) { "desc" } it "is succesful" do check_desc_order.call end end end describe "email" do let(:order_by) { "email" } let(:sorted_models) { assigned_volunteers.order :email } context "when ascending" do it "is successful" do check_asc_order.call end end context "when descending" do let(:order_direction) { "desc" } it "is successful" do check_desc_order.call end end end describe "supervisor_name" do let(:order_by) { "supervisor_name" } let(:sorted_models) { assigned_volunteers.sort_by { |v| v.supervisor.display_name } } context "when ascending" do it "is successful" do sorted_models.each_with_index do |model, idx| expect(CGI.unescapeHTML(values[idx][:supervisor][:name])).to eq model.supervisor.display_name end end end context "when descending" do let(:order_direction) { "desc" } let(:sorted_models) { assigned_volunteers.sort_by { |v| v.supervisor.display_name } } it "is successful" do sorted_models.reverse.each_with_index do |model, idx| expect(CGI.unescapeHTML(values[idx][:supervisor][:name])).to eq model.supervisor.display_name end end end end describe "active" do let(:order_by) { "active" } let(:sorted_models) { assigned_volunteers.order :active, :id } before do supervisors.each { |s| s.volunteers.first.update active: false } end context "when ascending" do it "is successful" do check_asc_order.call end end context "when descending" do let(:order_direction) { "desc" } let(:sorted_models) { assigned_volunteers.order :active, id: :desc } it "is successful" do check_desc_order.call end end end describe "has_transition_aged_youth_cases" do let(:order_by) { "has_transition_aged_youth_cases" } let(:transition_aged_youth_bool_to_int) do lambda { |volunteer| volunteer.casa_cases.exists?(birth_month_year_youth: ..CasaCase::TRANSITION_AGE.years.ago) ? 1 : 0 } end let(:sorted_models) { assigned_volunteers.sort_by(&transition_aged_youth_bool_to_int) } context "when ascending" do it "is successful" do sorted_models.each_with_index do |model, idx| expect(values[idx][:has_transition_aged_youth_cases]).to eq model.casa_cases.exists?(birth_month_year_youth: ..CasaCase::TRANSITION_AGE.years.ago).to_s end end end context "when descending" do let(:order_direction) { "desc" } let(:sorted_models) { assigned_volunteers.sort_by(&transition_aged_youth_bool_to_int) } it "is successful" do sorted_models.reverse.each_with_index do |model, idx| expect(values[idx][:has_transition_aged_youth_cases]).to eq model.casa_cases.exists?(birth_month_year_youth: ..CasaCase::TRANSITION_AGE.years.ago).to_s end end end end describe "most_recent_attempt_occurred_at" do let(:order_by) { "most_recent_attempt_occurred_at" } let(:sorted_models) do assigned_volunteers.order(:id).sort_by { |v| v.case_contacts.maximum :occurred_at } end before do CasaCase.all.each_with_index { |cc, idx| cc.case_contacts << build(:case_contact, contact_made: true, creator: cc.volunteers.first, occurred_at: idx.days.ago) } end context "when ascending" do it "is successful" do check_asc_order.call end end context "when descending" do let(:order_direction) { "desc" } it "is successful" do check_desc_order.call end end end describe "contacts_made_in_past_days" do let(:order_by) { "contacts_made_in_past_days" } let(:volunteer1) { assigned_volunteers.first } let(:casa_case1) { volunteer1.casa_cases.first } let(:volunteer2) { assigned_volunteers.second } let(:casa_case2) { volunteer2.casa_cases.first } let(:sorted_models) do assigned_volunteers .order(:id) .sort_by { |v| v.case_contacts.where(occurred_at: 60.days.ago.to_date..).count } .sort_by { |v| v.case_contacts.exists?(occurred_at: 60.days.ago.to_date..) ? 0 : 1 } end before do 4.times do |i| create(:case_contact, contact_made: true, casa_case: casa_case1, creator: volunteer1, occurred_at: (19 * (i + 1)).days.ago) end 3.times do |i| create(:case_contact, contact_made: true, casa_case: casa_case2, creator: volunteer2, occurred_at: (29 * (i + 1)).days.ago) end end context "when ascending" do it "is successful" do expect(values.pluck(:contacts_made_in_past_days)).to eq ["2", "3", "", "", "", ""] end end context "when descending" do let(:order_direction) { "desc" } let(:sorted_models) do assigned_volunteers .order(id: :desc) .sort_by { |v| v.case_contacts.where(occurred_at: 60.days.ago.to_date..).count } end it "is successful" do expect(values.pluck(:contacts_made_in_past_days)).to eq ["3", "2", "", "", "", ""] end it "moves blanks to the end" do expect(values[0][:contacts_made_in_past_days]).not_to be_blank end end end end describe "search" do let(:volunteer) { assigned_volunteers.first } let(:search_term) { volunteer.display_name } describe "recordsTotal" do it "includes all volunteers" do expect(subject[:recordsTotal]).to eq org.volunteers.count end end describe "recordsFiltered" do it "includes filtered volunteers" do expect(subject[:recordsFiltered]).to eq 1 end end describe "display_name" do it "is successful" do expect(subject[:data].length).to eq 1 expect(subject[:data].first[:id]).to eq volunteer.id.to_s end end describe "email" do let(:search_term) { volunteer.email } it "is successful" do expect(subject[:data].length).to eq 1 expect(subject[:data].first[:id]).to eq volunteer.id.to_s end end describe "supervisor_name" do let(:supervisor) { volunteer.supervisor } let(:search_term) { supervisor.display_name } let(:volunteers) { supervisor.volunteers } it "is successful" do expect(subject[:data].length).to eq volunteers.count expect(subject[:data].pluck(:id).sort).to eq volunteers.map { |v| v.id.to_s }.sort end end describe "case_numbers" do let(:casa_case) { volunteer.casa_cases.first } let(:search_term) { casa_case.case_number } # Sometimes the default case number is a substring of other case numbers before { casa_case.update case_number: Random.hex } it "is successful" do expect(subject[:data].length).to eq 1 expect(subject[:data].first[:id]).to eq volunteer.id.to_s end context "when search term case number matches unassigned case" do let(:new_supervisor) { create(:supervisor, casa_org: casa_case.casa_org) } let(:new_casa_case) { create(:casa_case, :pre_transition, casa_org: casa_case.casa_org, case_number: "ABC-123") } let(:volunteer_1) { create(:volunteer, display_name: "Volunteer 1", casa_org: casa_case.casa_org, supervisor: new_supervisor) } let(:volunteer_2) { create(:volunteer, display_name: "Volunteer 2", casa_org: casa_case.casa_org, supervisor: new_supervisor) } let(:search_term) { new_casa_case.case_number } before do create(:case_assignment, casa_case: new_casa_case, volunteer: volunteer_1) create(:case_assignment, casa_case: new_casa_case, volunteer: volunteer_2, active: false) end it "does not include unassigned case matches in search results" do expect(subject[:data].length).to eq 1 expect(subject[:data].first[:id]).to eq volunteer_1.id.to_s end end end end describe "filter" do describe "supervisor" do context "when unassigned excluded" do it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to eq assigned_volunteers.count end end context "when unassigned included" do before { additional_filters[:supervisor] << nil } it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to eq Volunteer.count end end context "when no selection" do before { additional_filters[:supervisor] = [] } it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to be_zero end end end describe "active" do before { assigned_volunteers.limit(3).update_all active: "false" } context "when active" do before { additional_filters[:active] = %w[true] } it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to eq assigned_volunteers.where(active: true).count end end context "when inactive" do before { additional_filters[:active] = %w[false] } it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to eq assigned_volunteers.where(active: false).count end end context "when both" do before { additional_filters[:active] = %w[false true] } it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to eq assigned_volunteers.count end end context "when no selection" do before { additional_filters[:active] = [] } it "is successful" do expect(subject[:recordsTotal]).to eq Volunteer.count expect(subject[:recordsFiltered]).to be_zero end end end describe "transition_aged_youth" do context "when yes" do before { additional_filters[:transition_aged_youth] = %w[true] } it "is successful" do expect(subject[:recordsTotal]).to eq 8 expect(subject[:recordsFiltered]).to eq 3 end end context "when no" do before { additional_filters[:transition_aged_youth] = %w[false] } it "is successful" do expect(subject[:recordsTotal]).to eq 8 expect(subject[:recordsFiltered]).to eq 3 end end context "when both" do before { additional_filters[:transition_aged_youth] = %w[false true] } it "is successful" do expect(subject[:recordsTotal]).to eq 8 expect(subject[:recordsFiltered]).to eq 6 end end context "when no selection" do before { additional_filters[:transition_aged_youth] = [] } it "is successful" do expect(subject[:recordsTotal]).to eq 8 expect(subject[:recordsFiltered]).to be_zero end end end end describe "pagination" do let(:page) { 2 } let(:per_page) { 5 } it "is successful" do expect(subject[:data].length).to eq assigned_volunteers.count - 5 end describe "recordsTotal" do it "includes all volunteers" do expect(subject[:recordsTotal]).to eq org.volunteers.count end end describe "recordsFiltered" do it "includes all filtered volunteers" do expect(subject[:recordsFiltered]).to eq assigned_volunteers.count end end end end describe "extra_languages filter" do let(:org) { build :casa_org } let(:supervisor) { create :supervisor, casa_org: org } let(:volunteer_with_language) { create :volunteer, casa_org: org, supervisor: supervisor } let(:volunteer_without_language) { create :volunteer, casa_org: org, supervisor: supervisor } let(:language) { create :language, casa_org: org } before do create :user_language, user: volunteer_with_language, language: language end context "when filtering for volunteers with extra languages with default ordering" do # Use an invalid order_by to trigger the default COALESCE ordering # which causes PG::InvalidColumnReference when combined with DISTINCT let(:order_by) { "invalid_column" } let(:additional_filters) do { active: %w[false true], supervisor: [supervisor.id], transition_aged_youth: %w[false true], extra_languages: %w[true] } end it "does not raise PG::InvalidColumnReference error" do # Bug: PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY # expressions must appear in select list # This occurs because extra_languages filter adds .distinct but the default # ORDER BY COALESCE(users.display_name, users.email) is not in the SELECT list expect { subject }.not_to raise_error end it "returns only volunteers with languages" do expect(subject[:data].map { |d| d[:id].to_i }).to contain_exactly(volunteer_with_language.id) end end context "when filtering for volunteers without extra languages with default ordering" do let(:order_by) { "invalid_column" } let(:additional_filters) do { active: %w[false true], supervisor: [supervisor.id], transition_aged_youth: %w[false true], extra_languages: %w[false] } end it "does not raise PG::InvalidColumnReference error" do expect { subject }.not_to raise_error end end context "when filtering with multiple extra_languages options with default ordering" do let(:order_by) { "invalid_column" } let(:additional_filters) do { active: %w[false true], supervisor: [supervisor.id], transition_aged_youth: %w[false true], extra_languages: %w[true false] } end it "does not raise PG::InvalidColumnReference error" do expect { subject }.not_to raise_error end end end end ================================================ FILE: spec/decorators/android_app_association_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe AndroidAppAssociationDecorator do end ================================================ FILE: spec/decorators/application_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationDecorator, type: :decorator do # TODO: Add tests for ApplicationDecorator pending "add some tests for ApplicationDecorator" end ================================================ FILE: spec/decorators/casa_case_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCaseDecorator do describe "#court_report_submission" do subject { casa_case.decorate.court_report_submission } let(:casa_case) { build(:casa_case, court_report_status: court_report_status) } context "when case_report_status is not_submitted" do let(:court_report_status) { :not_submitted } it { is_expected.to eq("Not submitted") } end context "when case_report_status is submitted" do let(:court_report_status) { :submitted } it { is_expected.to eq("Submitted") } end context "when case_report_status is in_review" do let(:court_report_status) { :in_review } it { is_expected.to eq("In review") } end context "when case_report_status is completed" do let(:court_report_status) { :completed } it { is_expected.to eq("Completed") } end end describe "#court_report_submission" do subject { casa_case.decorate.court_report_submitted_date } let(:submitted_time) { Time.parse("Sun Nov 08 11:06:20 2020") } let(:casa_case) { build(:casa_case, court_report_submitted_at: submitted_time) } it { is_expected.to eq "November 8, 2020" } context "when report is not submitted" do let(:submitted_time) { nil } it { is_expected.to be_nil } end end describe "#formatted_updated_at" do subject { casa_case.decorate.formatted_updated_at } let(:updated_at_time) { Time.parse("Wed Dec 9 12:51:20 2020") } let(:casa_case) { build(:casa_case, updated_at: updated_at_time) } it { is_expected.to eq "12-09-2020" } end describe "#transition_age_youth" do it "returns transition age youth status with icon if not transition age youth && birthday is nil" do casa_case = build(:casa_case, birth_month_year_youth: nil) expect(casa_case.decorate.transition_aged_youth) .to eq "No #{CasaCase::NON_TRANSITION_AGE_YOUTH_ICON}" end it "returns transition age youth status with icon if over 14 years old" do casa_case = build_stubbed(:casa_case, birth_month_year_youth: CasaCase::TRANSITION_AGE.years.ago) expect(casa_case.decorate.transition_aged_youth) .to include "Yes #{CasaCase::TRANSITION_AGE_YOUTH_ICON}" expect(casa_case.decorate.transition_aged_youth).to include "Emancipation" end it "returns non-transition age youth status with icon if not over 14 years old" do casa_case = build(:casa_case, birth_month_year_youth: 13.years.ago) expect(casa_case.decorate.transition_aged_youth) .to eq "No #{CasaCase::NON_TRANSITION_AGE_YOUTH_ICON}" end end describe "#transition_age_youth_icon" do it "returns transition age youth status with icon if not transition age youth && birthday is nil" do casa_case = build(:casa_case, birth_month_year_youth: nil) expect(casa_case.decorate.transition_aged_youth_icon) .to eq CasaCase::NON_TRANSITION_AGE_YOUTH_ICON end it "returns transition age youth icon if over 14 years old" do casa_case = build(:casa_case, birth_month_year_youth: CasaCase::TRANSITION_AGE.years.ago) expect(casa_case.decorate.transition_aged_youth_icon) .to eq CasaCase::TRANSITION_AGE_YOUTH_ICON end it "returns non-transition age youth icon if not over 14 years old" do casa_case = build(:casa_case, birth_month_year_youth: 13.years.ago) expect(casa_case.decorate.transition_aged_youth_icon) .to eq CasaCase::NON_TRANSITION_AGE_YOUTH_ICON end end describe "#emancipation_checklist_count" do it "returns a fraction indicating how many emancipation categories have been fulfilled" do casa_case = build(:casa_case) expect(casa_case).to( receive(:casa_case_emancipation_categories).and_return( double(:categories, count: 2) ) ) expect(EmancipationCategory).to receive(:count).and_return(5) expect(casa_case.decorate.emancipation_checklist_count).to eq "2 / 5" end end end ================================================ FILE: spec/decorators/case_assignment_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe CaseAssignmentDecorator, type: :decorator do # TODO: Add tests for CaseAssignmentDecorator pending "add some tests for CaseAssignmentDecorator" end ================================================ FILE: spec/decorators/case_contact_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactDecorator do let(:case_contact) { build(:case_contact) } describe "#duration_minutes" do context "when duration_minutes is less than 60" do let(:case_contact) { build(:case_contact, duration_minutes: 30) } it "returns only minutes" do expect(case_contact.decorate.duration_minutes).to eq "30 minutes" end end context "when duration_minutes is greater than 60" do let(:case_contact) { build(:case_contact, duration_minutes: 135) } it "returns minutes and hours" do case_contact.update_attribute(:duration_minutes, 135) expect(case_contact.decorate.duration_minutes).to eq "2 hours 15 minutes" end context "when is exactly on hour" do let(:case_contact) { build(:case_contact, duration_minutes: 120) } it "returns only hours" do expect(case_contact.decorate.duration_minutes).to eq "2 hours" end end end context "when minutes is nil" do let(:case_contact) { build(:case_contact, duration_minutes: nil) } it "returns not set" do expect(case_contact.decorate.duration_minutes).to eq "Duration not set" end end end describe "#contact_made" do context "when contact_made is false" do it "returns No Contact Made" do case_contact.update_attribute(:contact_made, false) expect(case_contact.decorate.contact_made).to eq "No Contact Made" end end context "when contact_made is true" do it "returns Yes" do case_contact.update_attribute(:contact_made, true) expect(case_contact.decorate.contact_made).to be_nil end end end describe "#contact_types" do subject(:contact_types) { decorated_case_contact.contact_types } let(:case_contact) { build(:case_contact, contact_types: contact_types) } let(:decorated_case_contact) do described_class.new(case_contact) end context "when the contact_types is an empty array" do let(:contact_types) { [] } it { is_expected.to eql("No contact type specified") } end context "when the contact_types is an array with three or more values" do let(:contact_types) do [ build_stubbed(:contact_type, name: "School"), build_stubbed(:contact_type, name: "Therapist"), build_stubbed(:contact_type, name: "Bio Parent") ] end it { is_expected.to eql("School, Therapist, and Bio Parent") } end context "when the contact types is an array with less than three values" do let(:contact_types) do [ build_stubbed(:contact_type, name: "School"), build_stubbed(:contact_type, name: "Therapist") ] end it { is_expected.to eql("School and Therapist") } end end describe "#medium_icon_classes" do context "when medium type is in-person" do it "returns the proper font-awesome classes" do case_contact.update_attribute(:medium_type, "in-person") expect(case_contact.decorate.medium_icon_classes).to eql("lni lni-users") end end context "when medium type is text/email" do it "returns the proper font-awesome classes" do case_contact.update_attribute(:medium_type, "text/email") expect(case_contact.decorate.medium_icon_classes).to eql("lni lni-envelope") end end context "when medium type is video" do it "returns the proper font-awesome classes" do case_contact.update_attribute(:medium_type, "video") expect(case_contact.decorate.medium_icon_classes).to eql("lni lni-camera") end end context "when medium type is voice-only" do it "returns the proper font-awesome classes" do case_contact.update_attribute(:medium_type, "voice-only") expect(case_contact.decorate.medium_icon_classes).to eql("lni lni-phone") end end context "when medium type is letter" do it "returns the proper font-awesome classes" do case_contact.update_attribute(:medium_type, "letter") expect(case_contact.decorate.medium_icon_classes).to eql("lni lni-empty-file") end end context "when medium type is anything else" do it "returns the proper font-awesome classes" do case_contact.update_attribute(:medium_type, "foo") expect(case_contact.decorate.medium_icon_classes).to eql("lni lni-question-circle") end end end describe "#subheading" do let(:contact_group) { build_stubbed(:contact_type_group, name: "Group X") } let(:contact_type) { build_stubbed(:contact_type, contact_type_group: contact_group, name: "Type X") } context "when all information is available" do it "returns a properly formatted string" do case_contact.update(occurred_at: "2020-12-01", duration_minutes: 99, contact_made: false, miles_driven: 100, want_driving_reimbursement: true) case_contact.contact_types = [contact_type] expect(case_contact.decorate.subheading).to eq( "December 1, 2020 | 1 hour 39 minutes | No Contact Made | 100 miles driven | Reimbursement" ) end end context "when some information is missing" do it "returns a properly formatted string without extra pipes" do case_contact.update(occurred_at: "2020-12-01", duration_minutes: 99, contact_made: true, miles_driven: 100, want_driving_reimbursement: true) case_contact.contact_types = [contact_type] expect(case_contact.decorate.subheading).to eq( "December 1, 2020 | 1 hour 39 minutes | 100 miles driven | Reimbursement" ) end end end end ================================================ FILE: spec/decorators/case_contacts/form_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContacts::FormDecorator do end ================================================ FILE: spec/decorators/contact_type_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTypeDecorator do let(:casa_org) { create(:casa_org) } let(:contact_type_group) { create(:contact_type_group, casa_org: casa_org) } let(:contact_type) { create(:contact_type, contact_type_group: contact_type_group) } describe "hash_for_multi_select_with_cases" do it "returns hash" do hash = contact_type.decorate.hash_for_multi_select_with_cases([]) expect(hash[:value]).to eq contact_type.id expect(hash[:text]).to eq contact_type.name expect(hash[:group]).to eq contact_type_group.name expect(hash[:subtext]).to eq "never" end context "with nil array" do it { expect(contact_type.decorate.hash_for_multi_select_with_cases(nil).class).to eq Hash } end end describe "last_time_used_with_cases" do subject { contact_type.decorate.last_time_used_with_cases casa_case_ids } let(:casa_case_ids) { [] } context "with empty array" do it { is_expected.to eq "never" } end context "with cases" do let(:casa_case) { create(:casa_case, casa_org: casa_org) } let(:casa_case_ids) { [casa_case.id] } context "with no case contacts" do it { expect(contact_type.decorate.last_time_used_with_cases([])).to eq "never" } end context "with case contacts" do let(:case_contact1) { create(:case_contact, casa_case: casa_case, occurred_at: 4.days.ago) } let(:case_contact2) { create(:case_contact, casa_case: casa_case, occurred_at: 3.days.ago) } it "is the most recent case contact" do case_contact1.contact_types << contact_type expect(subject).to eq "4 days ago" case_contact2.contact_types << contact_type expect(contact_type.decorate.last_time_used_with_cases(casa_case_ids)).to eq "3 days ago" end context "when case_contact occurred_at is nil" do let(:case_contact1) { build :case_contact, casa_case: casa_case, occurred_at: nil } it "returns 'never'" do case_contact1.contact_types << contact_type expect(subject).to eq "never" end end end end end end ================================================ FILE: spec/decorators/court_date_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe CourtDateDecorator, type: :decorator do # TODO: Add tests for CourtDateDecorator pending "add some tests for CourtDateDecorator" end ================================================ FILE: spec/decorators/learning_hour_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourDecorator, type: :decorator do # TODO: Add tests for LearningHourDecorator pending "add some tests for LearningHourDecorator" end ================================================ FILE: spec/decorators/learning_hour_topic_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourTopicDecorator, type: :decorator do # TODO: Add tests for LearningHourTopicDecorator pending "add some tests for LearningHourTopicDecorator" end ================================================ FILE: spec/decorators/other_duty_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe OtherDutyDecorator do let(:other_duty) { build(:other_duty) } describe "#duration_minutes" do context "when duration_minutes is less than 60" do it "returns only minutes" do other_duty.update_attribute(:duration_minutes, 45) expect(other_duty.decorate.duration_in_minutes).to eq "45 minutes" end end context "when duration_minutes is greater than 60" do it "returns minutes and hours" do other_duty.update_attribute(:duration_minutes, 182) expect(other_duty.decorate.duration_in_minutes).to eq "3 hours 2 minutes" end end end describe "#truncate_notes" do let(:truncated_od) { build(:other_duty, notes: "I have no fear, for fear is the little death that kills me over and over. Without fear, I die but once.") } context "when notes length is shorter than limit" do it "returns notes completely" do other_duty.update_attribute(:notes, "Short note.") expect(other_duty.decorate.truncate_notes).to eq("

    Short note.

    ") end end context "when notes length is bigger than limit" do it "returns a truncated string" do expect(truncated_od.decorate.truncate_notes).to eq( "

    I have no fear, for fear is the little death...

    " ) end end end end ================================================ FILE: spec/decorators/patch_note_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe PatchNoteDecorator do end ================================================ FILE: spec/decorators/placement_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactDecorator do let(:placement) { build(:placement) } let(:date_string) { "April 14, 2023" } before do placement.update_attribute(:placement_started_at, Date.new(2023, 4, 14)) end describe "#formatted_date" do it "returns correctly formatted date" do expect(placement.decorate.formatted_date).to eq date_string end end describe "#placement_info" do it "returns the correct placement info string" do expect(placement.decorate.placement_info).to eq "Started At: #{date_string} - Placement Type: #{placement.placement_type.name}" end end end ================================================ FILE: spec/decorators/user_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe UserDecorator do let(:decorated_user) { user.decorate } let(:user) { create(:user) } describe "#status" do context "when user role is inactive" do it "returns Inactive" do volunteer = build(:volunteer, :inactive) expect(volunteer.decorate.status).to eq "Inactive" end end context "when user role is volunteer" do it "returns Active" do volunteer = build(:volunteer) expect(volunteer.decorate.status).to eq "Active" end end end describe "#formatted_created_at" do context "when using the 'default'format string" it "returns the correctly formatted date" do user.update(created_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.created_at, format: :full, default: nil) expect(decorated_user.formatted_created_at).to eq expected_date end context "when passing in the custom :edit_profile format string" it "returns the correctly formatted date" do user.update(created_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.created_at, format: :edit_profile, default: nil) decorated_user.context[:format] = :edit_profile expect(decorated_user.formatted_created_at).to eq expected_date end end describe "#formatted_updated_at" do context "when using the 'default'format string" it "returns the correctly formatted date" do user.update(updated_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.updated_at, format: :full, default: nil) expect(decorated_user.formatted_updated_at).to eq expected_date end context "when passing in the custom :edit_profile format string" it "returns the correctly formatted date" do user.update(updated_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.updated_at, format: :edit_profile, default: nil) decorated_user.context[:format] = :edit_profile expect(decorated_user.formatted_updated_at).to eq expected_date end end describe "#formatted_current_sign_in_at" do context "when using the 'default'format string" it "returns the correctly formatted date" do user.update(current_sign_in_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.current_sign_in_at, format: :full, default: nil) expect(decorated_user.formatted_current_sign_in_at).to eq expected_date end context "when passing in the custom :edit_profile format string" it "returns the correctly formatted date" do user.update(current_sign_in_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.current_sign_in_at, format: :edit_profile, default: nil) decorated_user.context[:format] = :edit_profile expect(decorated_user.formatted_current_sign_in_at).to eq expected_date end end describe "#formatted_invitation_accepted_at" do context "when using the 'default'format string" it "returns the correctly formatted date" do user.update(invitation_accepted_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.invitation_accepted_at, format: :full, default: nil) expect(decorated_user.formatted_invitation_accepted_at).to eq expected_date end context "when passing in the custom :edit_profile format string" it "returns the correctly formatted date" do user.update(invitation_accepted_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.invitation_accepted_at, format: :edit_profile, default: nil) decorated_user.context[:format] = :edit_profile expect(decorated_user.formatted_invitation_accepted_at).to eq expected_date end end describe "#formatted_reset_password_sent_at" do context "when using the 'default'format string" it "returns the correctly formatted date" do user.update(reset_password_sent_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.reset_password_sent_at, format: :full, default: nil) expect(decorated_user.formatted_reset_password_sent_at).to eq expected_date end context "when passing in the custom :edit_profile format string" it "returns the correctly formatted date" do user.update(reset_password_sent_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.reset_password_sent_at, format: :edit_profile, default: nil) decorated_user.context[:format] = :edit_profile expect(decorated_user.formatted_reset_password_sent_at).to eq expected_date end end describe "#formatted_invitation_sent_at" do context "when using the 'default'format string" it "returns the correctly formatted date" do user.update(invitation_sent_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.invitation_sent_at, format: :full, default: nil) expect(decorated_user.formatted_invitation_sent_at).to eq expected_date end context "when passing in the custom :edit_profile format string" it "returns the correctly formatted date" do user.update(invitation_sent_at: Time.new(2023, 5, 1, 12, 0, 0)) expected_date = I18n.l(user.invitation_sent_at, format: :edit_profile, default: nil) decorated_user.context[:format] = :edit_profile expect(decorated_user.formatted_invitation_sent_at).to eq expected_date end end describe "#formatted_birthday" do context "when a user has no date of birth set" it "returns a blank string" do user.update(date_of_birth: nil) expect(decorated_user.formatted_birthday).to eq "" end context "when a user has a valid date of birth" it "returns the month and ordinal of their birthday" do user.update(date_of_birth: Date.new(1991, 7, 8)) expect(decorated_user.formatted_birthday).to eq "July 8th" end end describe "#formatted_date_of_birth" do context "when a user has no date of birth set" it "returns a blank string" do user.update(date_of_birth: nil) expect(decorated_user.formatted_date_of_birth).to eq "" end context "when a user has a valid date of birth" it "returns the YYYY/MM/DD of their date of birth" do user.update(date_of_birth: Date.new(1991, 7, 8)) expect(decorated_user.formatted_date_of_birth).to eq "1991/07/08" end end end ================================================ FILE: spec/decorators/volunteer_decorator_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerDecorator do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:supervisor) { create(:supervisor, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } describe "CC reminder text" do context "when user is admin" do it "includes both supervisor and admin in prompt" do sign_in admin expect(volunteer.decorate.cc_reminder_text).to include "Supervisor" expect(volunteer.decorate.cc_reminder_text).to include "Admin" end end context "when user is supervisor" do it "includes only supervisor in prompt" do sign_in supervisor expect(volunteer.decorate.cc_reminder_text).to include "Supervisor" expect(volunteer.decorate.cc_reminder_text).not_to include "Admin" end end end end ================================================ FILE: spec/documents/templates/prince_george_report_template_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" require "zip" require "rexml/document" RSpec.describe "Prince George report template" do let(:template_path) { Rails.root.join("app/documents/templates/prince_george_report_template.docx").to_s } let(:w_ns) { "http://schemas.openxmlformats.org/wordprocessingml/2006/main" } def extract_document_xml(docx_path) Zip::File.open(docx_path) do |zip| entry = zip.find_entry("word/document.xml") REXML::Document.new(entry.get_input_stream.read) end end def find_contacts_table(doc) tables = REXML::XPath.match(doc, "//w:tbl", "w" => w_ns) tables.find do |tbl| text = "" REXML::XPath.each(tbl, ".//w:t", "w" => w_ns) { |t| text += t.text.to_s } text.include?("Contact Dates") end end describe "contacts table column widths" do it "allocates more width to the Contact Dates column than Name or Title columns" do doc = extract_document_xml(template_path) table = find_contacts_table(doc) expect(table).not_to be_nil, "Could not find contacts table in template" grid_cols = REXML::XPath.match(table, ".//w:tblGrid/w:gridCol", "w" => w_ns) expect(grid_cols.length).to eq(3) widths = grid_cols.map { |col| col.attributes["w:w"].to_i } name_width, title_width, dates_width = widths expect(dates_width).to be > name_width, "Contact Dates column (#{dates_width}) should be wider than Name column (#{name_width})" expect(dates_width).to be > title_width, "Contact Dates column (#{dates_width}) should be wider than Title column (#{title_width})" expect(dates_width).to be >= 5760, "Contact Dates column should be at least 4 inches (5760 twips), got #{dates_width}" end it "preserves the total table width" do doc = extract_document_xml(template_path) table = find_contacts_table(doc) expect(table).not_to be_nil, "Could not find contacts table in template" grid_cols = REXML::XPath.match(table, ".//w:tblGrid/w:gridCol", "w" => w_ns) total = grid_cols.sum { |col| col.attributes["w:w"].to_i } expect(total).to eq(9606), "Total table width should be 9606 twips (original width), got #{total}" end end end ================================================ FILE: spec/factories/additional_expenses.rb ================================================ FactoryBot.define do factory :additional_expense do other_expense_amount { 20 } other_expenses_describe { "description of expense" } case_contact end end ================================================ FILE: spec/factories/addresses.rb ================================================ FactoryBot.define do factory :address do content { Faker::Address.full_address } association :user end end ================================================ FILE: spec/factories/all_casa_admins.rb ================================================ FactoryBot.define do factory :all_casa_admin, class: "AllCasaAdmin" do sequence(:email) { |n| "email#{n}@example.com" } password { "12345678" } password_confirmation { "12345678" } end end ================================================ FILE: spec/factories/api_credential.rb ================================================ FactoryBot.define do factory :api_credential do association :user api_token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(18)) } refresh_token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(18)) } token_expires_at { 1.hour.from_now } refresh_token_expires_at { 1.day.from_now } end end ================================================ FILE: spec/factories/banners.rb ================================================ FactoryBot.define do factory :banner do casa_org association :user, factory: :supervisor name { "Volunteer Survey" } active { true } content { "Please fill out this survey" } end end ================================================ FILE: spec/factories/casa_admins.rb ================================================ FactoryBot.define do factory :casa_admin, class: "CasaAdmin", parent: :user do trait :with_casa_cases do after(:create) do |user, _| create_list(:case_assignment, 2, volunteer: user) end end trait :with_case_contact do after(:create) do |user, _| create(:case_assignment, volunteer: user) create(:case_contact, creator: user, casa_case: user.casa_cases.first, contact_made: true) end end trait :with_case_contact_wants_driving_reimbursement do after(:create) do |user, _| create(:case_assignment, volunteer: user) create(:case_contact, :wants_reimbursement, creator: user, casa_case: user.casa_cases.first, contact_made: true) end end trait :inactive do active { false } end end end ================================================ FILE: spec/factories/casa_case_contact_types.rb ================================================ FactoryBot.define do factory :casa_case_contact_type do contact_type casa_case end end ================================================ FILE: spec/factories/casa_case_emancipation_categories.rb ================================================ FactoryBot.define do factory :casa_case_emancipation_category do casa_case do create(:casa_case) end emancipation_category do create(:emancipation_category) end end end ================================================ FILE: spec/factories/casa_case_emancipation_options.rb ================================================ FactoryBot.define do factory :casa_case_emancipation_option do casa_case do create(:casa_case) end emancipation_option do create(:emancipation_option) end end end ================================================ FILE: spec/factories/casa_cases.rb ================================================ FactoryBot.define do factory :casa_case do sequence(:case_number) { |n| "CINA-#{n}" } birth_month_year_youth { 16.years.ago } casa_org { CasaOrg.first || create(:casa_org) } court_report_status { :not_submitted } case_court_orders { [] } transient do volunteers { [] } end after(:create) do |casa_case, evaluator| Array.wrap(evaluator.volunteers).each do |volunteer| create(:case_assignment, casa_case:, volunteer:) end end trait :pre_transition do birth_month_year_youth { 13.years.ago } end trait :with_one_case_assignment do after(:create) do |casa_case, _| casa_org = casa_case.casa_org volunteer = create(:volunteer, casa_org: casa_org) create(:case_assignment, casa_case: casa_case, volunteer: volunteer) end end trait :with_case_assignments do after(:create) do |casa_case, _| casa_org = casa_case.casa_org 2.times.map do volunteer = create(:volunteer, casa_org: casa_org) create(:case_assignment, casa_case: casa_case, volunteer: volunteer) end end end trait :with_one_court_order do after(:create) do |casa_case| casa_case.case_court_orders << build(:case_court_order) casa_case.save end end trait :active do active { true } end trait :inactive do active { false } end end trait :with_case_contacts do after(:create) do |casa_case| 3.times do create(:case_contact, casa_case_id: casa_case.id) end end end trait :with_casa_case_contact_types do after(:create) do |casa_case, _| casa_org = casa_case.casa_org 2.times.map do contact_type_group = create(:contact_type_group, casa_org: casa_org) contact_type = create(:contact_type, contact_type_group: contact_type_group) create(:casa_case_contact_type, casa_case: casa_case, contact_type: contact_type) end end end trait :with_upcoming_court_date do after(:create) do |casa_case| create(:court_date, casa_case: casa_case, date: Date.tomorrow) end end trait :with_past_court_date do after(:create) do |casa_case| create(:court_date, casa_case: casa_case, date: Date.yesterday) end end trait :with_placement do after(:create) do |casa_case| create(:placement, casa_case: casa_case, placement_started_at: Date.tomorrow) end end end ================================================ FILE: spec/factories/casa_orgs.rb ================================================ FactoryBot.define do factory :casa_org do sequence(:name) { |n| "CASA Org #{n}" } sequence(:display_name) { |n| "CASA Org #{n}" } address { "123 Main St" } footer_links { [["www.example.com", "First Link"], ["www.foobar.com", "Second Link"]] } twilio_account_sid { "articuno34" } twilio_api_key_secret { "open sesame" } twilio_api_key_sid { "Aladdin" } twilio_phone_number { "+15555555555" } trait :with_logo do logo { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/files/org_logo.jpeg")) } end trait :all_reimbursements_enabled do additional_expenses_enabled { true } show_driving_reimbursement { true } end trait :with_placement_types do transient { placement_names { ["Reunification", "Adoption", "Foster Care", "Kinship"] } } after(:create) do |org, evaluator| evaluator.placement_names.each do |name| org.placement_types.create!(name: name) end end end end end ================================================ FILE: spec/factories/case_assignments.rb ================================================ FactoryBot.define do factory :case_assignment do transient do casa_org { CasaOrg.first || create(:casa_org) } pre_transition { false } end active { true } allow_reimbursement { true } casa_case do if pre_transition create(:casa_case, :pre_transition, casa_org: @overrides[:volunteer].try(:casa_org) || casa_org) else create(:casa_case, casa_org: @overrides[:volunteer].try(:casa_org) || casa_org) end end volunteer do create(:volunteer, casa_org: @overrides[:casa_case].try(:casa_org) || casa_org) end trait :disallow_reimbursement do allow_reimbursement { false } end trait :inactive do active { false } end end end ================================================ FILE: spec/factories/case_contact_contact_type.rb ================================================ FactoryBot.define do factory :case_contact_contact_type do contact_type case_contact end end ================================================ FILE: spec/factories/case_contacts.rb ================================================ FactoryBot.define do # NOTE: FactoryBot automatically creates traits for a model's enum attributes # https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#enum-traits # For example, CaseContact status enum includes `active: "active"` state, so following already defined: # trait :active do # status { "active" } # end # ALSO, we can use any trait within other traits: # https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#traits-within-traits # So, rather than `status { "active" }` - use enum trait like so: factory :case_contact do active # use the `:active` enum trait association :creator, factory: :user casa_case contact_types { [association(:contact_type)] } duration_minutes { 60 } occurred_at { Time.zone.today } contact_made { false } medium_type { CaseContact::CONTACT_MEDIUMS.first } want_driving_reimbursement { false } deleted_at { nil } draft_case_ids { [casa_case&.id] } trait :multi_line_note do notes { "line1\nline2\nline3" } end trait :long_note do notes { "1234567890 " * 11 } # longer than NOTES_CHARACTER_LIMIT end trait :miles_driven_no_reimbursement do miles_driven { 20 } want_driving_reimbursement { false } end trait :wants_reimbursement do miles_driven { 456 } want_driving_reimbursement { true } volunteer_address { "123 Contact Factory St" } end trait :started_status do started # enum trait casa_case { nil } contact_types { [] } draft_case_ids { [] } medium_type { nil } occurred_at { nil } duration_minutes { nil } notes { nil } miles_driven { 0 } end trait :details_status do details # enum trait casa_case { nil } draft_case_ids { [1] } notes { nil } miles_driven { 0 } end trait :notes_status do notes # enum trait casa_case { nil } draft_case_ids { [1] } miles_driven { 0 } end trait :expenses_status do expenses # enum trait draft_case_ids { [1] } end after(:create) do |case_contact, evaluator| if evaluator.metadata case_contact.update_columns(metadata: evaluator.metadata) elsif case_contact.status case_contact.update_columns(metadata: {"status" => {case_contact.status => case_contact.created_at}}) end end trait :with_org_topics do after(:create) do |case_contact, _| return if case_contact.casa_case.nil? casa_org = case_contact.casa_case.casa_org casa_org.contact_topics.active.each do |contact_topic| case_contact.contact_topic_answers << build(:contact_topic_answer, contact_topic: contact_topic) end end end end end ================================================ FILE: spec/factories/case_court_orders.rb ================================================ FactoryBot.define do factory :case_court_order do casa_case text { Faker::Lorem.paragraph(sentence_count: 5, supplemental: true, random_sentences_to_add: 20) } implementation_status { [:unimplemented, :partially_implemented, :implemented].sample } end end ================================================ FILE: spec/factories/case_court_report_context.rb ================================================ FactoryBot.define do factory :case_court_report_context do skip_create # This model has no presence in the database transient do casa_case { nil } court_date { nil } volunteer { nil } case_court_orders { nil } path_to_report { Rails.root.join("tmp/test_report.docx").to_s } path_to_template { Rails.root.join("app/documents/templates/default_report_template.docx").to_s } start_date { nil } end_date { nil } time_zone { nil } end initialize_with { volunteer_for_context = volunteer.nil? ? create(:volunteer) : volunteer casa_case_for_context = casa_case.nil? ? create(:casa_case) : casa_case if volunteer_for_context && volunteer_for_context.casa_cases.where(id: casa_case_for_context.id).none? volunteer_for_context.casa_cases << casa_case_for_context end new( case_id: casa_case_for_context.id, volunteer_id: volunteer_for_context.try(:id), path_to_report: path_to_report, path_to_template: path_to_template, court_date: court_date, case_court_orders: case_court_orders, start_date: start_date, end_date: end_date, time_zone: time_zone ) } end end ================================================ FILE: spec/factories/case_group_memberships.rb ================================================ FactoryBot.define do factory :case_group_membership do case_group { create(:case_group) } casa_case { create(:casa_case) } end end ================================================ FILE: spec/factories/case_groups.rb ================================================ FactoryBot.define do factory :case_group do transient do case_count { 1 } casa_cases { nil } end casa_org { CasaOrg.first || create(:casa_org) } sequence(:name) { |n| "Family #{n}" } after(:build) do |case_group, evaluator| casa_cases = if evaluator.casa_cases.present? evaluator.casa_cases elsif case_group.case_group_memberships.empty? build_list(:casa_case, evaluator.case_count, casa_org: case_group.casa_org) else [] end casa_cases.each do |casa_case| case_group.case_group_memberships.build(casa_case: casa_case) end end end end ================================================ FILE: spec/factories/checklist_items.rb ================================================ FactoryBot.define do factory :checklist_item do description { "checklist item description" } category { "checklist item category" } mandatory { false } association :hearing_type end end ================================================ FILE: spec/factories/contact_topic_answers.rb ================================================ FactoryBot.define do factory :contact_topic_answer do case_contact contact_topic selected { false } value { Faker::Lorem.paragraph_by_chars(number: 300) } end end ================================================ FILE: spec/factories/contact_topics.rb ================================================ FactoryBot.define do factory :contact_topic do casa_org active { true } question { Faker::Lorem.sentence } details { Faker::Lorem.paragraph_by_chars(number: 300) } end end ================================================ FILE: spec/factories/contact_type_group.rb ================================================ FactoryBot.define do factory :contact_type_group do casa_org { CasaOrg.first || create(:casa_org) } sequence(:name) { |n| "Group #{n}" } end end ================================================ FILE: spec/factories/contact_types.rb ================================================ FactoryBot.define do factory :contact_type do contact_type_group sequence(:name) { |n| "Type #{n}" } end end ================================================ FILE: spec/factories/court_dates.rb ================================================ FactoryBot.define do factory :court_date, class: "CourtDate" do casa_case date { 1.week.ago } trait :with_court_details do with_judge with_hearing_type with_court_order end trait(:with_judge) { judge } trait(:with_hearing_type) { hearing_type } trait :with_court_order do after(:create) do |court_date| court_date.case_court_orders << build(:case_court_order, casa_case: court_date.casa_case) court_date.save end end end end ================================================ FILE: spec/factories/custom_org_links.rb ================================================ FactoryBot.define do factory :custom_org_link do casa_org text { "Custom Link Text" } url { "https://custom.link" } active { true } end end ================================================ FILE: spec/factories/emancipation_categories.rb ================================================ FactoryBot.define do factory :emancipation_category do sequence(:name) { |n| "Emancipation category #{n}" } mutually_exclusive { false } end end ================================================ FILE: spec/factories/emancipation_options.rb ================================================ FactoryBot.define do factory :emancipation_option do emancipation_category { build(:emancipation_category) } sequence(:name) { |n| "Emancipation option #{n}" } end end ================================================ FILE: spec/factories/followups.rb ================================================ FactoryBot.define do factory :followup do association :creator, factory: :user status { :requested } case_contact # TODO polymorph Simulating the dual-writing setup during polymorphic migration # remove after migration completed after(:build) do |followup, evaluator| unless evaluator.instance_variable_defined?(:@without_dual_writing) followup.followupable = followup.case_contact end end trait :without_dual_writing do after(:build) do |followup| followup.followupable_id = nil followup.followupable_type = nil end end trait :with_note do note { Faker::Lorem.paragraph } end trait :without_note do note { "" } end end end ================================================ FILE: spec/factories/fund_requests.rb ================================================ FactoryBot.define do factory :fund_request do deadline { "tuesday the 12th of May" } extra_information { "extra_information" } impact { "impact" } other_funding_source_sought { "other_funding_source_sought" } payee_name { "payee_name" } payment_amount { "$123.45" } request_purpose { "shoes" } requested_by_and_relationship { "me, the CASA" } submitter_email { "casa@example.cmo" } youth_name { "The youth Name" } end end ================================================ FILE: spec/factories/healths.rb ================================================ FactoryBot.define do factory :health do latest_deploy_time { Time.now } singleton_guard { 0 } end end ================================================ FILE: spec/factories/hearing_types.rb ================================================ FactoryBot.define do factory :hearing_type do casa_org { CasaOrg.first || create(:casa_org) } sequence(:name) { |n| "Emergency Hearing #{n}" } active { true } end end ================================================ FILE: spec/factories/judges.rb ================================================ FactoryBot.define do factory :judge do casa_org { CasaOrg.first || create(:casa_org) } name { Faker::Name.name } active { true } end end ================================================ FILE: spec/factories/languages.rb ================================================ FactoryBot.define do factory :language do sequence(:name) { |n| "Language #{n} - #{Faker::Nation.language}" } casa_org { CasaOrg.first || create(:casa_org) } end end ================================================ FILE: spec/factories/learning_hour_topics.rb ================================================ FactoryBot.define do factory :learning_hour_topic do casa_org { CasaOrg.first || create(:casa_org) } sequence(:name) { |n| "Learning Hour Type #{n}" } position { 1 } end end ================================================ FILE: spec/factories/learning_hour_types.rb ================================================ FactoryBot.define do factory :learning_hour_type do casa_org { CasaOrg.first || create(:casa_org) } sequence(:name) { |n| "Learning Hour Type #{n}" } active { true } position { 1 } end end ================================================ FILE: spec/factories/learning_hours.rb ================================================ FactoryBot.define do factory :learning_hour do user { User.first || create(:user) } name { Faker::Book.title } duration_minutes { 25 } duration_hours { 1 } occurred_at { 2.days.ago } learning_hour_type { LearningHourType.first || create(:learning_hour_type) } end end ================================================ FILE: spec/factories/login_activities.rb ================================================ FactoryBot.define do factory :login_activity do association :user scope { "user" } strategy { "database_authenticatable" } identity { user.email } success { true } context { "session" } ip { "127.0.0.1" } user_agent { "Mozilla/5.0 (Macintosh; Intel Mac OS X)" } end end ================================================ FILE: spec/factories/mileage_rates.rb ================================================ FactoryBot.define do factory :mileage_rate do casa_org amount { "9.99" } effective_date { "2021-10-23" } is_active { true } end end ================================================ FILE: spec/factories/notes.rb ================================================ FactoryBot.define do factory :note do association :notable, factory: :user association :creator, factory: :user content { "I am a note" } end end ================================================ FILE: spec/factories/notifications.rb ================================================ FactoryBot.define do factory :notification, class: "Noticed::Notification" do association :recipient, factory: :volunteer association :event, factory: :followup_notifier recipient_type { "User" } type { "FollowupNotifier::Notification" } transient do created_by { nil } casa_case { nil } end before(:create) do |notification, eval| notification.params[:created_by] = eval.created_by if eval.created_by.present? notification.params[:casa_case] = eval.casa_case if eval.casa_case.present? end trait :followup_with_note do association :event, factory: [:followup_notifier, :with_note] end trait :followup_without_note do association :event, factory: [:followup_notifier, :without_note] end trait :followup_read do association :event, factory: [:followup_notifier, :read] read_at { DateTime.current } seen_at { DateTime.current } end trait :emancipation_checklist_reminder do association :event, factory: :emancipation_checklist_reminder_notifier type { "EmancipationChecklistReminderNotifier::Notification" } end trait :youth_birthday do association :event, factory: :youth_birthday_notifier type { "YouthBirthdayNotifier::Notification" } end trait :reimbursement_complete do association :event, factory: :reimbursement_complete_notifier type { "ReimbursementCompleteNotifier::Notification" } end end end ================================================ FILE: spec/factories/notifiers.rb ================================================ FactoryBot.define do factory :followup_notifier do type { "FollowupNotifier" } trait :with_note do params do { followup: create(:followup, :with_note), created_by: create(:user) } end end trait :without_note do params do { followup: create(:followup, :without_note, case_contact_id: create(:case_contact).id) } end end trait :read do params do { followup: create(:followup, :without_note, case_contact_id: create(:case_contact).id) } end end end factory :emancipation_checklist_reminder_notifier do type { "EmancipationChecklistReminderNotifier" } params do { casa_case: create(:casa_case) } end end factory :youth_birthday_notifier do type { "YouthBirthdayNotifier" } params do { casa_case: create(:casa_case) } end end factory :reimbursement_complete_notifier do type { "ReimbursementCompleteNotifier" } params do { case_contact: create(:case_contact) } end end end ================================================ FILE: spec/factories/other_duty.rb ================================================ FactoryBot.define do factory :other_duty do creator { association :user } creator_type { "" } occurred_at { Date.current } duration_minutes { rand(99) } notes { Faker::Lorem.paragraph(sentence_count: 5, supplemental: true, random_sentences_to_add: 20) } end end ================================================ FILE: spec/factories/patch_note_groups.rb ================================================ FactoryBot.define do factory :patch_note_group do sequence :value do |n| # Factory with default value includes no users n.to_s end trait :all_users do value { "CasaAdmin+Supervisor+Volunteer" } end trait :only_supervisors_and_admins do value { "CasaAdmin+Supervisor" } end end end ================================================ FILE: spec/factories/patch_note_types.rb ================================================ FactoryBot.define do factory :patch_note_type do sequence(:name) { |n| "Patch Note Type #{n}" } end end ================================================ FILE: spec/factories/patch_notes.rb ================================================ FactoryBot.define do factory :patch_note do sequence :note do |n| n.to_s end patch_note_type { create(:patch_note_type) } patch_note_group { create(:patch_note_group) } end end ================================================ FILE: spec/factories/placement_types.rb ================================================ FactoryBot.define do factory :placement_type do sequence(:name) { |n| "Placement Type #{n}" } casa_org end end ================================================ FILE: spec/factories/placements.rb ================================================ FactoryBot.define do factory :placement do association :creator, factory: :user casa_case placement_type placement_started_at { DateTime.now } end end ================================================ FILE: spec/factories/preference_sets.rb ================================================ FactoryBot.define do factory :preference_set do user case_volunteer_columns { {} } table_state { {} } end end ================================================ FILE: spec/factories/sent_emails.rb ================================================ FactoryBot.define do factory :sent_email do association :user, factory: :user casa_org { CasaOrg.first || create(:casa_org) } mailer_type { "Mailer Type" } category { "Mail Action Category" } sent_address { user.email } end end ================================================ FILE: spec/factories/sms_notification_events.rb ================================================ FactoryBot.define do factory :sms_notification_event do name { "name" } user_type { "user type" } end end ================================================ FILE: spec/factories/supervisor_volunteer.rb ================================================ FactoryBot.define do factory :supervisor_volunteer do supervisor { create(:supervisor) } volunteer { create(:volunteer) } transient do casa_org { CasaOrg.first || create(:casa_org) } end trait :inactive do is_active { false } end end end ================================================ FILE: spec/factories/supervisors.rb ================================================ FactoryBot.define do factory :supervisor, class: "Supervisor", parent: :user do display_name { Faker::Name.unique.name } active { true } transient do volunteers { [] } end after(:create) do |supervisor, evaluator| Array.wrap(evaluator.volunteers).each do |volunteer| create(:supervisor_volunteer, supervisor:, volunteer:) end end trait :with_casa_cases do after(:create) do |user, _| volunteer = create(:volunteer) create_list(:case_assignment, 2, volunteer: volunteer) end end trait :with_case_contact do after(:create) do |user, _| create(:case_assignment, volunteer: user) create(:case_contact, creator: user, casa_case: user.casa_cases.first, contact_made: true) end end trait :with_case_contact_wants_driving_reimbursement do after(:create) do |user, _| create(:case_assignment, volunteer: user) create(:case_contact, :wants_reimbursement, creator: user, casa_case: user.casa_cases.first, contact_made: true) end end trait :with_volunteers do after(:create) do |user, _| create_list(:supervisor_volunteer, 2, supervisor: user) end end trait :inactive do active { false } end trait :receive_reimbursement_attachment do receive_reimbursement_email { true } end end end ================================================ FILE: spec/factories/user_languages.rb ================================================ FactoryBot.define do factory :user_language do user language end end ================================================ FILE: spec/factories/user_reminder_time.rb ================================================ FactoryBot.define do factory :user_reminder_time do user { Volunteer.first } end trait :case_contact_types do case_contact_types { DateTime.now } end trait :quarterly_reminder do case_contact_types { DateTime.now } end end ================================================ FILE: spec/factories/user_sms_notification_events.rb ================================================ FactoryBot.define do factory :user_sms_notification_event do user sms_notification_event end end ================================================ FILE: spec/factories/users.rb ================================================ FactoryBot.define do factory :user do casa_org { CasaOrg.first || create(:casa_org) } sequence(:email) { |n| "email#{n}@example.com" } sequence(:display_name) { |n| "User #{n}" } password { "12345678" } password_confirmation { "12345678" } date_of_birth { nil } case_assignments { [] } phone_number { "" } confirmed_at { Time.now } after(:create) do |user| create(:api_credential, user: user) end trait :inactive do type { "Volunteer" } active { false } role { :inactive } end trait :with_casa_cases do after(:create) do |user, _| create_list(:case_assignment, 2, volunteer: user) end end trait :with_single_case do after(:create) do |user, _| create_list(:case_assignment, 1, volunteer: user) end end trait :with_case_contact do after(:create) do |user, _| create(:case_assignment, volunteer: user) create(:case_contact, creator: user, casa_case: user.casa_cases.first, contact_made: true) end end trait :with_case_contact_wants_driving_reimbursement do after(:create) do |user, _| create(:case_assignment, volunteer: user) create(:case_contact, :wants_reimbursement, creator: user, casa_case: user.casa_cases.first, contact_made: true) end end end end ================================================ FILE: spec/factories/volunteers.rb ================================================ FactoryBot.define do factory :volunteer, class: "Volunteer", parent: :user do trait :inactive do active { false } end trait :with_casa_cases do after(:create) do |user, _| create(:case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org), volunteer: user) create(:case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org), volunteer: user) end end trait :with_pretransition_age_case do after(:create) do |user, _| create(:case_assignment, casa_case: create(:casa_case, :pre_transition, casa_org: user.casa_org), volunteer: user) end end trait :with_cases_and_contacts do after(:create) do |user, _| assignment1 = create :case_assignment, casa_case: create(:casa_case, :pre_transition, casa_org: user.casa_org), volunteer: user create :case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org, birth_month_year_youth: 10.years.ago), volunteer: user create :case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org, birth_month_year_youth: 15.years.ago), volunteer: user contact = create :case_contact, creator: user, casa_case: assignment1.casa_case contact_types = create_list :contact_type, 3, contact_type_group: create(:contact_type_group, casa_org: user.casa_org) 3.times do CaseContactContactType.create(case_contact: contact, contact_type: contact_types.pop) end end end trait :with_assigned_supervisor do transient { supervisor { nil } } after(:create) do |volunteer, evaluator| supervisor = evaluator.supervisor || create(:supervisor, casa_org: volunteer.casa_org) create(:supervisor_volunteer, volunteer:, supervisor:) end end trait :with_inactive_supervisor do transient { supervisor { create(:supervisor) } } after(:create) do |user, evaluator| create(:supervisor_volunteer, :inactive, volunteer: user, supervisor: evaluator.supervisor) end end trait :with_disallow_reimbursement do after(:create) do |user, _| create(:case_assignment, :disallow_reimbursement, casa_case: create(:casa_case, casa_org: user.casa_org), volunteer: user) end end end end ================================================ FILE: spec/fixtures/files/casa_cases.csv ================================================ case_number,case_assignment,birth_month_year_youth,next_court_date CINA-01-4347,volunteer1@example.net, March 2011,Sept 16 2022 CINA-01-4348,"volunteer2@example.net, volunteer3@example.net",February 2000,Jan 1 2023 CINA-01-4349,,December 2016, ================================================ FILE: spec/fixtures/files/casa_cases_without_case_number.csv ================================================ case_number,case_assignment,birth_month_year_youth ,volunteer1@example.net, CINA-01-4348,"volunteer2@example.net, volunteer3@example.net",February 2000 ================================================ FILE: spec/fixtures/files/existing_casa_case.csv ================================================ case_number,case_assignment,birth_month_year_youth,next_court_date CINA-00-0000,volunteer1@example.net, ================================================ FILE: spec/fixtures/files/generic.csv ================================================ Name,Age Allie,30 Bob,40 ================================================ FILE: spec/fixtures/files/no_rows.csv ================================================ Name,Age ================================================ FILE: spec/fixtures/files/supervisor_volunteers.csv ================================================ email,display_name,supervisor_volunteers,phone_number s5@example.com,s5,volunteer1@example.net,11111111111 s6@example.com,s6,volunteer1@example.net,11111111111 ================================================ FILE: spec/fixtures/files/supervisors.csv ================================================ email,display_name,supervisor_volunteers,phone_number supervisor1@example.net,Supervisor One,volunteer1@example.net,11111111111 supervisor2@example.net,Supervisor Two,"volunteer2@example.net, volunteer3@example.net",11111111111 supervisor3@example.net,Supervisor Three,,11111111111 ================================================ FILE: spec/fixtures/files/supervisors_invalid_phone_numbers.csv ================================================ email,display_name,supervisor_volunteers,phone_number supervisor1@example.net,Supervisor One,volunteer1@example.net,12345678 supervisor2@example.net,Supervisor Two,"volunteer2@example.net, volunteer3@example.net",+++++++++++ supervisor3@example.net,Supervisor Three,,111111111111111111 supervisor4@example.net,Supervisor Four,,1.111.111.11112 ================================================ FILE: spec/fixtures/files/supervisors_without_display_names.csv ================================================ email,display_name,supervisor_volunteers,phone_number supervisor1@example.net,,volunteer1@example.net,11111111111 supervisor2@example.net,,"volunteer2@example.net, volunteer3@example.net",22222222222 supervisor3@example.net,,,33333333333 ================================================ FILE: spec/fixtures/files/supervisors_without_email.csv ================================================ email,display_name,supervisor_volunteers,phone_number supervisor1@example.net,Supervisor One,volunteer1@example.net,11111111111 ,Supervisor Two,"volunteer2@example.net, volunteer3@example.net",11111111111 ================================================ FILE: spec/fixtures/files/supervisors_without_phone_numbers.csv ================================================ email,display_name,supervisor_volunteers,phone_number supervisor1@example.net,Supervisor One,volunteer1@example.net, supervisor2@example.net,Supervisor Two,"volunteer2@example.net, volunteer3@example.net", supervisor3@example.net,Supervisor Three,, ================================================ FILE: spec/fixtures/files/volunteers.csv ================================================ display_name,email,phone_number Volunteer One,volunteer1@example.net,11234567890 Volunteer Two,volunteer2@example.net,11234567891 Volunteer Three,volunteer3@example.net,11234567892 ================================================ FILE: spec/fixtures/files/volunteers_invalid_phone_numbers.csv ================================================ display_name,email,phone_number Volunteer One,volunteer1@example.net,22222222222 Volunteer Two,volunteer2@example.net,1bc12345678 Volunteer Three,volunteer3@example.net,111111111 ================================================ FILE: spec/fixtures/files/volunteers_without_display_names.csv ================================================ display_name,email,phone_number ,volunteer1@example.net,111111111 ,volunteer2@example.net,22222222222 ,volunteer3@example.net,33333333333 ================================================ FILE: spec/fixtures/files/volunteers_without_email.csv ================================================ display_name,email,phone_number Volunteer One,volunteer1@example.net,11111111111 Volunteer Two,11111111111 Volunteer Three,,11111111111 ================================================ FILE: spec/fixtures/files/volunteers_without_phone_numbers.csv ================================================ display_name,email,phone_number Volunteer One,volunteer1@example.net, Volunteer Two,volunteer2@example.net, ================================================ FILE: spec/helpers/all_casa_admins/casa_orgs_helper_spec.rb ================================================ require "rails_helper" RSpec.describe AllCasaAdmins::CasaOrgsHelper, type: :helper do # TODO: Add tests for AllCasaAdmins::CasaOrgsHelper pending "add some tests for AllCasaAdmins::CasaOrgsHelper" end ================================================ FILE: spec/helpers/api_base_helper_spec.rb ================================================ require "rails_helper" RSpec.describe ApiBaseHelper, type: :helper do # TODO: Add tests for ApiBaseHelper pending "add some tests for ApiBaseHelper" end ================================================ FILE: spec/helpers/application_helper_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationHelper, type: :helper do describe "#page_header" do it "displays the header when user is logged in" do current_organization = build_stubbed(:casa_org) user = build_stubbed(:user, casa_org: current_organization) allow(helper).to receive(:user_signed_in?).and_return(true) allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_organization).and_return(current_organization) expect(helper.page_header).to eq(current_organization.display_name) end it "displays the header when user is not logged in" do allow(helper).to receive(:user_signed_in?).and_return(false) expect(helper.page_header).to eq(helper.default_page_header) end end describe "#session_link" do it "links to the sign_out page when user is signed in" do allow(helper).to receive(:user_signed_in?).and_return(true) expect(helper.session_link).to match(destroy_user_session_path) end it "links to the sign_out page when all_casa_admin is signed in" do allow(helper).to receive(:user_signed_in?).and_return(false) allow(helper).to receive(:all_casa_admin_signed_in?).and_return(true) expect(helper.session_link).to match(destroy_all_casa_admin_session_path) end it "links to the sign_in page when user is not signed in" do allow(helper).to receive(:user_signed_in?).and_return(false) allow(helper).to receive(:all_casa_admin_signed_in?).and_return(false) expect(helper.session_link).to match(new_user_session_path) end end describe "#og_tag" do subject { helper.og_tag(:title, content: "Website Title") } it { is_expected.to eql('') } end end ================================================ FILE: spec/helpers/banner_helper_spec.rb ================================================ require "rails_helper" RSpec.describe BannerHelper, type: :helper do describe "#conditionally_add_hidden_class" do it "returns d-none if current banner is inactive" do current_organization = double allow(helper).to receive(:current_organization).and_return(current_organization) banner = double(id: 1) assign(:banner, banner) allow(current_organization).to receive(:has_alternate_active_banner?).and_return(true) expect(helper.conditionally_add_hidden_class(false)).to eq("d-none") end it "returns d-none if current banner is active and org does not have an alternate active banner" do current_organization = double allow(helper).to receive(:current_organization).and_return(current_organization) banner = double(id: 1) assign(:banner, banner) allow(current_organization).to receive(:has_alternate_active_banner?).and_return(false) expect(helper.conditionally_add_hidden_class(true)).to eq("d-none") end it "returns nil if current banner is active and org has an alternate active banner" do current_organization = double allow(helper).to receive(:current_organization).and_return(current_organization) banner = double(id: 1) assign(:banner, banner) allow(current_organization).to receive(:has_alternate_active_banner?).and_return(true) expect(helper.conditionally_add_hidden_class(true)).to eq(nil) end end describe "#banner_expiration_time_in_words" do let(:banner) { create(:banner, expires_at: expires_at) } context "when expires_at isn't set" do let(:expires_at) { nil } it "returns No Expiration" do expect(helper.banner_expiration_time_in_words(banner)).to eq("No Expiration") end end context "when expires_at is in the future" do let(:expires_at) { 7.days.from_now } it "returns a word description of how far in the future" do expect(helper.banner_expiration_time_in_words(banner)).to eq("in 7 days") end end context "when expires_at is in the past" do let(:expired_banner) do banner = create(:banner, expires_at: nil) banner.update_columns(expires_at: 2.days.ago) banner end it "returns Expired" do expect(helper.banner_expiration_time_in_words(expired_banner)).to eq("Expired") end end end end ================================================ FILE: spec/helpers/case_contacts_helper_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactsHelper, type: :helper do describe "#render_back_link" do it "renders back link to home page when user is a volunteer" do current_user = create(:volunteer) casa_case = create(:casa_case) allow(helper).to receive(:current_user).and_return(current_user) expect(helper.render_back_link(casa_case)).to eq(root_path) end it "renders back link to home page when user does not exist" do casa_case = create(:casa_case) allow(helper).to receive(:current_user).and_return(nil) expect(helper.render_back_link(casa_case)).to eq(root_path) end it "renders back link to home page when user is a supervisor" do current_user = create(:supervisor) casa_case = create(:casa_case) allow(helper).to receive(:current_user).and_return(current_user) expect(helper.render_back_link(casa_case)).to eq(casa_case_path(casa_case)) end it "renders back link to home page when user is a administrator" do current_user = create(:casa_admin) casa_case = create(:casa_case) allow(helper).to receive(:current_user).and_return(current_user) expect(helper.render_back_link(casa_case)).to eq(casa_case_path(casa_case)) end end describe "#duration_minutes" do it "returns remainder if duration_minutes is set" do case_contact = build(:case_contact, duration_minutes: 80) expect(helper.duration_minutes(case_contact)).to eq(20) end it "returns zero if duration_minutes is zero" do case_contact = build(:case_contact, duration_minutes: 0) expect(helper.duration_minutes(case_contact)).to eq(0) end it "returns zero if duration_minutes is nil" do case_contact = build(:case_contact, duration_minutes: nil) expect(helper.duration_minutes(case_contact)).to eq(0) end end describe "#duration_hours" do it "returns minutes if duration_minutes is set" do case_contact = build(:case_contact, duration_minutes: 80) expect(helper.duration_hours(case_contact)).to eq(1) end it "returns zero if duration_minutes is zero" do case_contact = build(:case_contact, duration_minutes: 0) expect(helper.duration_hours(case_contact)).to eq(0) end it "returns zero if duration_minutes is nil" do case_contact = build(:case_contact, duration_minutes: nil) expect(helper.duration_hours(case_contact)).to eq(0) end end describe "#show_volunteer_reimbursement" do before do @casa_cases = [] @casa_cases << create(:casa_case) @casa_org = @casa_cases[0].casa_org @current_user = create(:volunteer, casa_org: @casa_org) end it "returns true if allow_reimbursement is true" do create(:case_assignment, casa_case: @casa_cases[0], volunteer: @current_user) allow(helper).to receive(:current_user).and_return(@current_user) expect(helper.show_volunteer_reimbursement(@casa_cases)).to eq(true) end it "returns false if allow_reimbursement is false" do create(:case_assignment, :disallow_reimbursement, casa_case: @casa_cases[0], volunteer: @current_user) allow(helper).to receive(:current_user).and_return(@current_user) expect(helper.show_volunteer_reimbursement(@casa_cases)).to eq(false) end it "returns false if no case_assigmnents are found" do allow(helper).to receive(:current_user).and_return(@current_user) expect(helper.show_volunteer_reimbursement(@casa_cases)).to eq(false) end end describe "#expand_filters?" do it "returns false if filterrific param does not exist" do allow(helper).to receive(:params) .and_return({}) expect(helper.expand_filters?).to eq(false) end it "returns false if filterrific contains only surfaced params" do allow(helper).to receive(:params) .and_return({filterrific: {surfaced_param: "true"}}) expect(helper.expand_filters?([:surfaced_param])).to eq(false) end it "returns true if filterrific contains any other key" do allow(helper).to receive(:params) .and_return({filterrific: {surfaced_param: "true", other_key: "value"}}) expect(helper.expand_filters?([:surfaced_param])).to eq(true) end end end ================================================ FILE: spec/helpers/contact_types_helper_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTypesHelper do end ================================================ FILE: spec/helpers/court_dates_helper_spec.rb ================================================ require "rails_helper" RSpec.describe CourtDatesHelper, type: :helper do describe "#when_do_we_have_court_dates" do subject { helper.when_do_we_have_court_dates(casa_case) } describe "when casa case has no court dates" do let(:casa_case) { create(:casa_case) } it { expect(subject).to eq("none") } end describe "when casa case has only dates in the past" do let(:casa_case) { create(:casa_case, :with_past_court_date) } it { expect(subject).to eq("past") } end describe "when casa case only has dates in the future" do let(:casa_case) { create(:casa_case, :with_upcoming_court_date) } it { expect(subject).to eq("future") } end describe "when casa case has dates both in the past and future" do let(:casa_case) { create(:casa_case, :with_upcoming_court_date, :with_past_court_date) } it { expect(subject).to be_nil } end end end ================================================ FILE: spec/helpers/court_orders_helper_spec.rb ================================================ require "rails_helper" RSpec.describe CourtDatesHelper, type: :helper do describe "#court_order_select_options" do context "when no court orders" do it "empty" do expect(helper.court_order_select_options).to eq([["Unimplemented", "unimplemented"], ["Partially implemented", "partially_implemented"], ["Implemented", "implemented"]]) end end end end ================================================ FILE: spec/helpers/date_helper_spec.rb ================================================ require "rails_helper" RSpec.describe DateHelper, type: :helper do # TODO: Add tests for DateHelper pending "add some tests for DateHelper" end ================================================ FILE: spec/helpers/emancipations_helper_spec.rb ================================================ require "rails_helper" # Specs in this file have access to a helper object that includes # the EmancipationsHelper. For example: # # describe EmancipationsHelper do # describe "string concat" do # it "concats two strings with spaces" do # expect(helper.concat_strings("this","that")).to eq("this that") # end # end # end RSpec.describe EmancipationsHelper, type: :helper do let(:casa_case) { create(:casa_case) } describe "#emancipation_category_checkbox_checked" do let(:emancipation_category) { create(:emancipation_category, name: "unique name") } it "returns \"checked\" when passed an associated casa case and emancipation category" do create(:casa_case_emancipation_category, casa_case_id: casa_case.id, emancipation_category_id: emancipation_category.id) expect(helper.emancipation_category_checkbox_checked(casa_case, emancipation_category)).to eq("checked") end it "returns nil when passed an unassociated casa case and emancipation category" do expect(helper.emancipation_category_checkbox_checked(casa_case, emancipation_category)).to eq(nil) end end describe "#emancipation_category_collapse_hidden" do let(:emancipation_category) { create(:emancipation_category, name: "another unique name") } it "returns nil when passed an associated casa case and emancipation category" do create(:casa_case_emancipation_category, casa_case_id: casa_case.id, emancipation_category_id: emancipation_category.id) expect(helper.emancipation_category_collapse_hidden(casa_case, emancipation_category)).to eq(nil) end it "returns \"display: none;\" when passed an unassociated casa case and emancipation category" do expect(helper.emancipation_category_collapse_hidden(casa_case, emancipation_category)).to eq("display: none;") end end describe "#emancipation_category_collapse_icon" do let(:emancipation_category) { create(:emancipation_category, name: "another unique name") } it "returns nil when passed an associated casa case and emancipation category" do create(:casa_case_emancipation_category, casa_case_id: casa_case.id, emancipation_category_id: emancipation_category.id) expect(helper.emancipation_category_collapse_icon(casa_case, emancipation_category)).to eq("−") end it "returns \"display: none;\" when passed an unassociated casa case and emancipation category" do expect(helper.emancipation_category_collapse_icon(casa_case, emancipation_category)).to eq("+") end end describe "#emancipation_option_checkbox_checked" do let(:emancipation_option) { create(:emancipation_option) } it "returns \"checked\" when passed an associated casa case and emancipation option" do create(:casa_case_emancipation_option, casa_case_id: casa_case.id, emancipation_option_id: emancipation_option.id) expect(helper.emancipation_option_checkbox_checked(casa_case, emancipation_option)).to eq("checked") end it "returns nil when passed an unassociated casa case and emancipation option id" do expect(helper.emancipation_option_checkbox_checked(casa_case, emancipation_option)).to eq(nil) end end end ================================================ FILE: spec/helpers/followup_helper_spec.rb ================================================ require "rails_helper" RSpec.describe FollowupHelper, type: :helper do describe "#followup_icon" do context "volunteer created followup" do it "is orange circle with an exclamation point" do creator = build_stubbed(:volunteer) expect(helper.followup_icon(creator)).to include("exclamation-circle") end end context "admin created followup" do it "is orange circle with an exclamation point" do creator = build_stubbed(:casa_admin) expect(helper.followup_icon(creator)).to include("exclamation-triangle") end end end end ================================================ FILE: spec/helpers/learning_hours_helper_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHoursHelper, type: :helper do describe "#format_time" do it "formats time correctly for positive values" do expect(helper.format_time(120)).to eq("2 hours 0 minutes") expect(helper.format_time(90)).to eq("1 hours 30 minutes") expect(helper.format_time(75)).to eq("1 hours 15 minutes") end it "formats time correctly for zero minutes" do expect(helper.format_time(0)).to eq("0 hours 0 minutes") end it "formats time correctly for large values" do expect(helper.format_time(360)).to eq("6 hours 0 minutes") expect(helper.format_time(1800)).to eq("30 hours 0 minutes") end end end ================================================ FILE: spec/helpers/mileage_rates_helper_spec.rb ================================================ require "rails_helper" RSpec.describe MileageRatesHelper, type: :helper do describe "#effective_date_parser" do context "with date" do let(:date) { DateTime.parse("01-01-2021") } it "returns date formated" do expect(helper.effective_date_parser(date)).to eq "January 01, 2021" end end end context "without date" do let(:date) { nil } it "returns current date formated" do expect(helper.effective_date_parser(date)).to eq DateTime.current.strftime(::DateHelper::RUBY_MONTH_DAY_YEAR_FORMAT) end end end ================================================ FILE: spec/helpers/notifications_helper_spec.rb ================================================ require "rails_helper" RSpec.describe NotificationsHelper, type: :helper do context "notifications with respect to deploy time" do let(:notification_created_after_deploy_a) { create(:notification) } let(:notification_created_after_deploy_b) { create(:notification, created_at: 1.day.ago) } let(:notification_created_at_deploy) { create(:notification, created_at: 2.days.ago) } let(:notification_created_before_deploy_a) { create(:notification, created_at: 2.days.ago - 1.hour) } let(:notification_created_before_deploy_b) { create(:notification, created_at: 3.days.ago) } before do travel_to Time.new(2022, 1, 1, 0, 0, 0) notification_created_after_deploy_a.update_attribute(:created_at, 1.hour.ago) notification_created_after_deploy_b.update_attribute(:created_at, 1.day.ago) Health.instance.update_attribute(:latest_deploy_time, 2.days.ago) notification_created_at_deploy.update_attribute(:created_at, 2.days.ago) notification_created_before_deploy_a.update_attribute(:created_at, 2.days.ago - 1.hour) notification_created_before_deploy_b.update_attribute(:created_at, 3.days.ago) end describe "#notifications_after_and_including_deploy" do let(:notifications_after_and_including_deploy) { helper.notifications_after_and_including_deploy(Noticed::Notification.all) } it "returns all notifications from the given list after and including deploy time" do expect(notifications_after_and_including_deploy).to include(notification_created_after_deploy_a) expect(notifications_after_and_including_deploy).to include(notification_created_after_deploy_b) expect(notifications_after_and_including_deploy).to include(notification_created_at_deploy) end it "does not contain notifications before the deploy time" do expect(notifications_after_and_including_deploy).not_to include(notification_created_before_deploy_a) expect(notifications_after_and_including_deploy).not_to include(notification_created_before_deploy_b) end end describe "#notifications_before_deploy" do let(:notifications_before_deploy) { helper.notifications_before_deploy(Noticed::Notification.all) } it "returns all notifications from the given list before deploy time" do expect(notifications_before_deploy).to include(notification_created_before_deploy_a) expect(notifications_before_deploy).to include(notification_created_before_deploy_b) end it "does not contain notifications after and including the deploy time" do expect(notifications_before_deploy).not_to include(notification_created_after_deploy_a) expect(notifications_before_deploy).not_to include(notification_created_after_deploy_b) expect(notifications_before_deploy).not_to include(notification_created_at_deploy) end end end describe "#patch_notes_as_hash_keyed_by_type_name" do it "returns a hash where the keys are the names of the patch note type and the values are lists of patch note strings belonging to the type" do patch_note_type_a = create(:patch_note_type, name: "patch_note_type_a") patch_note_type_b = create(:patch_note_type, name: "patch_note_type_b") patch_note_1 = create(:patch_note, note: "Patch Note 1", patch_note_type: patch_note_type_a) patch_note_2 = create(:patch_note, note: "Patch Note 2", patch_note_type: patch_note_type_b) patch_note_3 = create(:patch_note, note: "Patch Note 3", patch_note_type: patch_note_type_b) patch_notes_hash = helper.patch_notes_as_hash_keyed_by_type_name(PatchNote.all) expect(patch_notes_hash).to have_key(patch_note_type_a.name) expect(patch_notes_hash).to have_key(patch_note_type_b.name) expect(patch_notes_hash[patch_note_type_a.name]).to contain_exactly(patch_note_1.note) expect(patch_notes_hash[patch_note_type_b.name]).to contain_exactly(patch_note_2.note, patch_note_3.note) end end end ================================================ FILE: spec/helpers/other_duties_helper_spec.rb ================================================ require "rails_helper" RSpec.describe OtherDutiesHelper, type: :helper do describe "#duration_minutes" do it "returns remainder if duration_minutes is set" do other_duty = build(:other_duty, duration_minutes: 80) expect(helper.duration_minutes(other_duty)).to eq(20) end it "returns zero if duration_minutes is zero" do other_duty = build(:other_duty, duration_minutes: 0) expect(helper.duration_minutes(other_duty)).to eq(0) end it "returns zero if duration_minutes is nil" do other_duty = build(:other_duty, duration_minutes: nil) expect(helper.duration_minutes(other_duty)).to eq(0) end end describe "#duration_hours" do it "returns minutes if duration_minutes is set" do other_duty = build(:other_duty, duration_minutes: 80) expect(helper.duration_hours(other_duty)).to eq(1) end it "returns zero if duration_minutes is zero" do other_duty = build(:other_duty, duration_minutes: 0) expect(helper.duration_hours(other_duty)).to eq(0) end it "returns zero if duration_minutes is nil" do other_duty = build(:other_duty, duration_minutes: nil) expect(helper.duration_hours(other_duty)).to eq(0) end end end ================================================ FILE: spec/helpers/phone_number_helper_spec.rb ================================================ require "rails_helper" RSpec.describe PhoneNumberHelper, type: :helper do context "validates phone number" do it "with empty string" do valid, error = valid_phone_number("") expect(valid).to be(true) expect(error).to be_nil end it "with 10 digit phone number prepended with US country code" do valid, error = valid_phone_number("+12223334444") expect(valid).to be(true) expect(error).to be_nil end it "with 10 digit phone number prepended with US country code without the plus sign" do valid, error = valid_phone_number("12223334444") expect(valid).to be(true) expect(error).to be_nil end it "with 10 phone number with spaces" do valid, error = valid_phone_number("222 333 4444") expect(valid).to be(true) expect(error).to be_nil end it "with 10 phone number with parentheses" do valid, error = valid_phone_number("(222)3334444") expect(valid).to be(true) expect(error).to be_nil end it "with 10 phone number with dashes" do valid, error = valid_phone_number("222-333-4444") expect(valid).to be(true) expect(error).to be_nil end it "with 10 phone number with dots" do valid, error = valid_phone_number("222.333.4444") expect(valid).to be(true) expect(error).to be_nil end end context "invalidates phone number" do it "with incorrect country code" do valid, error = valid_phone_number("+22223334444") expect(valid).to be(false) expect(error).to have_text("must be 10 digits or 12 digits including country code (+1)") end it "with short phone number" do valid, error = valid_phone_number("+122") expect(valid).to be(false) expect(error).to have_text("must be 10 digits or 12 digits including country code (+1)") end it "with long phone number" do valid, error = valid_phone_number("+12223334444555") expect(valid).to be(false) expect(error).to have_text("must be 10 digits or 12 digits including country code (+1)") end end end ================================================ FILE: spec/helpers/preference_sets_helper_spec.rb ================================================ require "rails_helper" # Specs in this file have access to a helper object that includes # the PreferenceSetsHelper. For example: # # describe PreferenceSetsHelper do # describe "string concat" do # it "concats two strings with spaces" do # expect(helper.concat_strings("this","that")).to eq("this that") # end # end # end RSpec.describe PreferenceSetsHelper, type: :helper do pending "add some examples to (or delete) #{__FILE__}" end ================================================ FILE: spec/helpers/report_helper_spec.rb ================================================ require "rails_helper" RSpec.describe ReportHelper, type: :helper do describe "#boolean_choices" do it "returns array with correct options" do expect(helper.boolean_choices).to eq [["Both", ""], ["Yes", true], ["No", false]] end end end ================================================ FILE: spec/helpers/request_header_helper_spec.rb ================================================ require "rails_helper" RSpec.describe RequestHeaderHelper, type: :helper do # TODO: Add tests for RequestHeaderHelper pending "add some tests for RequestHeaderHelper" end ================================================ FILE: spec/helpers/sidebar_helper_spec.rb ================================================ require "rails_helper" RSpec.describe SidebarHelper, type: :helper do describe "#menu_item" do it "does not render sidebar menu item when not visible" do menu_item = helper.menu_item(label: "Supervisors", path: supervisors_path, visible: false) expect(menu_item).to be_nil end it "renders sidebar menu item label correctly" do allow(helper).to receive(:action_name).and_return("index") allow(helper).to receive(:current_page?).with({controller: "supervisors", action: "index"}).and_return(true) menu_item = helper.menu_item(label: "Supervisors", path: supervisors_path, visible: true) expect(menu_item).to match ">Supervisors" end describe "menu item active state" do context "when current page does not match the menu item path" do it "renders sidebar menu item as an inactive link" do allow(helper).to receive(:action_name).and_return("index") allow(helper).to receive(:current_page?).with({controller: "supervisors", action: "index"}).and_return(false) menu_item = helper.menu_item(label: "Supervisors", path: supervisors_path, visible: true) expect(menu_item).to match "class=\"list-group-item \"" end end context "when accessing an index route" do it "renders sidebar menu item as an active link" do helper.request.path = "/supervisors" menu_item = helper.menu_item(label: "Supervisors", path: supervisors_path, visible: true) expect(menu_item).to match "class=\"list-group-item active\"" end end context "when accessing an all casa admin menu item" do it "renders the sidebar menu item as an active link" do helper.request.path = "/all_casa_admins/patch_notes" menu_item = helper.menu_item(label: "Patch Notes", path: all_casa_admins_patch_notes_path, visible: true) expect(menu_item).to match "class=\"list-group-item active\"" end end context "when accessing an volunteer emancipation checklist" do it "renders the sidebar menu item as an active link with no redirect" do helper.request.path = "/emancipation_checklists" menu_item = helper.menu_item(label: "Emancipation Checklist(s)", path: emancipation_checklists_path, visible: true) expect(menu_item).to match "class=\"list-group-item active\"" end it "renders the sidebar menu item as an active link with redirect" do helper.request.path = "/casa_cases/some-case-slug/emancipation" menu_item = helper.menu_item(label: "Emancipation Checklist(s)", path: emancipation_checklists_path, visible: true) expect(menu_item).to match "class=\"list-group-item active\"" end end end end describe "#cases_index_title" do it "returns 'My Cases' when logged in as a volunteer" do volunteer = build :volunteer allow(helper).to receive(:current_user).and_return(volunteer) expect(helper.cases_index_title).to eq "My Cases" end it "returns 'Cases' when logged in as a supervisor" do volunteer = build :volunteer allow(helper).to receive(:current_user).and_return(volunteer) expect(helper.cases_index_title).to eq "My Cases" end end describe "#inbox_label" do it "returns 'Inbox' when there are no unread notifications" do volunteer = build :volunteer allow(helper).to receive(:current_user).and_return(volunteer) expect(helper.inbox_label).to eq "Inbox" end end end ================================================ FILE: spec/helpers/sms_body_helper_spec.rb ================================================ require "rails_helper" RSpec.describe SmsBodyHelper, type: :helper do describe "#account_activation_msg" do it "correct short links provided" do expected_response = account_activation_msg("primogems", {0 => "www.pasta.com", 1 => "www.yogurt.com"}) expect(expected_response).to include("First, set your password here www.pasta.com. Then visit www.yogurt.com to change your text message settings.") end it "incorrect short links provided" do expected_response = account_activation_msg("primogems", {0 => nil, 1 => nil}) expect(expected_response).to include("Please check your email to set up your password. Go to profile edit page to change SMS settings.") end it "set up password link invalid" do expected_response = account_activation_msg("primogems", {0 => nil, 1 => "www.carfax.com"}) expect(expected_response).to include("Please check your email to set up your password. Then visit www.carfax.com to change your text message settings.") end it "link to users/edit invalid" do expected_response = account_activation_msg("primogems", {0 => "www.yummy.com", 1 => nil}) expect(expected_response).to include("First, set your password here www.yummy.com. Go to profile edit page to change SMS settings.") end end end ================================================ FILE: spec/helpers/template_helper_spec.rb ================================================ require "rails_helper" RSpec.describe TemplateHelper, type: :helper do # TODO: Add tests for TemplateHelper pending "add some tests for TemplateHelper" end ================================================ FILE: spec/helpers/ui_helper_spec.rb ================================================ require "rails_helper" RSpec.describe UiHelper, type: :helper do describe "#grouped_options_for_assigning_case" do before do @casa_cases = create_list(:casa_case, 4) @volunteer = create(:volunteer, casa_org: @casa_cases[0].casa_org) current_user = create(:supervisor) allow(helper).to receive(:current_user).and_return(current_user) end it "does not render duplicate casa_case" do options = helper.grouped_options_for_assigning_case(@volunteer) expect(options[0]).to eq(options[0].uniq { |option| option[0] }) expect(options[1]).to eq(options[1].uniq { |option| option[0] }) end end end ================================================ FILE: spec/helpers/volunteer_helper_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe VolunteerHelper, type: :helper do let(:casa_case) { create(:casa_case) } let(:current_user) { create(:user) } context "when user is not a volunteer" do it "returns the assigned volunteers' names" do volunteer = create(:volunteer) casa_case.volunteers << volunteer badge_html = helper.volunteer_badge(casa_case, current_user) expect(badge_html).to include("badge") expect(badge_html).to include(volunteer.display_name) end it "returns 'Unassigned' when no volunteers are present" do badge_html = helper.volunteer_badge(casa_case, current_user) expect(badge_html).to include("badge") expect(badge_html).to include("Unassigned") end end context "when user is a volunteer" do let(:current_user) { create(:volunteer) } it "returns an empty string" do badge_html = helper.volunteer_badge(casa_case, current_user) expect(badge_html).to eq("") end end end ================================================ FILE: spec/jobs/application_job_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationJob, type: :job do # TODO: Add tests for ApplicationJob pending "add some tests for ApplicationJob" end ================================================ FILE: spec/lib/importers/case_importer_spec.rb ================================================ require "rails_helper" RSpec.describe CaseImporter do subject(:case_importer) { CaseImporter.new(import_file_path, casa_org_id) } let(:casa_org) { create :casa_org } let(:casa_org_id) { casa_org.id } let(:import_file_path) { file_fixture "casa_cases.csv" } before do allow(case_importer).to receive(:email_addresses_to_users) do |_clazz, comma_separated_emails| create_list(:volunteer, comma_separated_emails.split(",").size, casa_org_id: casa_org_id) end # next_court_date in casa_cases.csv needs to be a future date travel_to Date.parse("Sept 15 2022") end describe "#import_cases" do it "imports cases and associates volunteers with them" do expect { case_importer.import_cases }.to change(CasaCase, :count).by(3) # correctly imports birth_month_year_youth expect(CasaCase.find_by(case_number: "CINA-01-4347").birth_month_year_youth&.strftime("%Y-%m-%d")).to eql "2011-03-01" expect(CasaCase.find_by(case_number: "CINA-01-4348").birth_month_year_youth&.strftime("%Y-%m-%d")).to eql "2000-02-01" expect(CasaCase.find_by(case_number: "CINA-01-4349").birth_month_year_youth&.strftime("%Y-%m-%d")).to eql "2016-12-01" # correctly adds volunteers expect(CasaCase.find_by(case_number: "CINA-01-4347").volunteers.size).to eq(1) expect(CasaCase.find_by(case_number: "CINA-01-4348").volunteers.size).to eq(2) expect(CasaCase.find_by(case_number: "CINA-01-4349").volunteers.size).to eq(0) # correctly adds next court date expect(CasaCase.find_by(case_number: "CINA-01-4348").next_court_date.date.strftime("%Y-%m-%d")).to eql "2023-01-01" expect(CasaCase.find_by(case_number: "CINA-01-4347").next_court_date.date.strftime("%Y-%m-%d")).to eql "2022-09-16" expect(CasaCase.find_by(case_number: "CINA-01-4349").next_court_date).to be_nil end context "when updating records" do let!(:existing_case) { create(:casa_case, case_number: "CINA-01-4348") } it "assigns new volunteers to the case" do expect { case_importer.import_cases }.to change(existing_case.volunteers, :count).by(2) end it "updates outdated case fields" do expect { case_importer.import_cases existing_case.reload }.to change(existing_case, :birth_month_year_youth).to(Date.new(2000, 2, 1)) end it "adds a next court date" do expect { case_importer.import_cases existing_case.reload }.to change(existing_case, :court_dates).from([]) end end it "returns a success message with the number of cases imported" do alert = case_importer.import_cases expect(alert[:type]).to eq(:success) expect(alert[:message]).to eq("You successfully imported 3 casa_cases.") end specify "static and instance methods have identical results" do CaseImporter.new(import_file_path, casa_org_id).import_cases data_using_instance = CasaCase.pluck(:case_number).sort CourtDate.delete_all CasaCase.delete_all CaseImporter.import_cases(import_file_path, casa_org_id) data_using_static = CasaCase.pluck(:case_number).sort expect(data_using_static).to eq(data_using_instance) expect(data_using_static).not_to be_empty end context "when the importer has already run once" do before { case_importer.import_cases } it "does not duplicate casa case files from csv files" do expect { case_importer.import_cases }.not_to change(CasaCase, :count) end end context "when there's no case number" do let(:import_file_path) { file_fixture "casa_cases_without_case_number.csv" } it "returns an error message if row does not contain a case number" do alert = case_importer.import_cases expect(alert[:type]).to eq(:error) expect(alert[:message]).to eq("You successfully imported 1 casa_cases. Not all rows were imported.") expect(alert[:exported_rows]).to include("Row does not contain a case number.") end end end end ================================================ FILE: spec/lib/importers/file_importer_spec.rb ================================================ require "rails_helper" RSpec.describe FileImporter do let!(:import_user) { build_stubbed(:casa_admin) } let(:import_file_path) { file_fixture "generic.csv" } let(:file_importer) { FileImporter.new(import_file_path, import_user.casa_org.id, "something", ["header"]) } describe "import" do it "assumes headers" do file_importer.import { |_f| true } expect(file_importer.number_imported).to eq(2) end it "resets the count of how many have been imported, each time" do file_importer.import { |_f| true } file_importer.import { |_f| true } expect(file_importer.number_imported).to eq(2) end it "yields to a block" do names = [] file_importer.import do |row| names << row end expect(names.size).to eq(2) end it "captures errors" do expect { file_importer.import do |_row| raise "Something bad" end }.not_to raise_error expect(file_importer.failed_imports.size).to eq(2) end it "returns hash with expected attributes" do result = file_importer.import { |_f| true } expect(result.keys).to contain_exactly(:type, :message, :exported_rows) end it "returns an error if file has no rows" do no_row_path = file_fixture "no_rows.csv" no_row_importer = FileImporter.new(no_row_path, import_user.casa_org.id, "something", ["header"]) expect(no_row_importer.import[:message]).eql?(FileImporter::ERR_NO_ROWS) end end end ================================================ FILE: spec/lib/importers/supervisor_importer_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorImporter do let!(:import_user) { build_stubbed(:casa_admin) } let(:casa_org_id) { import_user.casa_org.id } # Use of the static method SupervisorImporter.import_volunteers functions identically to SupervisorImporter.new(...).import_volunteers # but is preferred. let(:supervisor_import_data_path) { file_fixture "supervisors.csv" } let(:supervisor_importer) do importer = SupervisorImporter.new(supervisor_import_data_path, casa_org_id) allow(importer).to receive(:email_addresses_to_users) do |_clazz, supervisor_volunteers| create_list(:volunteer, supervisor_volunteers.split(",").size, casa_org: import_user.casa_org) end importer end it "imports supervisors and associates volunteers with them" do expect { supervisor_importer.import_supervisors }.to change(Supervisor, :count).by(3) expect(Supervisor.find_by(email: "supervisor1@example.net").volunteers.size).to eq(1) expect(Supervisor.find_by(email: "supervisor2@example.net").volunteers.size).to eq(2) expect(Supervisor.find_by(email: "supervisor3@example.net").volunteers.size).to eq(0) end it "returns a success message with the number of supervisors imported" do alert = supervisor_importer.import_supervisors expect(alert[:type]).to eq(:success) expect(alert[:message]).to eq("You successfully imported 3 supervisors.") end context "when the supervisors have already been imported" do before { supervisor_importer.import_supervisors } it "does not import duplicate supervisors from csv files" do expect { supervisor_importer.import_supervisors }.not_to change(Supervisor, :count) end context "when any volunteer could not be assigned to the supervisor during the import" do let!(:existing_volunteer) { build(:volunteer, email: "volunteer1@example.net") } let(:supervisor_import_data_path) { file_fixture "supervisor_volunteers.csv" } it "returns an error message" do alert = SupervisorImporter.new(supervisor_import_data_path, casa_org_id).import_supervisors expect(alert[:type]).to eq(:error) expect(alert[:message]).to include("Not all rows were imported.") end context "because the volunteer has already been assigned to a supervisor" do let!(:supervisor_volunteer) { create(:supervisor_volunteer, volunteer: existing_volunteer) } it "returns an error message" do alert = SupervisorImporter.new(supervisor_import_data_path, casa_org_id).import_supervisors expect(alert[:type]).to eq(:error) expect(alert[:exported_rows]).to include("Volunteer #{existing_volunteer.email} already has a supervisor") end end end end context "when updating supervisors" do let!(:existing_supervisor) { create(:supervisor, display_name: "#", email: "supervisor2@example.net") } it "assigns unassigned volunteers" do expect { supervisor_importer.import_supervisors }.to change(existing_supervisor.volunteers, :count).by(2) end it "updates outdated supervisor fields" do expect { supervisor_importer.import_supervisors existing_supervisor.reload }.to change(existing_supervisor, :display_name).to("Supervisor Two") end it "updates phone number to valid number and turns on sms notifications" do expect { supervisor_importer.import_supervisors existing_supervisor.reload }.to change(existing_supervisor, :phone_number).to("+11111111111") .and change(existing_supervisor, :receive_sms_notifications).to(true) end end context "when row doesn't have e-mail address" do let(:supervisor_import_data_path) { file_fixture "supervisors_without_email.csv" } it "returns an error message" do alert = supervisor_importer.import_supervisors expect(alert[:type]).to eq(:error) expect(alert[:message]).to eq("You successfully imported 1 supervisors. Not all rows were imported.") expect(alert[:exported_rows]).to include("Row does not contain e-mail address.") end end context "when row doesn't have phone number" do let(:supervisor_import_data_path) { file_fixture "supervisors_without_phone_numbers.csv" } let!(:existing_supervisor_with_number) { create(:supervisor, display_name: "#", email: "supervisor1@example.net", phone_number: "+11111111111", receive_sms_notifications: true) } it "updates phone number to be deleted and turns off sms notifications" do expect { supervisor_importer.import_supervisors existing_supervisor_with_number.reload }.to change(existing_supervisor_with_number, :phone_number).to("") .and change(existing_supervisor_with_number, :receive_sms_notifications).to(false) end end context "when phone number in row is invalid" do let(:supervisor_import_data_path) { file_fixture "supervisors_invalid_phone_numbers.csv" } it "returns an error message" do alert = supervisor_importer.import_supervisors expect(alert[:type]).to eq(:error) expect(alert[:message]).to eq("Not all rows were imported.") expect(alert[:exported_rows]).to include("Phone number must be 10 digits or 12 digits including country code (+1)") end end specify "static and instance methods have identical results" do SupervisorImporter.new(supervisor_import_data_path, casa_org_id).import_supervisors data_using_instance = Supervisor.pluck(:email).sort SentEmail.destroy_all Supervisor.destroy_all SupervisorImporter.import_supervisors(supervisor_import_data_path, casa_org_id) data_using_static = Supervisor.pluck(:email).sort expect(data_using_static).to eq(data_using_instance) expect(data_using_static).not_to be_empty end end ================================================ FILE: spec/lib/importers/volunteer_importer_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerImporter do let!(:import_user) { build(:casa_admin) } let(:casa_org_id) { import_user.casa_org.id } # Use of the static method VolunteerImporter.import_volunteers functions identically to VolunteerImporter.new(...).import_volunteers # but is preferred. let(:import_file_path) { file_fixture "volunteers.csv" } let(:volunteer_importer) { -> { VolunteerImporter.import_volunteers(import_file_path, casa_org_id) } } it "imports volunteers from a csv file" do expect { volunteer_importer.call }.to change(User, :count).by(3) end it "returns a success message with the number of volunteers imported" do alert = volunteer_importer.call expect(alert[:type]).to eq(:success) expect(alert[:message]).to eq("You successfully imported 3 volunteers.") end context "when the volunteers have been imported already" do before { volunteer_importer.call } it "does not import duplicate volunteers from csv files" do expect { volunteer_importer.call }.not_to change(User, :count) end specify "static and instance methods have identical results" do VolunteerImporter.new(import_file_path, casa_org_id).import_volunteers data_using_instance = Volunteer.pluck(:email).sort SentEmail.destroy_all Volunteer.destroy_all VolunteerImporter.import_volunteers(import_file_path, casa_org_id) data_using_static = Volunteer.pluck(:email).sort expect(data_using_static).to eq(data_using_instance) expect(data_using_static).not_to be_empty end end context "when updating volunteers" do let!(:existing_volunteer) { create(:volunteer, display_name: "&&&&&", email: "volunteer1@example.net") } it "updates outdated volunteer fields" do expect { volunteer_importer.call existing_volunteer.reload }.to change(existing_volunteer, :display_name).to("Volunteer One") end it "updates phone number to valid number and turns sms notifications on" do expect { volunteer_importer.call existing_volunteer.reload }.to change(existing_volunteer, :phone_number).to("+11234567890") .and change(existing_volunteer, :receive_sms_notifications).to(true) end end context "when row doesn't have e-mail address" do let(:import_file_path) { file_fixture "volunteers_without_email.csv" } it "returns an error message" do alert = volunteer_importer.call expect(alert[:type]).to eq(:error) expect(alert[:message]).to eq("You successfully imported 1 volunteers. Not all rows were imported.") expect(alert[:exported_rows]).to include("Row does not contain an e-mail address.") end end context "when row doesn't have phone number" do let(:import_file_path) { file_fixture "volunteers_without_phone_numbers.csv" } let!(:existing_volunteer_with_number) { create(:volunteer, display_name: "#", email: "volunteer2@example.net", phone_number: "+11111111111", receive_sms_notifications: true) } it "updates phone number to be deleted and turns sms notifications off" do expect { volunteer_importer.call existing_volunteer_with_number.reload }.to change(existing_volunteer_with_number, :phone_number).to("") .and change(existing_volunteer_with_number, :receive_sms_notifications).to(false) end end context "when phone number in row is invalid" do let(:import_file_path) { file_fixture "volunteers_invalid_phone_numbers.csv" } it "returns an error message" do alert = volunteer_importer.call expect(alert[:type]).to eq(:error) expect(alert[:message]).to eq("Not all rows were imported.") expect(alert[:exported_rows]).to include("Phone number must be 10 digits or 12 digits including country code (+1)") end end end ================================================ FILE: spec/lib/tasks/case_contact_types_reminder_spec.rb ================================================ require "rails_helper" require_relative "../../../lib/tasks/case_contact_types_reminder" require "support/stubbed_requests/webmock_helper" RSpec.describe CaseContactTypesReminder do let!(:casa_org) do create( :casa_org, twilio_enabled: true, twilio_phone_number: "+15555555555", twilio_account_sid: "articuno34", twilio_api_key_sid: "Aladdin", twilio_api_key_secret: "open sesame" ) end let!(:volunteer) do create( :volunteer, casa_org_id: casa_org.id, phone_number: "+12222222222", receive_sms_notifications: true ) end let!(:contact_type) { create(:contact_type, name: "test") } let!(:case_contact) do create( :case_contact, creator: volunteer, contact_types: [contact_type], occurred_at: 4.months.ago ) end before do WebMockHelper.twilio_success_stub WebMockHelper.twilio_success_stub_messages_60_days WebMockHelper.short_io_stub_localhost end context "volunteer with uncontacted contact types, sms notifications on, and no reminder in last quarter" do it "sends sms reminder" do responses = CaseContactTypesReminder.new.send! expect(responses.count).to eq 1 expect(responses[0][:messages][0].body).to include CaseContactTypesReminder::FIRST_MESSAGE.strip expect(responses[0][:messages][1].body).to include contact_type.name expect(responses[0][:messages][2].body).to match CaseContactTypesReminder::THIRD_MESSAGE + "https://42ni.short.gy/jzTwdF" end end context "volunteer with contacted contact types within last 60 days, sms notifications on, and no reminder in last quarter" do it "does not send sms reminder" do CaseContact.update_all(occurred_at: 1.months.ago) responses = CaseContactTypesReminder.new.send! expect(responses.count).to match 0 end end context "volunteer with uncontacted contact types, sms notifications off, and no reminder in last quarter" do it "does not send sms reminder" do Volunteer.update_all(receive_sms_notifications: false) responses = CaseContactTypesReminder.new.send! expect(responses.count).to match 0 end end context "volunteer with uncontacted contact types, sms notifications on, and reminder in last quarter" do it "does not send sms reminder" do create(:user_reminder_time, :case_contact_types) Volunteer.update_all(receive_sms_notifications: true) responses = CaseContactTypesReminder.new.send! expect(responses.count).to match 0 end end context "volunteer with uncontacted contact types, sms notifications on, and reminder out of last quarter" do it "sends sms reminder" do UserReminderTime.destroy_all Volunteer.all do |v| create(:user_case_contact_types_reminder, user_id: v.id, reminder_sent: 4.months.ago) end responses = CaseContactTypesReminder.new.send! expect(responses.count).to match 1 expect(responses[0][:messages][0].body).to eq CaseContactTypesReminder::FIRST_MESSAGE.strip expect(responses[0][:messages][1].body).to include contact_type.name expect(responses[0][:messages][2].body).to match CaseContactTypesReminder::THIRD_MESSAGE + "https://42ni.short.gy/jzTwdF" end end end ================================================ FILE: spec/lib/tasks/data_post_processors/case_contact_populator_spec.rb ================================================ require "rails_helper" require "./lib/tasks/data_post_processors/case_contact_populator" RSpec.describe CaseContactPopulator do before do Rake::Task.clear Casa::Application.load_tasks end it "does nothing on an empty database" do described_class.populate expect(ContactType.count).to eq(0) expect(ContactTypeGroup.count).to eq(0) end it "does nothing if there are no contact types" do case_contact = create(:case_contact, contact_types: [], status: "started") described_class.populate expect(case_contact.contact_types).to be_empty expect(ContactType.count).to eq(0) expect(ContactTypeGroup.count).to eq(0) end it "creates a new contact type group with the org of the casa case" do contact_type = create(:contact_type) casa_org1 = contact_type.contact_type_group.casa_org casa_org2 = create(:casa_org) casa_case = create(:casa_case, casa_org: casa_org2) create(:case_contact, casa_case: casa_case, contact_types: [contact_type]) described_class.populate expect(ContactTypeGroup.count).to eq(2) expect(ContactTypeGroup.last.casa_org).to eq(casa_org2) expect(ContactType.count).to eq(1) expect(ContactType.first.contact_type_group.casa_org).to eq(casa_org1) end it "creates a new contact type with the org of the casa case" do ctg1 = create(:contact_type_group, casa_org: create(:casa_org), name: "Education") ctg2 = create(:contact_type_group, casa_org: create(:casa_org), name: "Education") contact_type = create(:contact_type, contact_type_group: ctg1, name: "School") casa_case = create(:casa_case, casa_org: ctg2.casa_org) create(:case_contact, casa_case: casa_case, contact_types: [contact_type]) described_class.populate expect(ContactTypeGroup.count).to eq(2) expect(ContactType.count).to eq(2) expect(ContactType.first.contact_type_group).to eq(ctg1) expect(ContactType.last.contact_type_group).to eq(ctg2) expect(contact_type.reload.contact_type_group).to eq(ctg1) end end ================================================ FILE: spec/lib/tasks/data_post_processors/contact_topic_populator_spec.rb ================================================ require "rails_helper" require "./lib/tasks/data_post_processors/contact_topic_populator" RSpec.describe "populates each existing organization with contact groups and types" do let(:fake_topics) { [1, 2, 3].map { |i| {"question" => "Question #{i}", "details" => "Details #{i}"} } } let(:org_one) { create(:casa_org) } let(:org_two) { create(:casa_org) } before do Rake::Task.clear Casa::Application.load_tasks allow(ContactTopic).to receive(:default_contact_topics).and_return(fake_topics) ContactTopicPopulator.populate end it "does nothing on an empty database" do ContactTopicPopulator.populate expect(ContactTopic.count).to eq(0) expect(ContactTopicAnswer.count).to eq(0) end context "there are orgs" do before do org_one org_two end it "creates 3 topics per each of two orgs totalling 6" do expect do ContactTopicPopulator.populate end.to change(ContactTopic, :count).from(0).to(6) end context "there are case_contacts" do let(:case_one) { create(:casa_case, casa_org: org_one) } let(:case_two) { create(:casa_case, casa_org: org_two) } before do create_list(:case_contact, 3, casa_case: case_one) create_list(:case_contact, 3, casa_case: case_two) end it "creates 3 topic answers for each case contact" do expect { ContactTopicPopulator.populate }.to change(ContactTopicAnswer, :count).from(0).to(18) expect(case_one.case_contacts.map(&:contact_topic_answers).flatten.size).to eq(9) expect(case_two.case_contacts.map(&:contact_topic_answers).flatten.size).to eq(9) CaseContact.all.each do |case_contact| expect(case_contact.contact_topic_answers.size).to eq(3) end end end end end ================================================ FILE: spec/lib/tasks/data_post_processors/contact_type_populator_spec.rb ================================================ require "rails_helper" RSpec.describe "populates each existing organization with contact groups and types" do before do Rake::Task.clear Casa::Application.load_tasks end it "creates the expected contact groups and contact types for each existing organization" do ContactTypePopulator.populate CasaOrg.all.each do |org| casa_group = org.contact_type_groups.find_by(name: "CASA") expect(casa_group.contact_types.pluck(:name)).to contain_exactly("Youth", "Supervisor") family_group = org.contact_type_groups.find_by(name: "Family") expect(family_group.contact_types.pluck(:name)).to contain_exactly("Parent", "Other Family", "Sibling", "Grandparent", "Aunt Uncle or Cousin", "Fictive Kin") placement_group = org.contact_type_groups.find_by(name: "Placement") expect(placement_group.contact_types.pluck(:name)).to contain_exactly("Foster Parent", "Caregiver Family", "Therapeutic Agency Worker") social_services_group = org.contact_type_groups.find_by(name: "Social Services") expect(social_services_group.contact_types.pluck(:name)).to contain_exactly("Social Worker") legal_group = org.contact_type_groups.find_by(name: "Legal") expect(legal_group.contact_types.pluck(:name)).to contain_exactly("Court", "Attorney") health_group = org.contact_type_groups.find_by(name: "Health") expect(health_group.contact_types.pluck(:name)).to contain_exactly("Medical Professional", "Mental Health Therapist", "Other Therapist", "Psychiatric Practitioner") education_group = org.contact_type_groups.find_by(name: "Education") expect(education_group.contact_types.pluck(:name)).to contain_exactly("School", "Guidance Counselor", "Teacher", "IEP Team") end end end ================================================ FILE: spec/lib/tasks/no_contact_made_reminder_spec.rb ================================================ require "rails_helper" require_relative "../../../lib/tasks/no_contact_made_reminder" require "support/stubbed_requests/webmock_helper" RSpec.describe NoContactMadeReminder do let!(:casa_org) do create( :casa_org, twilio_enabled: true, twilio_phone_number: "+15555555555", twilio_account_sid: "articuno34", twilio_api_key_sid: "Aladdin", twilio_api_key_secret: "open sesame" ) end let!(:volunteer) do create( :volunteer, casa_org_id: casa_org.id, phone_number: "+12222222222", receive_sms_notifications: true ) end let!(:contact_type) { create(:contact_type, name: "test") } let!(:case_contact) do create( :case_contact, creator: volunteer, contact_types: [contact_type], occurred_at: 1.week.ago, contact_made: false ) end let!(:expected_sms) { "It's been two weeks since you've tried reaching 'test'. Try again! https://42ni.short.gy/jzTwdF" } before do WebMockHelper.twilio_success_stub WebMockHelper.twilio_no_contact_made_stub WebMockHelper.short_io_stub_localhost end context "volunteer with no contact made in past 2 weeks in case contact" do it "sends sms reminder" do responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 1 expect(responses[0][:volunteer]).to eq(volunteer) expect(responses[0][:message].body).to eq expected_sms end end context "volunteer with contact made after not making contact" do let(:case_contact) do create( :case_contact, creator: volunteer, contact_types: [contact_type], occurred_at: 2.days.ago, contact_made: true ) end it "sends not sms reminder" do responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 0 end end context "volunteer with contact made after not making contact but volunteer is no longer assigned to the case" do let(:case_contact) do create( :case_contact, creator: create(:volunteer), # different volunteer assigned contact_types: [contact_type], occurred_at: 1.week.ago, contact_made: false ) end it "sends not sms reminder" do responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 0 end end context "volunteer with no case contacts" do it "sends not sms reminder" do CaseContact.destroy_all responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 0 end end context "volunteer with quarterly case contact type reminder sent on same day" do let(:quarterly_reminder) { create(:user_reminder_time, :quarterly_reminder) } it "sends not sms reminder" do CaseContact.destroy_all responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 0 end end context "volunteer with no contact made reminder sent within last 30 days" do let(:no_contact_made_reminder) { create(:user_reminder_time, no_contact_made: 1.weeks.ago) } it "sends not sms reminder" do CaseContact.destroy_all responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 0 end end context "volunteer with sms notification off" do let(:volunteer) { create( :volunteer, casa_org_id: casa_org.id, phone_number: "+12222222222", receive_sms_notifications: false ) } it "sends not sms reminder" do responses = NoContactMadeReminder.new.send! expect(responses.count).to eq 0 end end end ================================================ FILE: spec/lib/tasks/supervisor_weekly_digest_spec.rb ================================================ require "rails_helper" require_relative "../../../lib/tasks/supervisor_weekly_digest" RSpec.describe SupervisorWeeklyDigest do describe "#send!" do subject { described_class.new.send! } context "on monday" do context "with active and deactivated supervisor" do before do travel_to Time.zone.local(2021, 9, 27, 12, 0, 0) # monday noon create(:supervisor, active: true) create(:supervisor, active: false) end it "only sends to active supervisor" do expect { subject }.to change { ActionMailer::Base.deliveries.count }.by(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Weekly summary of volunteers' activities for the week of 2021-09-20") end end end context "not on monday" do before do travel_to Time.zone.local(2021, 9, 29, 12, 0, 0) # not monday create(:supervisor, active: true) end it "does not send email" do expect { subject }.not_to change { ActionMailer::Base.deliveries.count } end end end end ================================================ FILE: spec/mailers/application_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationMailer, type: :mailer do # TODO: Add tests for ApplicationMailer pending "add some tests for ApplicationMailer" end ================================================ FILE: spec/mailers/casa_admin_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe CasaAdminMailer, type: :mailer do let(:casa_admin) { create(:casa_admin) } describe ".account_setup for an admin" do let(:mail) { CasaAdminMailer.account_setup(casa_admin) } it "sends an email saying the account has been created" do expect(mail.body.encoded).to match("A #{casa_admin.casa_org.display_name}’s County Admin account") expect(mail.body.encoded).to match("has been created for you") end it "generates a password reset token and sends email" do expect(casa_admin.reset_password_token).to be_nil expect(casa_admin.reset_password_sent_at).to be_nil expect(mail.body.encoded.squish).to match("Set Your Password") expect(casa_admin.reset_password_token).not_to be_nil expect(casa_admin.reset_password_sent_at).not_to be_nil end end describe ".invitation_instructions for an all casa admin" do let!(:all_casa_admin) { create(:all_casa_admin) } let!(:mail) { all_casa_admin.invite! } it "informs the correct expiration date" do expiration_date = I18n.l(all_casa_admin.invitation_due_at, format: :full, default: nil) email_body = mail.html_part.body.to_s.squish expect(email_body).to include("This invitation will expire on #{expiration_date} (one week).") end end describe ".invitation_instructions for a casa admin" do let!(:casa_admin) { create(:casa_admin) } let!(:mail) { casa_admin.invite! } it "informs the correct expiration date" do expiration_date = I18n.l(casa_admin.invitation_due_at, format: :full, default: nil) email_body = mail.html_part.body.to_s.squish expect(email_body).to include("This invitation will expire on #{expiration_date} (two weeks).") end end end ================================================ FILE: spec/mailers/fund_request_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe FundRequestMailer, type: :mailer do let(:fund_request) { build(:fund_request) } let(:mail) { described_class.send_request(nil, fund_request) } it "sends email" do email_body = mail.body.encoded.squish expect(email_body).to include("Fund Request") end end ================================================ FILE: spec/mailers/learning_hours_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHoursMailer, type: :mailer do describe "#learning_hours_report_email" do let(:user) { create(:user) } let(:casa_org) { create(:casa_org, users: [user]) } let(:learning_hours) { [instance_double(LearningHour)] } let(:csv_data) { "dummy,csv,data" } before do allow(LearningHour).to receive(:where).and_return(learning_hours) allow(LearningHoursExportCsvService).to receive(:new).and_return(instance_double(LearningHoursExportCsvService, perform: csv_data)) end it "sends the email to the provided user with correct subject and attachment" do mail = LearningHoursMailer.learning_hours_report_email(user) expect(mail.to).to eq([user.email]) end_date = Date.today.end_of_month expected_subject = "Learning Hours Report for #{end_date.strftime("%B, %Y")}." expect(mail.subject).to eq(expected_subject) expect(mail.attachments.first.filename).to eq("learning-hours-report-#{Date.today}.csv") expect(mail.attachments.first.body.raw_source).to eq(csv_data) end end end ================================================ FILE: spec/mailers/previews/casa_admin_mailer_preview_spec.rb ================================================ require "rails_helper" require Rails.root.join("lib/mailers/previews/casa_admin_mailer_preview").to_s RSpec.describe CasaAdminMailerPreview do let!(:casa_admin) { create(:casa_admin) } describe "#account_setup" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.account_setup } it { expect(email.to).to eq [casa_admin.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: casa_admin.id) } let(:email) { preview.account_setup } it { expect(email.to).to eq [casa_admin.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.account_setup } it { expect(email.to).to eq ["missing_casa_admin@example.com"] } end end describe "#deactivation" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.deactivation } it { expect(email.to).to eq [casa_admin.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: casa_admin.id) } let(:email) { preview.deactivation } it { expect(email.to).to eq [casa_admin.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.deactivation } it { expect(email.to).to eq ["missing_casa_admin@example.com"] } end end end ================================================ FILE: spec/mailers/previews/devise_mailer_preview_spec.rb ================================================ require "rails_helper" require Rails.root.join("lib/mailers/previews/devise_mailer_preview").to_s RSpec.describe DeviseMailerPreview do let(:subject) { described_class.new } let!(:user) { create(:user) } describe "#reset_password_instructions" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.reset_password_instructions } it { expect(email.to).to eq [user.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: user.id) } let(:email) { preview.reset_password_instructions } it { expect(email.to).to eq [user.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.reset_password_instructions } it { expect(email.to).to eq ["missing_user@example.com"] } end end describe "#invitation_instructions_as_all_casa_admin" do let!(:all_casa_admin) { create(:all_casa_admin) } let(:message) { subject.invitation_instructions_as_all_casa_admin } it { expect(message.to).to eq [all_casa_admin.email] } end describe "#invitation_instructions_as_casa_admin" do let!(:casa_admin) { create(:casa_admin) } let(:message) { subject.invitation_instructions_as_casa_admin } it { expect(message.to).to eq [casa_admin.email] } end describe "#invitation_instructions_as_supervisor" do let!(:supervisor) { create(:supervisor) } let(:message) { subject.invitation_instructions_as_supervisor } it { expect(message.to).to eq [supervisor.email] } end describe "#invitation_instructions_as_volunteer" do let!(:volunteer) { create(:volunteer) } let(:message) { subject.invitation_instructions_as_volunteer } it { expect(message.to).to eq [volunteer.email] } end end ================================================ FILE: spec/mailers/previews/supervisor_mailer_preview_spec.rb ================================================ require "rails_helper" require Rails.root.join("lib/mailers/previews/supervisor_mailer_preview").to_s RSpec.describe SupervisorMailerPreview do let!(:supervisor) { create(:supervisor) } let!(:volunteer) { create(:volunteer, casa_org: supervisor.casa_org, supervisor: supervisor) } describe "#account_setup" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.account_setup } it { expect(email.to).to eq [supervisor.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: supervisor.id) } let(:email) { preview.account_setup } it { expect(email.to).to eq [supervisor.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.account_setup } it { expect(email.to).to eq ["missing_supervisor@example.com"] } end end describe "#weekly_digest" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.weekly_digest } it { expect(email.to).to eq [supervisor.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: supervisor.id) } let(:email) { preview.weekly_digest } it { expect(email.to).to eq [supervisor.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: 3500) } let(:email) { preview.weekly_digest } it { expect(email.to).to eq ["missing_supervisor@example.com"] } end end describe "#reimbursement_request_email" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.reimbursement_request_email } it { expect(email.to).to eq [supervisor.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(volunteer_id: volunteer.id, supervisor_id: supervisor.id) } let(:email) { preview.reimbursement_request_email } it { expect(email.to).to eq [supervisor.email] } end end end ================================================ FILE: spec/mailers/previews/volunteer_mailer_preview_spec.rb ================================================ require "rails_helper" require Rails.root.join("lib/mailers/previews/volunteer_mailer_preview").to_s RSpec.describe VolunteerMailerPreview do let!(:volunteer) { create(:volunteer) } describe "#account_setup" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.account_setup } it { expect(email.to).to eq [volunteer.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: volunteer.id) } let(:email) { preview.account_setup } it { expect(email.to).to eq [volunteer.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.account_setup } it { expect(email.to).to eq ["missing_volunteer@example.com"] } end end describe "#court_report_reminder" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.court_report_reminder } it { expect(email.to).to eq [volunteer.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: volunteer.id) } let(:email) { preview.court_report_reminder } it { expect(email.to).to eq [volunteer.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.court_report_reminder } it { expect(email.to).to eq ["missing_volunteer@example.com"] } end end describe "#case_contacts_reminder" do context "When no ID is passed" do let(:preview) { described_class.new } let(:email) { preview.case_contacts_reminder } it { expect(email.to).to eq [volunteer.email] } end context "When passed ID is valid" do let(:preview) { described_class.new(id: volunteer.id) } let(:email) { preview.case_contacts_reminder } it { expect(email.to).to eq [volunteer.email] } end context "When passed ID is invalid" do let(:preview) { described_class.new(id: -1) } let(:email) { preview.case_contacts_reminder } it { expect(email.to).to eq ["missing_volunteer@example.com"] } end end end ================================================ FILE: spec/mailers/supervisor_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorMailer, type: :mailer do describe ".weekly_digest" do let(:supervisor) { build(:supervisor, :receive_reimbursement_attachment) } let(:volunteer) { build(:volunteer, casa_org: supervisor.casa_org, supervisor: supervisor) } let(:casa_case) { create(:casa_case, casa_org: supervisor.casa_org) } let(:mail) { SupervisorMailer.weekly_digest(supervisor) } context "when a supervisor has volunteer assigned to a casa case" do let!(:case_assignment) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer) } it "shows a summary for a volunteer assigned to the supervisor" do expect(mail.body.encoded).to match("Summary for #{volunteer.display_name}") end it "does not show a case contact that did not occurr in the week" do build_stubbed(:case_contact, casa_case: casa_case, occurred_at: Date.today - 8.days) expect(mail.body.encoded).not_to match("Most recent contact attempted:") end it "shows the latest case contact that occurred in the week" do most_recent_contact = create(:case_contact, casa_case: casa_case, occurred_at: Date.today - 1.days, notes: "AAAAAAAAAAAAAAAAAAAAAAAA") other_contact = build(:case_contact, casa_case: casa_case, occurred_at: Date.today - 3.days, notes: "BBBBBBBBBBBBBBBBBBBB") expect(mail.body.encoded).to match("Notes: #{most_recent_contact.notes}") expect(mail.body.encoded).not_to match("Notes: #{other_contact.notes}") end it "has a CSV attachment" do expect(mail.attachments.count).to eq(1) end end context "when a supervisor has a volunteer who is unassigned from a casa case during the week" do let!(:case_assignment) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false, updated_at: Date.today - 2.days) } it "shows a summary for a volunteer recently unassigned from the supervisor" do expect(mail.body.encoded).to match("Summary for #{volunteer.display_name}") end it "shows a disclaimer for a volunteer recently unassigned from the supervisor" do expect(mail.body.encoded).to match("This case was unassigned from #{volunteer.display_name}") end it "does not show a case contact that occurred past the unassignment date in the week" do contact_past_unassignment = build_stubbed(:case_contact, casa_case: casa_case, occurred_at: Date.today - 1.days, notes: "AAAAAAAAAAAAAAAAAAAAAAAA") expect(mail.body.encoded).not_to match("Notes: #{contact_past_unassignment.notes}") end it "shows the latest case contact that occurred in the week before the unassignment date" do contact_past_unassignment = create(:case_contact, casa_case: casa_case, occurred_at: Date.today - 1.days, notes: "AAAAAAAAAAAAAAAAAAAAAAAA") most_recent_contact_before_unassignment = create(:case_contact, casa_case: casa_case, occurred_at: Date.today - 3.days, notes: "BBBBBBBBBBBBBBBBBB") older_contact = build_stubbed(:case_contact, casa_case: casa_case, occurred_at: Date.today - 4.days, notes: "CCCCCCCCCCCCCCCCCCCC") expect(mail.body.encoded).to match("Notes: #{most_recent_contact_before_unassignment.notes}") expect(mail.body.encoded).not_to match("Notes: #{contact_past_unassignment.notes}") expect(mail.body.encoded).not_to match("Notes: #{older_contact.notes}") end end it "does not show a summary for a volunteer unassigned from the supervisor before the week" do create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false, updated_at: Date.today - 8.days) expect(mail.body.encoded).not_to match("Summary for #{volunteer.display_name}") end context "when a supervisor has pending volunteer to accepts invitation" do let(:volunteer2) { create(:volunteer) } before do volunteer2.invite!(supervisor) end it "shows a summary of pending volunteers" do expect(mail.body.encoded).to match(volunteer2.display_name.to_s) end it "has a button to re-invite volunteer" do expect(mail.body.encoded).to match("") end it "do not shows a summary of pending volunteers if the volunteer already accepted" do volunteer2.invitation_accepted_at = DateTime.current volunteer2.save expect(mail.body.encoded).not_to match(volunteer2.display_name.to_s) end it "does not display 'There are no pending volunteers' message when there are pending volunteers" do expect(mail.body.encoded).not_to match("There are no pending volunteers.") end end it "displays no pending volunteers message when there are no pending volunteers" do expect(mail.body.encoded).to match("There are no pending volunteers.") end it "does not display 'There are no additional notes' message when there are additional notes" do create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false, updated_at: Date.today + 8.days) expect(mail.body.encoded).not_to match("There are no additional notes.") end it "displays no additional notes message when there are no additional notes" do expect(mail.body.encoded).to match("There are no additional notes.") end it "does not display no_recent_sign_in section" do expect(mail.body.encoded).not_to match("volunteers have not signed in or created case contacts in the last 30 days") end context "when supervisor has a volunteer that has not been active for the last 30 days" do let!(:volunteer) do create(:volunteer, casa_org: supervisor.casa_org, supervisor: supervisor, last_sign_in_at: 31.days.ago) end let(:casa_org) { volunteer.casa_org } let(:other_org) { create(:casa_org) } let!(:volunteer_for_other_supervisor_same_org) { create(:volunteer, last_sign_in_at: 31.days.ago, casa_org: casa_org, supervisor: create(:supervisor, casa_org: casa_org)) } let!(:volunteer_for_other_org) { create(:volunteer, last_sign_in_at: 31.days.ago, casa_org: other_org, supervisor: create(:supervisor, casa_org: other_org)) } it "display no_recent_sign_in section" do expect(mail.body.encoded).to match("volunteers have not signed in or created case contacts in the last 30 days") expect(mail.body.encoded).to match(volunteer.display_name) expect(mail.body.encoded).not_to match(volunteer_for_other_supervisor_same_org.display_name) expect(mail.body.encoded).not_to match(volunteer_for_other_org.display_name) end context "but volunteer has a recent case_contact to its name" do let!(:recent_case_contact) do create(:case_contact, casa_case: casa_case, occurred_at: 29.days.ago, creator: volunteer) end it "does not display no_recent_sign_in section" do expect(mail.body.encoded).not_to match("volunteers have not signed in or created case contacts in the last 30 days") end end end end describe ".invitation_instructions for a supervisor" do let(:supervisor) { create(:supervisor) } let(:mail) { supervisor.invite! } let(:expiration_date) { I18n.l(supervisor.invitation_due_at, format: :full, default: nil) } it "informs the correct expiration date" do email_body = mail.html_part.body.to_s.squish expect(email_body).to include("This invitation will expire on #{expiration_date} (two weeks).") end end describe ".reimbursement_request_email" do let(:supervisor) { create(:supervisor, receive_reimbursement_email: true) } let(:volunteer) { create(:volunteer, supervisor: supervisor) } let(:casa_organization) { volunteer.casa_org } let(:mail) { SupervisorMailer.reimbursement_request_email(volunteer, supervisor) } it "sends email reminder" do expect(mail.subject).to eq("New reimbursement request from #{volunteer.display_name}") expect(mail.to).to eq([supervisor.email]) expect(mail.body.encoded).to match("#{volunteer.display_name} has submitted a reimbursement request") end end end ================================================ FILE: spec/mailers/user_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe UserMailer, type: :mailer do describe "password_changed_reminder" do subject(:mail) { described_class.password_changed_reminder(user) } let(:user) { create(:user) } it "renders the headers", :aggregate_failures do expect(mail.subject).to eq("CASA Password Changed") expect(mail.to).to eq([user.email]) end it "renders the body", :aggregate_failures do expect(mail.body.encoded).to match("Hello #{user.display_name}") expect(mail.body.encoded).to match("Your CASA password has been changed.") end end end ================================================ FILE: spec/mailers/volunteer_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerMailer, type: :mailer do let(:volunteer) { create(:volunteer) } let(:volunteer_with_supervisor) { create(:volunteer, :with_assigned_supervisor) } describe ".account_setup" do let(:mail) { VolunteerMailer.account_setup(volunteer) } it "generates a password reset token and sends email" do expect(volunteer.reset_password_token).to be_nil expect(volunteer.reset_password_sent_at).to be_nil expect(mail.body.encoded.squish).to match("Set Your Password") expect(volunteer.reset_password_token).not_to be_nil expect(volunteer.reset_password_sent_at).not_to be_nil end end describe ".court_report_reminder" do let(:report_due_date) { Date.current + 7.days } let(:mail) { VolunteerMailer.court_report_reminder(volunteer, report_due_date) } it "sends email reminder" do expect(mail.body.encoded).to match("next court report is due on #{report_due_date}") end end describe ".case_contacts_reminder" do it "sends an email reminding volunteer" do mail = VolunteerMailer.case_contacts_reminder(volunteer, []) expect(mail.body.encoded).to match("Hello #{volunteer.display_name}") expect(mail.body.encoded).to match("as a reminder") expect(mail.body.encoded).to include(case_contacts_url.to_s) expect(mail.cc).to be_empty end it "sends and cc's recipients" do cc_recipients = %w[supervisor@example.com admin@example.com] mail = VolunteerMailer.case_contacts_reminder(volunteer, cc_recipients) expect(mail.cc).to match_array(cc_recipients) end end describe ".invitation_instructions for a volunteer" do let(:mail) { volunteer.invite! } let(:expiration_date) { I18n.l(volunteer.invitation_due_at, format: :full, default: nil) } it "informs the correct expiration date" do email_body = mail.html_part.body.to_s.squish expect(email_body).to include("This invitation will expire on #{expiration_date} (one year).") end end end ================================================ FILE: spec/models/acts_as_paranoid_spec.rb ================================================ require "rails_helper" RSpec.describe "acts_as_paranoid" do let(:currently_probably_buggy_classes_ignored) do %w[] end let(:allows_multiple_deleted) { "(deleted_at IS NULL)" } it "checks that all activerecord models using acts_as_paranoid have the deleted exclusions on unique indexes" do errors = [] found_ignored_error_indexes = [] Zeitwerk::Loader.eager_load_all expect(ApplicationRecord.descendants.count).to be >= 54 # make sure we are actually testing all model classes ApplicationRecord.descendants.each do |clazz| next if clazz.abstract_class? next unless clazz.paranoid? unique_indexes = ApplicationRecord.connection_pool.with_connection do |connection| connection.indexes(clazz.table_name).select(&:unique) end unique_indexes.each do |idx| next if idx.columns == ["external_id"] # it is ok for external_id to be unique if currently_probably_buggy_classes_ignored.include?(idx.name) found_ignored_error_indexes << idx.name next end unless idx.where&.include?(allows_multiple_deleted) errors << "#{idx.name} on #{clazz} uses acts_as_paranoid but has a unique index without #{allows_multiple_deleted} but it does have: #{idx.where}" end end end expect(errors).to be_empty expect(found_ignored_error_indexes).to match_array(currently_probably_buggy_classes_ignored) end end ================================================ FILE: spec/models/additional_expense_spec.rb ================================================ require "rails_helper" RSpec.describe AdditionalExpense, type: :model do it { is_expected.to belong_to(:case_contact) } it { is_expected.to have_one(:casa_case).through(:case_contact) } it { is_expected.to have_one(:casa_org).through(:case_contact) } it { is_expected.to have_one(:contact_creator).through(:case_contact) } it { is_expected.to have_one(:contact_creator_casa_org).through(:contact_creator) } describe "validations" do let(:case_contact) { build_stubbed :case_contact } it "requires describe only if amount is positive" do expense = build(:additional_expense, amount: 0, describe: nil, case_contact:) expect(expense).to be_valid expense.update(amount: 1) expect(expense).to be_invalid end end end ================================================ FILE: spec/models/address_spec.rb ================================================ require "rails_helper" RSpec.describe Address, type: :model do describe "validate associations" do it { is_expected.to belong_to(:user) } end end ================================================ FILE: spec/models/all_casa_admin_spec.rb ================================================ require "rails_helper" RSpec.describe AllCasaAdmin, type: :model do describe "#role" do subject(:all_casa_admin) { create :all_casa_admin } it { expect(all_casa_admin.role).to eq "All Casa Admin" } end end ================================================ FILE: spec/models/all_casa_admins/casa_org_metrics_spec.rb ================================================ require "rails_helper" RSpec.describe AllCasaAdmins::CasaOrgMetrics, type: :model do let(:organization) { create :casa_org } let(:user) { build(:all_casa_admin) } describe "#metrics" do subject { described_class.new(organization).metrics } context "minimal data" do it "shows stats" do expect(subject).to eq( { "Number of admins" => 0, "Number of supervisors" => 0, "Number of active volunteers" => 0, "Number of inactive volunteers" => 0, "Number of active cases" => 0, "Number of inactive cases" => 0, "Number of all case contacts including inactives" => 0, "Number of active supervisor to volunteer assignments" => 0, "Number of active case assignments" => 0 } ) end end context "with inactives" do let(:obj_types) { [ :casa_admin, :supervisor, :volunteer, :casa_case, :case_assignment, :supervisor_volunteer ] } before do obj_types.each do |obj_type| create(obj_type, casa_org: organization) create(obj_type, :inactive, casa_org: organization) end end it "shows stats" do expect(subject).to eq( { "Number of active case assignments" => 1, "Number of active cases" => 3, "Number of active supervisor to volunteer assignments" => 6, "Number of active volunteers" => 5, "Number of admins" => 2, "Number of all case contacts including inactives" => 1, "Number of inactive cases" => 1, "Number of inactive volunteers" => 1, "Number of supervisors" => 4 } ) end end end end ================================================ FILE: spec/models/api_credential_spec.rb ================================================ require "rails_helper" require "digest" RSpec.describe ApiCredential, type: :model do let(:api_credential) { create(:api_credential, user: user) } let(:user) { create(:user) } it { is_expected.to belong_to(:user) } describe "#authenticate_api_token" do it "returns true for a valid api_token" do api_token = api_credential.return_new_api_token![:api_token] expect(api_credential.authenticate_api_token(api_token)).to be true end it "returns false for an invalid api_token" do expect(api_credential.authenticate_api_token("invalid_token")).to be false end end describe "#authenticate_refresh_token" do it "returns true for a valid refresh_token" do refresh_token = api_credential.return_new_refresh_token!(false)[:refresh_token] expect(api_credential.authenticate_refresh_token(refresh_token)).to be true end it "returns false for an invalid refresh_token" do expect(api_credential.authenticate_refresh_token("invalid_token")).to be false end end describe "#return_new_api_token!" do it "updates the api_token digest" do old_digest = api_credential.api_token_digest api_credential.return_new_api_token![:api_token] api_credential.reload expect(api_credential.api_token_digest).not_to eq(old_digest) end it "sets a new api_token" do new_token = api_credential.return_new_api_token![:api_token] expect(new_token).not_to be_nil end end describe "#return_new_refresh_token!" do it "updates the refresh_token digest" do old_digest = api_credential.refresh_token_digest api_credential.return_new_refresh_token!(false)[:refresh_token] api_credential.reload expect(api_credential.refresh_token_digest).not_to eq(old_digest) end it "sets a new refresh_token" do new_token = api_credential.return_new_refresh_token!(false)[:refresh_token] expect(new_token).not_to be_nil end end describe "#is_api_token_expired?" do it "returns false if token is still valid" do api_credential.update!(token_expires_at: 1.hour.from_now) expect(api_credential.is_api_token_expired?).to be false end it "returns true if token is expired" do api_credential.update!(token_expires_at: 1.hour.ago) expect(api_credential.is_api_token_expired?).to be true end end describe "#is_refresh_token_expired?" do it "returns false if token is still valid" do api_credential.update!(refresh_token_expires_at: 1.hour.from_now) expect(api_credential.is_refresh_token_expired?).to be false end it "returns true if token is expired" do api_credential.update!(token_expires_at: 1.hour.ago) expect(api_credential.is_api_token_expired?).to be true end end describe "#generate_api_token" do it "creates a secure hashed api_token" do api_credential.api_token_digest api_token = api_credential.return_new_api_token![:api_token] expect(api_credential.api_token_digest).to eq(Digest::SHA256.hexdigest(api_token)) end end describe "#generate_refresh_token" do it "creates a secure hashed refresh_token" do api_credential.refresh_token_digest refresh_token = api_credential.return_new_refresh_token!(false)[:refresh_token] expect(api_credential.refresh_token_digest).to eq(Digest::SHA256.hexdigest(refresh_token)) end end describe "#revoke_api_token" do it "sets api token to nil" do api_credential.return_new_api_token![:api_token] api_credential.revoke_api_token expect(api_credential.api_token_digest).to be_nil end end describe "#revoke_refresh_token" do it "sets refresh token to nil" do api_credential.return_new_refresh_token!(false)[:refresh_token] api_credential.revoke_refresh_token expect(api_credential.refresh_token_digest).to be_nil end end describe "#generate_refresh_token_with_rememberme" do it "updates token to be valid for 1 year" do api_credential.refresh_token_digest api_credential.return_new_refresh_token!(true)[:refresh_token] expect(api_credential.refresh_token_expires_at).to be_within(1.minutes).of(1.year.from_now) end end describe "#generate_refresh_token_without_rememberme" do it "updates token to be valid for 30 days" do api_credential.refresh_token_digest api_credential.return_new_refresh_token!(false)[:refresh_token] expect(api_credential.refresh_token_expires_at).to be_within(1.minutes).of(30.days.from_now) end end end ================================================ FILE: spec/models/application_record_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationRecord, type: :model do # TODO: Add tests for ApplicationRecord pending "add some tests for ApplicationRecord" end ================================================ FILE: spec/models/banner_spec.rb ================================================ require "rails_helper" RSpec.describe Banner, type: :model do describe "#valid?" do let(:casa_org) { create(:casa_org) } let(:supervisor) { create(:supervisor, casa_org: casa_org) } it "does not allow multiple active banners for same organization" do create(:banner, casa_org: casa_org, user: supervisor) banner = build(:banner, casa_org: casa_org, user: supervisor) expect(banner).not_to be_valid end it "does allow multiple active banners for different organization" do create(:banner, casa_org: casa_org, user: supervisor) another_org = create(:casa_org) another_supervisor = create(:supervisor, casa_org: another_org) banner = build(:banner, casa_org: another_org, user: another_supervisor) expect(banner).to be_valid end it "does not allow an expiry date set in the past" do banner = build(:banner, casa_org: casa_org, user: supervisor, expires_at: 1.hour.ago) expect(banner).not_to be_valid end it "allows an expiry date set in the future" do banner = build(:banner, casa_org: casa_org, user: supervisor, expires_at: 1.day.from_now) expect(banner).to be_valid end it "does not allow content to be empty" do banner = build(:banner, casa_org: casa_org, user: supervisor, content: nil) expect(banner).not_to be_valid end end describe "#expired?" do it "is false when expires_at is nil" do banner = create(:banner, expires_at: nil) expect(banner).not_to be_expired end it "is false when expires_at is set but is in the future" do banner = create(:banner, expires_at: 7.days.from_now) expect(banner).not_to be_expired end it "is true when expires_at is set but is in the past" do banner = create(:banner, expires_at: nil) banner.update_columns(expires_at: 1.hour.ago) expect(banner).to be_expired end it "sets active to false when banner is expired" do banner = create(:banner, expires_at: 1.hour.from_now) expect(banner.active).to be true travel 2.hours banner.expired? expect(banner.active).to be false end end describe "#expires_at_in_time_zone" do it "can shift time by timezone for equivalent times" do travel_to Time.new(2024, 6, 1, 11, 0, 0, "UTC") banner = create(:banner, expires_at: "2024-06-13 12:00:00 UTC") expires_at_in_pacific_time = banner.expires_at_in_time_zone("America/Los_Angeles") expect(expires_at_in_pacific_time.to_s).to eq("2024-06-13 05:00:00 -0700") expires_at_in_eastern_time = banner.expires_at_in_time_zone("America/New_York") expect(expires_at_in_eastern_time.to_s).to eq("2024-06-13 08:00:00 -0400") expect(expires_at_in_pacific_time).to eq(expires_at_in_eastern_time) travel_back end end end ================================================ FILE: spec/models/casa_admin_spec.rb ================================================ require "rails_helper" RSpec.describe CasaAdmin, type: :model do let(:casa_admin) { build(:casa_admin) } describe "#deactivate" do it "deactivates the casa admin" do casa_admin.deactivate casa_admin.reload expect(casa_admin.active).to eq(false) end it "activates the casa admin" do casa_admin.active = false casa_admin.save casa_admin.activate casa_admin.reload expect(casa_admin.active).to eq(true) end end describe "#role" do subject(:admin) { build(:casa_admin) } it { expect(admin.role).to eq "Casa Admin" } end describe "invitation expiration" do let!(:mail) { casa_admin.invite! } let(:expiration_date) { I18n.l(casa_admin.invitation_due_at, format: :full, default: nil) } let(:two_weeks) { I18n.l(2.weeks.from_now, format: :full, default: nil) } it { expect(expiration_date).to eq two_weeks } it "expires invitation token after two weeks" do travel_to 2.weeks.from_now user = User.accept_invitation!(invitation_token: casa_admin.invitation_token) expect(user.errors.full_messages).to include("Invitation token is invalid") travel_back end end describe "change to supervisor" do subject(:admin) { create(:casa_admin) } it "returns true if the change was successful" do expect(subject.change_to_supervisor!).to be_truthy end it "changes the supervisor to an admin" do subject.change_to_supervisor! user = User.find(subject.id) # subject.reload will cause RecordNotFound because it's looking in the wrong table expect(user).not_to be_casa_admin expect(user).to be_supervisor end end end ================================================ FILE: spec/models/casa_case_contact_type_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCaseContactType, type: :model do it "does not allow adding the same contact type twice to a case" do expect { casa_case = create(:casa_case) contact_type = create(:contact_type) casa_case.contact_types << contact_type casa_case.contact_types << contact_type }.to raise_error(ActiveRecord::RecordInvalid) end end ================================================ FILE: spec/models/casa_case_emancipation_category_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCaseEmancipationCategory, type: :model do it { is_expected.to belong_to(:casa_case) } it { is_expected.to belong_to(:emancipation_category) } it "does not allow adding the same category twice to a case" do expect { casa_case = create(:casa_case) emancipation_category = create(:emancipation_category) casa_case.emancipation_categories << emancipation_category casa_case.emancipation_categories << emancipation_category }.to raise_error(ActiveRecord::RecordInvalid) end it "has a valid factory" do case_category_association = build(:casa_case_emancipation_category) expect(case_category_association.valid?).to be true end end ================================================ FILE: spec/models/casa_case_emancipation_option_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCaseEmancipationOption, type: :model do it { is_expected.to belong_to(:casa_case) } it { is_expected.to belong_to(:emancipation_option) } it "does not allow adding the same category twice to a case" do expect { casa_case = create(:casa_case) emancipation_option = build(:emancipation_option) casa_case.emancipation_options << emancipation_option casa_case.emancipation_options << emancipation_option }.to raise_error(ActiveRecord::RecordInvalid) end it "has a valid factory" do case_option_association = build(:casa_case_emancipation_option) expect(case_option_association.valid?).to be true end end ================================================ FILE: spec/models/casa_case_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCase, type: :model do subject { build(:casa_case) } it { is_expected.to have_many(:case_assignments).dependent(:destroy) } it { is_expected.to belong_to(:casa_org) } it { is_expected.to have_many(:casa_case_emancipation_categories).dependent(:destroy) } it { is_expected.to have_many(:emancipation_categories).through(:casa_case_emancipation_categories) } it { is_expected.to have_many(:casa_case_emancipation_options).dependent(:destroy) } it { is_expected.to have_many(:emancipation_options).through(:casa_case_emancipation_options) } it { is_expected.to have_many(:case_court_orders).dependent(:destroy) } it { is_expected.to have_many(:volunteers).through(:case_assignments) } describe "validations" do describe "case_number" do 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 } end describe "date_in_care" do it "is valid when blank" do casa_case = CasaCase.new(date_in_care: nil) casa_case.valid? expect(casa_case.errors[:date_in_care]).to eq([]) end it "is not valid before 1989" do casa_case = CasaCase.new(date_in_care: "1984-01-01".to_date) expect(casa_case.valid?).to be false expect(casa_case.errors[:date_in_care]).to eq(["is not valid: Youth's Date in Care cannot be prior to 1/1/1989."]) end it "is not valid with a future date" do casa_case = CasaCase.new(date_in_care: 1.day.from_now) expect(casa_case.valid?).to be false expect(casa_case.errors[:date_in_care]).to eq(["is not valid: Youth's Date in Care cannot be a future date."]) end it "is valid today" do casa_case = CasaCase.new(date_in_care: Time.current) casa_case.valid? expect(casa_case.errors[:date_in_care]).to eq([]) end it "is valid in the past after 1989" do casa_case = CasaCase.new(date_in_care: "1997-08-29".to_date) casa_case.valid? expect(casa_case.errors[:date_in_care]).to eq([]) end end describe "birth_month_year_youth" do it { is_expected.to validate_presence_of(:birth_month_year_youth) } it "is not valid before 1989" do casa_case = CasaCase.new(birth_month_year_youth: "1984-01-01".to_date) expect(casa_case.valid?).to be false expect(casa_case.errors[:birth_month_year_youth]).to eq(["is not valid: Youth's Birth Month & Year cannot be prior to 1/1/1989."]) end it "is not valid with a future date" do casa_case = CasaCase.new(birth_month_year_youth: 1.day.from_now) expect(casa_case.valid?).to be false expect(casa_case.errors[:birth_month_year_youth]).to eq(["is not valid: Youth's Birth Month & Year cannot be a future date."]) end it "is valid today" do casa_case = CasaCase.new(birth_month_year_youth: Time.current) casa_case.valid? expect(casa_case.errors[:birth_month_year_youth]).to eq([]) end it "is valid in the past after 1989" do casa_case = CasaCase.new(birth_month_year_youth: "1997-08-29".to_date) casa_case.valid? expect(casa_case.errors[:birth_month_year_youth]).to eq([]) end end end describe "scopes" do describe ".due_date_passed" do subject { described_class.due_date_passed } context "when casa_case is present" do let!(:court_date) { create(:court_date, date: 3.days.ago) } let(:casa_case) { court_date.casa_case } it { is_expected.to include(casa_case) } end context "when casa_case is not present" do let!(:court_date) { create(:court_date, date: 3.days.from_now) } let(:casa_case) { court_date.casa_case } it { is_expected.not_to include(casa_case) } end end describe ".birthday_next_month" do subject { described_class.birthday_next_month } context "when a youth has a birthday next month" do let(:casa_case) { create(:casa_case, birth_month_year_youth: 10.years.ago + 1.month) } it { is_expected.to include(casa_case) } end context "when no youth has a birthday next month" do let(:casa_case) { create(:casa_case) } it { is_expected.to be_empty } end end end describe ".unassigned_volunteers" do let!(:casa_case) { create(:casa_case) } let!(:volunteer_same_org) { create(:volunteer, display_name: "Yelena Belova", casa_org: casa_case.casa_org) } let!(:volunteer_same_org_1_with_cases) { create(:volunteer, :with_casa_cases, display_name: "Natasha Romanoff", casa_org: casa_case.casa_org) } let!(:volunteer_same_org_2_with_cases) { create(:volunteer, :with_casa_cases, display_name: "Melina Vostokoff", casa_org: casa_case.casa_org) } let!(:volunteer_different_org) { create(:volunteer, casa_org: create(:casa_org)) } it "only shows volunteers for the current volunteers organization" do expect(casa_case.unassigned_volunteers).to include(volunteer_same_org) expect(casa_case.unassigned_volunteers).not_to include(volunteer_different_org) end it "sorts volunteers by display name with no cases to the top" do expect(casa_case.unassigned_volunteers).to contain_exactly(volunteer_same_org, volunteer_same_org_2_with_cases, volunteer_same_org_1_with_cases) end end describe ".ordered" do it "orders the casa cases by updated at date" do very_old_casa_case = create(:casa_case, updated_at: 5.days.ago) old_casa_case = create(:casa_case, updated_at: 1.day.ago) new_casa_case = create(:casa_case) ordered_casa_cases = described_class.ordered expect(ordered_casa_cases.map(&:id)).to eq [new_casa_case.id, old_casa_case.id, very_old_casa_case.id] end end describe ".actively_assigned_to" do it "only returns cases actively assigned to a volunteer" do current_user = build(:volunteer) inactive_case = build(:casa_case, casa_org: current_user.casa_org) build_stubbed(:case_assignment, casa_case: inactive_case, volunteer: current_user, active: false) active_cases = create_list(:casa_case, 2, casa_org: current_user.casa_org) active_cases.each do |casa_case| create(:case_assignment, casa_case: casa_case, volunteer: current_user, active: true) end other_user = build(:volunteer) other_active_case = build(:casa_case, casa_org: other_user.casa_org) other_inactive_case = build(:casa_case, casa_org: other_user.casa_org) create(:case_assignment, casa_case: other_active_case, volunteer: other_user, active: true) create( :case_assignment, casa_case: other_inactive_case, volunteer: other_user, active: false ) assert_equal active_cases.map(&:case_number).sort, described_class.actively_assigned_to(current_user).map(&:case_number).sort end end describe ".not_assigned" do it "only returns cases NOT actively assigned to ANY volunteer" do current_user = create(:volunteer) never_assigned_case = create(:casa_case, casa_org: current_user.casa_org) inactive_case = create(:casa_case, casa_org: current_user.casa_org) create(:case_assignment, casa_case: inactive_case, volunteer: current_user, active: false) active_cases = create_list(:casa_case, 2, casa_org: current_user.casa_org) active_cases.each do |casa_case| create(:case_assignment, casa_case: casa_case, volunteer: current_user, active: true) end other_user = create(:volunteer) other_active_case = create(:casa_case, casa_org: other_user.casa_org) other_inactive_case = create(:casa_case, casa_org: other_user.casa_org) create(:case_assignment, casa_case: other_active_case, volunteer: other_user, active: true) create( :case_assignment, casa_case: other_inactive_case, volunteer: other_user, active: false ) expect(described_class.not_assigned(current_user.casa_org)).to contain_exactly(never_assigned_case, inactive_case, other_inactive_case) end end describe ".should_transition" do it "returns only youth who should have transitioned but have not" do not_transitioned_13_yo = create(:casa_case, birth_month_year_youth: Date.current - 13.years) already_transitioned_15_yo = create(:casa_case, birth_month_year_youth: Date.current - 15.years) should_transition_14_yo = create(:casa_case, birth_month_year_youth: Date.current - 14.years) cases = CasaCase.should_transition aggregate_failures do expect(cases.length).to eq 2 expect(cases.include?(should_transition_14_yo)).to eq true expect(cases.include?(already_transitioned_15_yo)).to eq true expect(cases.include?(not_transitioned_13_yo)).to eq false end end end describe "#active_case_assignments" do it "only includes active assignments" do casa_org = create(:casa_org) casa_case = create(:casa_case, casa_org: casa_org) case_assignments = 2.times.map { create(:case_assignment, casa_case: casa_case, volunteer: create(:volunteer, casa_org: casa_org)) } expect(casa_case.active_case_assignments).to match_array case_assignments case_assignments.first.update(active: false) expect(casa_case.reload.active_case_assignments).to eq [case_assignments.last] end end describe "#add_emancipation_category" do let(:casa_case) { create(:casa_case) } let(:emancipation_category) { create(:emancipation_category) } it "associates an emancipation category with the case when passed the id of the category" do expect { casa_case.add_emancipation_category(emancipation_category.id) }.to change { casa_case.emancipation_categories.count }.from(0).to(1) end end describe "#add_emancipation_option" do let(:casa_case) { create(:casa_case) } let(:emancipation_category) { build(:emancipation_category, mutually_exclusive: true) } let(:emancipation_option_a) { create(:emancipation_option, emancipation_category: emancipation_category) } let(:emancipation_option_b) { create(:emancipation_option, emancipation_category: emancipation_category, name: "Not the same name as option A to satisfy unique contraints") } it "associates an emancipation option with the case when passed the id of the option" do expect { casa_case.add_emancipation_option(emancipation_option_a.id) }.to change { casa_case.emancipation_options.count }.from(0).to(1) end it "raises an error when attempting to add multiple options belonging to a mutually exclusive category" do expect { casa_case.add_emancipation_option(emancipation_option_a.id) casa_case.add_emancipation_option(emancipation_option_b.id) }.to raise_error("Attempted adding multiple options belonging to a mutually exclusive category") end end describe "#assigned_volunteers" do let(:casa_org) { create(:casa_org) } let(:casa_case) { build(:casa_case, casa_org: casa_org) } let(:volunteer1) { build(:volunteer, casa_org: casa_org) } let(:volunteer2) { build(:volunteer, casa_org: casa_org) } let!(:case_assignment1) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer1) } let!(:case_assignment2) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer2) } it "only includes volunteers through active assignments" do expect(casa_case.assigned_volunteers.order(:id)).to eq [volunteer1, volunteer2].sort_by(&:id) case_assignment1.update(active: false) expect(casa_case.reload.assigned_volunteers).to eq [volunteer2] end it "only includes active volunteers" do expect(casa_case.assigned_volunteers.order(:id)).to eq [volunteer1, volunteer2].sort_by(&:id) volunteer1.update(active: false) expect(casa_case.reload.assigned_volunteers).to eq [volunteer2] end end describe "#clear_court_dates" do context "when court date has passed" do it "sets court report as unsubmitted" do casa_case = build(:casa_case, court_report_status: :submitted) casa_case.clear_court_dates expect(casa_case.court_report_status).to eq "not_submitted" end end end describe "#court_report_status" do subject { casa_case.court_report_status = court_report_status } let(:casa_case) { build(:casa_case) } let(:submitted_time) { Time.parse("Sun Nov 08 11:06:20 2020") } let(:the_future) { submitted_time + 2.days } before do travel_to submitted_time end context "when the case is already submitted" do let(:casa_case) { build(:casa_case, court_report_status: :submitted, court_report_submitted_at: submitted_time) } before do travel_to the_future end context "when the status is completed" do let(:court_report_status) { :completed } it "completes the court report and does not update time" do expect(subject).to eq :completed expect(casa_case.court_report_submitted_at).to eq(submitted_time) end end context "when the status is not_submitted" do let(:court_report_status) { :not_submitted } it "clears submission date and value" do expect(subject).to eq :not_submitted expect(casa_case.court_report_submitted_at).to be_nil end end end context "when status is submitted" do let(:court_report_status) { :submitted } it "tracks the court report submission" do expect(subject).to eq :submitted expect(casa_case.court_report_submitted_at).to eq(submitted_time) end end context "when the status is in review" do let(:court_report_status) { :in_review } it "tracks the court report submission" do expect(subject).to eq :in_review expect(casa_case.court_report_submitted_at).to eq(submitted_time) end end end describe "#most_recent_past_court_date" do let(:casa_case) { create(:casa_case) } it "returns the latest past court date" do most_recent_past_court_date = create(:court_date, date: 3.months.ago) casa_case.court_dates << create(:court_date, date: 9.months.ago) casa_case.court_dates << most_recent_past_court_date casa_case.court_dates << create(:court_date, date: 15.months.ago) expect(casa_case.most_recent_past_court_date).to eq(most_recent_past_court_date) end end describe "#formatted_latest_court_date" do let(:casa_case) { create(:casa_case) } before do travel_to Date.new(2021, 1, 1) end context "with a past court date" do it "returns the latest past court date as a formatted string" do most_recent_past_court_date = create(:court_date, date: 3.months.ago) casa_case.court_dates << create(:court_date, date: 9.months.ago) casa_case.court_dates << most_recent_past_court_date casa_case.court_dates << create(:court_date, date: 15.months.ago) expect(casa_case.formatted_latest_court_date).to eq("October 01, 2020") # 3 months before 1/1/21 end end context "without a past court date" do it "returns the current day as a formatted string" do allow(casa_case).to receive(:most_recent_past_court_date).and_return(nil) expect(casa_case.formatted_latest_court_date).to eq("January 01, 2021") end end end describe "#remove_emancipation_category" do let(:casa_case) { create(:casa_case) } let(:emancipation_category) { build(:emancipation_category) } it "dissociates an emancipation category with the case when passed the id of the category" do casa_case.emancipation_categories << emancipation_category expect { casa_case.remove_emancipation_category(emancipation_category.id) }.to change { casa_case.emancipation_categories.count }.from(1).to(0) end end describe "#remove_emancipation_option" do let(:casa_case) { create(:casa_case) } let(:emancipation_option) { build(:emancipation_option) } it "dissociates an emancipation option with the case when passed the id of the option" do casa_case.emancipation_options << emancipation_option expect { casa_case.remove_emancipation_option(emancipation_option.id) }.to change { casa_case.emancipation_options.count }.from(1).to(0) end end describe "#update_cleaning_contact_types" do it "cleans up contact types before saving" do group = build(:contact_type_group) type1 = build(:contact_type, contact_type_group: group) type2 = create(:contact_type, contact_type_group: group) casa_case = create(:casa_case, contact_types: [type1]) expect(casa_case.casa_case_contact_types.count).to be 1 expect(casa_case.contact_types).to contain_exactly(type1) casa_case.update_cleaning_contact_types({contact_type_ids: [type2.id]}) expect(casa_case.casa_case_contact_types.count).to be 1 expect(casa_case.contact_types.reload).to contain_exactly(type2) end end describe "report submission" do let(:bad_case) { build(:casa_case) } # Creating a case whith a status other than not_submitted and a nil submission date it "rejects cases with a court report status, but no submission date" do bad_case.court_report_status = :in_review bad_case.court_report_submitted_at = nil bad_case.valid? expect(bad_case.errors[:court_report_status]).to include( "Court report submission date can't be nil if status is anything but not_submitted." ) end it "rejects cases with a submission date, but no status" do bad_case.court_report_status = :not_submitted bad_case.court_report_submitted_at = DateTime.now bad_case.valid? expect(bad_case.errors[:court_report_submitted_at]).to include( "Submission date must be nil if court report status is not submitted." ) end end describe "slug" do let(:casa_case) { create(:casa_case, case_number: "CINA-21-1234") } it "is parameterized from the case number" do expect(casa_case.slug).to eq "cina-21-1234" end it "updates when the case number changes" do casa_case.case_number = "CINA-21-1234-changed" casa_case.save expect(casa_case.slug).to eq "cina-21-1234-changed" end end end ================================================ FILE: spec/models/casa_org_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe CasaOrg, type: :model do it { is_expected.to validate_presence_of(:name) } it { is_expected.to have_many(:users).dependent(:destroy) } it { is_expected.to have_many(:casa_cases).dependent(:destroy) } it { is_expected.to have_many(:contact_type_groups).dependent(:destroy) } it { is_expected.to have_many(:hearing_types).dependent(:destroy) } it { is_expected.to have_many(:mileage_rates).dependent(:destroy) } it { is_expected.to have_many(:case_assignments).through(:users) } it { is_expected.to have_one_attached(:logo) } it { is_expected.to have_one_attached(:court_report_template) } it { is_expected.to have_many(:contact_topics) } it { is_expected.to have_many(:custom_org_links).dependent(:destroy) } it "has unique name" do org = create(:casa_org) new_org = build(:casa_org, name: org.name) expect(new_org.valid?).to be false end describe "CasaOrgValidator" do let(:casa_org) { build(:casa_org) } it "delegates phone validation to PhoneNumberHelper" do expect_any_instance_of(PhoneNumberHelper).to receive(:valid_phone_number).once.with(casa_org.twilio_phone_number) casa_org.valid? end end describe "validate validate_twilio_credentials" do let(:casa_org) { create(:casa_org, twilio_enabled: true) } let(:twilio_rest_error) do error_response = double("error_response", status_code: 401, body: {}) Twilio::REST::RestError.new("Error message", error_response) end it "validates twillio credentials on update", :aggregate_failures do twillio_client = instance_double(Twilio::REST::Client) allow(Twilio::REST::Client).to receive(:new).and_return(twillio_client) allow(twillio_client).to receive_message_chain(:messages, :list).and_raise(twilio_rest_error) %i[twilio_account_sid twilio_api_key_sid twilio_api_key_secret].each do |field| update_successful = casa_org.update(field => "") aggregate_failures do expect(update_successful).to be false expect(casa_org.errors[:base]).to eq ["Your Twilio credentials are incorrect, kindly check and try again."] end end end it "returns error if credentials form invalid URI" do twillio_client = instance_double(Twilio::REST::Client) allow(Twilio::REST::Client).to receive(:new).and_return(twillio_client) allow(twillio_client).to receive_message_chain(:messages, :list).and_raise(URI::InvalidURIError) casa_org.update(twilio_account_sid: "some bad value") aggregate_failures do expect(casa_org).not_to be_valid expect(casa_org.errors[:base]).to eq ["Your Twilio credentials are incorrect, kindly check and try again."] end end context "org with disabled twilio" do let(:casa_org) { create(:casa_org, twilio_enabled: false) } it "validates twillio credentials on update", :aggregate_failures do %i[twilio_account_sid twilio_api_key_sid twilio_api_key_secret].each do |field| expect(casa_org.update(field => "")).to be true end end end end describe "Attachment" do it "is valid" do aggregate_failures do subject = build(:casa_org, twilio_enabled: false) expect(subject.org_logo).to eq(Pathname.new("#{Rails.public_path.join("logo.jpeg")}")) subject.logo.attach( io: File.open(file_fixture("company_logo.png")), filename: "company_logo.png", content_type: "image/png" ) subject.save! expect(subject.logo).to be_an_instance_of(ActiveStorage::Attached::One) expect(subject.org_logo).to eq("/rails/active_storage/blobs/redirect/#{subject.logo.signed_id}/#{subject.logo.filename}") end end end context "when creating an organization" do let(:org) { create(:casa_org, name: "Prince George CASA") } it "has a slug based on the name" do expect(org.slug).to eq "prince-george-casa" end end describe "generate_defaults" do let(:org) { create(:casa_org) } let(:fake_topics) { [{"question" => "Test Title", "details" => "Test details"}] } before do allow(ContactTopic).to receive(:default_contact_topics).and_return(fake_topics) org.generate_defaults end describe "generates default contact type groups" do let(:groups) { ContactTypeGroup.where(casa_org: org).joins(:contact_types).pluck(:name, "contact_types.name").sort } it "matches default contact type groups" do expect(groups).to eq([["CASA", "Supervisor"], ["CASA", "Youth"], ["Education", "Guidance Counselor"], ["Education", "IEP Team"], ["Education", "School"], ["Education", "Teacher"], ["Family", "Aunt Uncle or Cousin"], ["Family", "Fictive Kin"], ["Family", "Grandparent"], ["Family", "Other Family"], ["Family", "Parent"], ["Family", "Sibling"], ["Health", "Medical Professional"], ["Health", "Mental Health Therapist"], ["Health", "Other Therapist"], ["Health", "Psychiatric Practitioner"], ["Legal", "Attorney"], ["Legal", "Court"], ["Placement", "Caregiver Family"], ["Placement", "Foster Parent"], ["Placement", "Therapeutic Agency Worker"], ["Social Services", "Social Worker"]]) end end describe "generates default hearing types" do let(:hearing_types_names) { HearingType.where(casa_org: org).pluck(:name) } it "matches default hearing types" do expect(hearing_types_names).to include(*HearingType::DEFAULT_HEARING_TYPES) end end describe "generates default contact topics" do let(:contact_topics) { ContactTopic.where(casa_org: org).map(&:question) } it "matches default contact topics" do expected = fake_topics.pluck("question") expect(contact_topics).to include(*expected) end end end describe "mileage rate for a given date" do let(:casa_org) { build(:casa_org) } describe "with a casa org with no rates" do it "is nil" do expect(casa_org.mileage_rate_for_given_date(Date.today)).to be_nil end end describe "with a casa org with inactive dates" do let!(:mileage_rates) do [ create(:mileage_rate, casa_org: casa_org, effective_date: 10.days.ago, is_active: false), create(:mileage_rate, casa_org: casa_org, effective_date: 3.days.ago, is_active: false) ] end it "is nil" do expect(casa_org.mileage_rates.count).to eq 2 expect(casa_org.mileage_rate_for_given_date(Date.today)).to be_nil end end describe "with active dates in the future" do let!(:mileage_rate) { create(:mileage_rate, casa_org: casa_org, effective_date: 3.days.from_now) } it "is nil" do expect(casa_org.mileage_rates.count).to eq 1 expect(casa_org.mileage_rate_for_given_date(Date.today)).to be_nil end end describe "with active dates in the past" do let!(:mileage_rates) do [ create(:mileage_rate, casa_org: casa_org, amount: 4.50, effective_date: 20.days.ago), create(:mileage_rate, casa_org: casa_org, amount: 5.50, effective_date: 10.days.ago), create(:mileage_rate, casa_org: casa_org, amount: 6.50, effective_date: 3.days.ago) ] end it "uses the most recent date" do expect(casa_org.mileage_rate_for_given_date(12.days.ago.to_date)).to eq 4.50 expect(casa_org.mileage_rate_for_given_date(5.days.ago.to_date)).to eq 5.50 expect(casa_org.mileage_rate_for_given_date(Date.today)).to eq 6.50 end end end end ================================================ FILE: spec/models/case_assignment_spec.rb ================================================ require "rails_helper" RSpec.describe CaseAssignment, type: :model do let(:casa_org_1) { create(:casa_org) } let(:casa_case_1) { create(:casa_case, casa_org: casa_org_1) } let(:volunteer_1) { create(:volunteer, casa_org: casa_org_1) } let(:inactive) { create(:volunteer, :inactive, casa_org: casa_org_1) } let(:supervisor) { create(:supervisor, casa_org: casa_org_1) } let(:casa_case_2) { create(:casa_case, casa_org: casa_org_1) } let(:volunteer_2) { create(:volunteer, casa_org: casa_org_1) } let(:casa_org_2) { create(:casa_org) } it "only allow active volunteers to be assigned" do expect(casa_case_1.case_assignments.new(volunteer: volunteer_1)).to be_valid casa_case_1.reload expect(casa_case_1.case_assignments.new(volunteer: inactive)).to be_invalid casa_case_1.reload expect(casa_case_1.case_assignments.new(volunteer: supervisor)).to be_invalid end it "allows two volunteers to be assigned to the same case" do casa_case_1.volunteers << volunteer_1 casa_case_1.volunteers << volunteer_2 casa_case_1.save! expect(volunteer_1.casa_cases).to eq([casa_case_1]) expect(volunteer_2.casa_cases).to eq([casa_case_1]) end it "allows volunteer to be assigned to multiple cases" do volunteer_1.casa_cases << casa_case_1 volunteer_1.casa_cases << casa_case_2 volunteer_1.save! expect(casa_case_1.reload.volunteers).to eq([volunteer_1]) expect(casa_case_2.reload.volunteers).to eq([volunteer_1]) end it "does not allow a volunteer to be double assigned" do expect { volunteer_1.casa_cases << casa_case_1 volunteer_1.casa_cases << casa_case_1 }.to raise_error(ActiveRecord::RecordInvalid) end it "requires case and volunteer belong to the same organization" do case_assignment = casa_case_1.case_assignments.new(volunteer: volunteer_1) expect { volunteer_1.update(casa_org: casa_org_2) }.to change(case_assignment, :valid?).to false end describe ".active" do it "only includes active case assignments" do casa_case = create(:casa_case) case_assignments = 2.times.map { create(:case_assignment, casa_case: casa_case, volunteer: create(:volunteer, casa_org: casa_case.casa_org)) } expect(CaseAssignment.active).to match_array(case_assignments) case_assignments.first.update(active: false) expect(CaseAssignment.active).to eq [case_assignments.last] end end end ================================================ FILE: spec/models/case_contact_contact_type_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactContactType, type: :model do it "does not allow adding the same contact type twice to a case contact" do expect { case_contact = create(:case_contact) contact_type = create(:contact_type) case_contact.contact_types << contact_type case_contact.contact_types << contact_type }.to raise_error(ActiveRecord::RecordInvalid) end end ================================================ FILE: spec/models/case_contact_report_spec.rb ================================================ require "rails_helper" require "csv" RSpec.describe CaseContactReport, type: :model do describe "#generate_headers" do it "matches the length of row data" do create(:case_contact) csv = described_class.new.to_csv parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.length).to eq(1) # length doesn't include header row expect(parsed_csv.headers).to eq([ "Internal Contact Number", "Duration Minutes", "Contact Types", "Contact Made", "Contact Medium", "Occurred At", "Added To System At", "Miles Driven", "Wants Driving Reimbursement", "Casa Case Number", "Creator Email", "Creator Name", "Supervisor Name", "Case Contact Notes" ]) case_contact_data = parsed_csv.first expect(parsed_csv.headers.length).to eq(case_contact_data.length) end end describe "CSV body serialization" do subject { CaseContactReport.new(casa_org_id: long_case_contact.casa_case.casa_org.id).to_csv } let!(:long_case_contact) { create(:case_contact, :long_note) } let!(:multi_line_case_contact) { create(:case_contact, :multi_line_note, casa_case: long_case_contact.casa_case) } it "includes entire note" do expect(subject).to include(long_case_contact.notes) expect(subject).to include(multi_line_case_contact.notes) end end describe "filter behavior" do describe "casa organization" do let(:casa_org) { create(:casa_org) } let(:casa_case) { create(:casa_case, casa_org: casa_org) } let(:case_contact) { create(:case_contact, casa_case: casa_case) } it "includes case contacts from current org" do report = CaseContactReport.new(casa_org_id: casa_org.id) expect(report.case_contacts).to contain_exactly(case_contact) end context "from other orgs" do let(:other_casa_org) { create(:casa_org) } let(:casa_case) { create(:casa_case, casa_org: other_casa_org) } it "excludes case contacts" do report = CaseContactReport.new(casa_org_id: casa_org.id) expect(report.case_contacts).to be_empty end end end context "when result is empty" do it "returns only headers if result is empty" do report = CaseContactReport.new( { "start_date" => 1.days.ago, "end_date" => 1.days.ago, "contact_made" => true, "has_transitioned" => true, "want_driving_reimbursement" => true, "contact_type_ids" => ["4"], "contact_type_group_ids" => ["2", "3"], "supervisor_ids" => ["2"] } ) contacts = report.case_contacts expect(report.to_csv).to eq( "Internal Contact Number,Duration Minutes,Contact Types,Contact Made,Contact Medium,Occurred At,Added To System At,Miles Driven,Wants Driving Reimbursement,Casa Case Number,Creator Email,Creator Name,Supervisor Name,Case Contact Notes\n" ) expect(contacts.length).to eq(0) end end context "when result is not empty" do describe "occured at range filter" do it "uses date range if provided" do create(:case_contact, {occurred_at: 20.days.ago}) build(:case_contact, {occurred_at: 100.days.ago}) report = CaseContactReport.new({start_date: 30.days.ago, end_date: 10.days.ago}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns all date ranges if not provided" do create(:case_contact, {occurred_at: 20.days.ago}) create(:case_contact, {occurred_at: 100.days.ago}) report = CaseContactReport.new({}) contacts = report.case_contacts expect(contacts.length).to eq(2) end it "returns only the volunteer" do volunteer = create(:volunteer) create(:case_contact, {occurred_at: 20.days.ago, creator_id: volunteer.id}) build(:case_contact, {occurred_at: 100.days.ago}) report = CaseContactReport.new({creator_ids: [volunteer.id]}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns only the volunteer with date range" do volunteer = create(:volunteer) create(:case_contact, {occurred_at: 20.days.ago, creator_id: volunteer.id}) create(:case_contact, {occurred_at: 100.days.ago, creator_id: volunteer.id}) build(:case_contact, {occurred_at: 100.days.ago}) report = CaseContactReport.new({start_date: 30.days.ago, end_date: 10.days.ago, creator_ids: [volunteer.id]}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns only the volunteer with the specified supervisors" do casa_org = build(:casa_org) supervisor = create(:supervisor, casa_org: casa_org) volunteer = build(:volunteer, casa_org: casa_org) volunteer2 = create(:volunteer, casa_org: casa_org) create(:supervisor_volunteer, volunteer: volunteer, supervisor: supervisor) contact = create(:case_contact, {occurred_at: 20.days.ago, creator_id: volunteer.id}) build_stubbed(:case_contact, {occurred_at: 100.days.ago, creator_id: volunteer2.id}) build_stubbed(:case_contact, {occurred_at: 100.days.ago}) report = CaseContactReport.new({supervisor_ids: [supervisor.id]}) contacts = report.case_contacts expect(contacts.length).to eq(1) expect(contacts).to eq([contact]) end end end describe "case contact behavior" do before do create(:case_contact, {contact_made: true}) create(:case_contact, {contact_made: false}) end it "returns only the case contacts with where contact was made" do report = CaseContactReport.new({contact_made: true}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns only the case contacts with where contact was NOT made" do report = CaseContactReport.new({contact_made: false}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns only the case contacts with where contact was made or NOT made" do report = CaseContactReport.new({contact_made: [true, false]}) contacts = report.case_contacts expect(contacts.length).to eq(2) end end describe "has transitioned behavior" do let(:case_case_1) { create(:casa_case, birth_month_year_youth: 15.years.ago) } let(:case_case_2) { create(:casa_case, birth_month_year_youth: 10.years.ago) } before do create(:case_contact, {casa_case: case_case_1}) create(:case_contact, {casa_case: case_case_2}) end it "returns only case contacts the youth has transitioned" do contacts = CaseContactReport.new(has_transitioned: false).case_contacts expect(contacts.length).to eq(1) end it "returns only case contacts the youth has transitioned" do contacts = CaseContactReport.new(has_transitioned: true).case_contacts expect(contacts.length).to eq(1) end it "returns case contacts with both youth has transitioned and youth has not transitioned" do contacts = CaseContactReport.new(has_transitioned: "").case_contacts expect(contacts.length).to eq(2) end end describe "wanting driving reimbursement functionality" do before do create(:case_contact, {miles_driven: 50, want_driving_reimbursement: true}) create(:case_contact, {miles_driven: 50, want_driving_reimbursement: false}) end it "returns only contacts that want reimbursement" do report = CaseContactReport.new({want_driving_reimbursement: true}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns only contacts that DO NOT want reimbursement" do report = CaseContactReport.new({want_driving_reimbursement: false}) contacts = report.case_contacts expect(contacts.length).to eq(1) end it "returns contacts that both want reimbursement and do not want reimbursement" do report = CaseContactReport.new({want_driving_reimbursement: ""}) contacts = report.case_contacts expect(contacts.length).to eq(2) end end describe "contact type filter functionality" do it "returns only the case contacts that include the case contact" do casa_org = build(:casa_org) supervisor = create(:supervisor, casa_org: casa_org) volunteer = build(:volunteer, casa_org: casa_org) volunteer2 = create(:volunteer, casa_org: casa_org) court = build(:contact_type, name: "Court") school = build_stubbed(:contact_type, name: "School") create(:supervisor_volunteer, volunteer: volunteer, supervisor: supervisor) contact = create(:case_contact, {occurred_at: 20.days.ago, creator_id: volunteer.id, contact_types: [court]}) build_stubbed(:case_contact, {occurred_at: 100.days.ago, creator_id: volunteer2.id, contact_types: [school]}) build_stubbed(:case_contact, {occurred_at: 100.days.ago}) report = CaseContactReport.new({contact_type_ids: [court.id]}) contacts = report.case_contacts expect(contacts.length).to eq(1) expect(contacts).to eq([contact]) end end describe "contact type group filter functionality" do before do casa_org = build(:casa_org) supervisor = create(:supervisor, casa_org: casa_org) volunteer = build(:volunteer, casa_org: casa_org) volunteer2 = create(:volunteer, casa_org: casa_org) create(:supervisor_volunteer, volunteer: volunteer, supervisor: supervisor) @contact_type_group = build(:contact_type_group, name: "Legal") legal_court = build_stubbed(:contact_type, name: "Court", contact_type_group: @contact_type_group) legal_attorney = build(:contact_type, name: "Attorney", contact_type_group: @contact_type_group) placement_school = build_stubbed(:contact_type, name: "School", contact_type_group: build(:contact_type_group, name: "Placement")) @expected_contact = create(:case_contact, {occurred_at: 20.days.ago, creator_id: volunteer.id, contact_types: [legal_court, legal_attorney]}) create(:case_contact, {occurred_at: 100.days.ago, creator_id: volunteer2.id, contact_types: [placement_school]}) create(:case_contact, {occurred_at: 100.days.ago}) end context "3 contacts each with 1 contact type groups: Legal, Placement and 1 random" do context "when select 1 contact type group" do it "returns 1 case contact whose contact_types belong to that group" do report = CaseContactReport.new( {contact_type_group_ids: [@contact_type_group.id]} ) expect(report.case_contacts.length).to eq(1) expect(report.case_contacts).to eq([@expected_contact]) end end context "when select prompt option (value is empty) and 1 contact type group" do it "returns 1 case contact whose contact_types belong to that group" do report = CaseContactReport.new( {contact_type_group_ids: ["", @contact_type_group.id, ""]} ) expect(report.case_contacts.length).to eq(1) expect(report.case_contacts).to eq([@expected_contact]) end end context "when select ONLY prompt option (value is empty) and NO contact type group" do it "does no filtering & returns 3 case contacts" do report = CaseContactReport.new( {contact_type_group_ids: [""]} ) expect(report.case_contacts.length).to eq(3) expect(report.case_contacts).to eq(CaseContact.all) end end context "when select nothing on Case Type Group" do it "does no filtering & returns 3 case contacts" do report = CaseContactReport.new( {contact_type_group_ids: nil} ) expect(report.case_contacts.length).to eq(3) expect(report.case_contacts).to eq(CaseContact.all) end end end end describe "casa case number filter" do let!(:casa_case) { create(:casa_case) } let!(:case_contacts) { create_list(:case_contact, 3, casa_case: casa_case) } before { create_list(:case_contact, 8) } context "when providing casa case ids" do it "returns all case contacts with the casa case ids" do report = described_class.new({casa_case_ids: [casa_case.id]}) expect(report.case_contacts.length).to eq(case_contacts.length) expect(report.case_contacts).to match_array(case_contacts) end end context "when not providing casa case ids" do it "return all case contacts" do report = described_class.new({casa_case_ids: nil}) expect(report.case_contacts.length).to eq(CaseContact.count) expect(report.case_contacts).to eq(CaseContact.all) end end end describe "multiple filter behavior" do it "only returns records that occured less than 30 days ago, the youth has transitioned, and the contact type was either court or therapist" do court = build(:contact_type, name: "Court") school = build(:contact_type, name: "School") therapist = build(:contact_type, name: "Therapist") untransitioned_casa_case = create(:casa_case, :pre_transition) transitioned_casa_case = create(:casa_case) contact1 = create(:case_contact, occurred_at: 20.days.ago, casa_case: transitioned_casa_case, contact_types: [court]) build_stubbed(:case_contact, occurred_at: 40.days.ago, casa_case: transitioned_casa_case, contact_types: [court]) build_stubbed(:case_contact, occurred_at: 20.days.ago, casa_case: untransitioned_casa_case, contact_types: [court]) contact4 = create(:case_contact, occurred_at: 20.days.ago, casa_case: transitioned_casa_case, contact_types: [school]) contact5 = create(:case_contact, occurred_at: 20.days.ago, casa_case: transitioned_casa_case, contact_types: [court, school]) contact6 = create(:case_contact, occurred_at: 20.days.ago, casa_case: transitioned_casa_case, contact_types: [therapist]) aggregate_failures do report_1 = CaseContactReport.new({start_date: 30.days.ago, end_date: 10.days.ago, has_transitioned: true, contact_type_ids: [court.id]}) expect(report_1.case_contacts.length).to eq(2) expect((report_1.case_contacts - [contact1, contact5]).empty?).to eq(true) report_2 = CaseContactReport.new({start_date: 30.days.ago, end_date: 10.days.ago, has_transitioned: true, contact_type_ids: [school.id]}) expect(report_2.case_contacts.length).to eq(2) expect((report_2.case_contacts - [contact4, contact5]).empty?).to eq(true) report_3 = CaseContactReport.new({start_date: 30.days.ago, end_date: 10.days.ago, has_transitioned: true, contact_type_ids: [therapist.id]}) expect(report_3.case_contacts.length).to eq(1) expect(report_3.case_contacts.include?(contact6)).to eq(true) end end end context "when columns are filtered" do let(:args) do { filtered_csv_cols: { internal_contact_number: "true", duration_minutes: "true", contact_types: "false" } } end it "returns a report with only the selected columns" do create(:case_contact) csv = described_class.new(args).to_csv parsed_csv = CSV.parse(csv) expect(parsed_csv.length).to eq(2) expect(parsed_csv[0]).to eq([ "Internal Contact Number", "Duration Minutes" ]) end end end context "with court topics" do let(:report) { described_class.new(filtered_csv_cols: {court_topics: "true"}) } let(:csv) { CSV.parse(report.to_csv, headers: true) } let!(:used_topic_1) { create(:contact_topic, question: "Used topic 1") } let!(:used_topic_2) { create(:contact_topic, question: "Used topic 2") } let!(:unused_topic) { create(:contact_topic, question: "Unused topic") } let(:contacts) { create_list(:case_contact, 3) } # Create the answers in opposite order than the topics before do create(:contact_topic_answer, case_contact: contacts.first, contact_topic: used_topic_2, value: "Ans Contact 1 Topic 2") create(:contact_topic_answer, case_contact: contacts.first, contact_topic: used_topic_1, value: "Ans Contact 1 Topic 1") create(:contact_topic_answer, case_contact: contacts.second, contact_topic: used_topic_2, value: "Ans Contact 2 Topic 2") end it "appends headers for any topics referenced by case_contacts in the report" do headers = csv.headers expect(headers).not_to include(unused_topic.question) expect(headers).to include(used_topic_1.question, used_topic_2.question) expect(headers.select { |header| header == used_topic_1.question }.size).to be 1 end it "includes topic answers in csv rows" do expected_rows = [ # ['Used topic 1', 'Used topic 2'] (header) ["Ans Contact 1 Topic 1", "Ans Contact 1 Topic 2"], [nil, "Ans Contact 2 Topic 2"], [nil, nil] ] csv.by_row.each do |row| expect(expected_rows).to include(row.fields) end end context "when court topics are not requested" do let(:report) do described_class.new(filtered_csv_cols: { internal_contact_number: "true", court_topics: "false" }) end it "omits topics in headers and rows" do expect(csv.headers).not_to include(used_topic_1.question, used_topic_2.question) expect(csv.first.fields).not_to include("Ans Contact 1 Topic 1", "Ans Contact 1 Topic 2") end end end end ================================================ FILE: spec/models/case_contact_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContact, type: :model do it { is_expected.to have_many(:contact_topic_answers).dependent(:destroy) } it { is_expected.to validate_numericality_of(:miles_driven).is_less_than 10_000 } it { is_expected.to validate_numericality_of(:miles_driven).is_greater_than_or_equal_to 0 } it { is_expected.to belong_to(:creator) } it { is_expected.to have_one(:casa_org).through(:casa_case) } it { is_expected.to have_one(:creator_casa_org).through(:creator) } context "status is active" do it "belongs to a creator" do case_contact = build_stubbed(:case_contact, creator: nil) expect(case_contact).not_to be_valid expect(case_contact.errors[:creator]).to eq(["must exist"]) end it "belongs to a casa case" do case_contact = build_stubbed(:case_contact, casa_case: nil) expect(case_contact).not_to be_valid expect(case_contact.errors[:casa_case_id]).to eq(["can't be blank"]) end it "defaults miles_driven to zero" do case_contact = build_stubbed(:case_contact) expect(case_contact.miles_driven).to eq 0 end it "validates presence of occurred_at" do case_contact = build(:case_contact, occurred_at: nil) expect(case_contact).not_to be_valid expect(case_contact.errors[:occurred_at]).to eq(["can't be blank"]) end it "validates duration_minutes can be less than 15 minutes." do case_contact = build_stubbed(:case_contact, duration_minutes: 10) expect(case_contact).to be_valid end it "verifies occurred at is not in the future" do case_contact = build_stubbed(:case_contact, occurred_at: 1.week.from_now) expect(case_contact).not_to be_valid expect(case_contact.errors[:occurred_at]).to eq(["can't be in the future"]) end it "verifies occurred at is not before 1/1/1989" do case_contact = build_stubbed(:case_contact, occurred_at: "1984-01-01".to_date) expect(case_contact).not_to be_valid expect(case_contact.errors[:occurred_at]).to eq(["can't be prior to 01/01/1989."]) end it "validates want_driving_reimbursement can be true when miles_driven is positive" do case_contact = build_stubbed(:case_contact, want_driving_reimbursement: true, miles_driven: 1) expect(case_contact).to be_valid end it "validates want_driving_reimbursement cannot be true when miles_driven is nil" do case_contact = build_stubbed(:case_contact, want_driving_reimbursement: true, miles_driven: nil) expect(case_contact).not_to be_valid expect(case_contact.errors[:base]).to eq(["Must enter miles driven to receive driving reimbursement."]) end it "validates want_driving_reimbursement cannot be true when miles_driven is not positive" do case_contact = build_stubbed(:case_contact, want_driving_reimbursement: true, miles_driven: 0) expect(case_contact).not_to be_valid expect(case_contact.errors[:base]).to eq(["Must enter miles driven to receive driving reimbursement."]) end it "validates that contact_made cannot be null" do case_contact = build_stubbed(:case_contact, contact_made: nil) expect(case_contact).not_to be_valid expect(case_contact.errors.full_messages).to include("Contact made must be true or false") end it "can be updated even if it is old" do case_contact = build_stubbed(:case_contact) case_contact.occurred_at = 1.year.ago expect(case_contact).to be_valid end it "can be updated for 30 days after end of quarter" do expect(build_stubbed(:case_contact, occurred_at: 4.months.ago + 1.day)).to be_valid end end context "status is started" do it "ignores some validations" do case_contact = build_stubbed(:case_contact, :started_status, want_driving_reimbursement: true) expect(case_contact.casa_case).to be_nil expect(case_contact.medium_type).to be_nil expect(case_contact.draft_case_ids).to eq [] expect(case_contact.occurred_at).to be_nil expect(case_contact.miles_driven).to be 0 expect(case_contact.volunteer_address).to be_nil expect(case_contact).to be_valid end end context "status is details" do it "ignores some validations" do case_contact = build_stubbed(:case_contact, :details_status) expect(case_contact.casa_case).to be_nil expect(case_contact).to be_valid end it "requires medium type" do case_contact = build_stubbed(:case_contact, :details_status, medium_type: nil) expect(case_contact).not_to be_valid expect(case_contact.errors.full_messages).to include("Medium type can't be blank") end it "requires a case to be selected" do case_contact = build_stubbed(:case_contact, :details_status, draft_case_ids: []) expect(case_contact).not_to be_valid expect(case_contact.errors.full_messages).to include("CASA Case must be selected") end it "requires occurred at" do case_contact = build_stubbed(:case_contact, :details_status, occurred_at: nil) expect(case_contact).not_to be_valid expect(case_contact.errors.full_messages).to include("Date can't be blank") end it "requires duration minutes" do obj = build_stubbed(:case_contact, :details_status, duration_minutes: nil) expect(obj).not_to be_valid expect(obj.errors.full_messages).to include("Duration minutes can't be blank") end it "validates miles driven if want reimbursement" do obj = build_stubbed(:case_contact, :details_status, want_driving_reimbursement: true) expect(obj).not_to be_valid expect(obj.errors.full_messages).to include("Must enter miles driven to receive driving reimbursement.") end end describe "#update_cleaning_contact_types" do it "cleans up contact types before saving" do group = build_stubbed(:contact_type_group) type1 = build(:contact_type, contact_type_group: group) type2 = create(:contact_type, contact_type_group: group) case_contact = create(:case_contact, contact_types: [type1]) expect(case_contact.case_contact_contact_types.count).to be 1 expect(case_contact.contact_types).to contain_exactly(type1) case_contact.update_cleaning_contact_types(contact_type_ids: [type2.id]) expect(case_contact.case_contact_contact_types.count).to eq 1 expect(case_contact.contact_types.reload).to contain_exactly(type2) end end describe "scopes" do describe "date related scopes" do let!(:case_contacts) do [ create(:case_contact, occurred_at: Time.zone.yesterday - 1), create(:case_contact, occurred_at: Time.zone.yesterday), create(:case_contact, occurred_at: Time.zone.today) ] end let(:date) { Time.zone.yesterday } describe ".occurred_starting_at" do subject(:occurred_starting_at) { described_class.occurred_starting_at(date) } context "with specified date" do it { is_expected.to contain_exactly(case_contacts.second, case_contacts.third) } end context "with no specified date" do let(:date) { nil } it { is_expected.to match_array(case_contacts) } end end describe ".occurred_ending_at" do subject(:occurred_ending_at) { described_class.occurred_ending_at(date) } context "with specified date" do it { is_expected.to contain_exactly(case_contacts.first, case_contacts.second) } end context "with no specified date" do let(:date) { nil } it { is_expected.to match_array(case_contacts) } end end end describe ".contact_type" do subject(:contact_type) { described_class.contact_type([youth_type.id, supervisor_type.id]) } let(:group) { build(:contact_type_group) } let(:youth_type) { build(:contact_type, name: "Youth", contact_type_group: group) } let(:supervisor_type) { build(:contact_type, name: "Supervisor", contact_type_group: group) } let(:parent_type) { build(:contact_type, name: "Parent", contact_type_group: group) } let!(:case_contacts_to_match) do [ create(:case_contact, contact_types: [youth_type, supervisor_type]), create(:case_contact, contact_types: [supervisor_type]), create(:case_contact, contact_types: [youth_type, parent_type]) ] end let!(:other_case_contact) { build_stubbed(:case_contact, contact_types: [parent_type]) } it { is_expected.to match_array(case_contacts_to_match) } end describe ".contact_made" do context "with both option" do it "returns case contacts filtered by contact made option" do case_contact_1 = create(:case_contact, contact_made: false) case_contact_2 = create(:case_contact, contact_made: true) expect(CaseContact.contact_made("")).to contain_exactly(case_contact_1, case_contact_2) end end context "with yes option" do it "returns case contacts filtered by contact made option" do case_contact = create(:case_contact, contact_made: true) build_stubbed(:case_contact, contact_made: false) expect(CaseContact.contact_made(true)).to contain_exactly(case_contact) end end context "with no option" do it "returns case contacts filtered by contact made option" do case_contact = create(:case_contact, contact_made: false) build_stubbed(:case_contact, contact_made: true) expect(CaseContact.contact_made(false)).to contain_exactly(case_contact) end end end describe ".has_transitioned" do let(:casa_case_1) { create(:casa_case, birth_month_year_youth: 15.years.ago) } let(:casa_case_2) { create(:casa_case, birth_month_year_youth: 10.years.ago) } context "with both option" do let!(:case_contact_1) { create(:case_contact, {casa_case: casa_case_1}) } let!(:case_contact_2) { create(:case_contact, {casa_case: casa_case_2}) } it "returns case contacts filtered by contact made option" do expect(described_class.has_transitioned).to contain_exactly(case_contact_1, case_contact_2) end end context "with true option" do let!(:case_contact_1) { create(:case_contact, {casa_case: casa_case_1}) } let!(:case_contact_2) { create(:case_contact, {casa_case: casa_case_2}) } it "returns case contacts filtered by contact made option" do expect(described_class.has_transitioned(true)).to contain_exactly(case_contact_1) end end context "with false option" do let!(:case_contact_1) { create(:case_contact, {casa_case: casa_case_1}) } let!(:case_contact_2) { create(:case_contact, {casa_case: casa_case_2}) } it "returns case contacts filtered by contact made option" do expect(described_class.has_transitioned(false)).to contain_exactly(case_contact_2) end end end describe ".want_driving_reimbursement" do context "with both option" do it "returns case contacts filtered by contact made option" do case_contact_1 = create(:case_contact, {miles_driven: 50, want_driving_reimbursement: true}) case_contact_2 = create(:case_contact, {miles_driven: 50, want_driving_reimbursement: false}) expect(CaseContact.want_driving_reimbursement("")).to contain_exactly(case_contact_1, case_contact_2) end end context "with yes option" do it "returns case contacts filtered by contact made option" do case_contact = create(:case_contact, {miles_driven: 50, want_driving_reimbursement: true}) build_stubbed(:case_contact, {miles_driven: 50, want_driving_reimbursement: false}) expect(CaseContact.want_driving_reimbursement(true)).to contain_exactly(case_contact) end end context "with no option" do it "returns case contacts filtered by contact made option" do build_stubbed(:case_contact, {miles_driven: 50, want_driving_reimbursement: true}) case_contact = create(:case_contact, {miles_driven: 50, want_driving_reimbursement: false}) expect(CaseContact.want_driving_reimbursement(false)).to contain_exactly(case_contact) end end end describe ".contact_medium" do subject(:contact_medium) { described_class.contact_medium(medium_type) } let!(:case_contacts) do [ create(:case_contact, medium_type: "in-person"), create(:case_contact, medium_type: "letter") ] end describe "with specified medium parameter" do let(:medium_type) { "in-person" } it { is_expected.to contain_exactly case_contacts.first } end describe "without specified medium parameter" do let(:medium_type) { nil } it { is_expected.to match_array(case_contacts) } end end describe ".sorted_by" do subject(:sorted_by) { described_class.sorted_by(sort_option) } context "without sort option" do let(:sort_option) { nil } it { expect { sorted_by }.to raise_error(ArgumentError, "Invalid sort option: nil") } end context "with invalid sort option" do let(:sort_option) { "1254645" } it { expect { sorted_by }.to raise_error(ArgumentError, "Invalid sort option: \"1254645\"") } end context "with valid sort option" do context "with occurred_at option" do let(:sort_option) { "occurred_at_#{direction}" } let!(:case_contacts) do [ create(:case_contact, occurred_at: Time.zone.today - 3), create(:case_contact, occurred_at: Time.zone.today - 1), create(:case_contact, occurred_at: Time.zone.today - 2) ] end context "when sorting by ascending order" do let(:direction) { "asc" } it { is_expected.to contain_exactly(case_contacts[0], case_contacts[2], case_contacts[1]) } end context "when sorting by descending order" do let(:direction) { "desc" } it { is_expected.to contain_exactly(case_contacts[1], case_contacts[2], case_contacts[0]) } end end context "with contact_type option" do let(:sort_option) { "contact_type_#{direction}" } let(:group) { create(:contact_type_group) } let(:contact_types) do [ create(:contact_type, name: "Supervisor", contact_type_group: group), create(:contact_type, name: "Parent", contact_type_group: group), create(:contact_type, name: "Youth", contact_type_group: group) ] end let!(:case_contacts) do contact_types.map do |contact_type| create(:case_contact, contact_types: [contact_type]) end end context "when sorting by ascending order" do let(:direction) { "asc" } it { is_expected.to contain_exactly(case_contacts[1], case_contacts[0], case_contacts[2]) } end context "when sorting by descending order" do let(:direction) { "desc" } it { is_expected.to contain_exactly(case_contacts[2], case_contacts[0], case_contacts[1]) } end end context "with medium_type option" do let(:sort_option) { "contact_type_#{direction}" } let!(:case_contacts) do [ create(:case_contact, medium_type: "in-person"), create(:case_contact, medium_type: "text/email"), create(:case_contact, medium_type: "letter") ] end context "when sorting by ascending order" do let(:direction) { "asc" } it { is_expected.to contain_exactly(case_contacts[0], case_contacts[2], case_contacts[1]) } end context "when sorting by descending order" do let(:direction) { "desc" } it { is_expected.to contain_exactly(case_contacts[1], case_contacts[2], case_contacts[0]) } end end context "with want_driving_reimbursement option" do let(:sort_option) { "want_driving_reimbursement_#{direction}" } let!(:case_contacts) do [ create(:case_contact, miles_driven: 1, want_driving_reimbursement: true), create(:case_contact, miles_driven: 1, want_driving_reimbursement: false) ] end context "when sorting by ascending order" do let(:direction) { "asc" } it { is_expected.to contain_exactly(case_contacts[0], case_contacts[1]) } end context "when sorting by descending order" do let(:direction) { "desc" } it { is_expected.to contain_exactly(case_contacts[1], case_contacts[0]) } end end context "with contact_made option" do let(:sort_option) { "contact_made_#{direction}" } let!(:case_contacts) do [ create(:case_contact, contact_made: true), create(:case_contact, contact_made: false) ] end context "when sorting by ascending order" do let(:direction) { "asc" } it { is_expected.to contain_exactly(case_contacts[1], case_contacts[0]) } end context "when sorting by descending order" do let(:direction) { "desc" } it { is_expected.to contain_exactly(case_contacts[0], case_contacts[1]) } end end end end describe ".with_casa_case" do let!(:casa_case) { create(:casa_case) } let!(:case_contacts) { create_list(:case_contact, 3, casa_case: casa_case) } before { create_list(:case_contact, 3) } context "when parameter is nil" do it "returns all casa cases" do expect(described_class.with_casa_case(nil)).to eq(CaseContact.all) end end context "when parameter is not nil" do it "returns contacts with the given casa case ids" do expect(described_class.with_casa_case(casa_case.id)).to match_array(case_contacts) end end end describe ".used_create_another" do subject { described_class.used_create_another } let!(:scope_case_contact) { create(:case_contact, metadata: {"create_another" => true}) } let!(:false_case_contact) { create(:case_contact, metadata: {"create_another" => false}) } let!(:empty_meta_case_contact) { create(:case_contact) } it "returns only the case contacts with the metadata key 'create_another' set to true" do expect(subject).to include(scope_case_contact) expect(subject).not_to include(false_case_contact) expect(subject).not_to include(empty_meta_case_contact) end end end describe "#contact_groups_with_types" do it "returns the groups with their associated case types" do group1 = build(:contact_type_group, name: "Family") group2 = build(:contact_type_group, name: "Health") contact_type1 = build(:contact_type, contact_type_group: group1, name: "Parent") contact_type2 = build(:contact_type, contact_type_group: group2, name: "Medical Professional") contact_type3 = build(:contact_type, contact_type_group: group2, name: "Other Therapist") case_contact_types = [contact_type1, contact_type2, contact_type3] case_contact = create(:case_contact) case_contact.contact_types = case_contact_types groups_with_types = case_contact.contact_groups_with_types expect(groups_with_types.keys).to contain_exactly("Family", "Health") expect(groups_with_types["Family"]).to contain_exactly("Parent") expect(groups_with_types["Health"]).to contain_exactly("Medical Professional", "Other Therapist") end end describe "#requested_followup" do context "no followup exists in requested status" do it "returns nil" do case_contact = build_stubbed(:case_contact) expect(case_contact.requested_followup).to be_nil end end context "a followup exists in requested status" do it "returns nil" do case_contact = build_stubbed(:case_contact) followup = create(:followup, case_contact: case_contact) expect(case_contact.requested_followup).to eq(followup) end end end describe "reimbursement amount" do let(:case_contact) { build(:case_contact, :wants_reimbursement) } describe "when casa org has nil mileage_rate_for_given_date" do it "is nil" do expect(case_contact.casa_case.casa_org.mileage_rate_for_given_date(case_contact.occurred_at.to_datetime)).to be_nil expect(case_contact.reimbursement_amount).to be_nil end end describe "when casa org has value for mileage_rate_for_given_date" do let!(:mileage_rate) { create(:mileage_rate, casa_org: case_contact.casa_case.casa_org, effective_date: 3.days.ago, amount: 5.50) } it "is multiple of miles driven and mileage rate" do expect(case_contact.reimbursement_amount).to eq 2508 end end end describe "#should_send_reimbursement_email?" do let(:supervisor) { create(:supervisor, receive_reimbursement_email: true) } let(:volunteer) { create(:volunteer, supervisor: supervisor) } let(:casa_case) { create(:casa_case) } let(:case_contact) { build(:case_contact, :wants_reimbursement, casa_case: casa_case, creator: volunteer) } it "returns true if wants reimbursement, reimbursement changed, and has active supervisor" do expect(case_contact.want_driving_reimbursement_changed?).to be true expect(case_contact.should_send_reimbursement_email?).to be true end it "returns false if doesn't want reimbursement" do case_contact.want_driving_reimbursement = false expect(case_contact.should_send_reimbursement_email?).to be false end it "returns false if creator doesn't have supervisor" do volunteer.supervisor_volunteer = nil expect(case_contact.supervisor.blank?).to be true expect(case_contact.should_send_reimbursement_email?).to be false end it "returns false if creator's supervisor is inactive" do supervisor.update!(active: false) expect(case_contact.should_send_reimbursement_email?).to be false end end describe "volunteer assignment" do let(:casa_org) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let(:supervisor) { create(:supervisor, casa_org: casa_org) } let(:volunteer) { create(:volunteer, supervisor: supervisor, casa_org: casa_org) } let(:casa_case) { create(:casa_case, casa_org: casa_org) } let(:case_contact) { build(:case_contact, casa_case: casa_case, creator: creator) } context "when creator is volunteer" do let(:creator) { volunteer } it "creator is the volunteer" do expect(case_contact.volunteer).to eq volunteer end it "enables address field" do expect(case_contact.address_field_disabled?).to be false end end context "when creator is admin" do let(:creator) { admin } context "when casa case has one volunteer assigned" do let!(:contact_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } it "volunteer is the assigned volunteer" do expect(case_contact.volunteer).to eq volunteer end it "enables address field" do expect(case_contact.address_field_disabled?).to be false end end context "when casa case has no volunteers assigned" do it "volunteer is nil" do expect(case_contact.volunteer).to be_nil end it "disbales address field" do expect(case_contact.address_field_disabled?).to be true end end context "when casa case has more than 1 volunteer assigned" do let(:other_volunteer) { create(:volunteer, casa_org: casa_org) } let!(:contact_assignments) { [ create(:case_assignment, volunteer: volunteer, casa_case: casa_case), create(:case_assignment, volunteer: other_volunteer, casa_case: casa_case) ] } it "volunteer is nil" do expect(case_contact.volunteer).to be_nil end it "disbales address field" do expect(case_contact.address_field_disabled?).to be true end end end end end ================================================ FILE: spec/models/case_court_order_spec.rb ================================================ require "rails_helper" RSpec.describe CaseCourtOrder, type: :model do subject { build(:case_court_order) } it { is_expected.to belong_to(:casa_case) } it { is_expected.to validate_presence_of(:text) } describe ".court_order_options" do it "returns standard court order options" do expect(described_class.court_order_options.count).to eq(23) expect(described_class.court_order_options).to be_an(Array) expect(described_class.court_order_options).to all be_an(Array) end end end ================================================ FILE: spec/models/case_court_report_context_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" require "sablon" A_TIMEZONE = "America/New_York" RSpec.describe CaseCourtReportContext, type: :model do let(:volunteer) { create(:volunteer, :with_casa_cases) } let(:path_to_template) { Rails.root.join("app/documents/templates/default_report_template.docx").to_s } let(:path_to_report) { Rails.root.join("tmp/test_report.docx").to_s } before do travel_to Date.new(2021, 1, 1) end describe "#context" do it "has the right shape" do date = 1.day.ago court_date = build(:court_date, :with_hearing_type, date: date) context = create(:case_court_report_context, court_date: court_date) allow(context).to receive(:case_details).and_return({}) allow(context).to receive(:case_contacts).and_return([]) allow(context).to receive(:case_orders).and_return([]) allow(context).to receive(:org_address).and_return(nil) allow(context).to receive(:volunteer_info).and_return({}) allow(context).to receive(:latest_hearing_date).and_return("") allow(context).to receive(:court_topics).and_return({}) expected_shape = { created_date: "January 1, 2021", casa_case: {}, case_contacts: [], case_court_orders: [], case_mandates: [], latest_hearing_date: "", org_address: nil, volunteer: {}, hearing_type_name: court_date.hearing_type.name, case_topics: [] } expect(context.context).to eq(expected_shape) end end describe "case_orders" do it "returns the correct shape" do court_orders = [ build(:case_court_order, text: "Court order 1", implementation_status: :unimplemented), build(:case_court_order, text: "Court order 2", implementation_status: :implemented) ] expected = [ {order: "Court order 1", status: "Unimplemented"}, {order: "Court order 2", status: "Implemented"} ] context = build_stubbed(:case_court_report_context) expect(context.case_orders(court_orders)).to match_array(expected) end end describe "org_address" do let(:volunteer) { create(:volunteer) } let(:context) { build(:case_court_report_context, volunteer: volunteer) } context "when volunteer and default template are provided" do it "returns the CASA org address" do path_to_template = "default_report_template.docx" expected_address = volunteer.casa_org.address expect(context.org_address(path_to_template)).to eq(expected_address) end end context "when volunteer is provided but not default template" do it "returns nil" do path_to_template = "some_other_template.docx" expect(context.org_address(path_to_template)).to be_nil end end context "when volunteer is not provided" do let(:context) { build(:case_court_report_context, volunteer: false) } it "returns nil" do path_to_template = "default_report_template.docx" expect(context.org_address(path_to_template)).to be_nil end end end describe "#latest_hearing_date" do context "when casa_case has court_dates" do let(:court_date) { build(:court_date, date: 2.day.ago) } let(:casa_case) { create(:casa_case, court_dates: [court_date]) } let(:instance) { build(:case_court_report_context, casa_case: casa_case) } it "returns the formatted date" do expect(instance.latest_hearing_date).to eq("December 30, 2020") # 2 days before spec default date end end context "when most recent past court date is nil" do let(:instance) { build(:case_court_report_context) } it "returns the placeholder string" do expect(instance.latest_hearing_date).to eq("_______") end end context "when there are multiple hearing dates" do let(:casa_case_with_court_dates) { casa_case = create(:casa_case) casa_case.court_dates << build(:court_date, date: 9.months.ago) casa_case.court_dates << build(:court_date, date: 3.months.ago) casa_case.court_dates << build(:court_date, date: 15.months.ago) casa_case } let(:court_report_context_with_latest_hearing_date) { build(:case_court_report_context, casa_case: casa_case_with_court_dates) } it "sets latest_hearing_date as the latest past court date" do expect(court_report_context_with_latest_hearing_date.latest_hearing_date).to eq("October 1, 2020") end end end describe "#calculate_date_range" do context "when @time_zone is set" do it "converts to provided timezone" do context = build_context(start_date: 10.day.ago, end_date: 2.day.ago, court_date: nil, time_zone: A_TIMEZONE) expect(context.date_range).to eq(zone_days_ago(10)..zone_days_ago(2)) end it "uses current time if end_date not provided" do context = build_context(start_date: 10.day.ago, end_date: nil, court_date: nil, time_zone: A_TIMEZONE) expect(context.date_range).to eq(zone_days_ago(10)..nil) end it "uses court date if available if no start_date" do context = build_context(start_date: nil, end_date: 2.day.ago, court_date: 6.day.ago, time_zone: A_TIMEZONE) expect(context.date_range).to eq(zone_days_ago(6)..zone_days_ago(2)) end it "uses nil(includes everything) if no court date or start_date" do context = build_context(start_date: nil, end_date: 2.day.ago, court_date: nil, time_zone: A_TIMEZONE) expect(context.date_range).to eq(nil..zone_days_ago(2)) end end context "when @time_zone is not set" do it "uses server time zone" do context = build_context(start_date: 10.day.ago, end_date: 2.day.ago, court_date: nil, time_zone: nil) expect(context.date_range).to eq(days_ago(10)..days_ago(2)) end it "uses nil if end_date not provided" do context = build_context(start_date: 10.day.ago, end_date: nil, court_date: nil, time_zone: nil) expect(context.date_range).to eq(days_ago(10)..nil) end it "uses court date if available if no start_date" do context = build_context(start_date: nil, end_date: 2.day.ago, court_date: 6.day.ago, time_zone: nil) expect(context.date_range).to eq(days_ago(6)..days_ago(2)) end it "uses nil if no court date or start_date" do context = build_context(start_date: nil, end_date: 2.day.ago, court_date: nil, time_zone: nil) expect(context.date_range).to eq(nil..days_ago(2)) end end end describe "#court_topics" do let(:org) { create(:casa_org) } let(:casa_case) { create(:casa_case, casa_org: org) } let(:topics) { [1, 2, 3].map { |i| create(:contact_topic, casa_org: org, question: "Question #{i}", details: "Details #{i}") } } let(:contacts) do [1, 2, 3, 4].map do |i| create(:case_contact, casa_case: casa_case, occurred_at: 1.month.ago + i.days, contact_types: [ create(:contact_type, name: "Type A#{i}"), create(:contact_type, name: "Type B#{i}") ]) end end context "when given data" do before do # Contact 1 Answers create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[0], value: "Answer 1") create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[1], value: "Answer 2") # Contact 2 Answers create(:contact_topic_answer, case_contact: contacts[1], contact_topic: topics[0], value: "Answer 3") create(:contact_topic_answer, case_contact: contacts[1], contact_topic: topics[2], value: nil) # Contact 3 Answers create(:contact_topic_answer, case_contact: contacts[2], contact_topic: topics[1], value: "Answer 5") create(:contact_topic_answer, case_contact: contacts[2], contact_topic: topics[2], value: "") # Contacts that will be filtered one_day_ago_contact = create(:case_contact, casa_case: casa_case, medium_type: "in-person", occurred_at: 1.day.ago) create_list(:contact_topic_answer, 2, case_contact: one_day_ago_contact, contact_topic: topics[0], value: "Answer From One Day Ago") one_year_ago_contact = create(:case_contact, casa_case: casa_case, medium_type: "in-person", occurred_at: 1.year.ago) create_list(:contact_topic_answer, 2, case_contact: one_year_ago_contact, contact_topic: topics[0], value: "Answer From One Year Ago") other_case = create(:casa_case, casa_org: org) other_case_contact = create(:case_contact, casa_case: other_case, medium_type: "in-person", occurred_at: 1.month.ago) create_list(:contact_topic_answer, 2, case_contact: other_case_contact, contact_topic: topics[0], value: "Answer From Another Case") end it "returns a hash of topics with the correct shape" do court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics expect(court_topics).to be_a(Hash) expect(court_topics.keys).to all(a_kind_of(String)) expect(court_topics.values).to all( a_hash_including( topic: a_kind_of(String), details: a_kind_of(String), answers: all( a_hash_including( date: a_string_matching(/\d{2}\/\d{2}\/\d{2}/), medium: a_kind_of(String), value: a_kind_of(String) ) ) ) ) end it "returns topics related to the case" do court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics expect(court_topics.keys).to match_array(["Question 1", "Question 2", "Question 3"]) expect(court_topics["Question 1"][:answers].pluck(:value)).to match_array( ["Answer From One Year Ago", "Answer 1", "Answer 3", "Answer From One Day Ago"] ) expect(court_topics["Question 2"][:answers].pluck(:value)).to match_array(["Answer 2", "Answer 5"]) expect(court_topics["Question 3"][:answers].pluck(:value)).to match_array(["No Answer Provided", "No Answer Provided"]) end it "filters by date range" do court_topics = build(:case_court_report_context, start_date: 45.day.ago.to_s, end_date: 5.day.ago.to_s, casa_case: casa_case).court_topics expect(court_topics.keys).to match_array(["Question 1", "Question 2", "Question 3"]) expect(court_topics["Question 1"][:answers].pluck(:value)).to match_array(["Answer 1", "Answer 3"]) end it "filters answers from topics set be excluded from court report" do topics[0].update(exclude_from_court_report: true) court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics expect(court_topics.keys).not_to include("Question 1") expect(court_topics.keys).to include("Question 2", "Question 3") end end context "when there are no contact topics" do it "returns an empty hash" do court_report_context = build(:case_court_report_context, start_date: 45.day.ago.to_s, end_date: 5.day.ago.to_s, casa_case: casa_case) expect(court_report_context.court_topics).to eq({}) end end end describe "#filtered_interviewees" do it "filters based on date range" do casa_case = create(:casa_case) court_report_context = build(:case_court_report_context, start_date: 5.day.ago.to_s, end_date: 5.day.ago.to_s, casa_case: casa_case) create_list(:case_contact, 3, occurred_at: 10.day.ago, casa_case: casa_case) create_list(:case_contact, 3, occurred_at: 1.day.ago, casa_case: casa_case) included_interviewee = create(:case_contact, occurred_at: 5.day.ago, casa_case: casa_case) result = court_report_context.filtered_interviewees.map(&:case_contact) expect(result).to contain_exactly(included_interviewee) end it "filters if start of date range is nil" do casa_case = create(:casa_case) court_report_context = build(:case_court_report_context, start_date: nil, end_date: 5.day.ago.to_s, casa_case: casa_case) create_list(:case_contact, 3, occurred_at: 1.day.ago, casa_case: casa_case) interviewees = create_list(:case_contact, 3, occurred_at: 10.day.ago, casa_case: casa_case) interviewees.append(create(:case_contact, occurred_at: 5.day.ago, casa_case: casa_case)) result = court_report_context.filtered_interviewees.map(&:case_contact) expect(result).to match_array(interviewees) end it "filters if end of date range is nil" do casa_case = create(:casa_case) court_report_context = build(:case_court_report_context, start_date: 5.day.ago.to_s, end_date: nil, casa_case: casa_case) create_list(:case_contact, 3, occurred_at: 10.day.ago, casa_case: casa_case) interviewees = create_list(:case_contact, 3, occurred_at: 1.day.ago, casa_case: casa_case) interviewees.append(create(:case_contact, occurred_at: 5.day.ago, casa_case: casa_case)) result = court_report_context.filtered_interviewees.map(&:case_contact) expect(result).to match_array(interviewees) end it "does not filter if both start and end of date range are nil" do casa_case = create(:casa_case) court_report_context = build(:case_court_report_context, start_date: nil, end_date: nil, casa_case: casa_case) create_list(:case_contact, 3, occurred_at: 10.day.ago, casa_case: casa_case) create_list(:case_contact, 3, occurred_at: 1.day.ago, casa_case: casa_case) create(:case_contact, occurred_at: 5.day.ago, casa_case: casa_case) result = court_report_context.filtered_interviewees.map(&:case_contact) expect(result).to match_array(CaseContact.all) end it "returns an empty array if there are no interviewees" do casa_case = create(:casa_case) court_report_context = build(:case_court_report_context, start_date: 5.day.ago.to_s, end_date: nil, casa_case: casa_case) result = court_report_context.filtered_interviewees.map(&:case_contact) expect(result).to be_empty end end describe "#context" do let(:court_report_context) { build(:case_court_report_context) } describe ":created_date" do it "has a created date equal to the current date" do expect(court_report_context.context[:created_date]).to eq("January 1, 2021") end end end describe "#volunteer_info" do let(:volunteer) { create(:volunteer, display_name: "Y>cy%F7v;\\].-g$", supervisor: build(:supervisor, display_name: "Mm^ED;`zg(gcy%F7v;\\].-g$", supervisor_name: "Mm^ED;`zg(g "4/09*"}}, {name: "Some Other Name", type: "Type 4", dates: "4/09*", dates_by_medium_type: {"in-person" => "4/09*"}} ], case_court_orders: [ {order: "case_court_order_text", status: "Partially implemented"} ], case_mandates: [ {order: "case_mandates_text", status: "Partially implemented"} ], latest_hearing_date: "_______", org_address: "596 Unique Avenue Seattle, Washington", volunteer: { name: "name_of_volunteer", supervisor_name: "name_of_supervisor", assignment_date: "February 9, 2024" }, hearing_type_name: "None", case_topics: [ {topic: "Question 1", details: "Details 1", answers: [ {date: "12/01/20", medium: "Type A1, Type B1", value: "Answer 1"}, {date: "12/02/20", medium: "Type A2, Type B2", value: "Answer 3"} ]}, {topic: "Question 2", details: "Details 2", answers: [ {date: "12/01/20", medium: "Type A1, Type B1", value: "Answer 2"}, {date: "12/02/20", medium: "Type A3, Type B3", value: "Answer 5"} ]}, {topic: "Question 3", details: "Details 3", answers: [ {date: "12/01/20", medium: "Type A3, Type B3", value: "No Answer Provided"}, {date: "12/02/20", medium: "Type A2, Type B2", value: "No Answer Provided"} ]} ] } end describe "contact_topics" do it "all contact topics are present in the report" do docx_response = generate_doc(full_context, path_to_template) expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 1.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 2.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 3.*/) end it "all topic details are present in the report" do docx_response = generate_doc(full_context, path_to_template) expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 1.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 2.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 3.*/) end it "all answers are present with correct format" do docx_response = generate_doc(full_context, path_to_template) expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A1, Type B1 \(12\/01\/20\): Answer 1.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A2, Type B2 \(12\/02\/20\): Answer 3.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A1, Type B1 \(12\/01\/20\): Answer 2.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A3, Type B3 \(12\/02\/20\): Answer 5.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A3, Type B3 \(12\/01\/20\): No Answer Provided.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A2, Type B2 \(12\/02\/20\): No Answer Provided.*/) end context "when there are topics but no answers" do let(:curr_context) do full_context[:case_topics] = [ {topic: "Question 1", details: "Details 1", answers: []}, {topic: "Question 2", details: "Details 2", answers: []}, {topic: "Question 3", details: "Details 3", answers: []} ] end it "all contact topics are present in the report" do docx_response = generate_doc(full_context, path_to_template) expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 1.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 2.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 3.*/) end it "all topic details are present in the report" do docx_response = generate_doc(full_context, path_to_template) expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 1.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 2.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 3.*/) end end context "when there no topics" do it "report does not error and puts old defaults" do full_context[:case_topics] = [] docx_response = nil expect { docx_response = generate_doc(full_context, path_to_template) }.not_to raise_error expect(docx_response).not_to be_nil expect(docx_response.paragraphs.map(&:to_s)).to include(/Placement.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Education\/Vocation.*/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Objective Information.*/) end end end describe "when receiving valid case, volunteer, and path_to_template" do let(:volunteer) { create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor) } let(:casa_case_with_contacts) { volunteer.casa_cases.first } let(:casa_case_without_contacts) { volunteer.casa_cases.second } let(:report) do args = { case_id: casa_case_with_contacts.id, volunteer_id: volunteer.id, path_to_template: path_to_template, path_to_report: path_to_report } context = CaseCourtReportContext.new(args).context CaseCourtReport.new(path_to_template: path_to_template, context: context) end describe "with volunteer without supervisor" do let(:volunteer) { create(:volunteer, :with_cases_and_contacts) } it "has supervisor name placeholder" do expect(report.context[:volunteer][:supervisor_name]).to eq("") end end describe "has valid @context" do subject { report.context } it { is_expected.not_to be_empty } it { is_expected.to be_instance_of Hash } it "has the following keys [:created_date, :casa_case, :case_contacts, :latest_hearing_date, :org_address, :volunteer]" do expected = %i[created_date casa_case case_contacts volunteer] expect(subject.keys).to include(*expected) end it "must have Case Contacts as type Array" do expect(subject[:case_contacts]).to be_instance_of Array end it "created_date is not nil" do expect(subject[:created_date]).not_to be_nil end context "when the case has multiple past court dates" do before do casa_case_with_contacts.court_dates << create(:court_date, date: 9.months.ago) casa_case_with_contacts.court_dates << create(:court_date, date: 3.months.ago) casa_case_with_contacts.court_dates << create(:court_date, date: 15.months.ago) end it "sets latest_hearing_date as the latest past court date" do expect(subject[:latest_hearing_date]).to eq(I18n.l(3.months.ago, format: :full, default: nil)) end end end describe "the default generated report" do context "when passed all displayable information" do let(:document_data) do { case_birthday: 12.years.ago, case_contact_time: 3.days.ago, case_contact_type: "Unique Case Contact Type", case_hearing_date: 2.weeks.from_now, case_number: "A-CASA-CASE-NUMBER-12345", text: "This text shall not be strikingly similar to other text in the document", org_address: "596 Unique Avenue Seattle, Washington", supervisor_name: "A very unique supervisor name", volunteer_case_assignment_date: 2.months.ago, volunteer_name: "An unmistakably unique volunteer name" } end let(:contact_type) { create(:contact_type, name: document_data[:case_contact_type]) } let(:case_contact) { create(:case_contact, contact_made: false, occurred_at: document_data[:case_contact_time]) } let(:court_order) { create(:case_court_order, implementation_status: :partially_implemented) } before do casa_case_with_contacts.casa_org.update_attribute(:address, document_data[:org_address]) casa_case_with_contacts.update_attribute(:birth_month_year_youth, document_data[:case_birthday]) casa_case_with_contacts.update_attribute(:case_number, document_data[:case_number]) create(:court_date, casa_case: casa_case_with_contacts, date: document_data[:case_hearing_date]) case_contact.contact_types << contact_type casa_case_with_contacts.case_contacts << case_contact casa_case_with_contacts.case_court_orders << court_order court_order.update_attribute(:text, document_data[:text]) CaseAssignment.find_by(casa_case_id: casa_case_with_contacts.id, volunteer_id: volunteer.id).update_attribute(:created_at, document_data[:volunteer_case_assignment_date]) volunteer.update_attribute(:display_name, document_data[:volunteer_name]) volunteer.supervisor.update_attribute(:display_name, document_data[:supervisor_name]) end it "displays the org address" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(header_text(docx_response)).to include(document_data[:org_address]) end it "displays today's date formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{Date.current.strftime("%B %-d, %Y")}.*/) end it "displays the case hearing date date formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{document_data[:case_hearing_date].strftime("%B %-d, %Y")}.*/) end it "displays the case number" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{document_data[:case_number]}.*/) end it "displays the case contact type" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(/#{document_data[:case_contact_type]}.*/) end it "displays the case contact time date formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(/#{document_data[:case_contact_time].strftime("%-m/%d")}.*/) end it "displays the text" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(/#{document_data[:text]}.*/) end it "displays the order status" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include("Partially implemented") end it "displays the volunteer name" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(/#{document_data[:volunteer_name]}.*/) end it "displays the volunteer case assignment date formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(/#{document_data[:volunteer_case_assignment_date].strftime("%B %-d, %Y")}.*/) end it "displayes the supervisor name" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(/#{document_data[:supervisor_name]}.*/) end end context "when missing a volunteer" do let(:report) do args = { case_id: casa_case.id, volunteer_id: nil, path_to_template: path_to_template, path_to_report: path_to_report } context = CaseCourtReportContext.new(args).context CaseCourtReport.new(path_to_template: path_to_template, context: context) end let(:document_data) do { case_birthday: 12.years.ago, case_contact_time: 3.days.ago, case_contact_type: "Unique Case Contact Type", case_hearing_date: 2.weeks.from_now, case_number: "A-CASA-CASE-NUMBER-12345", text: "This text shall not be strikingly similar to other text in the document", org_address: nil, supervisor_name: nil, volunteer_case_assignment_date: 2.months.ago, volunteer_name: nil } end let(:casa_case) { create(:casa_case) } let(:contact_type) { create(:contact_type, name: document_data[:case_contact_type]) } let(:case_contact) { create(:case_contact, contact_made: false, occurred_at: document_data[:case_contact_time]) } let(:court_order) { create(:case_court_order, implementation_status: :partially_implemented) } before do casa_case.casa_org.update_attribute(:address, document_data[:org_address]) casa_case.update_attribute(:birth_month_year_youth, document_data[:case_birthday]) casa_case.update_attribute(:case_number, document_data[:case_number]) create(:court_date, casa_case: casa_case, date: document_data[:case_hearing_date]) case_contact.contact_types << contact_type casa_case.case_contacts << case_contact casa_case.case_court_orders << court_order court_order.update_attribute(:text, document_data[:text]) end it "displays today's date formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{Date.current.strftime("%B %-d, %Y")}.*/) end it "displays the case hearing date formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{document_data[:case_hearing_date].strftime("%B %-d, %Y")}.*/) end it "displays the case number" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/.*#{document_data[:case_number]}.*/) end it "displays the case contact type" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(document_data[:case_contact_type]) end it "displays the case contact time formatted" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include(document_data[:case_contact_time].strftime("%-m/%d*")) end it "displays the test" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include("This text shall not be strikingly similar to other text in the document") end it "displays the order status" do docx_response = Docx::Document.open(StringIO.new(report.generate_to_string)) table_data = docx_response.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten expect(table_data).to include("Partially implemented") end end end end describe "when receiving INVALID path_to_template" do let(:volunteer) { create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor) } let(:casa_case_with_contacts) { volunteer.casa_cases.first } let(:nonexistent_path) { "app/documents/templates/nonexisitent_report_template.docx" } it "raises Zip::Error when generating report" do args = { case_id: casa_case_with_contacts.id, volunteer_id: volunteer.id, path_to_template: nonexistent_path } context = CaseCourtReportContext.new(args).context expect { CaseCourtReport.new(path_to_template: nonexistent_path, context: context) }.to raise_error(Zip::Error, /Template file not found/) end end describe "when court orders has different implementation statuses" do let(:casa_case) { create(:casa_case, case_number: "Sample-Case-12345") } let(:court_order_implemented) { create(:case_court_order, casa_case: casa_case, text: "an order that got done", implementation_status: :implemented) } let(:court_order_unimplemented) { create(:case_court_order, casa_case: casa_case, text: "an order that got not done", implementation_status: :unimplemented) } let(:court_order_partially_implemented) { create(:case_court_order, casa_case: casa_case, text: "an order that got kinda done", implementation_status: :partially_implemented) } let(:court_order_not_specified) { create(:case_court_order, casa_case: casa_case, text: "what is going on", implementation_status: nil) } let(:args) do { case_id: casa_case.id, path_to_template: path_to_template, path_to_report: path_to_report } end let(:context) { CaseCourtReportContext.new(args).context } let(:case_report) { CaseCourtReport.new(path_to_template: path_to_template, context: context) } before do casa_case.case_court_orders << court_order_implemented casa_case.case_court_orders << court_order_unimplemented casa_case.case_court_orders << court_order_partially_implemented casa_case.case_court_orders << court_order_not_specified end it "contains the case number" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{casa_case.case_number}*/) end it "contains the court order text" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/#{court_order_implemented.text}.*/) end it "contains the exact value of 'Implemented'" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/Implemented.*/) end it "contains the court order text" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/#{court_order_unimplemented.text}.*/) end it "contains the exact value of 'Unimplemented'" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/Unimplemented.*/) end it "contains the court order text" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/#{court_order_partially_implemented.text}.*/) end it "contains the exact value of 'Partially implemented'" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/Partially implemented.*/) end it "contains the court order text" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/#{court_order_not_specified.text}.*/) end it "contains the exact value of 'Not specified'" do docx_response = Docx::Document.open(StringIO.new(case_report.generate_to_string)) expect(table_text(docx_response)).to include(/Not specified.*/) end end end end def generate_doc(context, path_to_template) report = CaseCourtReport.new(path_to_template: path_to_template, context: context) Docx::Document.open(StringIO.new(report.generate_to_string)) end ================================================ FILE: spec/models/case_group_membership_spec.rb ================================================ require "rails_helper" RSpec.describe CaseGroupMembership, type: :model do it "has a valid factory" do case_group_membership = build(:case_group_membership) expect(case_group_membership).to be_valid end end ================================================ FILE: spec/models/case_group_spec.rb ================================================ require "rails_helper" RSpec.describe CaseGroup, type: :model do describe "validations" do it { is_expected.to validate_presence_of(:case_group_memberships) } it "validates uniqueness of name scoped to casa_org" do casa_org = create(:casa_org) create(:case_group, casa_org: casa_org, name: "The Johnson Family") non_uniq_case_group = build(:case_group, casa_org: casa_org, name: "The Johnson Family") non_uniq_case_group_whitespace = build(:case_group, casa_org: casa_org, name: "The Johnson Family ") non_uniq_case_group_case_sensitive = build(:case_group, casa_org: casa_org, name: "The Johnson family") expect(non_uniq_case_group).not_to be_valid expect(non_uniq_case_group_case_sensitive).not_to be_valid expect(non_uniq_case_group_whitespace).not_to be_valid expect(non_uniq_case_group.errors[:name]).to include("has already been taken") expect(non_uniq_case_group_case_sensitive.errors[:name]).to include("has already been taken") expect(non_uniq_case_group_whitespace.errors[:name]).to include("has already been taken") end end describe "relationships" do it { is_expected.to have_many(:case_group_memberships) } it { is_expected.to have_many(:casa_cases).through(:case_group_memberships) } end it "has a valid factory" do case_group = build(:case_group) expect(case_group).to be_valid end end ================================================ FILE: spec/models/checklist_item_spec.rb ================================================ require "rails_helper" RSpec.describe ChecklistItem, type: :model do describe "associations" do it { is_expected.to belong_to(:hearing_type) } end describe "validations" do it { is_expected.to validate_presence_of(:description) } it { is_expected.to validate_presence_of(:category) } end end ================================================ FILE: spec/models/concerns/CasaCase/validations_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCase::Validations, type: :model do # TODO: Add tests for CasaCase::Validations pending "add some tests for CasaCase::Validations" end ================================================ FILE: spec/models/concerns/api_spec.rb ================================================ require "rails_helper" RSpec.describe Api, type: :model do # TODO: Add tests for Api pending "add some tests for Api" end ================================================ FILE: spec/models/concerns/by_organization_scope_spec.rb ================================================ require "rails_helper" RSpec.describe ByOrganizationScope, type: :model do # TODO: Add tests for ByOrganizationScope pending "add some tests for ByOrganizationScope" end ================================================ FILE: spec/models/concerns/roles_spec.rb ================================================ require "rails_helper" RSpec.describe Roles, type: :model do # TODO: Add tests for Roles pending "add some tests for Roles" end ================================================ FILE: spec/models/contact_topic_answer_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTopicAnswer, type: :model do it { is_expected.to belong_to(:case_contact) } it { is_expected.to belong_to(:contact_topic).optional(true) } it { is_expected.to have_one(:contact_creator).through(:case_contact) } it { is_expected.to have_one(:contact_creator_casa_org).through(:contact_creator) } it "can hold more than 255 characters" do expect { create(:contact_topic_answer, value: Faker::Lorem.characters(number: 300)) }.not_to raise_error end it "soft deletes record instead of removing it from database" do answer = create(:contact_topic_answer) answer.destroy expect(answer.deleted_at).not_to be_nil expect(ContactTopicAnswer.with_deleted).to include(answer) expect(ContactTopicAnswer.all).not_to include(answer) answer.restore expect(answer.deleted_at).to be_nil expect(ContactTopicAnswer.all).to include(answer) end end ================================================ FILE: spec/models/contact_topic_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTopic, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to have_many(:contact_topic_answers) } it { is_expected.to validate_presence_of(:question) } it { is_expected.to validate_presence_of(:details) } describe "scopes" do describe ".active" do it "returns only active and non-soft deleted contact topics" do active_contact_topic = create(:contact_topic, active: true, soft_delete: false) inactive_contact_topic = create(:contact_topic, active: false, soft_delete: false) soft_deleted_contact_topic = create(:contact_topic, active: true, soft_delete: true) expect(ContactTopic.active).to include(active_contact_topic) expect(ContactTopic.active).not_to include(inactive_contact_topic) expect(ContactTopic.active).not_to include(soft_deleted_contact_topic) end end end describe "generate for org" do let(:org) { create(:casa_org) } let(:fake_topics) { [{"question" => "Test Title", "details" => "Test details"}] } describe "generate_contact_topics" do before do allow(ContactTopic).to receive(:default_contact_topics).and_return(fake_topics) end it "creates contact topics" do expect { ContactTopic.generate_for_org!(org) }.to change { org.contact_topics.count }.by(1) created_topic = org.contact_topics.first expect(created_topic.question).to eq(fake_topics.first["question"]) expect(created_topic.details).to eq(fake_topics.first["details"]) end context "there are no default topics" do let(:fake_topics) { [] } it { expect { ContactTopic.generate_for_org!(org) }.not_to(change { org.contact_topics.count }) } end it "generates from parameter" do topics = fake_topics.push({"question" => "a", "details" => "a"}) expect { ContactTopic.generate_for_org!(org) }.to change { org.contact_topics.count }.by(2) questions = org.contact_topics.map(&:question) details = org.contact_topics.map(&:details) expect(questions).to match_array(topics.pluck("question")) expect(details).to match_array(topics.pluck("details")) end it "fails if not all required attrs are present" do fake_topics.first["question"] = nil expect { ContactTopic.generate_for_org!(org) }.to raise_error(ActiveRecord::RecordInvalid) end it "creates if needed fields all present" do fake_topics.first["invalid_field"] = "invalid" expect { ContactTopic.generate_for_org!(org) }.to change { org.contact_topics.count }.by(1) end end end describe "details" do it "can hold more than 255 characters" do contact_topic_details = build(:contact_topic, details: Faker::Lorem.characters(number: 300)) expect { contact_topic_details.save! }.not_to raise_error end end end ================================================ FILE: spec/models/contact_type_group_spec.rb ================================================ require "rails_helper" # require "contact_type_group" require "./app/models/contact_type_group" RSpec.describe ContactTypeGroup, type: :model do it "does not have duplicate names" do org_id = create(:casa_org).id create_contact_type_group = -> { create(:contact_type_group, {name: "Test1", casa_org_id: org_id}) } create_contact_type_group.call expect { create_contact_type_group.call }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name has already been taken") end describe "for_organization" do let!(:casa_org_1) { create(:casa_org) } let!(:casa_org_2) { create(:casa_org) } let!(:record_1) { create(:contact_type_group, casa_org: casa_org_1) } let!(:record_2) { create(:contact_type_group, casa_org: casa_org_2) } it "returns only records matching the specified organization" do expect(described_class.for_organization(casa_org_1)).to eq([record_1]) expect(described_class.for_organization(casa_org_2)).to eq([record_2]) end end describe ".alphabetically scope" do subject { described_class.alphabetically } let!(:contact_type_group1) { create(:contact_type_group, name: "Family") } let!(:contact_type_group2) { create(:contact_type_group, name: "Placement") } it "orders alphabetically", :aggregate_failures do expect(subject.first).to eq(contact_type_group1) expect(subject.last).to eq(contact_type_group2) end end end ================================================ FILE: spec/models/contact_type_spec.rb ================================================ require "rails_helper" RSpec.describe ContactType, type: :model do let(:contact_type_group) { create(:contact_type_group, name: "Group 1") } let(:contact_type) { create(:contact_type, name: "Type 1", contact_type_group: contact_type_group) } describe "#create" do it "does have a unique name" do new_contact_type = create(:contact_type, name: "Type 1", contact_type_group: contact_type_group) expect(subject).to validate_presence_of(:name) expect(new_contact_type).to validate_uniqueness_of(:name).scoped_to(:contact_type_group_id) .with_message("should be unique per contact type group") end end describe "#update" do it "can update to a valid name" do contact_type.name = "New name" contact_type.save expect(contact_type.name).to eq("New name") end it "can't update to an invalid name" do contact_type.name = nil expect { contact_type.save! }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank") end it "can update contact type group" do new_group = create(:contact_type_group, name: "New contact group") contact_type.contact_type_group_id = new_group.id expect(contact_type.contact_type_group.name).to eq("New contact group") end it "can deactivate contact type" do contact_type.active = false expect(contact_type.active?).to be_falsey end end describe "for_organization" do let!(:casa_org_1) { create(:casa_org) } let!(:casa_org_2) { create(:casa_org) } let!(:contact_type_group_record_1) { create(:contact_type_group, casa_org: casa_org_1) } let!(:contact_type_group_record_2) { create(:contact_type_group, casa_org: casa_org_2) } let!(:record_1) { create(:contact_type, contact_type_group: contact_type_group_record_1) } let!(:record_2) { create(:contact_type, contact_type_group: contact_type_group_record_2) } it "returns only records matching the specified organization" do expect(described_class.for_organization(casa_org_1)).to eq([record_1]) expect(described_class.for_organization(casa_org_2)).to eq([record_2]) end end describe ".alphabetically scope" do subject { described_class.alphabetically } let!(:contact_type1) { create(:contact_type, name: "Aunt Uncle or Cousin") } let!(:contact_type2) { create(:contact_type, name: "Parent") } it "orders alphabetically", :aggregate_failures do expect(subject.first).to eq(contact_type1) expect(subject.last).to eq(contact_type2) end end end ================================================ FILE: spec/models/court_date_spec.rb ================================================ require "rails_helper" RSpec.describe CourtDate, type: :model do subject(:court_date) { create(:court_date, casa_case: casa_case) } let(:casa_case) { create(:casa_case, case_number: "AAA123123") } let(:volunteer) { create(:volunteer, casa_org: casa_case.casa_org) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } let(:this_court_date) { subject.date } let(:older_court_date) { subject.date - 6.months } let(:path_to_template) { Rails.root.join("app/documents/templates/default_report_template.docx").to_s } let(:path_to_report) { Rails.root.join("tmp/test_report.docx").to_s } before do travel_to Date.new(2021, 1, 1) end it { is_expected.to belong_to(:casa_case) } it { is_expected.to validate_presence_of(:date) } it { is_expected.to have_many(:case_court_orders) } it { is_expected.to belong_to(:hearing_type).optional } it { is_expected.to belong_to(:judge).optional } describe "date validation" do it "is not valid before 1989" do court_date = CourtDate.new(date: "1984-01-01".to_date) expect(court_date.valid?).to be false expect(court_date.errors[:date]).to eq(["is not valid. Court date cannot be prior to 1/1/1989."]) end it "is not valid more than 1 year in the future" do court_date = CourtDate.new(date: 367.days.from_now) expect(court_date.valid?).to be false expect(court_date.errors[:date]).to eq(["is not valid. Court date must be within one year from today."]) end it "is valid within one year in the future" do court_date = CourtDate.new(date: 6.months.from_now) court_date.valid? expect(court_date.errors[:date]).to eq([]) end it "is valid in the past after 1989" do court_date = CourtDate.new(date: "1997-08-29".to_date) court_date.valid? expect(court_date.errors[:date]).to eq([]) end end it "is invalid without a casa_case" do court_date = build(:court_date, casa_case: nil) expect do court_date.casa_case = casa_case end.to change { court_date.valid? }.from(false).to(true) end describe ".ordered_ascending" do subject { described_class.ordered_ascending } it "orders the casa cases by updated at date" do very_old_pcd = create(:court_date, date: 10.days.ago) old_pcd = create(:court_date, date: 5.day.ago) recent_pcd = create(:court_date, date: 1.day.ago) ordered_pcds = described_class.ordered_ascending expect(ordered_pcds.map(&:id)).to eq [very_old_pcd.id, old_pcd.id, recent_pcd.id] end end describe "reports" do let!(:reports) do [10, 30, 60].map do |days_ago| path_to_template = "app/documents/templates/default_report_template.docx" args = { case_id: casa_case.id, volunteer_id: volunteer.id, path_to_template: path_to_template } context = CaseCourtReportContext.new(args).context report = CaseCourtReport.new(path_to_template: path_to_template, context: context) casa_case.court_reports.attach(io: StringIO.new(report.generate_to_string), filename: "report-#{days_ago}.docx") attached_report = casa_case.latest_court_report attached_report.created_at = days_ago.days.ago attached_report.save! attached_report end end let(:ten_days_ago_report) { reports[0] } let(:thirty_days_ago_report) { reports[1] } let(:sixty_days_ago_report) { reports[2] } describe "#associated_reports" do subject(:associated_reports) { court_date.associated_reports } context "without other court dates" do it { is_expected.to eq [ten_days_ago_report, thirty_days_ago_report, sixty_days_ago_report] } end context "with a previous court date" do let!(:other_court_date) { create(:court_date, casa_case: casa_case, date: 40.days.ago) } it { is_expected.to eq [ten_days_ago_report, thirty_days_ago_report] } end end describe "#latest_associated_report" do it "is the most recent report for the case" do expect(subject.latest_associated_report).to eq(ten_days_ago_report) end end end describe "#additional_info?" do subject(:additional_info) { court_date.additional_info? } context "with orders" do it "returns true" do create(:case_court_order, casa_case: casa_case, court_date: court_date) expect(subject).to be_truthy end end context "with hearing type" do it "returns true" do hearing_type = create(:hearing_type) court_date.update!(hearing_type_id: hearing_type.id) expect(subject).to be_truthy end end context "with judge" do it "returns true" do judge = create(:judge) court_date.update!(judge_id: judge.id) expect(subject).to be_truthy end end context "with no extra data" do it "returns false" do expect(subject).to be_falsy end end end describe "#display_name" do subject { court_date.display_name } it "contains case number and date" do travel_to Time.zone.local(2020, 1, 2) expect(subject).to eq("AAA123123 - Court Date - 2019-12-26") travel_back end end end ================================================ FILE: spec/models/custom_org_link_spec.rb ================================================ require "rails_helper" RSpec.describe CustomOrgLink, type: :model do it { is_expected.to belong_to :casa_org } it { is_expected.to validate_presence_of :text } it { is_expected.to validate_presence_of :url } it { is_expected.to validate_length_of(:text).is_at_most described_class::TEXT_MAX_LENGTH } describe "#trim_name" do let(:casa_org) { create(:casa_org) } context "when text is present" do it "trims leading and trailing whitespace from text" do custom_link = build(:custom_org_link, casa_org: casa_org, text: " Example Text ") custom_link.save expect(custom_link.text).to eq("Example Text") end end end describe "url validation - only allow http or https schemes" do it { is_expected.to allow_value("http://example.com").for(:url) } it { is_expected.to allow_value("https://example.com").for(:url) } it { is_expected.not_to allow_value("ftp://example.com").for(:url) } it { is_expected.not_to allow_value("example.com").for(:url) } it { is_expected.not_to allow_value("some arbitrary string").for(:url) } end describe "#active" do it "only allows true or false" do casa_org = build(:casa_org) expect(build(:custom_org_link, casa_org: casa_org, active: false)).to be_valid expect(build(:custom_org_link, casa_org: casa_org, active: true)).to be_valid expect(build(:custom_org_link, casa_org: casa_org, active: nil)).to be_invalid end end end ================================================ FILE: spec/models/emancipation_category_spec.rb ================================================ require "rails_helper" RSpec.describe EmancipationCategory, type: :model do it { is_expected.to have_many(:casa_case_emancipation_categories).dependent(:destroy) } it { is_expected.to have_many(:casa_cases).through(:casa_case_emancipation_categories) } it { is_expected.to have_many(:emancipation_options) } it { is_expected.to validate_presence_of(:name) } context "When creating a new category" do it "raises an exception for duplicate entries" do duplicate_category_name = "test category" expect { create(:emancipation_category, name: duplicate_category_name) create(:emancipation_category, name: duplicate_category_name) }.to raise_error(ActiveRecord::RecordNotUnique) end end describe "#add_option" do let(:emancipation_category) { create(:emancipation_category) } after do EmancipationOption.category_options(emancipation_category.id).destroy_all end it "creates an option" do option_name = "test option" expect { emancipation_category.add_option(option_name) }.to change(EmancipationOption, :count).by(1) end end describe "#delete_option" do let(:emancipation_category) { create(:emancipation_category) } after do EmancipationOption.category_options(emancipation_category.id).destroy_all end it "deletes an existing option" do option_name = "test option" emancipation_category.add_option(option_name) expect { emancipation_category.delete_option(option_name) }.to change(EmancipationOption, :count).by(-1) end end end ================================================ FILE: spec/models/emancipation_option_spec.rb ================================================ require "rails_helper" RSpec.describe EmancipationOption, type: :model do it { is_expected.to belong_to(:emancipation_category) } it { is_expected.to have_many(:casa_case_emancipation_options).dependent(:destroy) } it { is_expected.to have_many(:casa_cases).through(:casa_case_emancipation_options) } it { is_expected.to validate_presence_of(:name) } context "When creating a new option" do context "duplicate name entries" do duplicate_option_name = "test option" let(:duplicate_category) { create(:emancipation_category) } let(:non_duplicate_category) { create(:emancipation_category, name: "Not the same name as the other category to satisfy unique contraints") } it "is unique across emancipation_category, name" do eo = create(:emancipation_option) eo_new = build(:emancipation_option, emancipation_category: eo.emancipation_category, name: eo.name) expect(eo_new.valid?).to be false end it "creates two new entries given different categories and same names" do expect { build_stubbed(:emancipation_option, emancipation_category_id: non_duplicate_category.id, name: duplicate_option_name) build_stubbed(:emancipation_option, emancipation_category_id: duplicate_category.id, name: duplicate_option_name) }.not_to raise_error end end end describe ".category_options" do let(:category_a) { create(:emancipation_category, name: "A") } let(:category_b) { create(:emancipation_category, name: "B") } let(:option_a) { create(:emancipation_option, emancipation_category_id: category_a.id, name: "A") } let(:option_b) { create(:emancipation_option, emancipation_category_id: category_a.id, name: "B") } let(:option_c) { create(:emancipation_option, emancipation_category_id: category_a.id, name: "C") } let(:option_d) { create(:emancipation_option, emancipation_category_id: category_b.id, name: "D") } it "contains exactly the options belonging to the category passed to it" do expect(EmancipationOption.category_options(category_a.id)).to contain_exactly(option_a, option_b, option_c) expect(EmancipationOption.category_options(category_b.id)).to contain_exactly(option_d) end end describe ".options_with_category_and_case" do let(:case_a) { create(:casa_case) } let(:case_b) { create(:casa_case) } let(:category_a) { create(:emancipation_category, name: "A") } let(:category_b) { create(:emancipation_category, name: "B") } let(:option_a) { build(:emancipation_option, emancipation_category_id: category_a.id, name: "A") } let(:option_b) { build(:emancipation_option, emancipation_category_id: category_a.id, name: "B") } let(:option_c) { build(:emancipation_option, emancipation_category_id: category_a.id, name: "C") } let(:option_d) { build(:emancipation_option, emancipation_category_id: category_b.id, name: "D") } it "contains exactly the options belonging to the category and case passed to it" do case_a.emancipation_options += [option_a, option_b] case_b.emancipation_options += [option_b, option_d] expect(EmancipationOption.options_with_category_and_case(category_a.id, case_a.id)).to contain_exactly(option_a, option_b) expect(EmancipationOption.options_with_category_and_case(category_a.id, case_b.id)).to contain_exactly(option_b) expect(EmancipationOption.options_with_category_and_case(category_b.id, case_a.id)).to be_empty expect(EmancipationOption.options_with_category_and_case(category_b.id, case_b.id)).to contain_exactly(option_d) end end end ================================================ FILE: spec/models/followup_spec.rb ================================================ require "rails_helper" RSpec.describe Followup, type: :model do subject { build(:followup) } it { is_expected.to belong_to(:case_contact) } # TOOD polymorph remove after migraion complete it { is_expected.to belong_to(:creator).class_name("User") } it { is_expected.to belong_to(:followupable).optional } it "has polymorphic fields" do expect(Followup.new).to respond_to(:followupable_id) expect(Followup.new).to respond_to(:followupable_type) end # TODO polymorph temporary test for dual writing it "writes to case_contact_id and both polymorphic columns when creating new followups" do case_contact = create(:case_contact) followup = create(:followup, :with_note, case_contact: case_contact) expect(followup.case_contact_id).not_to be_nil expect(followup.followupable_id).not_to be_nil expect(followup.followupable_type).to eq "CaseContact" expect(followup.followupable_id).to eq followup.case_contact_id end it "only allows 1 followup in requested status" do case_contact = build_stubbed(:case_contact) create(:followup, case_contact: case_contact) invalid_followup = build(:followup, status: :requested, case_contact: case_contact) expect(invalid_followup).to be_invalid end it "allows followup to be flipped to resolved" do followup = create(:followup, :with_note) expect(followup.resolved!).to be_truthy end describe ".in_organization" do # this needs to run first so it is generated using a new "default" organization subject { described_class.in_organization(second_org) } let!(:followup_first_org) { create(:followup) } # then these lets are generated for the org_to_search organization let!(:second_org) { create(:casa_org) } let!(:casa_case) { create(:casa_case, casa_org: second_org) } let!(:casa_case_another) { create(:casa_case, casa_org: second_org) } let!(:case_contact) { create(:case_contact, casa_case: casa_case) } let!(:case_contact_another) { create(:case_contact, casa_case: casa_case_another) } let!(:followup_second_org) { create(:followup, case_contact: case_contact) } let!(:followup_second_org_another) { create(:followup, case_contact: case_contact_another) } it "includes followups from same organization" do expect(subject).to contain_exactly(followup_second_org, followup_second_org_another) end it "excludes followups from other organizations" do expect(subject).not_to include(followup_first_org) end end end ================================================ FILE: spec/models/fund_request_spec.rb ================================================ require "rails_helper" RSpec.describe FundRequest, type: :model do it { is_expected.to validate_presence_of(:submitter_email) } end ================================================ FILE: spec/models/health_spec.rb ================================================ require "rails_helper" RSpec.describe Health, type: :model do describe "#instance" do it "returns an instance of the health class" do expect(Health.instance).not_to eq nil end it "returns a new instance of the health class if there are none" do Health.destroy_all expect(Health.instance).not_to eq nil end it "singleton_guard column is 0" do expect(Health.instance.singleton_guard).to eq 0 end end end ================================================ FILE: spec/models/hearing_type_spec.rb ================================================ require "rails_helper" RSpec.describe HearingType, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to have_many(:checklist_items) } describe "for_organization" do let!(:casa_org_1) { create(:casa_org) } let!(:casa_org_2) { create(:casa_org) } let!(:record_1) { create(:hearing_type, casa_org: casa_org_1) } let!(:record_2) { create(:hearing_type, casa_org: casa_org_2) } it "returns only records matching the specified organization" do expect(described_class.for_organization(casa_org_1)).to eq([record_1]) expect(described_class.for_organization(casa_org_2)).to eq([record_2]) end end describe "default scope" do let(:casa_org) { create(:casa_org) } let(:hearing_types) do 5.times.map { create(:hearing_type, casa_org: casa_org) } end it "orders alphabetically by name" do expect(described_class.for_organization(casa_org)).to eq(hearing_types.sort_by(&:name)) end end end ================================================ FILE: spec/models/judge_spec.rb ================================================ require "rails_helper" RSpec.describe Judge, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to validate_presence_of(:name) } it "has a valid factory" do judge = build(:judge) expect(judge).to be_valid end describe ".for_organization" do it "returns only records matching the specified organization" do casa_org_1 = create(:casa_org) casa_org_2 = create(:casa_org) record_1 = create(:judge, casa_org: casa_org_1) record_2 = create(:judge, casa_org: casa_org_2) expect(described_class.for_organization(casa_org_1)).to eq([record_1]) expect(described_class.for_organization(casa_org_2)).to eq([record_2]) end end describe "default scope" do it "orders alphabetically by name" do casa_org = create(:casa_org) judge1 = create(:judge, name: "Gamma") judge2 = create(:judge, name: "Alpha") judge3 = create(:judge, name: "Epsilon") expect(described_class.for_organization(casa_org)).to eq [judge2, judge3, judge1] end end end ================================================ FILE: spec/models/language_spec.rb ================================================ require "rails_helper" RSpec.describe Language, type: :model do let(:organization) { create(:casa_org) } let!(:language) { create(:language, name: "Spanish", casa_org: organization) } it { is_expected.to belong_to(:casa_org) } it { is_expected.to have_many(:user_languages) } it { is_expected.to have_many(:users).through(:user_languages) } it { is_expected.to validate_presence_of(:name) } it "validates uniqueness of language for an organization" do subject = build(:language, name: "spanish", casa_org: organization) expect(subject).not_to be_valid end context "when calling valid?" do it "removes surrounding spaces from the name attribute" do subject = build(:language, name: " spanish ", casa_org: organization) subject.valid? expect(subject.name).to eq "spanish" end it "removes surrounding spaces from the name attribute but leaves in middle spaces" do subject = build(:language, name: " Western Punjabi ", casa_org: organization) subject.valid? expect(subject.name).to eq "Western Punjabi" end end end ================================================ FILE: spec/models/learning_hour_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHour, type: :model do it "has a title" do learning_hour = build_stubbed(:learning_hour, name: nil) expect(learning_hour).not_to be_valid expect(learning_hour.errors[:name]).to eq(["/ Title cannot be blank"]) end it "has a learning_hour_type" do learning_hour = build_stubbed(:learning_hour, learning_hour_type: nil) expect(learning_hour).not_to be_valid expect(learning_hour.errors[:learning_hour_type]).to eq(["must exist"]) end context "duration_hours is zero" do it "has a duration in minutes that is greater than 0" do learning_hour = build_stubbed(:learning_hour, duration_hours: 0, duration_minutes: 0) expect(learning_hour).not_to be_valid expect(learning_hour.errors[:duration_minutes]).to eq(["and hours (total duration) must be greater than 0"]) end end context "duration_hours is greater than zero" do it "has a duration in minutes that is greater than 0" do learning_hour = build_stubbed(:learning_hour, duration_hours: 1, duration_minutes: 0) expect(learning_hour).to be_valid expect(learning_hour.errors[:duration_minutes]).to eq([]) end end it "has an occurred_at date" do learning_hour = build_stubbed(:learning_hour, occurred_at: nil) expect(learning_hour).not_to be_valid expect(learning_hour.errors[:occurred_at]).to eq(["can't be blank"]) end it "has date that is not in the future" do learning_hour = build_stubbed(:learning_hour, occurred_at: 1.day.from_now.strftime("%d %b %Y")) expect(learning_hour).not_to be_valid end it "Date cannot be before 01-01-1989" do learning_hour = build_stubbed(:learning_hour, occurred_at: "1984-01-01".to_date) expect(learning_hour).not_to be_valid expect(learning_hour.errors[:occurred_at]).to eq(["is not valid: Occurred on Date cannot be prior to 1/1/1989."]) end it "does not require learning_hour_topic if casa_org learning_hour_topic disabled" do learning_hour = build_stubbed(:learning_hour, learning_hour_topic: nil) expect(learning_hour).to be_valid end it "requires learning_hour_topic if casa_org learning_hour_topic enabled" do casa_org = build(:casa_org, learning_topic_active: true) user = build(:user, casa_org: casa_org) learning_hour = build(:learning_hour, user: user) expect(learning_hour).not_to be_valid expect(learning_hour.errors[:learning_hour_topic]).to eq(["can't be blank"]) end describe "scopes" do let(:casa_org_1) { build(:casa_org) } let!(:learning_hours) do [ create(:learning_hour, user: volunteer1, duration_hours: 1, duration_minutes: 0), create(:learning_hour, user: volunteer1, duration_hours: 2, duration_minutes: 0), create(:learning_hour, user: volunteer2, duration_hours: 1, duration_minutes: 0), create(:learning_hour, user: volunteer2, duration_hours: 3, duration_minutes: 0), create(:learning_hour, user: volunteer3, duration_hours: 1, duration_minutes: 0) ] end let(:casa_org_2) { build(:casa_org) } let(:casa_admin) { create(:casa_admin, display_name: "Supervisor", casa_org: casa_org_1) } let(:supervisor) { create(:supervisor, display_name: "Supervisor", casa_org: casa_org_1) } let(:volunteer1) { create(:volunteer, display_name: "Volunteer 1", casa_org: casa_org_1) } let(:volunteer2) { create(:volunteer, display_name: "Volunteer 2", casa_org: casa_org_1) } let(:volunteer3) { create(:volunteer, display_name: "Volunteer 3", casa_org: casa_org_2) } before do supervisor.volunteers << volunteer1 end describe ".supervisor_volunteers_learning_hours" do subject(:supervisor_volunteers_learning_hours) { described_class.supervisor_volunteers_learning_hours(supervisor.id) } context "with specified supervisor" do it "returns the total time spent for supervisor's volunteers" do expect(supervisor_volunteers_learning_hours.length).to eq(1) expect(supervisor_volunteers_learning_hours.first.total_time_spent).to eq(180) end end end describe ".all_volunteers_learning_hours" do subject(:all_volunteers_learning_hours) { described_class.all_volunteers_learning_hours(casa_admin.casa_org_id) } it "returns the total time spent for all volunteers" do expect(all_volunteers_learning_hours.length).to eq(2) expect(all_volunteers_learning_hours.last.total_time_spent).to eq(240) end end end end ================================================ FILE: spec/models/learning_hour_topic_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe LearningHourTopic, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to validate_presence_of(:name) } it "has a valid factory" do expect(build(:learning_hour_topic).valid?).to be true end it "has unique names for the specified organization" do casa_org_one = create(:casa_org) casa_org_two = create(:casa_org) create(:learning_hour_topic, casa_org: casa_org_one, name: "Ethics") expect { create(:learning_hour_topic, casa_org: casa_org_one, name: "Ethics") } .to raise_error(ActiveRecord::RecordInvalid) expect { create(:learning_hour_topic, casa_org: casa_org_one, name: "Ethics ") } .to raise_error(ActiveRecord::RecordInvalid) expect { create(:learning_hour_topic, casa_org: casa_org_one, name: "ethics") } .to raise_error(ActiveRecord::RecordInvalid) expect { create(:learning_hour_topic, casa_org: casa_org_two, name: "Ethics") } .not_to raise_error end describe "for_organization" do let!(:casa_org_one) { create(:casa_org) } let!(:casa_org_two) { create(:casa_org) } let!(:record_1) { create(:learning_hour_topic, casa_org: casa_org_one) } let!(:record_2) { create(:learning_hour_topic, casa_org: casa_org_two) } it "returns only records matching the specified organization" do expect(described_class.for_organization(casa_org_one)).to eq([record_1]) expect(described_class.for_organization(casa_org_two)).to eq([record_2]) end end end ================================================ FILE: spec/models/learning_hour_type_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourType, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to validate_presence_of(:name) } it "has a valid factory" do expect(build(:learning_hour_type).valid?).to be true end it "has unique names for the specified organization" do casa_org_1 = create(:casa_org) casa_org_2 = create(:casa_org) create(:learning_hour_type, casa_org: casa_org_1, name: "Book") expect { create(:learning_hour_type, casa_org: casa_org_1, name: "Book") }.to raise_error(ActiveRecord::RecordInvalid) expect { create(:learning_hour_type, casa_org: casa_org_1, name: "Book ") }.to raise_error(ActiveRecord::RecordInvalid) expect { create(:learning_hour_type, casa_org: casa_org_1, name: "book") }.to raise_error(ActiveRecord::RecordInvalid) expect { create(:learning_hour_type, casa_org: casa_org_2, name: "Book") }.not_to raise_error end describe "for_organization" do let!(:casa_org_1) { create(:casa_org) } let!(:casa_org_2) { create(:casa_org) } let!(:record_1) { create(:learning_hour_type, casa_org: casa_org_1) } let!(:record_2) { create(:learning_hour_type, casa_org: casa_org_2) } it "returns only records matching the specified organization" do expect(described_class.for_organization(casa_org_1)).to eq([record_1]) expect(described_class.for_organization(casa_org_2)).to eq([record_2]) end end describe "default scope" do let(:casa_org) { create(:casa_org) } it "orders alphabetically by position and then name" do create(:learning_hour_type, casa_org: casa_org, name: "Book") create(:learning_hour_type, casa_org: casa_org, name: "Webinar") create(:learning_hour_type, casa_org: casa_org, name: "Other", position: 99) create(:learning_hour_type, casa_org: casa_org, name: "YouTube") type_names = %w[Book Webinar YouTube Other] expect(described_class.for_organization(casa_org).map(&:name)).to eq(type_names) end end end ================================================ FILE: spec/models/learning_hours_report_spec.rb ================================================ require "rails_helper" require "csv" RSpec.describe LearningHoursReport, type: :model do describe "#to_csv" do context "when there are learning hours" do it "includes all learning hours" do casa_org = build(:casa_org) users = create_list(:user, 3, casa_org: casa_org) learning_hour_types = create_list(:learning_hour_type, 3) learning_hours = [ create(:learning_hour, user: users[0], learning_hour_type: learning_hour_types[0]), create(:learning_hour, user: users[1], learning_hour_type: learning_hour_types[1]), create(:learning_hour, user: users[2], learning_hour_type: learning_hour_types[2]) ] result = CSV.parse(described_class.new(casa_org.id).to_csv) expect(result.length).to eq(learning_hours.length + 1) result.each_with_index do |row, index| next if index.zero? expect(row[0]).to eq learning_hours[index - 1].user.display_name expect(row[1]).to eq learning_hours[index - 1].name expect(row[2]).to eq learning_hours[index - 1].learning_hour_type.name expect(row[3]).to eq "#{learning_hours[index - 1].duration_hours}:#{learning_hours[index - 1].duration_minutes}" expect(row[4]).to eq learning_hours[index - 1].occurred_at.strftime("%F") end end end context "when there are no learning hours" do it "returns only the header" do casa_org = build(:casa_org) result = CSV.parse(described_class.new(casa_org.id).to_csv) expect(result.length).to eq(1) expect(result[0]).to eq([ "Volunteer Name", "Learning Hours Title", "Learning Hours Type", "Duration", "Date Of Learning" ]) end end end end ================================================ FILE: spec/models/login_activity_spec.rb ================================================ require "rails_helper" RSpec.describe LoginActivity, type: :model do it "has a valid factory" do login_activity = build(:login_activity) expect(login_activity).to be_valid end end ================================================ FILE: spec/models/mileage_rate_spec.rb ================================================ require "rails_helper" RSpec.describe MileageRate, type: :model do subject { build(:mileage_rate) } it { is_expected.to belong_to(:casa_org) } it { is_expected.to validate_presence_of(:effective_date) } it { is_expected.to validate_presence_of(:casa_org) } it { is_expected.to validate_presence_of(:amount) } describe "for_organization" do let!(:casa_org_1) { create(:casa_org) } let!(:casa_org_2) { create(:casa_org) } let!(:record_1) { create(:mileage_rate, casa_org: casa_org_1) } let!(:record_2) { create(:mileage_rate, casa_org: casa_org_2) } it "returns only records matching the specified organization" do expect(described_class.for_organization(casa_org_1)).to eq([record_1]) expect(described_class.for_organization(casa_org_2)).to eq([record_2]) end end describe "#effective_date" do it "cannot be before 1/1/1989" do mileage_rate = build(:mileage_rate, effective_date: "1984-01-01".to_date) expect(mileage_rate).not_to be_valid expect(mileage_rate.errors[:effective_date]).to eq(["cannot be prior to 1/1/1989."]) end it "cannot be more than one year in the future" do mileage_rate = build(:mileage_rate, effective_date: 367.days.from_now) expect(mileage_rate).not_to be_valid expect(mileage_rate.errors[:effective_date]).to eq(["must not be more than one year in the future."]) end it "is valid in the past after 1/1/1989" do mileage_rate = build(:mileage_rate, effective_date: "1997-08-29".to_date) expect(mileage_rate).to be_valid expect(mileage_rate.errors[:effective_date]).to eq([]) end it "is valid today" do mileage_rate = build(:mileage_rate, effective_date: Time.current) expect(mileage_rate).to be_valid expect(mileage_rate.errors[:effective_date]).to eq([]) end it "is unique within is_active and casa_org" do effective_date = Date.new(2020, 1, 1) casa_org = create(:casa_org) create(:mileage_rate, effective_date: effective_date, is_active: true, casa_org: casa_org) expect do create(:mileage_rate, effective_date: effective_date, is_active: true, casa_org: create(:casa_org)) end.not_to raise_error expect do create(:mileage_rate, effective_date: effective_date, is_active: false, casa_org: casa_org) end.not_to raise_error expect do create(:mileage_rate, effective_date: effective_date, is_active: true, casa_org: casa_org) end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Effective date must not have duplicate active dates") end end end ================================================ FILE: spec/models/mileage_report_spec.rb ================================================ require "rails_helper" require "csv" RSpec.describe MileageReport, type: :model do describe "#to_csv" do it "includes only case contacts that are eligible for driving reimbursement and not already reimbursed" do user1 = create(:volunteer, display_name: "Linda") contact_type1 = create(:contact_type, name: "Therapist") casa_case1 = create(:casa_case, case_number: "Hello") case_contact1 = create(:case_contact, want_driving_reimbursement: true, miles_driven: 5, creator: user1, contact_types: [contact_type1], occurred_at: Date.new(2020, 1, 1), casa_case: casa_case1) create(:case_contact, want_driving_reimbursement: false, miles_driven: 10, reimbursement_complete: false) create(:case_contact, want_driving_reimbursement: false) create(:case_contact, want_driving_reimbursement: true, miles_driven: 15, created_at: 2.years.ago) csv = described_class.new(case_contact1.casa_case.casa_org_id).to_csv parsed_csv = CSV.parse(csv) expect(parsed_csv.length).to eq(2) expect(parsed_csv[0].length).to eq(parsed_csv[1].length) expect(parsed_csv[0]).to eq([ "Contact Types", "Occurred At", "Miles Driven", "Casa Case Number", "Creator Name", "Supervisor Name", "Volunteer Address", "Reimbursed" ]) case_contact_data = parsed_csv[1] expect(case_contact_data[0]).to eq("Therapist") expect(case_contact_data[1]).to eq("January 1, 2020") expect(case_contact_data[2]).to eq("5") expect(case_contact_data[3]).to eq("Hello") expect(case_contact_data[4]).to eq("Linda") end it "generates an empty csv when there are no eligible case contacts" do faux_casa_org_id = 0 csv = described_class.new(faux_casa_org_id).to_csv parsed_csv = CSV.parse(csv) expect(parsed_csv.length).to eq(1) expect(parsed_csv[0]).to eq([ "Contact Types", "Occurred At", "Miles Driven", "Casa Case Number", "Creator Name", "Supervisor Name", "Volunteer Address", "Reimbursed" ]) end it "includes case contacts from current org" do casa_org = create(:casa_org) create(:casa_case, casa_org: casa_org) create(:case_contact, want_driving_reimbursement: true, miles_driven: 15) csv = described_class.new(casa_org.id).to_csv parsed_csv = CSV.parse(csv) expect(parsed_csv.length).to eq(2) end it "excludes case contacts from other orgs" do casa_org = create(:casa_org) other_casa_org = create(:casa_org) casa_case = create(:casa_case, casa_org: other_casa_org) create(:case_contact, casa_case: casa_case, want_driving_reimbursement: true, miles_driven: 60) csv = described_class.new(casa_org.id).to_csv parsed_csv = CSV.parse(csv) expect(parsed_csv.length).to eq(1) end end end ================================================ FILE: spec/models/missing_data_report_spec.rb ================================================ require "rails_helper" require "csv" RSpec.describe MissingDataReport, type: :model do describe "#to_csv" do let!(:casa_org) { create(:casa_org) } let(:result) { CSV.parse(described_class.new(casa_org.id).to_csv) } shared_examples "report_with_header" do it "contains header" do expect(result[0]).to eq([ "Casa Case Number", "Youth Birth Month And Year", "Upcoming Hearing Date", "Court Orders" ]) end end context "when there are casa cases" do let!(:incomplete_casa_cases) do [ create(:casa_case), create(:casa_case, :with_one_court_order), create(:casa_case, :with_upcoming_court_date) ] end let!(:incomplete_casa_cases_from_other_org) { create_list(:casa_case, 3, casa_org: create(:casa_org)) } let!(:complete_casa_cases) { create_list(:casa_case, 3, :with_upcoming_court_date, :with_one_court_order) } let(:expected_result) do [ [incomplete_casa_cases[0].case_number, "OK", "MISSING", "MISSING"], [incomplete_casa_cases[1].case_number, "OK", "MISSING", "OK"], [incomplete_casa_cases[2].case_number, "OK", "OK", "MISSING"] ] end it "includes only cases with missing data" do expect(result.length).to eq(4) expect(result).to include(*expected_result) end it_behaves_like "report_with_header" end context "when there are no casa cases" do it "includes only the header" do expect(result.length).to eq(1) end it_behaves_like "report_with_header" end end end ================================================ FILE: spec/models/note_spec.rb ================================================ require "rails_helper" RSpec.describe Note, type: :model do it { is_expected.to belong_to(:notable) } it { is_expected.to belong_to(:creator) } it "has a valid factory" do note = build(:note) expect(note).to be_valid end end ================================================ FILE: spec/models/other_duty_spec.rb ================================================ require "rails_helper" RSpec.describe OtherDuty, type: :model do it { is_expected.to belong_to(:creator) } it "validates presence of notes" do duty = build(:other_duty, notes: nil) expect(duty).not_to be_valid expect(duty.errors[:notes]).to eq(["can't be blank"]) end it "cannot be saved without a user" do other_duty = OtherDuty.new other_duty.creator = nil expect { other_duty.save!(validate: false) }.to raise_error ActiveRecord::NotNullViolation end describe "occurred_at validation" do it "is not valid before 1989" do other_duty = OtherDuty.new(occurred_at: "1984-01-01".to_date) expect(other_duty.valid?).to be false expect(other_duty.errors[:occurred_at]).to eq(["is not valid. Occured on date cannot be prior to 1/1/1989."]) end it "is not valid more than 1 year in the future" do other_duty = OtherDuty.new(occurred_at: 367.days.from_now) expect(other_duty.valid?).to be false expect(other_duty.errors[:occurred_at]).to eq(["is not valid. Occured on date must be within one year from today."]) end it "is valid within one year in the future" do other_duty = OtherDuty.new(occurred_at: 6.months.from_now) other_duty.valid? expect(other_duty.errors[:occurred_at]).to eq([]) end it "is valid in the past after 1989" do other_duty = OtherDuty.new(occurred_at: "1997-08-29".to_date) other_duty.valid? expect(other_duty.errors[:occurred_at]).to eq([]) end end end ================================================ FILE: spec/models/patch_note_group_spec.rb ================================================ require "rails_helper" RSpec.describe PatchNoteGroup, type: :model do let!(:patch_note_group) { create(:patch_note_group, value: "test") } it { is_expected.to validate_uniqueness_of(:value) } it { is_expected.to validate_presence_of(:value) } end ================================================ FILE: spec/models/patch_note_spec.rb ================================================ require "rails_helper" RSpec.describe PatchNote, type: :model do let!(:patch_note) { create(:patch_note) } it { is_expected.to belong_to(:patch_note_group) } it { is_expected.to belong_to(:patch_note_type) } it { is_expected.to validate_presence_of(:note) } end ================================================ FILE: spec/models/patch_note_type_spec.rb ================================================ require "rails_helper" RSpec.describe PatchNoteType, type: :model do let!(:patch_note_type) { create(:patch_note_type) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name) } end ================================================ FILE: spec/models/placement_spec.rb ================================================ require "rails_helper" RSpec.describe Placement, type: :model do let!(:object) { create(:placement) } it { is_expected.to belong_to(:placement_type) } it { is_expected.to belong_to(:creator) } it { is_expected.to belong_to(:casa_case) } context "placement_started_at" do it "cannot be before 1/1/1989" do placement = build(:placement, placement_started_at: "1984-01-01".to_date) expect(placement).not_to be_valid expect(placement.errors[:placement_started_at]).to eq(["cannot be prior to 1/1/1989."]) end it "cannot be more than one year in the future" do placement = build(:placement, placement_started_at: 367.days.from_now) expect(placement).not_to be_valid expect(placement.errors[:placement_started_at]).to eq(["must not be more than one year in the future."]) end it "is valid in the past after 1/1/1989" do placement = build(:placement, placement_started_at: "1997-08-29".to_date) expect(placement).to be_valid expect(placement.errors[:placement_started_at]).to eq([]) end it "is valid today" do placement = build(:placement, placement_started_at: Time.current) expect(placement).to be_valid expect(placement.errors[:placement_started_at]).to eq([]) end end end ================================================ FILE: spec/models/placement_type_spec.rb ================================================ require "rails_helper" RSpec.describe PlacementType, type: :model do let!(:object) { create(:placement_type) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to belong_to(:casa_org) } end ================================================ FILE: spec/models/preference_set_spec.rb ================================================ require "rails_helper" RSpec.describe PreferenceSet, type: :model do let(:preference_set) { PreferenceSet.create(params) } describe "allows setting values for case_volunteer_columns" do let(:params) do { case_volunteer_columns: { case_number: "show", hearing_type_name: "hide", judge_name: "show", status: "show", transition_aged_youth: "show", assigned_to: "show", actions: "hide" } } end it { expect(preference_set.case_volunteer_columns["case_number"]).to eq "show" } it { expect(preference_set.case_volunteer_columns["hearing_type_name"]).to eq "hide" } it { expect(preference_set.case_volunteer_columns["judge_name"]).to eq "show" } it { expect(preference_set.case_volunteer_columns["status"]).to eq "show" } it { expect(preference_set.case_volunteer_columns["transition_aged_youth"]).to eq "show" } it { expect(preference_set.case_volunteer_columns["assigned_to"]).to eq "show" } it { expect(preference_set.case_volunteer_columns["actions"]).to eq "hide" } end end ================================================ FILE: spec/models/sent_email_spec.rb ================================================ require "rails_helper" RSpec.describe SentEmail, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to belong_to(:user) } it { is_expected.to validate_presence_of(:mailer_type) } it { is_expected.to validate_presence_of(:category) } it { is_expected.to validate_presence_of(:sent_address) } end ================================================ FILE: spec/models/sms_notification_event_spec.rb ================================================ require "rails_helper" RSpec.describe SmsNotificationEvent, type: :model do it { is_expected.to have_many(:user_sms_notification_events) } it { is_expected.to have_many(:users).through(:user_sms_notification_events) } end ================================================ FILE: spec/models/soft_deleted_model_shared_example_coverage_spec.rb ================================================ require "rails_helper" RSpec.describe "soft-deleted model shared example coverage" do let(:skip_classes) do %w[] end let(:todo_currently_missing_specs) do %w[ ContactTopicAnswer CaseContact ] end it "checks that all acts_as_paranoid models have specs that include the soft-deleted model shared example" do missing = [] Zeitwerk::Loader.eager_load_all ApplicationRecord.descendants.each do |clazz| next if clazz.abstract_class? next unless clazz.paranoid? next if skip_classes.include?(clazz.name) next if todo_currently_missing_specs.include?(clazz.name) source_file = Object.const_source_location(clazz.name)&.first next unless source_file spec_file = source_file .sub(%r{/app/models/}, "/spec/models/") .sub(/\.rb$/, "_spec.rb") unless File.exist?(spec_file) missing << "#{clazz.name}: spec file not found (expected #{spec_file})" next end contents = File.read(spec_file) unless contents.include?('"a soft-deleted model"') missing << clazz.name.to_s end end expect(missing).to be_empty, "The following paranoid models are missing the shared example:\n#{missing.join("\n")}" end end ================================================ FILE: spec/models/supervisor_spec.rb ================================================ require "rails_helper" RSpec.describe Supervisor, type: :model do include Devise::Test::IntegrationHelpers subject(:supervisor) { create :supervisor } describe "#role" do it { expect(supervisor.role).to eq "Supervisor" } it { is_expected.to have_many(:supervisor_volunteers) } it { is_expected.to have_many(:active_supervisor_volunteers) } it { is_expected.to have_many(:unassigned_supervisor_volunteers) } it { is_expected.to have_many(:volunteers).through(:active_supervisor_volunteers) } it { is_expected.to have_many(:volunteers_ever_assigned).through(:supervisor_volunteers) } end describe "invitation expiration" do let!(:mail) { supervisor.invite! } let(:expiration_date) { I18n.l(supervisor.invitation_due_at, format: :full, default: nil) } let(:two_weeks) { I18n.l(2.weeks.from_now, format: :full, default: nil) } it { expect(expiration_date).to eq two_weeks } it "expires invitation token after two weeks" do travel_to 2.weeks.from_now user = User.accept_invitation!(invitation_token: supervisor.invitation_token) expect(user.errors.full_messages).to include("Invitation token is invalid") travel_back end end describe "pending volunteers" do let(:volunteer) { create(:volunteer) } let(:assign_volunteer) { create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer) } it "returns volunteers invited by the supervisor" do volunteer.invite!(supervisor) expect(supervisor.pending_volunteers).to eq([volunteer]) end it "returns volunteers invited by others but assigned to supervisor" do volunteer.invite! assign_volunteer expect(supervisor.pending_volunteers).to eq([volunteer]) end end describe "not_signed_in_nor_have_case_contacts_volunteers" do subject { supervisor.inactive_volunteers } let(:supervisor) { create(:supervisor) } let(:volunteer) { create(:volunteer, last_sign_in_at: 31.days.ago) } let(:other_volunteer) { create(:volunteer, last_sign_in_at: 29.days.ago) } before do create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer) create(:supervisor_volunteer, supervisor: supervisor, volunteer: other_volunteer) end context "when volunteer has logged in in the last 30 days" do let(:volunteer) { create(:volunteer, last_sign_in_at: 29.days.ago) } it { is_expected.to be_empty } context "when volunteer then logged out" do it "is empty" do sign_out volunteer expect(subject).to be_empty end end end context "when volunteer hasn't logged in in the last 30 days" do it { is_expected.to contain_exactly(volunteer) } context "when volunteer is inactive" do let(:volunteer) { create(:volunteer, last_sign_in_at: 31.days.ago, active: false) } it { is_expected.to be_empty } end context "when volunteer has never logged in" do let(:volunteer) { create(:volunteer, last_sign_in_at: nil) } it { is_expected.to contain_exactly(volunteer) } end context "when volunteer has a recent case_contact" do let(:casa_case) { create(:casa_case, casa_org: supervisor.casa_org) } let!(:case_contact) { create(:case_contact, casa_case: casa_case, occurred_at: 31.days.ago, creator: volunteer, created_at: 29.days.ago) } it { is_expected.to be_empty } end end end describe "change to admin" do it "returns true if the change was successful" do expect(subject.change_to_admin!).to be_truthy end it "changes the supervisor to an admin" do subject.change_to_admin! user = User.find(subject.id) # subject.reload will cause RecordNotFound because it's looking in the wrong table expect(user).not_to be_supervisor expect(user).to be_casa_admin end end end ================================================ FILE: spec/models/supervisor_volunteer_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorVolunteer do let(:casa_org_1) { build(:casa_org) } let(:casa_org_2) { build(:casa_org) } let(:volunteer_1) { build(:volunteer, casa_org: casa_org_1) } let(:supervisor_1) { create(:supervisor, casa_org: casa_org_1) } let(:supervisor_2) { create(:supervisor, casa_org: casa_org_1) } it "assigns a volunteer to a supervisor" do supervisor_1.volunteers << volunteer_1 expect(volunteer_1.supervisor).to eq(supervisor_1) end it "only allow 1 supervisor per volunteer" do supervisor_1.volunteers << volunteer_1 supervisor_1.save expect { supervisor_2.volunteers << volunteer_1 }.to raise_error(StandardError) end it "does not allow a volunteer to be double assigned" do expect { supervisor_1.volunteers << volunteer_1 supervisor_1.volunteers << volunteer_1 }.to raise_error(ActiveRecord::RecordInvalid) end it "requires supervisor and volunteer belong to same casa_org" do supervisor_volunteer = supervisor_1.supervisor_volunteers.new(volunteer: volunteer_1) expect { volunteer_1.update(casa_org: casa_org_2) }.to change(supervisor_volunteer, :valid?).to(false) end end ================================================ FILE: spec/models/user_language_spec.rb ================================================ require "rails_helper" RSpec.describe UserLanguage, type: :model do it { is_expected.to belong_to(:language) } it { is_expected.to belong_to(:user) } it "validates uniqueness of language scoped to user" do existing_record = create(:user_language) new_record = build(:user_language, user: existing_record.user, language: existing_record.language) expect(new_record).not_to be_valid end end ================================================ FILE: spec/models/user_reminder_time.rb ================================================ require "rails_helper" RSpec.describe UserReminderTime, type: :model do it { is_expected.to belong_to(:user) } end ================================================ FILE: spec/models/user_sms_notification_event_spec.rb ================================================ require "rails_helper" RSpec.describe UserSmsNotificationEvent, type: :model do it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:sms_notification_event) } end ================================================ FILE: spec/models/user_spec.rb ================================================ require "rails_helper" RSpec.describe User, type: :model do it { is_expected.to belong_to(:casa_org) } it { is_expected.to have_many(:case_assignments) } it { is_expected.to have_many(:casa_cases).through(:case_assignments) } it { is_expected.to have_many(:case_contacts) } it { is_expected.to have_many(:sent_emails) } it { is_expected.to have_many(:user_languages) } it { is_expected.to have_many(:languages).through(:user_languages) } it { is_expected.to have_many(:followups).with_foreign_key(:creator_id) } it { is_expected.to have_one(:supervisor_volunteer) } it { is_expected.to have_one(:supervisor).through(:supervisor_volunteer) } it { is_expected.to have_many(:notes) } describe "model validations" do it "requires display name" do user = build(:user, display_name: "") expect(user.valid?).to be false end it "requires email" do user = build(:user, email: "") expect(user.valid?).to be false end it "requires 12 digit phone numbers" do user = build(:user, phone_number: "+1416321") expect(user.valid?).to be false end it "requires phone number to only contain numbers" do user = build(:user, phone_number: "+1416eee4325") expect(user.valid?).to be false end it "requires phone number with US area code" do user = build(:user, phone_number: "+76758890432") expect(user.valid?).to be false end it "requires date of birth to be in the past" do user = build(:user, date_of_birth: 10.days.from_now) expect(user.valid?).to be false expect(user.errors[:base]).to eq([" Date of birth must be in the past."]) end it "requires date of birth to be no earlier than 1/1/1920" do user = build(:user, date_of_birth: "1919-12-31".to_date) expect(user.valid?).to be false expect(user.errors[:base]).to eq([" Date of birth must be on or after 1/1/1920."]) end it "shows custom email uniqueness error message" do create(:user, email: "volunteer1@example.com") user = build(:user, email: "volunteer1@example.com") expect(user.valid?).to be false expect(user.errors[:base]).to eq([I18n.t("activerecord.errors.messages.email_uniqueness")]) end it "has an empty old_emails array when initialized" do user = build(:user) expect(user.old_emails).to eq([]) end end describe "#case_contacts_for" do let(:volunteer) { create(:volunteer, :with_casa_cases) } let(:case_of_interest) { volunteer.casa_cases.first } let!(:contact_a) { create(:case_contact, creator: volunteer, casa_case: case_of_interest) } let!(:contact_b) { create(:case_contact, creator: volunteer, casa_case: volunteer.casa_cases.second) } it "returns all case_contacts associated with this user and the casa case id supplied" do sample_casa_case_id = case_of_interest.id result = volunteer.case_contacts_for(sample_casa_case_id) expect(result.length).to eq(1) end it "does not return case_contacts associated with another volunteer user" do other_volunteer = build(:volunteer, :with_casa_cases, casa_org: volunteer.casa_org) create(:case_assignment, casa_case: case_of_interest, volunteer: other_volunteer) create(:case_contact, creator: other_volunteer, casa_case: case_of_interest) build_stubbed(:case_contact) sample_casa_case_id = case_of_interest.id result = volunteer.case_contacts_for(sample_casa_case_id) expect(result.length).to eq(1) result = other_volunteer.case_contacts_for(sample_casa_case_id) expect(result.length).to eq(1) end it "does not return case_contacts neither unassigned cases or inactive cases" do inactive_case_assignment = create(:case_assignment, casa_case: create(:casa_case, casa_org: volunteer.casa_org), active: false, volunteer: volunteer) case_assignment_to_inactve_case = create(:case_assignment, casa_case: create(:casa_case, active: false, casa_org: volunteer.casa_org), volunteer: volunteer) expect { volunteer.case_contacts_for(inactive_case_assignment.casa_case.id) }.to raise_error(ActiveRecord::RecordNotFound) expect { volunteer.case_contacts_for(case_assignment_to_inactve_case.casa_case.id) }.to raise_error(ActiveRecord::RecordNotFound) end end describe "supervisors" do describe "#volunteers_serving_transition_aged_youth" do let(:casa_org) { build(:casa_org) } let(:supervisor) { build(:supervisor, casa_org: casa_org) } it "returns the number of transition aged youth on a supervisor" do casa_cases = [ build(:casa_case, casa_org: casa_org), build(:casa_case, casa_org: casa_org), create(:casa_case, :pre_transition, casa_org: casa_org) ] casa_cases.each do |casa_case| volunteer = create(:volunteer, supervisor: supervisor, casa_org: casa_org) volunteer.casa_cases << casa_case end expect(supervisor.volunteers_serving_transition_aged_youth).to eq(2) end it "ignores volunteers' inactive and unassgined cases" do volunteer = create(:volunteer, supervisor: supervisor, casa_org: casa_org) create(:case_assignment, casa_case: create(:casa_case, casa_org: casa_org, active: false), volunteer: volunteer) create(:case_assignment, casa_case: create(:casa_case, casa_org: casa_org), active: false, volunteer: volunteer) expect(supervisor.volunteers_serving_transition_aged_youth).to eq(0) end end describe "#no_attempt_for_two_weeks" do let(:supervisor) { create(:supervisor) } it "returns zero for a volunteer that has attempted contact in at least one contact_case within the last 2 weeks" do volunteer_1 = create(:volunteer, :with_casa_cases, supervisor: supervisor) case_of_interest_1 = volunteer_1.casa_cases.first create(:case_contact, creator: volunteer_1, casa_case: case_of_interest_1, contact_made: true, created_at: 1.week.ago) expect(supervisor.no_attempt_for_two_weeks).to eq(0) end it "returns one for a supervisor with two volunteers, only one of which has a contact newer than 2 weeks old" do volunteer_1 = create(:volunteer, :with_casa_cases, supervisor: supervisor) volunteer_2 = create(:volunteer, :with_casa_cases, supervisor: supervisor) case_of_interest_1 = volunteer_1.casa_cases.first case_of_interest_2 = volunteer_2.casa_cases.first create(:case_contact, creator: volunteer_1, casa_case: case_of_interest_1, contact_made: true, created_at: 1.week.ago) create(:case_contact, creator: volunteer_2, casa_case: case_of_interest_2, contact_made: true, created_at: 3.weeks.ago) expect(supervisor.no_attempt_for_two_weeks).to eq(1) end it "returns one for a volunteer that has not made any contact_cases within the last 2 weeks" do create(:volunteer, :with_casa_cases, supervisor: supervisor) expect(supervisor.no_attempt_for_two_weeks).to eq(1) end it "returns zero for a volunteer that is not assigned to any casa cases" do create(:volunteer, supervisor: supervisor) expect(supervisor.no_attempt_for_two_weeks).to eq(0) end it "returns one for a volunteer who has attempted contact in at least one contact_case with created_at after 2 weeks" do volunteer_1 = create(:volunteer, :with_casa_cases, supervisor: supervisor) case_of_interest_1 = volunteer_1.casa_cases.first create(:case_contact, creator: volunteer_1, casa_case: case_of_interest_1, contact_made: true, created_at: 3.weeks.ago) expect(supervisor.no_attempt_for_two_weeks).to eq(1) end it "returns zero for a volunteer that has no active casa case assignments" do volunteer_1 = create(:volunteer, :with_casa_cases, supervisor: supervisor) case_of_interest_1 = volunteer_1.casa_cases.first case_of_interest_2 = volunteer_1.casa_cases.last case_assignment_1 = case_of_interest_1.case_assignments.find_by(volunteer: volunteer_1) case_assignment_2 = case_of_interest_2.case_assignments.find_by(volunteer: volunteer_1) case_assignment_1.update!(active: false) case_assignment_2.update!(active: false) expect(supervisor.no_attempt_for_two_weeks).to eq(0) end end end describe "#active_for_authentication?" do it "is false when the user is inactive" do user = build(:volunteer, :inactive) expect(user).not_to be_active_for_authentication expect(user.inactive_message).to eq(:inactive) end it "is true otherwise" do user = build(:volunteer) expect(user).to be_active_for_authentication user = build(:supervisor) expect(user).to be_active_for_authentication end end describe "#actively_assigned_and_active_cases" do let(:user) { build(:volunteer) } let!(:active_case_assignment_with_active_case) do create(:case_assignment, casa_case: build(:casa_case, casa_org: user.casa_org), volunteer: user) end let!(:active_case_assignment_with_inactive_case) do create(:case_assignment, casa_case: build(:casa_case, casa_org: user.casa_org, active: false), volunteer: user) end let!(:inactive_case_assignment_with_active_case) do create(:case_assignment, casa_case: build(:casa_case, casa_org: user.casa_org), active: false, volunteer: user) end let!(:inactive_case_assignment_with_inactive_case) do create(:case_assignment, casa_case: build(:casa_case, casa_org: user.casa_org, active: false), active: false, volunteer: user) end it "only returns the user's active cases with active case assignments" do expect(user.actively_assigned_and_active_cases).to contain_exactly(active_case_assignment_with_active_case.casa_case) end end describe "#serving_transition_aged_youth?" do let(:user) { build(:volunteer) } let!(:case_assignment_without_transition_aged_youth) do create(:case_assignment, casa_case: create(:casa_case, :pre_transition, casa_org: user.casa_org), volunteer: user) end context "when the user has a transition-aged-youth case" do it "is true" do create(:case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org), volunteer: user) expect(user).to be_serving_transition_aged_youth end end context "when the user does not have a transition-aged-youth case" do it "is false" do expect(user).not_to be_serving_transition_aged_youth end end context "when the user's only transition-aged-youth case is inactive" do it "is false" do create(:case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org, active: false), volunteer: user) expect(user).not_to be_serving_transition_aged_youth end end context "when the user is unassigned from a transition-aged-youth case" do it "is false" do create(:case_assignment, casa_case: create(:casa_case, casa_org: user.casa_org), volunteer: user, active: false) expect(user).not_to be_serving_transition_aged_youth end end end context "when there is an associated Other Duty record" do let(:user) { create(:supervisor) } let!(:duty) { create(:other_duty, creator: user) } it "cannot be destroyed without destroying the associated Other Duty record" do expect { user.delete }.to raise_error ActiveRecord::InvalidForeignKey end end describe ".no_recent_sign_in" do it "returns users who haven't signed in in 30 days" do old_sign_in_user = create(:user, last_sign_in_at: 39.days.ago) create(:user, last_sign_in_at: 5.days.ago) expect(User.no_recent_sign_in).to contain_exactly(old_sign_in_user) end it "returns users who haven't signed in ever" do user = create(:user, last_sign_in_at: nil) expect(User.no_recent_sign_in).to contain_exactly(user) end end describe "#record_previous_emails" do # create user, check for side effects, test method let!(:new_volunteer) { create(:user, email: "firstemail@example.com") } it "instantiates with an empty old_emails attribute" do expect(new_volunteer.old_emails).to be_empty end it "saves the old email when a volunteer changes their email" do new_volunteer.update(email: "secondemail@example.com") new_volunteer.confirm expect(new_volunteer.email).to eq("secondemail@example.com") expect(new_volunteer.old_emails).to contain_exactly("firstemail@example.com") end end describe "#filter_old_emails" do let!(:new_volunteer) { create(:user, email: "firstemail@example.com") } it "correctly filters out reinstated emails from old_emails when updating" do new_volunteer.update(email: "secondemail@example.com") new_volunteer.confirm new_volunteer.filter_old_emails!(new_volunteer.email) new_volunteer.update(email: "firstemail@example.com") new_volunteer.confirm new_volunteer.filter_old_emails!(new_volunteer.email) expect(new_volunteer.email).to eq("firstemail@example.com") expect(new_volunteer.old_emails).to contain_exactly("secondemail@example.com") end end end ================================================ FILE: spec/models/volunteer_spec.rb ================================================ require "rails_helper" RSpec.describe Volunteer, type: :model do describe ".email_court_report_reminder" do let!(:casa_org) { build(:casa_org) } let!(:casa_org_twilio_disabled) { build(:casa_org, twilio_enabled: false) } # Should send email for this case let!(:casa_case1) { create(:casa_case, casa_org: casa_org) } let!(:court_date1) { create(:court_date, casa_case: casa_case1, court_report_due_date: Date.current + 7.days) } # Should NOT send emails for these cases let!(:casa_case2) { build(:casa_case, casa_org: casa_org) } let!(:court_date2) { create(:court_date, casa_case: casa_case2, court_report_due_date: Date.current + 8.days) } let!(:casa_case3) { build(:casa_case, casa_org: casa_org, court_report_submitted_at: Time.current, court_report_status: :submitted) } let!(:court_date3) { create(:court_date, casa_case: casa_case3, court_report_due_date: Date.current + 7.days) } let!(:casa_case4) { build(:casa_case, casa_org: casa_org) } let!(:court_date4) { create(:court_date, casa_case: casa_case4, court_report_due_date: Date.current + 7.days) } let!(:casa_case5) { create(:casa_case, casa_org: casa_org_twilio_disabled) } let!(:court_date5) { create(:court_date, casa_case: casa_case5, court_report_due_date: Date.current + 7.days) } let(:case_assignment1) { build(:case_assignment, casa_org: casa_org, casa_case: casa_case1) } let(:case_assignment2) { build(:case_assignment, casa_org: casa_org, casa_case: casa_case2) } let(:case_assignment3) { build(:case_assignment, casa_org: casa_org, casa_case: casa_case3) } let(:case_assignment_unassigned) { build(:case_assignment, casa_org: casa_org, casa_case: casa_case4, active: false) } let(:case_assignment5) { build(:case_assignment, casa_org: casa_org_twilio_disabled, casa_case: casa_case5) } let!(:v1) { create(:volunteer, casa_org: casa_org, case_assignments: [case_assignment1, case_assignment2, case_assignment3]) } let!(:v2) { create(:volunteer, casa_org: casa_org, active: false) } let!(:v3) { create(:volunteer, casa_org: casa_org) } let!(:v4) { create(:volunteer, casa_org: casa_org, case_assignments: [case_assignment_unassigned]) } let!(:v5) { create(:volunteer, casa_org: casa_org_twilio_disabled, case_assignments: [case_assignment5]) } before do stub_const("Volunteer::COURT_REPORT_SUBMISSION_REMINDER", 7.days) WebMockHelper.short_io_court_report_due_date_stub end it "sends one mailer" do expect(VolunteerMailer).to receive(:court_report_reminder).with(v1, Date.current + 7.days) expect(VolunteerMailer).not_to receive(:court_report_reminder).with(v2, anything) expect(VolunteerMailer).not_to receive(:court_report_reminder).with(v3, anything) described_class.send_court_report_reminder end it "does not send reminders about unassigned cases" do expect(VolunteerMailer).not_to receive(:court_report_reminder).with(v4, anything) described_class.send_court_report_reminder end it "sends one sms" do expect(CourtReportDueSmsReminderService).to receive(:court_report_reminder).with(v1, Date.current + 7.days) expect(CourtReportDueSmsReminderService).not_to receive(:court_report_reminder).with(v2, anything) expect(CourtReportDueSmsReminderService).not_to receive(:court_report_reminder).with(v3, anything) described_class.send_court_report_reminder end it "does not send sms about unassigned cases" do expect(CourtReportDueSmsReminderService).not_to receive(:court_report_reminder).with(v4, anything) described_class.send_court_report_reminder end it "returns nil when twilio is disabled" do response = CourtReportDueSmsReminderService.court_report_reminder(v5, Date.current + 7.days) expect(response).to eq(nil) end end describe "#activate" do let(:volunteer) { build(:volunteer, :inactive) } it "activates the volunteer" do volunteer.activate volunteer.reload expect(volunteer.active).to eq(true) end end describe "#deactivate" do let(:volunteer) { build(:volunteer) } it "deactivates the volunteer" do expect(volunteer.deactivate.reload.active).to eq(false) end it "sets all of a volunteer's case assignments to inactive" do case_contacts = 3.times.map { create(:case_assignment, casa_case: build(:casa_case, casa_org: volunteer.casa_org), volunteer: volunteer) } volunteer.deactivate case_contacts.each { |c| c.reload } expect(case_contacts).to all(satisfy { |c| !c.active }) end context "when volunteer has previously been assigned a supervisor" do let!(:supervisor_volunteer) { create(:supervisor_volunteer, volunteer: volunteer) } it "deactivates the supervisor-volunteer relationship" do expect { volunteer.deactivate.reload }.to change(volunteer, :supervisor_volunteer) end end context "when volunteer had no supervisor previously assigned" do it "does not attempt to update a supervisor-volunteer table" do expect { volunteer.deactivate.reload }.not_to change(volunteer, :supervisor_volunteer) end end end describe "#display_name" do it "allows user to input dangerous values" do volunteer = build(:volunteer) UserInputHelpers::DANGEROUS_STRINGS.each do |dangerous_string| volunteer.update_attribute(:display_name, dangerous_string) volunteer.reload expect(volunteer.display_name).to eq dangerous_string end end end describe "#has_supervisor?" do context "when no supervisor_volunteer record" do let(:volunteer) { build_stubbed(:volunteer) } it "returns false" do expect(volunteer.has_supervisor?).to be false end end context "when active supervisor_volunteer record" do let(:sv) { create(:supervisor_volunteer, is_active: true) } let(:volunteer) { sv.volunteer } it "returns true" do expect(volunteer.has_supervisor?).to be true end end context "when inactive supervisor_volunteer record" do let(:sv) { build_stubbed(:supervisor_volunteer, is_active: false) } let(:volunteer) { sv.volunteer } it "returns false" do expect(volunteer.has_supervisor?).to be false end end end describe "#made_contact_with_all_cases_in_days?" do let(:volunteer) { build(:volunteer) } let(:casa_case) { build(:casa_case, casa_org: volunteer.casa_org) } context "when a volunteer is assigned to an active case" do let(:create_case_contact) do lambda { |occurred_at, contact_made| create(:case_contact, casa_case: casa_case, creator: volunteer, occurred_at: occurred_at, contact_made: contact_made) } end before do create(:case_assignment, casa_case: casa_case, volunteer: volunteer) end context "when volunteer has made recent contact" do it "returns true" do create_case_contact.call(Date.current, true) expect(volunteer.made_contact_with_all_cases_in_days?).to eq(true) end end context "when volunteer has made recent contact attempt but no contact made" do it "returns true" do create_case_contact.call(Date.current, false) expect(volunteer.made_contact_with_all_cases_in_days?).to eq(false) end end context "when volunteer has not made recent contact" do it "returns false" do create_case_contact.call(Date.current - 60.days, true) expect(volunteer.made_contact_with_all_cases_in_days?).to eq(false) end end context "when volunteer has not made recent contact in just one case" do it "returns false" do casa_case2 = build(:casa_case, casa_org: volunteer.casa_org) create(:case_assignment, casa_case: casa_case2, volunteer: volunteer) create(:case_contact, casa_case: casa_case2, creator: volunteer, occurred_at: Date.current - 60.days, contact_made: true) create_case_contact.call(Date.current, true) expect(volunteer.made_contact_with_all_cases_in_days?).to eq(false) end end end context "when volunteer has no case assignments" do it "returns true" do expect(volunteer.made_contact_with_all_cases_in_days?).to eq(true) end end context "when a volunteer has only an inactive case where contact was not made recently" do it "returns true" do inactive_case = create(:casa_case, casa_org: volunteer.casa_org, active: false) create(:case_assignment, casa_case: inactive_case, volunteer: volunteer) create(:case_contact, casa_case: inactive_case, creator: volunteer, occurred_at: Date.current - 60.days, contact_made: true) expect(volunteer.made_contact_with_all_cases_in_days?).to eq(true) end end context "when a volunteer has only an unassigned case where contact was not made recently" do it "returns true" do create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false) create(:case_contact, casa_case: casa_case, creator: volunteer, occurred_at: Date.current - 60.days, contact_made: true) expect(volunteer.made_contact_with_all_cases_in_days?).to eq(true) end end end describe "#supervised_by?" do it "is supervised by the currently active supervisor" do supervisor = create :supervisor volunteer = create :volunteer, supervisor: supervisor expect(volunteer).to be_supervised_by(supervisor) end it "is not supervised by supervisors that have never supervised the volunteer before" do supervisor = create :supervisor volunteer = create :volunteer expect(volunteer).not_to be_supervised_by(supervisor) end it "is not supervised by supervisor that had the volunteer unassinged" do old_supervisor = build :supervisor new_supervisor = build :supervisor volunteer = build :volunteer, supervisor: old_supervisor volunteer.update supervisor: new_supervisor expect(volunteer).not_to be_supervised_by(old_supervisor) expect(volunteer).to be_supervised_by(new_supervisor) end end describe "#role" do subject(:volunteer) { build :volunteer } it { expect(volunteer.role).to eq "Volunteer" } end describe "#with_no_supervisor" do subject { Volunteer.with_no_supervisor(casa_org) } let(:casa_org) { build(:casa_org) } context "no volunteers" do it "returns none" do expect(subject).to eq [] end end context "volunteers" do let!(:unassigned1) { create(:volunteer, display_name: "aaa", casa_org: casa_org) } let!(:unassigned2) { create(:volunteer, display_name: "bbb", casa_org: casa_org) } let!(:unassigned_inactive) { create(:volunteer, display_name: "unassigned inactive", casa_org: casa_org, active: false) } let!(:different_org) { create(:casa_org) } let!(:unassigned2_different_org) { create(:volunteer, display_name: "ccc", casa_org: different_org) } let!(:assigned1) { create(:volunteer, display_name: "ddd", casa_org: casa_org) } let!(:supervisor) { create(:supervisor, display_name: "supe", casa_org: casa_org) } let!(:assignment1) { create(:supervisor_volunteer, volunteer: assigned1, supervisor: supervisor) } let!(:assigned2_different_org) { assignment1.volunteer } let!(:unassigned_inactive_volunteer) { create(:volunteer, display_name: "eee", casa_org: casa_org, active: false) } let!(:previously_assigned) { create(:volunteer, display_name: "fff", casa_org: casa_org) } let!(:inactive_assignment) { create(:supervisor_volunteer, volunteer: previously_assigned, is_active: false, supervisor: supervisor) } it "returns unassigned volunteers" do expect(subject.map(&:display_name).sort).to eq ["aaa", "bbb", "fff"] end end end describe ".with_supervisor" do subject { Volunteer.with_supervisor } context "no volunteers" do it { is_expected.to be_empty } end context "volunteers" do let!(:unassigned1) { create(:volunteer, display_name: "aaa") } let!(:unassigned2) { create(:volunteer, display_name: "bbb") } let!(:supervisor1) { create(:supervisor, display_name: "supe1") } let!(:assigned1) { create(:volunteer, display_name: "ccc") } let!(:assignment1) { create(:supervisor_volunteer, volunteer: assigned1, supervisor: supervisor1) } let!(:supervisor2) { create(:supervisor, display_name: "supe2") } let!(:assigned2) { create(:volunteer, display_name: "ddd") } let!(:assignment2) { create(:supervisor_volunteer, volunteer: assigned2, supervisor: supervisor2) } let!(:assigned3) { create(:volunteer, display_name: "eee") } let!(:assignment3) { create(:supervisor_volunteer, volunteer: assigned3, supervisor: supervisor2) } it { is_expected.to contain_exactly(assigned1, assigned2, assigned3) } end end describe ".birthday_next_month" do subject { Volunteer.birthday_next_month } before do travel_to Date.new(2022, 10, 1) end context "there are volunteers whose birthdays are not next month" do let!(:volunteer1) { create(:volunteer, date_of_birth: Date.new(1990, 9, 1)) } let!(:volunteer2) { create(:volunteer, date_of_birth: Date.new(1998, 10, 15)) } let!(:volunteer3) { create(:volunteer, date_of_birth: Date.new(1920, 12, 1)) } it { is_expected.to be_empty } end context "there are volunteers whose birthdays are next month" do let!(:volunteer1) { create(:volunteer, date_of_birth: Date.new(2001, 11, 1)) } let!(:volunteer2) { create(:volunteer, date_of_birth: Date.new(1920, 11, 15)) } let!(:volunteer3) { create(:volunteer, date_of_birth: Date.new(1989, 11, 30)) } let!(:volunteer4) { create(:volunteer, date_of_birth: Date.new(2001, 6, 1)) } let!(:volunteer5) { create(:volunteer, date_of_birth: Date.new(1920, 1, 15)) } let!(:volunteer6) { create(:volunteer, date_of_birth: Date.new(1967, 2, 21)) } it { is_expected.to contain_exactly(volunteer1, volunteer2, volunteer3) } end end describe "#with_assigned_cases" do subject { Volunteer.with_assigned_cases } let!(:volunteers) { create_list(:volunteer, 3) } let!(:volunteer_with_cases) { create_list(:volunteer, 3, :with_casa_cases) } it "returns only volunteers assigned to active casa cases" do expect(subject).to match_array(volunteer_with_cases) end end describe "#with_no_assigned_cases" do subject { Volunteer.with_no_assigned_cases } let!(:volunteers) { create_list(:volunteer, 3) } let!(:volunteer_with_cases) { create_list(:volunteer, 3, :with_casa_cases) } it "returns only volunteers with no assigned active casa cases" do expect(subject).to match_array(volunteers) end end describe "#casa_cases" do let(:volunteer) { create :volunteer } let!(:ca1) { create :case_assignment, volunteer: volunteer, active: true } let!(:ca2) { create :case_assignment, volunteer: volunteer, active: false } let!(:ca3) { create :case_assignment, volunteer: create(:volunteer), active: true } let!(:ca4) { create :case_assignment, casa_case: create(:casa_case, active: false), active: true } let!(:ca5) { create :case_assignment, casa_case: create(:casa_case, active: false), active: false } it "returns only active and actively assigned casa cases" do expect(volunteer.casa_cases.count).to eq(1) expect(volunteer.casa_cases).to eq([ca1.casa_case]) end end describe "invitation expiration" do let(:volunteer) { create :volunteer } let!(:mail) { volunteer.invite! } let(:expiration_date) { I18n.l(volunteer.invitation_due_at, format: :full, default: nil) } let(:one_year) { I18n.l(1.year.from_now, format: :full, default: nil) } it { expect(expiration_date).to eq one_year } it "expires invitation token after one year" do travel_to 1.year.from_now user = User.accept_invitation!(invitation_token: volunteer.invitation_token) expect(user.errors.full_messages).to include("Invitation token is invalid") travel_back end end describe "invitation" do it "delivers an email invite" do volunteer = build(:volunteer, email: "new_volunteer@example.com") volunteer.invite! email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil expect(email.to).to eq ["new_volunteer@example.com"] expect(email.subject).to eq("CASA Console invitation instructions") expect(email.html_part.body.encoded).to match(/your new Volunteer account/i) expect(volunteer.reload.invitation_created_at).to be_present end end describe "#learning_hours_spent_in_one_year" do let(:volunteer) { create :volunteer } let(:learning_hour_type) { create :learning_hour_type } let!(:learning_hours) do [ create(:learning_hour, user: volunteer, duration_hours: 1, duration_minutes: 30, learning_hour_type: learning_hour_type), create(:learning_hour, user: volunteer, duration_hours: 3, duration_minutes: 45, learning_hour_type: learning_hour_type), create(:learning_hour, user: volunteer, duration_hours: 1, duration_minutes: 30, occurred_at: 2.year.ago, learning_hour_type: learning_hour_type) ] end it "returns the hours spent in one year" do expect(volunteer.learning_hours_spent_in_one_year).to eq("5h 15min") end end end ================================================ FILE: spec/notifications/base_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe BaseNotifier do # TODO: Add tests for BaseNotifier pending "add some tests for BaseNotifier" end ================================================ FILE: spec/notifications/delivery_methods/sms_spec.rb ================================================ require "rails_helper" RSpec.describe DeliveryMethods::Sms do # TODO: Add tests for DeliveryMethods::Sms pending "add some tests for DeliveryMethods::Sms" end ================================================ FILE: spec/notifications/emancipation_checklist_reminder_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe EmancipationChecklistReminderNotifier, type: :model do let(:casa_case) { create :casa_case } let(:notification) { EmancipationChecklistReminderNotifier.with(casa_case: casa_case) } describe "message" do it "contains the case number" do case_number = casa_case.case_number expect(notification.message).to include case_number end end describe "url" do it "contains the case id" do case_id = casa_case.id.to_s expect(notification.url).to include case_id end end end ================================================ FILE: spec/notifications/followup_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe FollowupNotifier do # TODO: Add tests for FollowupNotifier pending "add some tests for FollowupNotifier" end ================================================ FILE: spec/notifications/followup_resolved_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe FollowupResolvedNotifier do # TODO: Add tests for FollowupResolvedNotifier pending "add some tests for FollowupResolvedNotifier" end ================================================ FILE: spec/notifications/reimbursement_complete_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe ReimbursementCompleteNotifier, type: :model do describe "message" do let(:case_contact) { create(:case_contact, :wants_reimbursement) } describe "with case org with nil mileage rate" do it "does not include reimbursement amount" do notification = ReimbursementCompleteNotifier.with(case_contact: case_contact) expect(notification.message).not_to include "$" end end describe "with casa org with active mileage rate" do let!(:mileage_rate) { create(:mileage_rate, casa_org: case_contact.casa_case.casa_org, amount: 6.50, effective_date: 3.days.ago) } it "does include reimbursement amount" do notification = ReimbursementCompleteNotifier.with(case_contact: case_contact) expect(notification.message).to include "$2964" end end end end ================================================ FILE: spec/notifications/volunteer_birthday_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerBirthdayNotifier, type: :model do describe "message" do let(:volunteer) do create(:volunteer, display_name: "Biday Sewn", date_of_birth: Date.new(1968, 2, 8)) end let(:volunteer_notification) { VolunteerBirthdayNotifier.with(volunteer: volunteer) } it "contains a short ordinal form of the volunteer's date of birth" do expect(volunteer_notification.message).to include "February 8th" end it "contains the volunteer's name" do expect(volunteer_notification.message).to include "Biday Sewn" end end end ================================================ FILE: spec/notifications/youth_birthday_notifier_spec.rb ================================================ require "rails_helper" RSpec.describe YouthBirthdayNotifier do # TODO: Add tests for YouthBirthdayNotifier pending "add some tests for YouthBirthdayNotifier" end ================================================ FILE: spec/policies/additional_expense_policy_spec.rb ================================================ require "rails_helper" RSpec.describe AdditionalExpensePolicy, :aggregate_failures, type: :policy do subject { described_class } let(:casa_org) { create :casa_org } let(:volunteer) { create :volunteer, :with_single_case, casa_org: } let(:supervisor) { create :supervisor, casa_org: } let(:casa_admin) { create :casa_admin, casa_org: } let(:all_casa_admin) { create :all_casa_admin } let(:casa_case) { volunteer.casa_cases.first } let(:case_contact) { create :case_contact, casa_case:, creator: volunteer } let(:additional_expense) { create :additional_expense, case_contact: } let(:draft_case_contact) { create :case_contact, :started_status, casa_case: nil, creator: volunteer } let(:draft_additional_expense) { create :additional_expense, case_contact: draft_case_contact } let(:new_additional_expense) do build :additional_expense, case_contact: draft_case_contact, other_expense_amount: 0, other_expenses_describe: "" end let(:same_case_volunteer) { create :volunteer, casa_org: } let(:same_case_volunteer_case_assignment) { create :case_assignment, volunteer: same_case_volunteer, casa_case: } let(:same_case_volunteer_case_contact) do same_case_volunteer_case_assignment create :case_contact, casa_case:, creator: same_case_volunteer end let(:same_case_volunteer_additional_expense) do create :additional_expense, case_contact: same_case_volunteer_case_contact end let(:other_org) { create :casa_org } let(:other_org_volunteer) { create :volunteer, casa_org: other_org } let(:other_org_casa_case) { create :casa_case, casa_org: other_org } let(:other_org_case_contact) { create :case_contact, casa_case: other_org_casa_case, creator: other_org_volunteer } let(:other_org_additional_expense) { create :additional_expense, case_contact: other_org_case_contact } permissions :create?, :destroy? do it "does not permit a nil user" do expect(subject).not_to permit(nil, additional_expense) end it "permits volunteers assigned to the expense's case contact" do expect(subject).to permit(volunteer, additional_expense) expect(subject).to permit(volunteer, draft_additional_expense) expect(subject).to permit(volunteer, new_additional_expense) expect(subject).not_to permit(volunteer, same_case_volunteer_additional_expense) expect(subject).not_to permit(volunteer, other_org_additional_expense) end it "permits same org supervisors" do expect(subject).to permit(supervisor, additional_expense) expect(subject).to permit(supervisor, draft_additional_expense) expect(subject).to permit(supervisor, draft_additional_expense) expect(subject).to permit(supervisor, same_case_volunteer_additional_expense) expect(subject).not_to permit(supervisor, other_org_additional_expense) end it "permits same org casa admins" do expect(subject).to permit(casa_admin, additional_expense) expect(subject).to permit(casa_admin, draft_additional_expense) expect(subject).to permit(casa_admin, new_additional_expense) expect(subject).to permit(casa_admin, same_case_volunteer_additional_expense) expect(subject).not_to permit(casa_admin, other_org_additional_expense) end it "does not permit an all casa admin" do expect(subject).not_to permit(all_casa_admin, additional_expense) end end describe "Scope#resolve" do subject { described_class::Scope.new(user, AdditionalExpense.all).resolve } before do additional_expense draft_additional_expense same_case_volunteer_additional_expense other_org_additional_expense end context "when user is a visitor" do let(:user) { nil } it "returns no expenses" do expect(subject).not_to include(additional_expense, other_org_additional_expense) end end context "when user is a volunteer" do let(:user) { volunteer } it "includes expenses for contacts created by volunteer only" do expect(subject).to include(additional_expense, draft_additional_expense) expect(subject).not_to include(same_case_volunteer_additional_expense, other_org_additional_expense) end end context "when user is a supervisor" do let(:user) { supervisor } it "includes same org expenses only" do expect(subject).to include(additional_expense, draft_additional_expense, same_case_volunteer_additional_expense) expect(subject).not_to include(other_org_additional_expense) end end context "when user is a casa_admin" do let(:user) { casa_admin } it "includes same org expenses only" do expect(subject).to include(additional_expense, draft_additional_expense, draft_additional_expense) expect(subject).not_to include(other_org_additional_expense) end end context "when user is an all_casa_admin" do let(:user) { all_casa_admin } it "returns no expenses" do expect(subject).not_to include(additional_expense, other_org_additional_expense) end end end end ================================================ FILE: spec/policies/application_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ApplicationPolicy do subject { described_class } let(:casa_org) { build_stubbed(:casa_org) } let(:casa_admin) { build_stubbed(:casa_admin, casa_org: casa_org) } let(:supervisor) { build_stubbed(:supervisor, casa_org: casa_org) } let(:volunteer) { build_stubbed(:volunteer, casa_org: casa_org) } let(:all_casa_admin) { build_stubbed(:all_casa_admin) } permissions :see_reports_page? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisors" do expect(subject).to permit(supervisor) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer) end end permissions :see_import_page? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not allow supervisors" do expect(subject).not_to permit(supervisor) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer) end end permissions :see_court_reports_page? do it "allows volunteers" do expect(subject).to permit(create(:volunteer)) end it "allows casa_admins" do expect(subject).to permit(create(:casa_admin)) end it "allows supervisors" do expect(subject).to permit(create(:supervisor)) end end permissions :see_emancipation_checklist? do it "allows volunteers" do expect(subject).to permit(create(:volunteer)) end it "does not allow casa_admins" do expect(subject).not_to permit(create(:casa_admin)) end it "does not allow supervisors" do expect(subject).not_to permit(create(:supervisor)) end end permissions :see_mileage_rate? do it "does not allow volunters" do expect(subject).not_to permit(volunteer) end it "does not allow supervisors" do expect(subject).not_to permit(supervisor) end it "allow casa_admins for same org" do expect(subject).to permit(casa_admin) end context "when org reimbursement is disabled" do before do casa_org.show_driving_reimbursement = false end it "does not allow casa_admins" do expect(subject).not_to permit(casa_admin) end end end describe "#same_org?" do let(:org_record) { double } before { allow(org_record).to receive(:casa_org).and_return(casa_org) } context "record with same casa_org" do before { expect(org_record).to receive(:casa_org).and_return(casa_org) } permissions :same_org? do it { is_expected.to permit(volunteer, org_record) } it { is_expected.to permit(supervisor, org_record) } it { is_expected.to permit(casa_admin, org_record) } end end context "record with different casa_org" do let(:other_org_record) { double } before { expect(other_org_record).to receive(:casa_org).and_return(build_stubbed(:casa_org)) } permissions :same_org? do it { is_expected.not_to permit(volunteer, other_org_record) } it { is_expected.not_to permit(supervisor, other_org_record) } it { is_expected.not_to permit(casa_admin, other_org_record) } end end context "all_casa_admin user" do it "raises a no method error for all_casa_admin.casa_org" do expect { subject.new(all_casa_admin, org_record).same_org? }.to raise_error(NoMethodError) end end context "user with no casa_org" do let(:volunteer) { build_stubbed(:volunteer, casa_org: nil) } let(:supervisor) { build_stubbed(:supervisor, casa_org: nil) } let(:casa_admin) { build_stubbed(:casa_admin, casa_org: nil) } permissions :same_org? do it { is_expected.not_to permit(volunteer, org_record) } it { is_expected.not_to permit(supervisor, org_record) } it { is_expected.not_to permit(casa_admin, org_record) } end end context "no user" do let(:user) { nil } permissions :same_org? do it { is_expected.not_to permit(user, org_record) } end end context "called with a class instead of a record" do let(:klass) { CasaCase } [:volunteer, :casa_admin, :supervisor].each do |user_type| it "raises a no method error for #{user_type}" do user = send(user_type) expect { subject.new(user, klass).same_org? }.to raise_error(NoMethodError) end end end end end ================================================ FILE: spec/policies/bulk_court_date_policy_spec.rb ================================================ require "rails_helper" RSpec.describe BulkCourtDatePolicy, type: :policy do subject { described_class } let(:casa_org) { build :casa_org } let(:casa_admin) { build :casa_admin, casa_org: } let(:volunteer) { build :volunteer, casa_org: } let(:supervisor) { build :supervisor, casa_org: } permissions :new?, :create? do it "permits casa_admins" do expect(subject).to permit(casa_admin, :bulk_court_date) end it "permits supervisor" do expect(subject).to permit(supervisor, :bulk_court_date) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer, :bulk_court_date) end end end ================================================ FILE: spec/policies/casa_admin_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CasaAdminPolicy do subject { described_class } let(:organization) { build(:casa_org) } let(:casa_admin) { create(:casa_admin, casa_org: organization) } let(:volunteer) { build(:volunteer, casa_org: organization) } let(:supervisor) { build(:supervisor, casa_org: organization) } permissions :edit? do context "same org" do let(:record) { build_stubbed(:casa_admin, casa_org: casa_admin.casa_org) } it "allows editing admin" do expect(subject).to permit(casa_admin, record) end end context "different org" do let(:record) { build_stubbed(:casa_admin, casa_org: build_stubbed(:casa_org)) } it "does not allow editing admin" do expect(subject).not_to permit(casa_admin, record) end end end permissions :index?, :activate?, :change_to_supervisor?, :create?, :new?, :resend_invitation?, :update? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end end permissions :index?, :activate?, :change_to_supervisor?, :create?, :edit?, :new?, :resend_invitation?, :update? do it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end permissions :deactivate? do context "when user is a casa admin" do let(:admin_inactive) { build_stubbed(:casa_admin, active: false, casa_org: organization) } it "does not permit if is a inactive user" do expect(subject).not_to permit(admin_inactive, :casa_admin) end it "does not permit if is the only admin" do expect(subject).not_to permit(casa_admin, :casa_admin) end it "permits if is a active user and exist other casa admins" do create(:casa_admin, casa_org: organization) expect(subject).to permit(casa_admin, :casa_admin) end end context "when user is a supervisor" do it "does not permit" do expect(subject).not_to permit(supervisor, :casa_admin) end end context "when user is a volunteer" do it "does not permit" do expect(subject).not_to permit(volunteer) end end end end ================================================ FILE: spec/policies/casa_case_policy/scope_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCasePolicy::Scope do let(:organization) { build(:casa_org) } describe "#resolve" do it "returns all CasaCases when user is admin" do user = build(:casa_admin, casa_org: organization) all_casa_cases = create_list(:casa_case, 2, casa_org: organization) new_org = build(:casa_org) create_list(:casa_case, 2, casa_org: new_org) scope = described_class.new(user, organization.casa_cases) expect(scope.resolve).to match_array(all_casa_cases) end it "returns active cases of the volunteer when user is volunteer" do user = create(:volunteer, casa_org: organization) casa_cases = create_list(:casa_case, 2, volunteers: [user], casa_org: organization) more_user = build(:volunteer, casa_org: organization) create_list(:casa_case, 2, volunteers: [more_user], casa_org: organization) other_org = build(:casa_org) other_user = create(:volunteer, casa_org: other_org) create_list(:casa_case, 2, volunteers: [other_user], casa_org: other_org) scope = described_class.new(user, organization.casa_cases) expect(CasaCase.count).to eq 6 expect(scope.resolve.count).to eq 2 expect(scope.resolve).to match_array casa_cases end end end ================================================ FILE: spec/policies/casa_case_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCasePolicy do subject { described_class } let(:organization) { build(:casa_org) } let(:different_organization) { create(:casa_org) } let(:casa_admin) { build(:casa_admin, casa_org: organization) } let(:other_org_casa_admin) { build(:casa_admin, casa_org: different_organization) } let(:casa_case) { build(:casa_case, casa_org: organization) } let(:volunteer) { build(:volunteer, casa_org: organization) } let(:other_org_volunteer) { build(:volunteer, casa_org: different_organization) } let(:supervisor) { build(:supervisor, casa_org: organization) } let(:other_org_supervisor) { build(:supervisor, casa_org: different_organization) } permissions :update_case_number? do context "when user is an admin" do context "from the same organization" do it "does allow update" do expect(subject).to permit(casa_admin, casa_case) end end context "from a different organization" do it "does not allow an update" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end end context "when user is a volunteer" do it "does not allow update case number" do expect(subject).not_to permit(volunteer, casa_case) end end end permissions :update_court_date?, :update_court_report_due_date? do context "when part of the same organization" do context "an admin user" do it "can update" do expect(subject).to permit(casa_admin, casa_case) end end context "a supervisor user" do it "can update" do expect(subject).to permit(supervisor, casa_case) end end context "a volunteer user" do it "can update" do expect(subject).to permit(volunteer, casa_case) end end end context "when not part of the same organization" do context "an admin user" do it "can not update" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end context "a supervisor user" do it "can not update" do expect(subject).not_to permit(other_org_supervisor, casa_case) end end context "a volunteer user" do it "can not update" do expect(subject).not_to permit(other_org_volunteer, casa_case) end end end end permissions :update_hearing_type?, :update_court_orders?, :update_judge? do context "when part of the same organization" do context "an admin user" do it "is allowed to update" do expect(subject).to permit(casa_admin, casa_case) end end context "a supervisor user" do it "is allowed to update" do expect(subject).to permit(supervisor, casa_case) end end context "a volunteer user" do it "is allowed to update" do expect(subject).to permit(volunteer, casa_case) end end end context "when not part of the same organization" do context "an admin user" do it "is not allowed to update" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end context "a supervisor user" do it "is not allowed to update" do expect(subject).not_to permit(other_org_supervisor, casa_case) end end context "a volunteer user" do it "is not allowed to update" do expect(subject).not_to permit(other_org_volunteer, casa_case) end end end end permissions :update_contact_types? do context "when part of the same organization" do context "an admin user" do it "can update" do expect(subject).to permit(casa_admin, casa_case) end end context "a supervisor" do it "can update" do expect(subject).to permit(supervisor, casa_case) end end end context "when not part of the same organization" do context "an admin user" do it "can not update" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end context "a supervisor" do it "can not update" do expect(subject).not_to permit(other_org_supervisor, casa_case) end end end context "a volunteer" do it "does not allow update" do expect(subject).not_to permit(volunteer, casa_case) expect(subject).not_to permit(other_org_volunteer, casa_case) end end end permissions :assign_volunteers? do context "when part of the same organization" do context "an admin user" do it "can do volunteer assignment" do expect(subject).to permit(casa_admin, casa_case) end end end context "when not part of the same organization" do context "an admin user" do it "can not do volunteer assignment" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end end # TODO: What is the supervisor permission? context "when user is a volunteer" do it "does not allow volunteer assignment" do expect(subject).not_to permit(volunteer, casa_case) expect(subject).not_to permit(other_org_volunteer, casa_case) end end end permissions "update_emancipation_option?" do context "when an admin belongs to the same org as the case" do it "allows casa_admins" do expect(subject).to permit(casa_admin, casa_case) end end context "when an admin belongs to a different org as the case" do it "does not allow admin to update" do casa_case = build_stubbed(:casa_case, casa_org: different_organization) expect(subject).to permit(other_org_casa_admin, casa_case) end end context "when a supervisor belongs to the same org as the case" do it "allows the supervisor" do supervisor = build(:supervisor, casa_org: organization) casa_case = build_stubbed(:casa_case, casa_org: organization) expect(subject).to permit(supervisor, casa_case) end end context "when a supervisor does not belong to the same org as the case" do it "does not allow the supervisor" do supervisor = build(:supervisor, casa_org: organization) casa_case = build_stubbed(:casa_case, casa_org: different_organization) expect(subject).not_to permit(supervisor, casa_case) end end context "when volunteer is assigned" do it "allows the volunteer" do volunteer = create(:volunteer, casa_org: organization) casa_case = build(:casa_case, casa_org: organization) volunteer.casa_cases << casa_case expect(subject).to permit(volunteer, casa_case) end end context "when volunteer is from another organization" do it "does not allow the volunteer" do volunteer = create(:volunteer, casa_org: different_organization) casa_case = build(:casa_case, casa_org: organization) expect { volunteer.casa_cases << casa_case } .to raise_error( ActiveRecord::RecordInvalid, /must belong to the same organization/ ) end end context "when volunteer is not assigned" do it "does not allow the volunteer" do expect(subject).not_to permit(volunteer, casa_case) expect(subject).not_to permit(other_org_volunteer, casa_case) end end end permissions :show? do context "when part of the same organization" do context "an admin user" do it "allows casa_admins" do expect(subject).to permit(casa_admin, casa_case) end end end context "when not part of the same organization" do context "and admin user" do it "does not allow admin to view" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end end context "when a supervisor belongs to the same org as the case" do it "allows the supervisor" do supervisor = create(:supervisor, casa_org: organization) casa_case = build_stubbed(:casa_case, casa_org: organization) expect(subject).to permit(supervisor, casa_case) end end context "when a supervisor does not belong to the same org as the case" do it "does not allow the supervisor" do supervisor = build_stubbed(:supervisor, casa_org: organization) casa_case = create(:casa_case, casa_org: different_organization) expect(subject).not_to permit(supervisor, casa_case) end end context "when volunteer is assigned" do it "allows the volunteer" do volunteer = create(:volunteer, casa_org: organization) casa_case = create(:casa_case, casa_org: organization) volunteer.casa_cases << casa_case expect(subject).to permit(volunteer, casa_case) end end context "when volunteer is not assigned" do it "does not allow the volunteer" do expect(subject).not_to permit(volunteer, casa_case) end end context "when volunteer is from another organization" do it "does not allow the volunteer" do volunteer = create(:volunteer, casa_org: different_organization) casa_case = build(:casa_case, casa_org: organization) expect { volunteer.casa_cases << casa_case } .to raise_error( ActiveRecord::RecordInvalid, /must belong to the same organization/ ) end end end permissions :edit? do context "when part of the same organization" do it "allows casa_admins" do expect(subject).to permit(casa_admin, casa_case) end end context "when not part of the same organization" do it "does not allow admin to edit" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end context "when a supervisor belongs to the same org as the case" do it "allows the supervisor" do supervisor = create(:supervisor, casa_org: organization) casa_case = build(:casa_case, casa_org: organization) expect(subject).to permit(supervisor, casa_case) end end context "when a supervisor does not belong to the same org as the case" do it "does not allow the supervisor" do supervisor = build_stubbed(:supervisor, casa_org: organization) casa_case = build(:casa_case, casa_org: different_organization) expect(subject).not_to permit(supervisor, casa_case) end end context "when volunteer is assigned" do it "allows the volunteer" do volunteer = create(:volunteer, casa_org: organization) casa_case = build(:casa_case, casa_org: organization) volunteer.casa_cases << casa_case expect(subject).to permit(volunteer, casa_case) end end context "when volunteer is not assigned" do it "does not allow the volunteer" do expect(subject).not_to permit(volunteer, casa_case) end end context "when volunteer is from another organization" do it "does not allow the volunteer" do volunteer = create(:volunteer, casa_org: different_organization) casa_case = build(:casa_case, casa_org: organization) expect { volunteer.casa_cases << casa_case } .to raise_error( ActiveRecord::RecordInvalid, /must belong to the same organization/ ) end end end permissions :update? do context "when part of the same organization" do it "allows casa_admins" do expect(subject).to permit(casa_admin, casa_case) end end context "when not part of the same organization" do it "does not allow admin to update" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end context "when a supervisor belongs to the same org as the case" do it "allows the supervisor" do supervisor = create(:supervisor, casa_org: organization) casa_case = create(:casa_case, casa_org: organization) expect(subject).to permit(supervisor, casa_case) end end context "when a supervisor does not belong to the same org as the case" do it "does not allow the supervisor" do supervisor = create(:supervisor, casa_org: organization) casa_case = build_stubbed(:casa_case, casa_org: different_organization) expect(subject).not_to permit(supervisor, casa_case) end end context "when volunteer is assigned" do it "allows the volunteer" do volunteer = create(:volunteer, casa_org: organization) casa_case = build(:casa_case, casa_org: organization) volunteer.casa_cases << casa_case expect(subject).to permit(volunteer, casa_case) end end it "does not allow volunteers who are unassigned" do expect(subject).not_to permit(volunteer, casa_case) end context "when volunteer is from another organization" do it "does not allow the volunteer" do volunteer = create(:volunteer, casa_org: different_organization) casa_case = build(:casa_case, casa_org: organization) expect { volunteer.casa_cases << casa_case } .to raise_error( ActiveRecord::RecordInvalid, /must belong to the same organization/ ) end end end permissions :new?, :create?, :destroy? do context "when part of the same organizaton" do it "allows casa_admins" do expect(subject).to permit(casa_admin, casa_case) end end context "when not part of the same organization" do it "does not allow admin to create" do expect(subject).not_to permit(other_org_casa_admin, casa_case) end end it "does not allow superivsors" do expect(subject).not_to permit(supervisor, casa_case) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer, casa_case) expect(subject).not_to permit(other_org_volunteer, casa_case) end end permissions :index?, :save_emancipation? do # Should :save_emancipation belong with :index? # Because we are authorizing without an instance, should we only check a user's # role? it "allows casa_admins" do expect(subject).to permit(casa_admin, CasaCase) expect(subject).to permit(other_org_casa_admin, CasaCase) end it "allows supervisor" do expect(subject).to permit(supervisor, CasaCase) expect(subject).to permit(other_org_supervisor, CasaCase) end it "allows volunteer" do expect(subject).to permit(volunteer, CasaCase) expect(subject).to permit(other_org_volunteer, CasaCase) end end end ================================================ FILE: spec/policies/casa_org_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CasaOrgPolicy do subject { described_class } let(:organization) { build(:casa_org, users: [volunteer, supervisor, casa_admin]) } let(:different_organization) { create(:casa_org) } let(:volunteer) { build(:volunteer) } let(:supervisor) { build(:supervisor) } let(:casa_admin) { build(:casa_admin) } permissions :edit?, :update? do context "when admin belongs to the same org" do it "allows casa_admins" do expect(subject).to permit(casa_admin, organization) end end context "when admin does not belong to org" do it "does not permit admin" do expect(subject).not_to permit(casa_admin, different_organization) end end it "does not permit supervisor" do expect(subject).not_to permit(supervisor, organization) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer, organization) end end end ================================================ FILE: spec/policies/case_assignment_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CaseAssignmentPolicy do subject { described_class } let(:organization) { build(:casa_org) } let(:casa_admin) { build(:casa_admin, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:volunteer) { build(:volunteer, casa_org: organization) } let(:case_assignment) { build(:case_assignment, casa_case: casa_case, volunteer: volunteer) } let(:case_assignment_inactive) { build(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false) } let(:supervisor) { create(:supervisor, casa_org: organization) } let(:other_organization) { create(:casa_org) } let(:other_casa_case) do create(:casa_case, casa_org: other_organization) end let(:other_case_assignment) do create(:case_assignment, casa_case: other_casa_case) end permissions :create? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisor" do expect(subject).to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end permissions :unassign? do it "does not allow unassign if case_assignment is inactive" do expect(subject).not_to permit(casa_admin, case_assignment_inactive) end context "when user is an admin" do it "allow update when case_assignment is active" do expect(subject).to permit(casa_admin, case_assignment) end end context "when user is a supervisor" do it "allow update when case_assignment is active" do expect(subject).to permit(supervisor, case_assignment) end end context "when user is a volunteer" do it "does not allow unassign" do expect(subject).not_to permit(volunteer, case_assignment) end end context "when is a different organization" do it "does not allow unassign" do expect(subject).not_to permit(supervisor, other_case_assignment) end end end permissions :show_or_hide_contacts? do context "when an admin" do context "in the same organization" do it "allows user to show or hide contacts" do expect(subject).to permit(casa_admin, case_assignment_inactive) end end context "in a different organization" do it "does not allow user to show or hide contacts" do other_case_assignment.update(active: false) expect(subject).not_to permit(casa_admin, other_case_assignment) end end end context "when a supervisor" do context "in the same organization" do it "allows user to show or hide contacts" do expect(subject).to permit(supervisor, case_assignment_inactive) end end context "in a different organization" do it "does not allow user to show or hide contacts" do other_case_assignment.update(active: false) expect(subject).not_to permit(supervisor, other_case_assignment) end end end context "when a volunteer" do it "does not allow user to show or hide contacts" do expect(subject).not_to permit(volunteer, case_assignment_inactive) end end context "when the case_assignment is active" do describe "it does not allow any user to show/hide contacts" do it { is_expected.not_to permit(casa_admin, case_assignment) } it { is_expected.not_to permit(supervisor, case_assignment) } it { is_expected.not_to permit(volunteer, case_assignment) } end end end permissions :destroy? do context "when user is an admin" do it "allow destroy" do expect(subject).to permit(casa_admin, case_assignment) end end context "when user is a supervisor" do it "allow destroy" do expect(subject).to permit(supervisor, case_assignment) end end context "when is a different organization" do it "does not allow admins to destroy" do expect(subject).not_to permit(casa_admin, other_case_assignment) end it "does not allow supervisor to destroy" do expect(subject).not_to permit(supervisor, other_case_assignment) end end end end ================================================ FILE: spec/policies/case_contact_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactPolicy, :aggregate_failures do subject { described_class } let(:casa_org) { create(:casa_org) } let(:casa_admin) { build(:casa_admin, casa_org:) } let(:supervisor) { build(:supervisor, casa_org:) } let(:volunteer) { create(:volunteer, :with_single_case, supervisor:, casa_org:) } let(:casa_case) { volunteer.casa_cases.first } let(:case_contact) { create(:case_contact, casa_case:, creator: volunteer) } let(:draft_case_contact) { create(:case_contact, :started_status, casa_case: nil, creator: volunteer) } # another volunteer assigned to the same case let(:same_case_volunteer) { create :volunteer, casa_org: } let(:same_case_volunteer_case_assignment) { create :case_assignment, volunteer: same_case_volunteer, casa_case: } let(:same_case_volunteer_case_contact) do same_case_volunteer_case_assignment create :case_contact, casa_case:, creator: same_case_volunteer end # same org case that volunteer is not assigned to let(:unassigned_case_case_contact) do create :case_contact, casa_case: create(:casa_case, casa_org:), creator: create(:volunteer, casa_org:) end let(:other_org_case_contact) { build(:case_contact, casa_org: create(:casa_org)) } permissions :index? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisor" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end end permissions :show? do it "allows same org casa_admins" do expect(subject).to permit(casa_admin, case_contact) expect(subject).to permit(casa_admin, draft_case_contact) expect(subject).to permit(casa_admin, same_case_volunteer_case_contact) expect(subject).to permit(casa_admin, unassigned_case_case_contact) expect(subject).not_to permit(casa_admin, other_org_case_contact) end it "allows same org supervisors" do expect(subject).to permit(supervisor, case_contact) expect(subject).to permit(supervisor, draft_case_contact) expect(subject).to permit(supervisor, same_case_volunteer_case_contact) expect(subject).to permit(supervisor, unassigned_case_case_contact) expect(subject).not_to permit(supervisor, other_org_case_contact) end it "allows volunteer only if they created the case contact" do expect(subject).to permit(volunteer, case_contact) expect(subject).to permit(volunteer, draft_case_contact) expect(subject).not_to permit(volunteer, unassigned_case_case_contact) expect(subject).not_to permit(volunteer, other_org_case_contact) end end permissions :edit?, :update? do it "allows same org casa_admins" do expect(subject).to permit(casa_admin, case_contact) expect(subject).to permit(casa_admin, draft_case_contact) expect(subject).to permit(casa_admin, same_case_volunteer_case_contact) expect(subject).to permit(casa_admin, unassigned_case_case_contact) expect(subject).not_to permit(casa_admin, other_org_case_contact) end it "allows same org supervisors" do expect(subject).to permit(supervisor, case_contact) expect(subject).to permit(supervisor, draft_case_contact) expect(subject).to permit(supervisor, same_case_volunteer_case_contact) expect(subject).not_to permit(supervisor, other_org_case_contact) end it "allows volunteer only if they created the case contact" do expect(subject).to permit(volunteer, case_contact) expect(subject).to permit(volunteer, draft_case_contact) expect(subject).not_to permit(volunteer, same_case_volunteer_case_contact) expect(subject).not_to permit(volunteer, unassigned_case_case_contact) expect(subject).not_to permit(volunteer, other_org_case_contact) end end permissions :new? do it "allows same org casa_admins" do expect(subject).to permit(volunteer, case_contact.dup) expect(subject).to permit(volunteer, draft_case_contact.dup) expect(subject).not_to permit(casa_admin, CaseContact.new) end it "allows same org supervisors" do expect(subject).to permit(volunteer, case_contact.dup) expect(subject).to permit(volunteer, draft_case_contact.dup) expect(subject).not_to permit(supervisor, CaseContact.new) end it "allows volunteers who are the contact creator" do expect(subject).to permit(volunteer, case_contact.dup) expect(subject).to permit(volunteer, draft_case_contact.dup) expect(subject).not_to permit(volunteer, CaseContact.new) end end permissions :datatable? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisors" do expect(subject).to permit(supervisor) end it "allows volunteers" do expect(subject).to permit(volunteer) end end permissions :drafts? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisors" do expect(subject).to permit(supervisor) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer) end end permissions :destroy? do it "allows same org casa_admins" do expect(subject).to permit(casa_admin, case_contact) expect(subject).to permit(casa_admin, draft_case_contact) expect(subject).to permit(casa_admin, same_case_volunteer_case_contact) expect(subject).to permit(casa_admin, unassigned_case_case_contact) expect(subject).not_to permit(casa_admin, other_org_case_contact) end it "allows supervisors" do expect(subject).to permit(supervisor, case_contact) expect(subject).to permit(supervisor, draft_case_contact) expect(subject).to permit(supervisor, same_case_volunteer_case_contact) expect(subject).not_to permit(supervisor, other_org_case_contact) end it "allows volunteer only for draft contacts they created" do expect(subject).to permit(volunteer, draft_case_contact) expect(subject).not_to permit(volunteer, case_contact) expect(subject).not_to permit(volunteer, same_case_volunteer_case_contact) expect(subject).not_to permit(volunteer, unassigned_case_case_contact) expect(subject).not_to permit(volunteer, other_org_case_contact) end end permissions :restore? do it "allows same org casa_admins" do expect(subject).to permit(casa_admin, case_contact) expect(subject).to permit(casa_admin, draft_case_contact) expect(subject).to permit(casa_admin, same_case_volunteer_case_contact) expect(subject).to permit(casa_admin, unassigned_case_case_contact) expect(subject).not_to permit(casa_admin, other_org_case_contact) end it "does not allow supervisors" do expect(subject).not_to permit(supervisor, case_contact) expect(subject).not_to permit(supervisor, draft_case_contact) expect(subject).not_to permit(supervisor, same_case_volunteer_case_contact) expect(subject).not_to permit(supervisor, unassigned_case_case_contact) expect(subject).not_to permit(supervisor, other_org_case_contact) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer, draft_case_contact) expect(subject).not_to permit(volunteer, case_contact) expect(subject).not_to permit(volunteer, same_case_volunteer_case_contact) expect(subject).not_to permit(volunteer, unassigned_case_case_contact) expect(subject).not_to permit(volunteer, other_org_case_contact) end end describe "Scope#resolve" do subject { described_class::Scope.new(user, CaseContact.all).resolve } before do case_contact draft_case_contact same_case_volunteer_case_contact unassigned_case_case_contact other_org_case_contact end context "when user is a visitor" do let(:user) { nil } it "returns no case contacts" do expect(subject).not_to include(case_contact, other_org_case_contact) end end context "when user is a volunteer" do let(:user) { volunteer } it "returns case contacts created by the volunteer" do expect(subject).to include(case_contact, draft_case_contact) expect(subject) .not_to include(same_case_volunteer_case_contact, unassigned_case_case_contact, other_org_case_contact) end end context "when user is a supervisor" do let(:user) { supervisor } it "returns same org case contacts" do expect(subject) .to include(case_contact, draft_case_contact, same_case_volunteer_case_contact, unassigned_case_case_contact) expect(subject).not_to include(other_org_case_contact) end end context "when user is a casa_admin" do let(:user) { casa_admin } it "returns same org case contacts" do expect(subject) .to include(case_contact, draft_case_contact, same_case_volunteer_case_contact, unassigned_case_case_contact) expect(subject).not_to include(other_org_case_contact) end end context "when user is an all_casa_admin" do let(:user) { create :all_casa_admin } it "returns no case contacts" do expect(subject).not_to include(case_contact, other_org_case_contact) end end end end ================================================ FILE: spec/policies/case_court_order_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CaseCourtOrderPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:supervisor) { build_stubbed(:supervisor) } let(:volunteer) { build_stubbed(:volunteer) } permissions :destroy? do it { is_expected.to permit(casa_admin) } it { is_expected.to permit(supervisor) } it { is_expected.to permit(volunteer) } end end ================================================ FILE: spec/policies/case_court_report_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CaseCourtReportPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :index?, :generate?, :show? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisor" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end end end ================================================ FILE: spec/policies/case_group_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CaseGroupPolicy, type: :policy do subject { described_class } let(:casa_org) { create :casa_org } let(:volunteer) { create :volunteer, :with_casa_cases, casa_org: } let(:supervisor) { create :supervisor, casa_org: } let(:casa_admin) { create :casa_admin, casa_org: } let(:all_casa_admin) { create :all_casa_admin } let(:case_group) { create :case_group, casa_org: } let(:volunteer_case_group) { create :case_group, casa_org:, casa_cases: volunteer.casa_cases } permissions :new?, :create?, :destroy?, :edit?, :show?, :update? do it "does not permit a nil user" do expect(described_class).not_to permit(nil, case_group) end it "does not permit a volunteer" do expect(described_class).not_to permit(volunteer, case_group) end it "does not permit a volunteer assigned to the case group" do expect(described_class).not_to permit(volunteer, volunteer_case_group) end it "permits a supervisor" do expect(described_class).to permit(supervisor, case_group) end it "does not permit a supervisor for a different casa org" do other_org_supervisor = create :supervisor, casa_org: create(:casa_org) expect(described_class).not_to permit(other_org_supervisor, case_group) end it "permits a casa admin" do expect(described_class).to permit(casa_admin, case_group) end it "does not permit a casa admin for a different casa org" do other_org_casa_admin = create :casa_admin, casa_org: create(:casa_org) expect(described_class).not_to permit(other_org_casa_admin, case_group) end it "does not permit an all casa admin" do expect(described_class).not_to permit(all_casa_admin, case_group) end end permissions :index? do it "does not permit a nil user" do expect(described_class).not_to permit(nil, :case_group) end it "does not permit a volunteer" do expect(described_class).not_to permit(volunteer, :case_group) end it "permits a supervisor" do expect(described_class).to permit(supervisor, :case_group) end it "permits a casa admin" do expect(described_class).to permit(casa_admin, :case_group) end it "does not permit an all casa admin" do expect(described_class).not_to permit(all_casa_admin, :case_group) end end describe "Scope#resolve" do subject { described_class::Scope.new(user, CaseGroup.all).resolve } let!(:casa_org_case_group) { create :case_group, casa_org: } let!(:other_casa_org_case_group) { create :case_group, casa_org: create(:casa_org) } context "when user is a visitor" do let(:user) { nil } it { is_expected.not_to include(casa_org_case_group) } it { is_expected.not_to include(other_casa_org_case_group) } end context "when user is a volunteer" do let(:user) { volunteer } let!(:user_case_group) { volunteer_case_group } it { is_expected.not_to include(user_case_group) } it { is_expected.not_to include(casa_org_case_group) } it { is_expected.not_to include(other_casa_org_case_group) } end context "when user is a supervisor" do let(:user) { supervisor } it { is_expected.to include(casa_org_case_group) } it { is_expected.not_to include(other_casa_org_case_group) } end context "when user is a casa_admin" do let(:user) { casa_admin } it { is_expected.to include(casa_org_case_group) } it { is_expected.not_to include(other_casa_org_case_group) } end context "when user is an all_casa_admin" do let(:user) { all_casa_admin } it { is_expected.not_to include(casa_org_case_group) } it { is_expected.not_to include(other_casa_org_case_group) } end end end ================================================ FILE: spec/policies/checklist_item_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ChecklistItemPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :new?, :create?, :destroy?, :edit?, :update? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/contact_topic_answer_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTopicAnswerPolicy, :aggregate_failures, type: :policy do subject { described_class } let(:casa_org) { create :casa_org } let(:volunteer) { create :volunteer, :with_single_case, casa_org: } let(:supervisor) { create :supervisor, casa_org: } let(:casa_admin) { create :casa_admin, casa_org: } let(:all_casa_admin) { create :all_casa_admin } let(:contact_topic) { create :contact_topic, casa_org: } let(:casa_case) { volunteer.casa_cases.first } let(:case_contact) { create :case_contact, creator: volunteer } let(:contact_topic_answer) { create :contact_topic_answer, contact_topic:, case_contact: } let(:draft_case_contact) { create :case_contact, :started_status, casa_case: nil, creator: volunteer } let(:draft_contact_topic_answer) { create :contact_topic_answer, contact_topic:, case_contact: draft_case_contact } let(:same_case_volunteer) { create :volunteer, casa_org: } let(:same_case_volunteer_case_assignment) { create :case_assignment, volunteer: same_case_volunteer, casa_case: } let(:same_case_volunteer_case_contact) do same_case_volunteer_case_assignment create :case_contact, casa_case:, creator: same_case_volunteer end let(:same_case_volunteer_contact_topic_answer) do create :contact_topic_answer, contact_topic:, case_contact: same_case_volunteer_case_contact end let(:other_org) { create :casa_org } let(:other_org_volunteer) { create :volunteer, casa_org: other_org } let(:other_org_contact_topic) { create :contact_topic, casa_org: other_org } let(:other_org_casa_case) { create :casa_case, casa_org: other_org } let(:other_org_case_contact) { create :case_contact, casa_case: other_org_casa_case, creator: other_org_volunteer } let(:other_org_contact_topic_answer) do create :contact_topic_answer, case_contact: other_org_case_contact, contact_topic: other_org_contact_topic end permissions :create?, :destroy? do it "does not permit a nil user" do expect(subject).not_to permit(nil, contact_topic_answer) end it "permits a volunteer who created the case contact" do expect(subject).to permit(volunteer, contact_topic_answer) expect(subject).to permit(volunteer, draft_contact_topic_answer) expect(subject).not_to permit(volunteer, same_case_volunteer_contact_topic_answer) expect(subject).not_to permit(volunteer, other_org_contact_topic_answer) end it "permits same_org supervisors" do expect(subject).to permit(supervisor, contact_topic_answer) expect(subject).to permit(supervisor, draft_contact_topic_answer) expect(subject).to permit(supervisor, same_case_volunteer_contact_topic_answer) expect(subject).not_to permit(supervisor, other_org_contact_topic_answer) end it "permits same org casa admins" do expect(subject).to permit(casa_admin, contact_topic_answer) expect(subject).to permit(casa_admin, draft_contact_topic_answer) expect(subject).to permit(casa_admin, same_case_volunteer_contact_topic_answer) expect(subject).not_to permit(casa_admin, other_org_contact_topic_answer) end it "does not permit an all casa admin" do expect(subject).not_to permit(all_casa_admin, contact_topic_answer) end end describe "Scope#resolve" do subject { described_class::Scope.new(user, ContactTopicAnswer.all).resolve } before do contact_topic_answer draft_contact_topic_answer same_case_volunteer_contact_topic_answer other_org_contact_topic_answer end context "when user is a visitor" do let(:user) { nil } it "returns no contact topic answers" do expect(subject).not_to include(contact_topic_answer, other_org_contact_topic_answer) end end context "when user is a volunteer" do let(:user) { volunteer } it "returns contact topic answers of contacts created by the volunteer" do expect(subject).to include(contact_topic_answer, draft_contact_topic_answer) expect(subject).not_to include(same_case_volunteer_contact_topic_answer, other_org_contact_topic_answer) end end context "when user is a supervisor" do let(:user) { supervisor } it "returns same org contact topic answers" do expect(subject) .to include(contact_topic_answer, draft_contact_topic_answer, same_case_volunteer_contact_topic_answer) expect(subject).not_to include(other_org_contact_topic_answer) end end context "when user is a casa_admin" do let(:user) { casa_admin } it "includes same org contact topic answers" do expect(subject) .to include(contact_topic_answer, draft_contact_topic_answer, same_case_volunteer_contact_topic_answer) expect(subject).not_to include(other_org_contact_topic_answer) end end context "when user is an all_casa_admin" do let(:user) { all_casa_admin } it "returns no contact topic answers" do expect(subject).not_to include(contact_topic_answer, other_org_contact_topic_answer) end end end end ================================================ FILE: spec/policies/contact_topic_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTopicPolicy, type: :policy do subject { described_class } let(:contact_topic) { build(:contact_topic, casa_org: organization) } let(:organization) { build(:casa_org) } let(:casa_admin) { create(:casa_admin, casa_org: organization) } let(:other_org_admin) { create(:casa_admin) } let(:volunteer) { build(:volunteer, casa_org: organization) } let(:supervisor) { build(:supervisor, casa_org: organization) } permissions :create?, :edit?, :new?, :show?, :soft_delete?, :update? do it "allows same org casa_admins" do expect(subject).to permit(casa_admin, contact_topic) end it "allows does not allow different org casa_admins" do expect(subject).not_to permit(other_org_admin, contact_topic) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor, contact_topic) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer, contact_topic) end end end ================================================ FILE: spec/policies/contact_type_group_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTypeGroupPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :new?, :create?, :edit?, :update? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/contact_type_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ContactTypePolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :new?, :create?, :edit?, :update? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/court_date_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CourtDatePolicy do subject { described_class } let(:organization) { create(:casa_org) } let(:different_organization) { create(:casa_org) } let(:court_date) { create(:court_date, casa_case: casa_case) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:casa_admin) { create(:casa_admin, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } let(:supervisor) { create(:supervisor, casa_org: organization) } permissions :show? do it { is_expected.to permit(casa_admin, court_date) } context "when a supervisor belongs to the same org as the case" do it { expect(subject).to permit(supervisor, court_date) } end context "when a supervisor does not belong to the same org as the case" do let(:casa_case) { create(:casa_case, casa_org: different_organization) } it { expect(subject).not_to permit(supervisor, court_date) } end context "when volunteer is assigned" do before { volunteer.casa_cases << casa_case } it { is_expected.to permit(volunteer, court_date) } end context "when volunteer is not assigned" do it { is_expected.not_to permit(volunteer, court_date) } end end permissions :destroy? do it { is_expected.to permit(casa_admin, court_date) } it { is_expected.to permit(supervisor, court_date) } it { is_expected.not_to permit(volunteer, court_date) } end end ================================================ FILE: spec/policies/custom_org_link_policy_spec.rb ================================================ require "rails_helper" RSpec.describe CustomOrgLinkPolicy, type: :policy do let(:casa_admin) { build_stubbed(:casa_admin) } let(:supervisor) { build_stubbed(:supervisor) } let(:volunteer) { build_stubbed(:volunteer) } permissions :new?, :create?, :edit?, :update? do it "permits casa_admins" do expect(described_class).to permit(casa_admin) end it "does not permit supervisor" do expect(described_class).not_to permit(supervisor) end it "does not permit volunteer" do expect(described_class).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/dashboard_policy_spec.rb ================================================ require "rails_helper" RSpec.describe DashboardPolicy do subject { described_class } let(:user) { build(:user) } let(:casa_admin) { build(:casa_admin) } let(:supervisor) { build(:supervisor) } let(:volunteer) { build(:volunteer) } permissions :show? do it "permits user to show" do expect(subject).to permit(user) end end permissions :see_volunteers_section? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer) end end permissions :create_cases_section? do context "when user is a volunteer with casa_cases" do it "permits user to see cases section" do volunteer.casa_cases << build_stubbed(:casa_case, casa_org: volunteer.casa_org) expect(Pundit.policy(volunteer, :dashboard).create_case_contacts?).to eq true end end context "when user is a volunteer without casa_cases" do it "permits user to see cases section" do expect(Pundit.policy(volunteer, :dashboard).create_case_contacts?).to eq false end end context "when user is an admin" do it "permits user to see cases section" do expect(Pundit.policy(casa_admin, :dashboard).create_case_contacts?).to eq false end end end permissions :see_cases_section? do context "when user is a volunteer" do it "permits user to see cases section" do expect(subject).to permit(volunteer, :dashboard) end end end permissions :see_admins_section? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not allow supervisors and volunteers" do expect(subject).not_to permit(supervisor) end it "does not allow volunteers" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/followup_policy_spec.rb ================================================ require "rails_helper" RSpec.describe FollowupPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :create?, :resolve? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisor" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end end end ================================================ FILE: spec/policies/fund_request_policy_spec.rb ================================================ require "rails_helper" RSpec.describe FundRequestPolicy, type: :policy do # TODO: Add tests for FundRequestPolicy pending "add some tests for FundRequestPolicy" end ================================================ FILE: spec/policies/hearing_type_policy_spec.rb ================================================ require "rails_helper" RSpec.describe HearingTypePolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :new?, :create?, :edit?, :update? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end describe "scope" do it "onlies return hearing types that belong to a given casa organization" do hearing_type_1 = create(:hearing_type) hearing_type_2 = create(:hearing_type) hearing_type_3 = create(:hearing_type) casa_org_2 = create(:casa_org) hearing_type_3.update_attribute(:casa_org_id, casa_org_2.id) hearing_type_3.update_attribute(:name, "unwanted hearing type") scoped_hearing_types = Pundit.policy_scope!(create(:casa_admin), HearingType).to_a expect(scoped_hearing_types).to contain_exactly(hearing_type_1, hearing_type_2) end end end ================================================ FILE: spec/policies/import_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ImportPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :index?, :create?, :download_failed? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/judge_policy_spec.rb ================================================ require "rails_helper" RSpec.describe JudgePolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :new?, :create?, :edit?, :update? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "does not permit supervisor" do expect(subject).not_to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/language_policy_spec.rb ================================================ require "rails_helper" RSpec.describe LanguagePolicy do subject { described_class } let(:admin) { build_stubbed(:casa_admin) } let(:supervisor) { build_stubbed(:supervisor) } let(:volunteer) { build_stubbed(:volunteer) } permissions :add_language?, :remove_from_volunteer? do context "when user is a casa admin" do it "doesn't permit" do expect(subject).not_to permit(admin) end end context "when user is a supervisor" do it "doesn't permit" do expect(subject).not_to permit(supervisor) end end context "when user is a volunteer" do it "allows" do expect(subject).to permit(volunteer) end end end end ================================================ FILE: spec/policies/learning_hour_policy/scope_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourPolicy::Scope do describe "#resolve" do it "returns all volunteers learning hours when user is a CasaAdmin" do casa_admin = create(:casa_admin) scope = described_class.new(casa_admin, LearningHour) expect(scope.resolve).to match_array(LearningHour.all_volunteers_learning_hours(casa_admin.casa_org_id)) end it "returns supervisor's volunteers learning hours when user is a Supervisor" do supervisor = create(:supervisor) create(:supervisor_volunteer, supervisor: supervisor) scope = described_class.new(supervisor, LearningHour) expect(scope.resolve).to match_array(LearningHour.supervisor_volunteers_learning_hours(supervisor.id)) end it "returns volunteer's learning hours when user is a Volunteer" do volunteer = create(:volunteer) scope = described_class.new(volunteer, LearningHour) expect(scope.resolve).to match_array(LearningHour.where(user_id: volunteer.id)) end end end ================================================ FILE: spec/policies/learning_hour_policy_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:supervisor) { build_stubbed(:supervisor) } let(:volunteer) { build_stubbed(:volunteer) } permissions :index? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisors" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end end end ================================================ FILE: spec/policies/learning_hour_topic_policy_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourTopicPolicy, type: :policy do # TODO: Add tests for LearningHourTopicPolicy pending "add some tests for LearningHourTopicPolicy" end ================================================ FILE: spec/policies/learning_hour_type_policy_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHourTypePolicy, type: :policy do # TODO: Add tests for LearningHourTypePolicy pending "add some tests for LearningHourTypePolicy" end ================================================ FILE: spec/policies/nil_class_policy_spec.rb ================================================ require "rails_helper" RSpec.describe NilClassPolicy do subject { described_class.new(nil, nil) } it "doesn't permit index action" do expect(subject).not_to be_index end it "doesn't permit show action" do expect(subject).not_to be_show end it "doesn't permit create action" do expect(subject).not_to be_create end it "doesn't permit new action" do expect(subject).not_to be_new end it "doesn't permit update action" do expect(subject).not_to be_update end it "doesn't permit edit action" do expect(subject).not_to be_edit end it "doesn't permit destroy action" do expect(subject).not_to be_destroy end end ================================================ FILE: spec/policies/note_policy_spec.rb ================================================ require "rails_helper" RSpec.describe NotePolicy, type: :policy do # TODO: Add tests for NotePolicy pending "add some tests for NotePolicy" end ================================================ FILE: spec/policies/notification_policy_spec.rb ================================================ require "rails_helper" RSpec.describe NotificationPolicy, type: :policy do subject { described_class } let(:recipient) { create(:volunteer) } let(:casa_admin) { create(:casa_admin) } let(:volunteer) { build(:volunteer) } let(:supervisor) { build(:supervisor) } permissions :index? do it "allows any volunteer" do expect(subject).to permit(casa_admin) end it "allows any supervisor" do expect(subject).to permit(supervisor) end it "allows any admin" do expect(subject).to permit(volunteer) end end permissions :mark_as_read? do let(:notification) { create(:notification, recipient: recipient) } it "allows recipient" do expect(subject).to permit(recipient, notification) end it "does not allow other volunteer" do expect(subject).not_to permit(volunteer, notification) end it "does not permit other supervisor" do expect(subject).not_to permit(supervisor, notification) end it "does not permit other admin" do expect(subject).not_to permit(casa_admin, notification) end end end ================================================ FILE: spec/policies/other_duty_policy_spec.rb ================================================ require "rails_helper" RSpec.describe OtherDutyPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:supervisor) { build_stubbed(:supervisor) } let(:volunteer) { build_stubbed(:volunteer) } permissions :index? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisors" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end context "when other_duties_enabled is false in casa_org" do before do casa_admin.casa_org.other_duties_enabled = false supervisor.casa_org.other_duties_enabled = false volunteer.casa_org.other_duties_enabled = false end it "not allows casa_admins" do expect(subject).not_to permit(casa_admin) end it "not allows supervisors" do expect(subject).not_to permit(supervisor) end it "not allows volunteer" do expect(subject).not_to permit(volunteer) end end context "when other_duties_enabled is true in casa_org" do before do casa_admin.casa_org.other_duties_enabled = true supervisor.casa_org.other_duties_enabled = true volunteer.casa_org.other_duties_enabled = true end it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisors" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end end end end ================================================ FILE: spec/policies/patch_note_policy_spec.rb ================================================ require "rails_helper" RSpec.describe PatchNotePolicy, type: :policy do subject { described_class } let(:user) { User.new } permissions ".scope" do pending "add some examples to (or delete) #{__FILE__}" end end ================================================ FILE: spec/policies/placement_policy_spec.rb ================================================ require "rails_helper" RSpec.describe PlacementPolicy do subject { described_class } let(:casa_org) { create(:casa_org) } let(:diff_org) { create(:casa_org) } let(:casa_case) { create(:casa_case, casa_org:) } let(:placement) { create(:placement, casa_case:) } let(:casa_admin) { build(:casa_admin, casa_org:) } let(:supervisor) { build(:supervisor, casa_org:) } let(:volunteer) { build(:volunteer, casa_org:) } let(:casa_admin_diff_org) { build(:casa_admin, casa_org: diff_org) } let(:supervisor_diff_org) { build(:supervisor, casa_org: diff_org) } let(:volunteer_diff_org) { build(:volunteer, casa_org: diff_org) } permissions :create?, :edit?, :new?, :show?, :update? do it { is_expected.to permit(casa_admin, placement) } context "when a supervisor belongs to the same org as the case" do it { expect(subject).to permit(supervisor, placement) } end context "when a supervisor does not belong to the same org as the case" do let(:casa_case) { create(:casa_case, casa_org: diff_org) } it { expect(subject).not_to permit(supervisor, placement) } end context "when volunteer is assigned" do before { create(:case_assignment, volunteer:, casa_case:, active: true) } it { is_expected.to permit(volunteer, placement) } end context "when volunteer is not assigned" do it { is_expected.not_to permit(volunteer, placement) } end end permissions :destroy? do it { is_expected.to permit(casa_admin, placement) } it { is_expected.to permit(supervisor, placement) } it { is_expected.not_to permit(volunteer, placement) } end end ================================================ FILE: spec/policies/placement_type_policy_spec.rb ================================================ require "rails_helper" RSpec.describe PlacementTypePolicy, type: :policy do let(:casa_org) { create :casa_org } let(:volunteer) { create :volunteer, casa_org: } let(:supervisor) { create :supervisor, casa_org: } let(:casa_admin) { create :casa_admin, casa_org: } let(:all_casa_admin) { create :all_casa_admin } let(:placement_type) { create :placement_type, casa_org: } subject { described_class } permissions :edit?, :new?, :update?, :create? do it "does not permit a nil user" do expect(described_class).not_to permit(nil, placement_type) end it "does not permit a volunteer" do expect(described_class).not_to permit(volunteer, placement_type) end it "does not permit a supervisor" do expect(described_class).not_to permit(supervisor, placement_type) end it "permits a casa admin" do expect(described_class).to permit(casa_admin, placement_type) end it "does not permit a casa admin for a different casa org" do other_org_casa_admin = create :casa_admin, casa_org: create(:casa_org) expect(described_class).not_to permit(other_org_casa_admin, placement_type) end it "does not permit an all casa admin" do expect(described_class).not_to permit(all_casa_admin, placement_type) end end end ================================================ FILE: spec/policies/reimbursement_policy_spec.rb ================================================ require "rails_helper" RSpec.describe ReimbursementPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } let(:organization) { build(:casa_org, users: [volunteer, supervisor, casa_admin]) } context "when org reimbursement is enabled" do permissions :index?, :change_complete_status? do it { is_expected.to permit(casa_admin) } it { is_expected.to permit(supervisor) } it { is_expected.not_to permit(volunteer) } end permissions :datatable? do it { is_expected.to permit(casa_admin) } it { is_expected.to permit(supervisor) } it { is_expected.not_to permit(volunteer) } end end context "when org reimbursement is disabled" do before do organization.show_driving_reimbursement = false end permissions :index?, :change_complete_status? do it { is_expected.not_to permit(casa_admin) } end permissions :datatable? do it { is_expected.not_to permit(casa_admin) } end end describe "ReimbursementPolicy::Scope #resolve" do subject { described_class::Scope.new(user, scope).resolve } let(:user) { build_stubbed(:casa_admin, casa_org: casa_org1) } let(:scope) { CaseContact.joins(:casa_case) } let(:casa_org1) { create(:casa_org) } let(:casa_case1) { create(:casa_case, casa_org: casa_org1) } let(:casa_case2) { create(:casa_case, casa_org: create(:casa_org)) } let!(:contact1) { create(:case_contact, casa_case: casa_case1) } let!(:contact2) { create(:case_contact, casa_case: casa_case2) } it { is_expected.to include(contact1) } it { is_expected.not_to include(contact2) } end end ================================================ FILE: spec/policies/supervisor_policy_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorPolicy do subject { described_class } let(:organization) { create(:casa_org) } let(:different_organization) { create(:casa_org) } let!(:casa_admin) { create(:casa_admin, casa_org: organization) } let!(:supervisor) { create(:supervisor, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } permissions :update_supervisor_email? do context "when user is an admin or is the record" do it "permits an admin to update supervisor email" do expect(subject).to permit(casa_admin, supervisor) end it "permits the supervisor to update their own email" do expect(subject).to permit(supervisor, supervisor) end end context "when user is not an admin or the record" do let(:second_supervisor) { build_stubbed(:supervisor) } it "does not permit the other supervisor user to update volunteer email" do expect(subject).not_to permit(supervisor, second_supervisor) end it "does not permit the volunteer user to update volunteer email" do expect(subject).not_to permit(volunteer, second_supervisor) end end end permissions :update? do context "same organization" do it "allows casa_admins" do expect(subject).to permit(casa_admin, supervisor) end end context "different organization" do let(:other_admin) { create(:casa_admin, casa_org: different_organization) } it "does not allow casa_admins" do expect(subject).not_to permit(other_admin, supervisor) end end it "allows supervisors to update themselves" do expect(subject).to permit(supervisor, supervisor) end it "does not allow supervisors to update other supervisors" do another_supervisor = build_stubbed(:supervisor) expect(subject).not_to permit(supervisor, another_supervisor) end end permissions :edit? do context "same org" do let(:record) { build_stubbed(:supervisor, casa_org: casa_admin.casa_org) } context "when user is admin" do it "can edit a supervisor" do expect(subject).to permit(casa_admin, record) end end context "when user is supervisor" do it "can edit a supervisor" do expect(subject).to permit(supervisor, record) end end end context "different org" do let(:record) { build_stubbed(:supervisor, casa_org: different_organization) } context "when user is admin" do it "cannot edit a supervisor" do expect(subject).not_to permit(casa_admin, record) end end context "when user is a supervisor" do it "cannot edit a supervisor" do expect(subject).not_to permit(supervisor, record) end end end end permissions :index?, :datatable? do context "when user is an admin" do it "has access to the supervisors index action" do expect(subject).to permit(casa_admin, Supervisor) end end context "when user is a supervisor" do it "has access to the supervisors index action" do expect(subject).to permit(supervisor, Supervisor) end end end permissions :index?, :datatable?, :edit? do context "when user is a volunteer" do it "does not have access to the supervisors index action" do expect(subject).not_to permit(volunteer, Supervisor) end end end permissions :create?, :new? do it "allows admins to modify supervisors" do expect(subject).to permit(casa_admin, Supervisor) end it "does not allow supervisors to modify supervisors" do expect(subject).not_to permit(supervisor, Supervisor) end it "does not allow volunteers to modify supervisors" do expect(subject).not_to permit(volunteer, Supervisor) end end permissions :resend_invitation?, :activate?, :deactivate? do context "same organization" do it "allows admins to modify supervisors" do expect(subject).to permit(casa_admin, supervisor) end end context "different organization" do let(:other_admin) { create(:casa_admin, casa_org: different_organization) } it "does not allow admin to modify supervisors" do expect(subject).not_to permit(other_admin, supervisor) end end it "does not allow supervisors to modify supervisors" do expect(subject).not_to permit(supervisor, supervisor) end it "does not allow volunteers to modify supervisors" do expect(subject).not_to permit(volunteer, supervisor) end end permissions :change_to_admin? do it "allows admins to change to admin" do expect(subject).to permit(casa_admin, supervisor) end it "does not allow supervisors to change to admin" do expect(subject).not_to permit(supervisor, Supervisor) end it "does not allow volunteers to change to admin" do expect(subject).not_to permit(volunteer, Supervisor) end end end ================================================ FILE: spec/policies/supervisor_volunteer_policy_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorVolunteerPolicy do subject { described_class } let(:casa_admin) { build_stubbed(:casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } let(:supervisor) { build_stubbed(:supervisor) } permissions :create? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisor" do expect(subject).to permit(supervisor) end it "allows volunteer" do expect(subject).to permit(volunteer) end end permissions :unassign? do it "allows casa_admins" do expect(subject).to permit(casa_admin) end it "allows supervisor" do expect(subject).to permit(supervisor) end it "does not permit volunteer" do expect(subject).not_to permit(volunteer) end end end ================================================ FILE: spec/policies/user_policy/scope_spec.rb ================================================ require "rails_helper" RSpec.describe UserPolicy::Scope do describe "#resolve" do it "returns all Users when user is admin" do admin = create(:casa_admin) scope = described_class.new(admin, User) expect(scope.resolve).to contain_exactly(admin) end it "returns the user when user is volunteer" do user = create(:volunteer) scope = described_class.new(user, User) expect(scope.resolve).to eq [user] end end end ================================================ FILE: spec/policies/user_policy_spec.rb ================================================ require "rails_helper" RSpec.describe UserPolicy do subject { described_class } let(:org_a) { build_stubbed(:casa_org) } let(:org_b) { build_stubbed(:casa_org) } let(:casa_admin_a) { build_stubbed(:casa_admin, casa_org: org_a) } let(:casa_admin_b) { build_stubbed(:casa_admin, casa_org: org_b) } let(:supervisor_a) { build_stubbed(:supervisor, casa_org: org_a) } let(:supervisor_b) { build_stubbed(:supervisor, casa_org: org_b) } let(:volunteer_a) { build_stubbed(:volunteer, casa_org: org_a) } let(:volunteer_b) { build_stubbed(:volunteer, casa_org: org_b) } permissions :edit?, :update?, :update_password? do it "allows casa_admins" do expect(subject).to permit(casa_admin_a) end it "allows supervisor" do expect(subject).to permit(supervisor_a) end it "allows volunteer" do expect(subject).to permit(volunteer_a) end end permissions :update_user_setting? do context "when user is an admin" do it "allows update settings of all roles" do expect(subject).to permit(casa_admin_a) expect(subject).to permit(casa_admin_b) end end context "when user is a supervisor" do it "allows supervisors to update another volunteer settings in their casa org" do expect(subject).to permit(supervisor_a, volunteer_a) expect(subject).to permit(supervisor_b, volunteer_b) end it "does not allow supervisor to update a volunteer in a different casa org" do expect(subject).not_to permit(supervisor_a, volunteer_b) expect(subject).not_to permit(supervisor_b, volunteer_a) end it "allows supervisors to update their own settings" do expect(subject).to permit(supervisor_a, supervisor_a) expect(subject).to permit(supervisor_b, supervisor_b) end it "does not allow supervisor to update another supervisor settings" do expect(subject).not_to permit(supervisor_a, supervisor_b) expect(subject).not_to permit(supervisor_b, supervisor_a) end end end permissions :add_language? do context "when user is a volunteer" do it "allows volunteer to add a language to themselves" do expect(subject).to permit(volunteer_a, volunteer_a) expect(subject).to permit(volunteer_b, volunteer_b) end it "does not allow another volunteer to add a language to another volunteer" do expect(subject).not_to permit(volunteer_a, volunteer_b) expect(subject).not_to permit(volunteer_b, volunteer_a) end end context "when user is a supervisor" do it "allows supervisors to add a language to a volunteer in their organizations" do expect(subject).to permit(supervisor_a, volunteer_a) expect(subject).to permit(supervisor_b, volunteer_b) end it "does not allow a supervisor to add a language to a volunteer in a different organization" do expect(subject).not_to permit(supervisor_a, volunteer_b) expect(subject).not_to permit(supervisor_b, volunteer_a) end end context "when user is an admin" do it "allows admins to add a language to a volunteer in their organizations" do expect(subject).to permit(casa_admin_a, volunteer_a) expect(subject).to permit(casa_admin_b, volunteer_b) end it "does not allow an admin to add a language to a volunteer in a different organization" do expect(subject).not_to permit(casa_admin_a, volunteer_b) expect(subject).not_to permit(casa_admin_b, volunteer_a) end end end end ================================================ FILE: spec/policies/volunteer_policy_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerPolicy do subject { described_class } let(:casa_org) { build_stubbed(:casa_org) } let(:other_org) { build_stubbed(:casa_org) } let(:admin) { build_stubbed(:casa_admin, casa_org: casa_org) } let(:supervisor) { build_stubbed(:supervisor, casa_org: casa_org) } let(:volunteer) { build_stubbed(:volunteer, casa_org: casa_org) } permissions :edit? do context "same org" do let(:record) { build_stubbed(:volunteer, casa_org: casa_org) } context "when user is a casa admin" do it "allows for same org" do expect(subject).to permit(admin, record) end end context "when user is a supervisor" do it "allows" do expect(subject).to permit(supervisor, record) end end context "when user is a volunteer" do it "does not permit" do expect(subject).not_to permit(volunteer) end end end context "different org" do let(:record) { build_stubbed(:volunteer, casa_org: other_org) } context "when user is a casa admin" do it "does not allow for different org" do expect(subject).not_to permit(admin, record) end end context "when user is a supervisor" do it "does not allow" do expect(subject).not_to permit(supervisor, record) end end context "when user is a volunteer" do it "does not permit" do expect(subject).not_to permit(volunteer) end end end end permissions :index?, :activate?, :create?, :datatable?, :deactivate?, :new?, :show?, :update? do context "when user is a casa admin" do let(:record) { build_stubbed(:volunteer, casa_org: casa_org) } it "allows" do expect(subject).to permit(admin, record) end end context "when user is a supervisor" do let(:record) { build_stubbed(:volunteer, casa_org: casa_org) } it "allows" do expect(subject).to permit(supervisor, record) end end context "when user is a volunteer" do it "does not permit" do expect(subject).not_to permit(volunteer) end end end permissions :update_volunteer_email? do context "when user is a casa admin" do it "allows" do expect(subject).to permit(admin) end end context "when user is a supervisor" do it "does not permit" do expect(subject).to permit(supervisor) end end context "when user is a volunteer" do it "does not permit" do expect(subject).not_to permit(volunteer) end end end describe "VolunteerPolicy::Scope" do describe "#resolve" do subject { described_class::Scope.new(user, Volunteer.all).resolve } let!(:volunteer1) { create(:volunteer, casa_org: casa_org) } let!(:another_org_volunteer) { create(:volunteer, casa_org: another_casa_org) } let(:casa_org) { create(:casa_org) } let(:another_casa_org) { create(:casa_org) } context "when admin" do let(:user) { build_stubbed(:casa_admin, casa_org: casa_org) } it { is_expected.to include(volunteer1) } it { is_expected.not_to include(another_org_volunteer) } end context "when supervisor" do let(:user) { build_stubbed(:supervisor, casa_org: casa_org) } it { is_expected.to include(volunteer1) } it { is_expected.not_to include(another_org_volunteer) } end context "when volunteer" do let(:user) { build_stubbed(:volunteer, casa_org: casa_org) } it { is_expected.to include(volunteer1) } it { is_expected.not_to include(another_org_volunteer) } end end end end ================================================ FILE: spec/presenters/base_presenter_spec.rb ================================================ require "rails_helper" RSpec.describe BasePresenter do # TODO: Add tests for BasePresenter pending "add some tests for BasePresenter" end ================================================ FILE: spec/presenters/case_contact_presenter_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactPresenter do let(:organization) { build(:casa_org) } let(:user) { create(:casa_admin, casa_org: organization) } let(:case_contacts) { create_list(:case_contact, 5, casa_case: casa_case) } let(:presenter) { described_class.new(case_contacts) } before do allow_any_instance_of(described_class).to receive(:current_user).and_return(user) allow_any_instance_of(described_class).to receive(:current_organization).and_return(organization) end describe "#display_case_number" do context "with transition aged youth" do let(:casa_case) { create(:casa_case, birth_month_year_youth: 15.years.ago, casa_org: organization) } it "displays the case number with correct icon" do casa_case_id = casa_case.id case_number = casa_case.case_number expect(presenter.display_case_number(casa_case_id)).to eql("🦋 #{case_number}") end it "does not error when case number is nil" do expect(presenter.display_case_number(nil)).to eql("") end end context "with non-transition aged youth" do let(:casa_case) { create(:casa_case, birth_month_year_youth: 12.years.ago, casa_org: organization) } it "displays the case number with correct icon" do casa_case_id = casa_case.id case_number = casa_case.case_number expect(presenter.display_case_number(casa_case_id)).to eql("🐛 #{case_number}") end it "does not error when case number is nil" do expect(presenter.display_case_number(nil)).to eql("") end end end end ================================================ FILE: spec/rails_helper.rb ================================================ # This file is copied to spec/ when you run 'rails generate rspec:install' require "spec_helper" ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? # Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file # that will avoid rails generators crashing because migrations haven't been run yet # return unless Rails.env.test? require "rspec/rails" # Add additional requires below this line. Rails is not loaded until this point! require "pry" require "email_spec" require "email_spec/rspec" require "pundit/rspec" require "view_component/test_helpers" require "capybara/rspec" require "action_text/system_test_helper" require "webmock/rspec" # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end # in _spec.rb will both be required and run as specs, causing the specs to be # run twice. It is recommended that you do not name files matching this glob to # end with _spec.rb. You can configure this pattern with the --pattern # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. # # The following line is provided for convenience purposes. It has the downside # of increasing the boot-up time by auto-requiring all files in the support # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # Rails.root.glob("spec/support/**/*.rb").sort_by(&:to_s).each { |f| require f } # Checks for pending migrations and applies them before tests are run. # If you are not using ActiveRecord, you can remove these lines. begin ActiveRecord::Migration.maintain_test_schema! rescue ActiveRecord::PendingMigrationError => e abort e.to_s.strip end ci_environment = (ENV["GITHUB_ACTIONS"] || ENV["CI"]).present? RSpec.configure do |config| config.include ActiveSupport::Testing::TimeHelpers config.include DatatableHelper, type: :datatable config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::IntegrationHelpers, type: :request config.include Devise::Test::IntegrationHelpers, type: :system config.include Organizational, type: :helper config.include Organizational, type: :view config.include PunditHelper, type: :view config.include SessionHelper, type: :view config.include SessionHelper, type: :request config.include ViewComponent::TestHelpers, type: :component config.include ViewComponent::SystemTestHelpers, type: :component config.include Capybara::RSpecMatchers, type: :component config.include TwilioHelper, type: :request config.include TwilioHelper, type: :system config.include Support::RequestHelpers, type: :request config.include CaseCourtReportHelpers, type: :system # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_paths = [ Rails.root.join("spec/fixtures") ] # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = true # You can uncomment this line to turn off ActiveRecord support entirely. # config.use_active_record = false # RSpec Rails uses metadata to mix in different behaviours to your tests, # for example enabling you to call `get` and `post` in request specs. e.g.: # # RSpec.describe UsersController, type: :request do # # ... # end # # The different available types are documented in the features, such as in # https://rspec.info/features/7-0/rspec-rails # # You can also this infer these behaviours automatically by location, e.g. # /spec/models would pull in the same behaviour as `type: :model` but this # behaviour is considered legacy and will be removed in a future version. # # To enable this behaviour uncomment the line below. # config.infer_spec_type_from_file_location! # Auto detect datatable type specs config.define_derived_metadata(file_path: Regexp.new("/spec/datatables/")) do |metadata| metadata[:type] = :datatable end # Aggregate failures by default, except slow/sequence-dependant examples (as in system specs) config.define_derived_metadata do |metadata| non_aggregate_types = %i[system] metadata[:aggregate_failures] = true unless non_aggregate_types.include?(metadata[:type]) end # Filter lines from Rails gems in backtraces. # Use `rspec --backtrace` option to see full backtraces. config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: config.filter_gems_from_backtrace(*%w[ bootsnap capybara factory_bot puma rack railties shoulda-matchers sprockets-rails pundit ]) config.around do |example| # If timeout is not set it will run without a timeout Timeout.timeout(ENV["TEST_MAX_DURATION"].to_i) do example.run end rescue Timeout::Error raise StandardError.new "\"#{example.full_description}\" in #{example.location} timed out." end # NOTE: not applicable currently, left to show how to skip bullet errrors # config.around :each, :disable_bullet do |example| # Bullet.raise = false # example.run # Bullet.raise = true # end config.around do |example| Capybara.server_port = 7654 + ENV["TEST_ENV_NUMBER"].to_i example.run end config.filter_run_excluding :ci_only unless ci_environment end RSpec::Matchers.define_negated_matcher :not_change, :change Shoulda::Matchers.configure do |shoulda_config| shoulda_config.integrate do |with| with.test_framework :rspec with.library :rails end end WebMock.disable_net_connect!( allow_localhost: true, allow: "selenium_chrome:4444" ) ================================================ FILE: spec/requests/additional_expenses_spec.rb ================================================ require "rails_helper" RSpec.describe "/additional_expenses", type: :request do let(:casa_org) { create :casa_org } let(:volunteer) { create :volunteer, :with_single_case, casa_org: } let(:casa_case) { volunteer.casa_cases.first } let(:case_contact) { create :case_contact, casa_case:, creator: volunteer } let(:valid_attributes) do attributes_for(:additional_expense) .merge({case_contact_id: case_contact.id}) end let(:invalid_attributes) { valid_attributes.merge({other_expenses_describe: nil, other_expense_amount: 1}) } before { sign_in volunteer } describe "POST /create" do subject { post additional_expenses_path, params:, as: :json } let(:params) { {additional_expense: valid_attributes} } it "creates a record and responds created" do expect { subject }.to change(AdditionalExpense, :count).by(1) expect(response).to have_http_status(:created) end it "returns the new contact topic answer as json" do subject expect(response.content_type).to match(a_string_including("application/json")) answer = AdditionalExpense.last expect(response_json[:id]).to eq answer.id expect(response_json.keys) .to include(:id, :case_contact_id, :other_expense_amount, :other_expenses_describe) end context "with invalid parameters" do let(:params) { {additional_expense: invalid_attributes} } it "fails and responds unprocessable_content" do expect { subject }.not_to change(ContactTopicAnswer, :count) expect(response).to have_http_status(:unprocessable_content) end it "returns errors as json" do subject expect(response.content_type).to match(a_string_including("application/json")) expect(response.body).to be_present expect(response_json[:other_expenses_describe]).to include("can't be blank") end end context "html request" do subject { post additional_expenses_path, params: } it "redirects to referrer/root without creating an additional expense" do expect { subject }.to not_change(AdditionalExpense, :count) expect(response).to redirect_to(root_url) end end end describe "DELETE /destroy" do subject { delete additional_expense_url(additional_expense), as: :json } let!(:additional_expense) { create :additional_expense, case_contact: } it "destroys the record and responds no content" do expect { subject } .to change(AdditionalExpense, :count).by(-1) expect(response).to have_http_status(:no_content) expect(response.body).to be_empty end context "html request" do subject { delete additional_expense_url(additional_expense) } it "redirects to referrer/root without destroying the additional expense" do expect { subject }.to not_change(AdditionalExpense, :count) expect(response).to redirect_to(root_url) end end end end ================================================ FILE: spec/requests/all_casa_admins/casa_admins_spec.rb ================================================ require "rails_helper" RSpec.describe "All-Casa Admin", type: :request do let(:all_casa_admin) { build(:all_casa_admin) } let(:casa_admin) { create(:casa_admin, email: "admin1@example.com", display_name: "Example Admin") } let(:casa_org) { create(:casa_org) } before { sign_in all_casa_admin expect_any_instance_of(AllCasaAdmins::CasaAdminsController).to receive(:authenticate_all_casa_admin!).and_call_original } describe "GET /new" do it "allows access to the new admin page" do get new_all_casa_admins_casa_org_casa_admin_path(casa_org) expect(response).to be_successful end end describe "POST /create" do subject { post all_casa_admins_casa_org_casa_admins_path(casa_org), params: } context "with valid parameters" do let(:params) { {casa_admin: {email: "admin1@example.com", display_name: "Example Admin"}} } it "creates a new CASA admin for the organization" do expect { subject }.to change(CasaAdmin, :count).by(1) end it { is_expected.to redirect_to all_casa_admins_casa_org_path(casa_org) } it "shows correct flash message" do subject expect(flash[:notice]).to include("New admin created successfully") end end context "with invalid parameters" do let(:params) { {casa_admin: {email: "", display_name: ""}} } it "renders new page" do expect { subject }.not_to change(CasaAdmin, :count) expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template "casa_admins/new" end end end describe "GET /edit" do subject { get edit_all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin) } it "allows access to the edit admin page" do subject expect(response).to be_successful end it "shows correct admin" do subject expect(response.body).to include(casa_admin.email) end end describe "PATCH /update" do subject { patch all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin), params: } context "with valid parameters" do let(:params) { {all_casa_admin: {email: "casa_admin@example.com"}} } it "allows current user to begin to update other casa admin's email and send a confirmation email" do subject casa_admin.reload expect(casa_admin.unconfirmed_email).to eq("casa_admin@example.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it { is_expected.to redirect_to edit_all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin) } it "shows correct flash message" do subject expect(flash[:notice]).to eq("Casa Admin was successfully updated. Confirmation Email Sent.") end end context "with invalid parameters" do let(:params) { {all_casa_admin: {email: ""}} } it "does not allow current user to successfully update other casa admin's email" do expect { subject }.not_to change { casa_admin.reload.email } end it "renders new page" do subject expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template "casa_admins/edit" end end end describe "PATCH /activate" do subject { patch activate_all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin) } let(:casa_admin) { create(:casa_admin, :inactive) } it "successfullies activate another casa admin's profile" do expect { subject }.to change { casa_admin.reload.active }.from(false).to(true) end it "calls for CasaAdminMailer" do expect(CasaAdminMailer).to( receive(:account_setup).with(casa_admin).once.and_return(double("mailer", deliver: true)) ) subject end it { is_expected.to redirect_to edit_all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin) } it "shows correct flash message" do subject expect(flash[:notice]).to include("Admin was activated. They have been sent an email.") end context "when activation fails" do before { allow_any_instance_of(CasaAdmin).to receive(:activate).and_return(false) } it "does not activate the casa admin's profile" do expect { subject }.not_to change { casa_admin.reload.active } end it "renders edit page" do subject expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template "casa_admins/edit" end end end describe "PATCH /deactivate" do subject { patch deactivate_all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin) } let(:casa_admin) { create(:casa_admin, active: true) } it "successfullies deactivate another casa admin's profile" do expect { subject }.to change { casa_admin.reload.active }.from(true).to(false) end it "calls for CasaAdminMailer" do expect(CasaAdminMailer).to( receive(:deactivation).with(casa_admin).once.and_return(double("mailer", deliver: true)) ) subject end it { is_expected.to redirect_to edit_all_casa_admins_casa_org_casa_admin_path(casa_org, casa_admin) } it "shows correct flash message" do subject expect(flash[:notice]).to include("Admin was deactivated.") end context "when deactivation fails" do before { allow_any_instance_of(CasaAdmin).to receive(:deactivate).and_return(false) } it "does not deactivate the casa admin's profile" do expect { subject }.not_to change { casa_admin.reload.active } end it "renders edit page" do subject expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template "casa_admins/edit" end end end end ================================================ FILE: spec/requests/all_casa_admins/casa_orgs_spec.rb ================================================ require "rails_helper" RSpec.describe "AllCasaAdmin::CasaOrgs", type: :request do let(:all_casa_admin) { create(:all_casa_admin) } before { sign_in all_casa_admin } describe "GET /show" do subject(:request) do get all_casa_admins_casa_org_path(casa_org) response end let!(:casa_org) { create(:casa_org) } let!(:other_casa_org) { create(:casa_org) } it { is_expected.to be_successful } it "shows casa org correctly", :aggregate_failures do page = request.body expect(page).to include(casa_org.name) expect(page).not_to include(other_casa_org.name) end it "load metrics correctly" do expect(AllCasaAdmins::CasaOrgMetrics).to receive(:new).with(casa_org).and_call_original request end end describe "GET /new" do subject(:get_new) { get new_all_casa_admins_casa_org_path } it "returns http success" do get_new expect(response).to have_http_status(:success) end end describe "POST /create" do subject(:post_create) { post all_casa_admins_casa_orgs_path, params: params } context "when successfully" do let(:params) do {casa_org: {name: "New Org", display_name: "New org display", address: "29207 Weimann Canyon, New Andrew, PA 40510-7416"}} end let(:contact_topics) { [{"question" => "Title 1", "details" => "details 1"}, {"question" => "Title 2", "details" => "details 2"}] } before do allow(ContactTopic).to receive(:default_contact_topics).and_return(contact_topics) end it "creates a new CASA org" do expect { post_create }.to change(CasaOrg, :count).by(1) end it "generates correct defaults during creation" do expect { post_create }.to change(ContactTopic, :count).by(2) casa_org = CasaOrg.last expect(casa_org.contact_topics.map(&:question)).to match_array(contact_topics.pluck("question")) expect(casa_org.contact_topics.map(&:details)).to match_array(contact_topics.pluck("details")) expect(casa_org.contact_topics.pluck(:active)).to be_all true end it "redirects to CASA org show page, with notice flash", :aggregate_failures do post_create expect(response).to redirect_to all_casa_admins_casa_org_path(assigns(:casa_org)) expect(flash[:notice]).to eq "CASA Organization was successfully created." end it "also responds as json", :aggregate_failures do post all_casa_admins_casa_orgs_path(format: :json), params: params expect(response.content_type).to eq "application/json; charset=utf-8" expect(response).to have_http_status :created expect(response.body).to match "29207 Weimann Canyon, New Andrew, PA 40510-7416" end end context "when failure" do let(:params) do {casa_org: {name: nil, display_name: nil, address: "29207 Weimann Canyon, New Andrew, PA 40510-7416"}} end it "does not create a new CASA org" do expect { post_create }.not_to change(CasaOrg, :count) end it "renders new template" do post_create expect(response).to render_template :new end it "also responds as json", :aggregate_failures do post all_casa_admins_casa_orgs_path(format: :json), params: params expect(response.content_type).to eq "application/json; charset=utf-8" expect(response).to have_http_status :unprocessable_content expect(response.body).to match "Name can't be blank" end end end end ================================================ FILE: spec/requests/all_casa_admins/dashboard_spec.rb ================================================ require "rails_helper" RSpec.describe "AllCasaAdmin::Dashboard", type: :request do let(:all_casa_admin) { create(:all_casa_admin) } before { sign_in all_casa_admin } describe "GET /show" do subject(:request) do get authenticated_all_casa_admin_root_path response end let!(:casa_orgs) { create_list(:casa_org, 3) } it { is_expected.to have_http_status(:success) } it "shows casa orgs" do # Code changes to fix response as earlier HTML String instead of Nokogiri::HTML5::Document object as received in Rails 7.1.0 to pass expectation page = request.parsed_body.to_html expect(page).to include(*casa_orgs.map(&:name)) end end end ================================================ FILE: spec/requests/all_casa_admins/patch_notes_spec.rb ================================================ require "rails_helper" # This spec was generated by rspec-rails when you ran the scaffold generator. # It demonstrates how one might use RSpec to test the controller code that # was generated by Rails when you ran the scaffold generator. # # It assumes that the implementation code is generated by the rails scaffold # generator. If you are using any extension libraries to generate different # controller code, this generated spec may or may not pass. # # It only uses APIs available in rails and/or rspec-rails. There are a number # of tools you can use to make these specs even more expressive, but we're # sticking to rails and rspec-rails APIs to keep things simple and stable. RSpec.describe "/all_casa_admins/patch_notes", type: :request do # This should return the minimal set of attributes required to create a valid # PatchNote. As you add validations to PatchNote, be sure to # adjust the attributes here as well. let(:all_casa_admin) { build(:all_casa_admin) } let(:patch_note_group) { create(:patch_note_group) } let(:patch_note_type) { create(:patch_note_type) } let(:valid_attributes) do { note: "not an empty note", patch_note_group_id: patch_note_group.id, patch_note_type_id: patch_note_type.id } end let(:invalid_attributes) do { note: "", patch_note_group_id: patch_note_group.id + 1, patch_note_type_id: patch_note_type.id + 1 } end before { sign_in all_casa_admin } describe "GET /index" do it "renders a successful response" do PatchNote.create! valid_attributes get all_casa_admins_patch_notes_path expect(response).to be_successful end end describe "POST /create" do context "with valid parameters" do it "creates a new PatchNote" do expect { post all_casa_admins_patch_notes_path, params: valid_attributes }.to change(PatchNote, :count).by(1) end it "shows json indicating the patch note was created" do post all_casa_admins_patch_notes_path, params: valid_attributes expect(response.header["Content-Type"]).to match(/application\/json/) expect(response.body).not_to be_nil expect(response).to have_http_status(:created) end end context "with invalid parameters" do it "does not create a new PatchNote" do expect { post all_casa_admins_patch_notes_path, params: invalid_attributes }.not_to change(PatchNote, :count) end it "shows json indicating the patch note could not be created" do post all_casa_admins_patch_notes_path, params: invalid_attributes expect(response.header["Content-Type"]).to match(/application\/json/) expect(response.body).not_to be_nil expect(response).to have_http_status(:unprocessable_content) expect(JSON.parse(response.body)).to have_key("errors") end it "shows json with the id of the patch note created" do post all_casa_admins_patch_notes_path, params: valid_attributes expect(response.header["Content-Type"]).to match(/application\/json/) expect(response.body).not_to be_nil expect(response).to have_http_status(:created) expect(JSON.parse(response.body)).to have_key("id") end end end describe "PATCH /update" do context "with valid parameters" do let(:patch_note_group_2) { create(:patch_note_group) } let(:patch_note_type_2) { create(:patch_note_type) } let(:new_attributes) do { note: "a different update note", patch_note_group_id: patch_note_group_2.id, patch_note_type_id: patch_note_type_2.id } end it "updates the requested patch_note" do patch_note = PatchNote.create! valid_attributes patch all_casa_admins_patch_note_path(patch_note), params: new_attributes patch_note.reload expect(patch_note.note).to eq(new_attributes[:note]) expect(patch_note.patch_note_group_id).to eq(patch_note_group_2.id) expect(patch_note.patch_note_type_id).to eq(patch_note_type_2.id) end it "renders a successful response (a json with an ok status)" do patch_note = PatchNote.create! valid_attributes patch all_casa_admins_patch_note_path(patch_note), params: new_attributes expect(response.header["Content-Type"]).to match(/application\/json/) expect(response.body).not_to be_nil expect(response).to have_http_status(:ok) end end context "with invalid parameters" do it "renders a successful response (a json with a list of errors)" do patch_note = PatchNote.create! valid_attributes patch all_casa_admins_patch_note_path(patch_note), params: invalid_attributes expect(response.body).not_to be_nil expect(response).to have_http_status(:unprocessable_content) expect(JSON.parse(response.body)).to have_key("errors") end end end describe "DELETE /destroy" do it "destroys the requested patch_note" do patch_note = PatchNote.create! valid_attributes expect { delete all_casa_admins_patch_note_path(patch_note) }.to change(PatchNote, :count).by(-1) end it "renders a successful response (a json with an ok status)" do patch_note = PatchNote.create! valid_attributes delete all_casa_admins_patch_note_path(patch_note) expect(response.header["Content-Type"]).to match(/application\/json/) expect(response.body).not_to be_nil expect(response).to have_http_status(:ok) end end end ================================================ FILE: spec/requests/all_casa_admins/sessions_spec.rb ================================================ require "rails_helper" RSpec.describe "AllCasaAdmin::SessionsController", type: :request do let(:all_casa_admin) { create(:all_casa_admin) } describe "GET /new" do subject(:request) do get new_all_casa_admin_session_path response end it { is_expected.to be_successful } end describe "POST /create" do subject(:request) do post all_casa_admin_session_path response end let(:params) { {email: all_casa_admin.email, password: all_casa_admin.password} } it { is_expected.to be_successful } end describe "GET /destroy" do subject(:request) do get destroy_all_casa_admin_session_path response end it { is_expected.to have_http_status(:redirect) } end end ================================================ FILE: spec/requests/all_casa_admins_spec.rb ================================================ require "rails_helper" RSpec.describe "/all_casa_admins", type: :request do let(:admin) { create(:all_casa_admin) } before { sign_in admin } describe "GET /new" do it "authenticates the user" do sign_out admin get new_all_casa_admin_path expect(response).to have_http_status(:redirect) end it "only allows all_casa_admin users" do sign_out admin casa_admin = create(:casa_admin) sign_in casa_admin get new_all_casa_admin_path expect(response).to have_http_status(:redirect) end context "with a all_casa_admin signed in" do it "renders a successful response" do get new_all_casa_admin_path expect(response).to be_successful end end it "authenticates the user" do sign_out :admin get new_all_casa_admin_path expect(response).to be_successful end it "only allows all_casa_admin users" do sign_out :admin casa_admin = create(:casa_admin) sign_in casa_admin get new_all_casa_admin_path expect(response).to be_successful end end describe "GET /edit" do context "with a all_casa_admin signed in" do it "renders a successful response" do get edit_all_casa_admins_path expect(response).to be_successful end end end describe "POST /create" do context "with valid parameters" do it "creates a new All CASA admin" do expect do post all_casa_admins_path, params: { all_casa_admin: { email: "admin1@example.com" } } end.to change(AllCasaAdmin, :count).by(1) expect(response).to have_http_status(:redirect) expect(flash[:notice]).to eq("New All CASA admin created successfully") end it "also responds as json", :aggregate_failures do post all_casa_admins_path(format: :json), params: { all_casa_admin: { email: "admin1@example.com" } } expect(response).to have_http_status(:created) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to match("admin1@example.com".to_json) end end context "with invalid parameters" do it "renders new page" do post all_casa_admins_path, params: { all_casa_admin: { email: "" } } expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template "all_casa_admins/new" end it "also responds as json", :aggregate_failures do post all_casa_admins_path(format: :json), params: { all_casa_admin: { email: "" } } expect(response).to have_http_status(:unprocessable_content) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to match("Email can't be blank".to_json) end end end describe "PATCH /update" do context "with valid parameters" do it "updates the all_casa_admin" do patch all_casa_admins_path, params: {all_casa_admin: {email: "newemail@example.com"}} expect(response).to have_http_status(:redirect) expect(admin.email).to eq "newemail@example.com" end it "also responds as json", :aggregate_failures do patch all_casa_admins_path(format: :json), params: {all_casa_admin: {email: "newemail@example.com"}} expect(response).to have_http_status(:ok) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to match("newemail@example.com".to_json) end end context "with invalid parameters" do it "does not update the all_casa_admin" do other_admin = create(:all_casa_admin) patch all_casa_admins_path, params: {all_casa_admin: {email: other_admin.email}} expect(response).to have_http_status(:unprocessable_content) expect(admin.email).not_to eq "newemail@example.com" end it "also responds as json", :aggregate_failures do other_admin = create(:all_casa_admin) patch all_casa_admins_path(format: :json), params: {all_casa_admin: {email: other_admin.email}} expect(response).to have_http_status(:unprocessable_content) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to match("Email has already been taken".to_json) end end end describe "PATCH /update_password" do context "with valid parameters" do subject { patch update_password_all_casa_admins_path, params: params } let(:params) do { all_casa_admin: { password: "newpassword", password_confirmation: "newpassword" } } end it "updates the all_casa_admin password", :aggregate_failures do subject expect(response).to have_http_status(:redirect) expect(admin.valid_password?("newpassword")).to be true end it "call UserMailer.password_chaned_reminder" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(admin).and_return(mailer) expect(mailer).to receive(:deliver) subject end it "also responds as json", :aggregate_failures do patch update_password_all_casa_admins_path(format: :json), params: params expect(response).to have_http_status(:ok) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to match("Password was successfully updated.") end end context "with invalid parameters", :aggregate_failures do subject { patch update_password_all_casa_admins_path, params: params } let(:params) do { all_casa_admin: { password: "newpassword", password_confirmation: "badmatch" } } end it "does not update the all_casa_admin password" do subject expect(response).to have_http_status(:unprocessable_content) expect(admin.reload.valid_password?("newpassword")).to be false end it "does not call UserMailer.password_changed_reminder" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(admin).and_return(mailer) expect(mailer).not_to receive(:deliver) subject end it "also responds as json", :aggregate_failures do patch update_password_all_casa_admins_path(format: :json), params: params expect(response).to have_http_status(:unprocessable_content) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to match("Password confirmation doesn't match Password".to_json) end end end end ================================================ FILE: spec/requests/android_app_associations_spec.rb ================================================ require "rails_helper" RSpec.describe "AndroidAppAssociations", type: :request do describe "GET /.well-known/assetlinks.json" do let(:reponse_json) do [ { relation: [ "delegate_permission/common.handle_all_urls" ], target: { namespace: "android_app", package_name: "org.rubyforgood.casa", sha256_cert_fingerprints: ["fingerprint"] } } ].to_json end before do allow(ENV).to receive(:[]).with("ANDROID_CERTIFICATE_FINGERPRINT").and_return("fingeprint") end it "renders a json file" do get "/.well-known/assetlinks.json" expect(response.header["Content-Type"]).to include("application/json") expect(response.body).to match(reponse_json) end end end ================================================ FILE: spec/requests/api/v1/base_spec.rb ================================================ require "rails_helper" RSpec.describe "Base Controller", type: :request do before do base_controller = Class.new(Api::V1::BaseController) do def index render json: {message: "Successfully autenticated"} end end stub_const("BaseController", base_controller) Rails.application.routes.disable_clear_and_finalize = true Rails.application.routes.draw do get "/index", to: "base#index" end end after { Rails.application.reload_routes! } # test authenticate_user! works describe "GET #index" do let(:user) { create(:volunteer) } it "returns http unauthorized if invalid token" do get "/index", headers: {"Authorization" => "Token token=, email=#{user.email}"} expect(response).to have_http_status(:unauthorized) expect(response.body).to eq({message: "Incorrect email or password."}.to_json) end end end ================================================ FILE: spec/requests/api/v1/users/sessions_spec.rb ================================================ require "swagger_helper" RSpec.describe "sessions API", type: :request do let(:casa_org) { create(:casa_org) } let(:volunteer) { create(:volunteer, casa_org: casa_org) } path "/api/v1/users/sign_in" do post "Signs in a user" do tags "Sessions" consumes "application/json" produces "application/json" parameter name: :user, in: :body, schema: { type: :object, properties: { email: {type: :string}, password: {type: :string} }, required: %w[email password] } response "201", "user signed in" do let(:user) { {email: volunteer.email, password: volunteer.password} } schema "$ref" => "#/components/schemas/login_success" run_test! do |response| parsed_response = JSON.parse(response.body) expect(parsed_response["api_token"]).not_to be_nil expect(parsed_response["refresh_token"]).not_to be_nil expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.status).to eq(201) end end response "401", "invalid credentials" do let(:user) { {email: "foo", password: "bar"} } schema "$ref" => "#/components/schemas/login_failure" run_test! do |response| expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to eq({message: "Incorrect email or password."}.to_json) expect(response.status).to eq(401) end end end end path "/api/v1/users/sign_out" do delete "Signs out a user" do tags "Sessions" produces "application/json" parameter name: :authorization, in: :header, type: :string, required: true let(:api_token) { create(:api_credential, user: volunteer).return_new_api_token![:api_token] } response "200", "user signed out" do let(:authorization) { "Bearer #{api_token}" } schema "$ref" => "#/components/schemas/sign_out" run_test! do |response| expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to eq({message: "Signed out successfully."}.to_json) expect(response.status).to eq(200) end end response "401", "unauthorized" do let(:authorization) { "Bearer foo" } schema "$ref" => "#/components/schemas/sign_out" run_test! do |response| expect(response.content_type).to eq("application/json; charset=utf-8") expect(response.body).to eq({message: "An error occured when signing out."}.to_json) expect(response.status).to eq(401) end end end end end ================================================ FILE: spec/requests/banners_spec.rb ================================================ require "rails_helper" RSpec.describe "Banners", type: :request do let!(:casa_org) { create(:casa_org) } let!(:active_banner) { create(:banner, casa_org: casa_org) } let(:volunteer) { create(:volunteer, casa_org: casa_org) } context "when user dismisses a banner" do subject do get dismiss_banner_path(active_banner) end it "sets session variable" do sign_in volunteer subject expect(session[:dismissed_banner]).to eq active_banner.id end it "does not display banner on page reloads" do sign_in volunteer get casa_cases_path expect(response.body).to include "Please fill out this survey" subject get casa_cases_path expect(response.body).not_to include "Please fill out this survey" end context "when user logs out and back in" do it "nils out session variable" do sign_in volunteer subject get destroy_user_session_path sign_in volunteer expect(session[:dismissed_banner]).to be_nil end it "displays banner" do sign_in volunteer subject get destroy_user_session_path sign_in volunteer get casa_cases_path expect(response.body).to include "Please fill out this survey" end end end context "when a banner has expires_at" do context "when expires_at is after today" do let!(:active_banner) { create(:banner, casa_org: casa_org, expires_at: 7.days.from_now) } it "displays the banner" do sign_in volunteer get casa_cases_path expect(response.body).to include "Please fill out this survey" end end context "when expires_at is before today" do let!(:active_banner) do banner = create(:banner, casa_org: casa_org, expires_at: nil) banner.update_columns(expires_at: 1.day.ago) end it "does not display the banner" do sign_in volunteer get casa_cases_path expect(response.body).not_to include "Please fill out this survey" end end end context "when creating a banner" do let(:admin) { create(:casa_admin, casa_org: casa_org) } let(:banner_params) do { user: admin, active: false, content: "Test", name: "Test Announcement", expires_at: expires_at } end context "when client timezone is ahead of UTC" do before { travel_to Time.new(2024, 6, 1, 11, 0, 0, "+03:00") } # 08:00 UTC context "when submitted time is behind client but ahead of UTC" do let(:expires_at) { Time.new(2024, 6, 1, 9, 0, 0, "UTC") } # 12:00 +03:00 it "succeeds" do sign_in admin post banners_path, params: {banner: banner_params} expect(response).to redirect_to banners_path end end context "when submitted time is behind client and behind UTC" do let(:expires_at) { Time.new(2024, 6, 1, 7, 0, 0, "UTC") } # 10:00 +03:00 it "fails" do sign_in admin post banners_path, params: {banner: banner_params} expect(response).to render_template "banners/new" expect(response.body).to include "Expires at must take place in the future (after 2024-06-01 08:00:00 UTC)" end end end context "when client timezone is behind UTC" do before { travel_to Time.new(2024, 6, 1, 11, 0, 0, "-04:00") } # 15:00 UTC context "when submitted time is ahead of client and ahead of UTC" do let(:expires_at) { Time.new(2024, 6, 1, 16, 0, 0, "UTC") } # 12:00 -04:00 it "succeeds" do sign_in admin post banners_path, params: {banner: banner_params} expect(response).to redirect_to banners_path end end context "when submitted time is ahead of client but behind UTC" do let(:expires_at) { Time.new(2024, 6, 1, 14, 0, 0, "UTC") } # 10:00 -04:00 it "fails" do sign_in admin post banners_path, params: {banner: banner_params} expect(response).to render_template "banners/new" expect(response.body).to include "Expires at must take place in the future (after 2024-06-01 15:00:00 UTC)" end end end end end ================================================ FILE: spec/requests/bulk_court_dates_spec.rb ================================================ require "rails_helper" RSpec.describe "BulkCourtDates", type: :request do let(:user) { create(:supervisor) } before { sign_in user } describe "GET /new" do subject { get "/bulk_court_dates/new" } it "renders the new template" do subject expect(response).to have_http_status :success expect(response).to render_template :new end end describe "POST /create" do subject { post "/bulk_court_dates", params: } let(:judge) { create :judge } let(:hearing_type) { create :hearing_type } let(:case_count) { 2 } let(:case_group) { create :case_group, case_count:, casa_org: user.casa_org } let(:court_date) { Date.tomorrow } let(:case_court_orders_attributes) { {} } let(:params) do { court_date: { case_group_id: case_group.id, date: Date.tomorrow, court_report_due_date: Date.today, judge_id: judge.id, hearing_type_id: hearing_type.id, case_court_orders_attributes: } } end it "renders the new template on success" do subject expect(response).to have_http_status(:found) expect(response).to redirect_to(new_bulk_court_date_path) end it "adds the court date to each group case" do expect(case_group.casa_cases.count).to be > 1 cc_one = case_group.casa_cases.first cc_two = case_group.casa_cases.last expect { subject } .to change { cc_one.court_dates.count }.by(1) .and change { cc_two.court_dates.count }.by(1) end context "when different casa org's case group" do let(:case_group) { create :case_group, case_count:, casa_org: build(:casa_org) } it "raises ActiveRecord::RecordNotFound" do expect { subject }.to raise_error(ActiveRecord::RecordNotFound) end end context "when court orders are in params" do let(:case_court_orders_attributes) do { "0" => { text: "Some court order", implementation_status: "partially_implemented" }, "1" => { text: "Another court order", implementation_status: "implemented" } } end it "adds the court orders to each group case" do expect(case_group.casa_cases.count).to be > 1 cc_one = case_group.casa_cases.first cc_two = case_group.casa_cases.last expect { subject } .to change { cc_one.case_court_orders.count }.by(case_court_orders_attributes.size) .and change { cc_two.case_court_orders.count }.by(case_court_orders_attributes.size) end end end end ================================================ FILE: spec/requests/casa_admins_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe "/casa_admins", type: :request do describe "GET /casa_admins" do it "is successful" do admins = create_pair(:casa_admin) sign_in admins.first get casa_admins_path expect(response).to be_successful end end describe "GET /casa_admins/:id/edit" do context "logged in as admin user" do context "same org" do it "can successfully access a casa admin edit page" do casa_one = create(:casa_org) casa_admin_one = create(:casa_admin, casa_org: casa_one) sign_in(casa_admin_one) get edit_casa_admin_path(create(:casa_admin, casa_org: casa_one)) expect(response).to be_successful end end context "different org" do it "cannot access a casa admin edit page" do casa_admin = create(:casa_admin) diff_org = create(:casa_org) casa_admin_diff_org = create(:casa_admin, casa_org: diff_org) sign_in casa_admin get edit_casa_admin_path(casa_admin_diff_org) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end context "logged in as a non-admin user" do it "cannot access a casa admin edit page" do sign_in_as_volunteer admin = create(:casa_admin) get edit_casa_admin_path(admin) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a casa admin edit page" do admin = create(:casa_admin) get edit_casa_admin_path(admin) expect(response).to redirect_to new_user_session_path end end end describe "PUT /casa_admins/:id" do context "logged in as admin user" do it "can successfully update a casa admin user", :aggregate_failures do casa_admin = create(:casa_admin) expected_display_name = "Admin 2" expected_phone_number = "+14163218092" sign_in casa_admin put casa_admin_path(casa_admin), params: { casa_admin: { display_name: expected_display_name, phone_number: expected_phone_number } } casa_admin.reload expect(casa_admin.display_name).to eq expected_display_name expect(casa_admin.phone_number).to eq expected_phone_number expect(response).to redirect_to edit_casa_admin_path(casa_admin) expect(response.request.flash[:notice]).to eq "Casa Admin was successfully updated." end it "can update a casa admin user's email and send them a confirmation email", :aggregate_failures do casa_admin = create(:casa_admin) expected_email = "admin2@casa.com" sign_in casa_admin put casa_admin_path(casa_admin), params: { casa_admin: { email: expected_email } } casa_admin.reload expect(response).to have_http_status(:redirect) expect(casa_admin.unconfirmed_email).to eq("admin2@casa.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "also respond as json", :aggregate_failures do casa_admin = create(:casa_admin) expected_display_name = "Admin 2" sign_in casa_admin put casa_admin_path(casa_admin, format: :json), params: { casa_admin: { display_name: expected_display_name } } casa_admin.reload expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) end end context "when logged in as admin, but invalid data" do it "cannot update the casa admin", :aggregate_failures do casa_admin = create(:casa_admin) sign_in casa_admin put casa_admin_path(casa_admin), params: { casa_admin: {email: nil}, phone_number: {phone_number: "dsadw323"} } casa_admin.reload expect(casa_admin.email).not_to eq nil expect(casa_admin.phone_number).not_to eq "dsadw323" expect(response).to render_template :edit end it "also respond as json", :aggregate_failures do casa_admin = create(:casa_admin) sign_in casa_admin put casa_admin_path(casa_admin, format: :json), params: { casa_admin: {email: nil} } expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match("Email can't be blank".to_json) end end context "logged in as a non-admin user" do it "cannot update a casa admin user" do sign_in_as_volunteer admin = create(:casa_admin) put casa_admin_path(admin), params: { casa_admin: { email: "admin@casa.com", display_name: "The admin" } } expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot update a casa admin user" do admin = create(:casa_admin) put casa_admin_path(admin), params: { casa_admin: { email: "admin@casa.com", display_name: "The admin" } } expect(response).to redirect_to new_user_session_path end end end describe "PATCH /activate" do context "when successfully" do it "activates an inactive casa_admin" do casa_admin = create(:casa_admin) casa_admin_other = create(:casa_admin, active: false) sign_in casa_admin patch activate_casa_admin_path(casa_admin_other) casa_admin_other.reload expect(casa_admin_other).to be_active end it "sends an activation email" do casa_admin = create(:casa_admin) casa_admin_inactive = create(:casa_admin, active: false) sign_in casa_admin expect { patch activate_casa_admin_path(casa_admin_inactive) } .to change { ActionMailer::Base.deliveries.count } .by(1) end it "also respond as json", :aggregate_failures do casa_admin = create(:casa_admin) casa_admin_inactive = create(:casa_admin, active: false) sign_in casa_admin patch activate_casa_admin_path(casa_admin_inactive, format: :json) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).to match(casa_admin.reload.active.to_json) end end context "when occurs send errors" do it "redirects to admin edition page" do casa_admin = create(:casa_admin) casa_admin_inactive = create(:casa_admin, active: false) allow(CasaAdminMailer).to receive_message_chain(:account_setup, :deliver) { raise Errno::ECONNREFUSED } sign_in casa_admin patch activate_casa_admin_path(casa_admin_inactive) expect(response).to redirect_to(edit_casa_admin_path(casa_admin_inactive)) end it "shows error message" do casa_admin = create(:casa_admin) casa_admin_inactive = create(:casa_admin, active: false) allow(CasaAdminMailer).to receive_message_chain(:account_setup, :deliver) { raise Errno::ECONNREFUSED } sign_in casa_admin patch activate_casa_admin_path(casa_admin_inactive) expect(flash[:alert]).to eq("Email not sent.") end it "also respond as json", :aggregate_failures do casa_admin = create(:casa_admin) casa_admin_inactive = create(:casa_admin, active: false) allow_any_instance_of(CasaAdmin).to receive(:activate).and_return(false) allow_any_instance_of(CasaAdmin).to receive_message_chain(:errors, :full_messages) .and_return ["Error message test"] sign_in casa_admin patch activate_casa_admin_path(casa_admin_inactive, format: :json) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match("Error message test".to_json) end end end describe "PATCH /casa_admins/:id/deactivate" do context "logged in as admin user" do context "when successfully" do it "can successfully deactivate a casa admin user" do casa_admin = create(:casa_admin) casa_admin_other = create(:casa_admin, active: true) sign_in casa_admin patch deactivate_casa_admin_path(casa_admin_other) casa_admin_other.reload expect(casa_admin_other).not_to be_active expect(response).to redirect_to edit_casa_admin_path(casa_admin_other) expect(response.request.flash[:notice]).to eq "Admin was deactivated." end it "sends a deactivation email" do casa_admin = create(:casa_admin) casa_admin_active = create(:casa_admin, active: true) sign_in casa_admin expect { patch deactivate_casa_admin_path(casa_admin_active) } .to change { ActionMailer::Base.deliveries.count } .by(1) end it "also respond as json", :aggregate_failures do casa_admin = create(:casa_admin) casa_admin_active = create(:casa_admin, active: true) sign_in casa_admin patch deactivate_casa_admin_path(casa_admin_active, format: :json) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).to match(casa_admin.reload.active.to_json) end end context "when occurs send errors" do it "redirects to admin edit page" do casa_admin = create(:casa_admin) casa_admin_active = create(:casa_admin, active: true) allow(CasaAdminMailer).to receive_message_chain(:deactivation, :deliver) { raise Errno::ECONNREFUSED } sign_in casa_admin patch deactivate_casa_admin_path(casa_admin_active) expect(response).to redirect_to(edit_casa_admin_path(casa_admin_active)) end it "shows error message" do casa_admin = create(:casa_admin) casa_admin_active = create(:casa_admin, active: true) allow(CasaAdminMailer).to receive_message_chain(:deactivation, :deliver) { raise Errno::ECONNREFUSED } sign_in casa_admin patch deactivate_casa_admin_path(casa_admin_active) expect(flash[:alert]).to eq("Email not sent.") end it "also respond as json", :aggregate_failures do casa_admin = create(:casa_admin) casa_admin_active = create(:casa_admin, active: true) allow_any_instance_of(CasaAdmin).to receive(:deactivate).and_return(false) allow_any_instance_of(CasaAdmin).to receive_message_chain(:errors, :full_messages) .and_return ["Error message test"] sign_in casa_admin patch deactivate_casa_admin_path(casa_admin_active, format: :json) expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match("Error message test".to_json) end end end context "logged in as a non-admin user" do it "cannot update a casa admin user" do admin = create(:casa_admin) sign_in_as_volunteer patch deactivate_casa_admin_path(admin) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot update a casa admin user" do admin = create(:casa_admin) patch deactivate_casa_admin_path(admin) expect(response).to redirect_to new_user_session_path end end end describe "PATCH /resend_invitation" do it "resends an invitation email" do casa_admin = create(:casa_admin, active: true) sign_in casa_admin expect(casa_admin.invitation_created_at.present?).to eq(false) patch resend_invitation_casa_admin_path(casa_admin) casa_admin.reload expect(casa_admin.invitation_created_at.present?).to eq(true) expect(Devise.mailer.deliveries.count).to eq(1) expect(Devise.mailer.deliveries.first.subject).to eq(I18n.t("devise.mailer.invitation_instructions.subject")) expect(response).to redirect_to(edit_casa_admin_path(casa_admin)) end end describe "POST /casa_admins" do context "when successfully" do it "creates a new casa_admin" do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) params = attributes_for(:casa_admin) sign_in admin expect { post casa_admins_path, params: {casa_admin: params} }.to change(CasaAdmin, :count).by(1) expect(response).to redirect_to casa_admins_path expect(flash[:notice]).to eq("New admin created successfully.") end it "also respond to json", :aggregate_failures do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) params = attributes_for(:casa_admin) sign_in admin post casa_admins_path(format: :json), params: {casa_admin: params} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:created) expect(response.body).to match(params[:display_name].to_json) end end context "when creating new admin" do it "sends SMS when phone number is provided" do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) twilio_activation_success_stub = WebMockHelper.twilio_activation_success_stub("admin") short_io_stub = WebMockHelper.short_io_stub_sms params = attributes_for(:casa_admin) params[:phone_number] = "+12222222222" sign_in admin post casa_admins_path, params: {casa_admin: params} expect(short_io_stub).to have_been_requested.times(2) expect(twilio_activation_success_stub).to have_been_requested.times(1) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New admin created successfully. SMS has been sent!/) end it "does not send SMS when phone number not given" do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) twilio_activation_success_stub = WebMockHelper.twilio_activation_success_stub("admin") twilio_activation_error_stub = WebMockHelper.twilio_activation_error_stub("admin") short_io_stub = WebMockHelper.short_io_stub_sms params = attributes_for(:casa_admin) sign_in admin post casa_admins_path, params: {casa_admin: params} expect(short_io_stub).to have_been_requested.times(0) expect(twilio_activation_success_stub).to have_been_requested.times(0) expect(twilio_activation_error_stub).to have_been_requested.times(0) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New admin created successfully./) end it "does not send SMS when Twilio has an error" do org = create(:casa_org, twilio_account_sid: "articuno31", twilio_enabled: true) admin = build(:casa_admin, casa_org: org) short_io_stub = WebMockHelper.short_io_stub_sms twilio_activation_error_stub = WebMockHelper.twilio_activation_error_stub("admin") params = attributes_for(:casa_admin) params[:phone_number] = "+12222222222" sign_in admin post casa_admins_path, params: {casa_admin: params} expect(short_io_stub).to have_been_requested.times(2) # TODO: why is this called at all? expect(twilio_activation_error_stub).to have_been_requested.times(1) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New admin created successfully. SMS not sent. Error: ./) end it "does not send SMS when Twilio is not enabled" do org = create(:casa_org, twilio_enabled: false) admin = build(:casa_admin, casa_org: org) params = attributes_for(:casa_admin) params[:phone_number] = "+12222222222" short_io_stub = WebMockHelper.short_io_stub_sms sign_in admin post casa_admins_path, params: {casa_admin: params} expect(short_io_stub).to have_been_requested.times(2) # TODO: why is this called at all? expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New admin created successfully./) end end context "when failure" do it "does not create a new casa_admin" do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) allow_any_instance_of(CreateCasaAdminService).to receive(:create!).and_raise(ActiveRecord::RecordInvalid) params = attributes_for(:casa_admin) sign_in admin expect { post casa_admins_path, params: {casa_admin: params} }.not_to change(CasaAdmin, :count) expect(response).to render_template :new end it "also responds to json", :aggregate_failures do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) sign_in admin casa_admin = instance_spy(CasaAdmin) allow(casa_admin).to receive_message_chain(:errors, :full_messages).and_return(["Some error message"]) allow_any_instance_of(CreateCasaAdminService).to receive(:casa_admin).and_return(casa_admin) allow_any_instance_of(CreateCasaAdminService).to receive(:create!) .and_raise(ActiveRecord::RecordInvalid) params = attributes_for(:casa_admin) post casa_admins_path(format: :json), params: {casa_admin: params} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match("Some error message".to_json) end end end describe "PATCH /change_to_supervisor" do context "when signed in as an admin" do it "changes the admin to a supervisor" do casa_admin = create(:casa_admin) sign_in_as_admin patch change_to_supervisor_casa_admin_path(casa_admin) expect(response).to redirect_to(edit_supervisor_path(casa_admin)) # find the user after their type has changed user = User.find(casa_admin.id) expect(user).not_to be_casa_admin expect(user).to be_supervisor end end context "when signed in as a supervisor" do it "does not change the admin to a supervisor" do casa_admin = create(:casa_admin) supervisor = create(:supervisor) sign_in supervisor patch change_to_supervisor_casa_admin_path(casa_admin) casa_admin.reload expect(casa_admin).to be_casa_admin expect(casa_admin).not_to be_supervisor end end end end ================================================ FILE: spec/requests/casa_cases_spec.rb ================================================ require "rails_helper" RSpec.describe "/casa_cases", type: :request do let(:date_in_care) { Date.today } let(:organization) { build(:casa_org) } let(:group) { build(:contact_type_group) } let(:volunteer) { create(:volunteer) } let(:type1) { create(:contact_type, contact_type_group: group) } let(:pre_transition_aged_youth_age) { Date.current - 14.years } let(:valid_attributes) do { case_number: "1234", birth_month_year_youth: pre_transition_aged_youth_age, "date_in_care(3i)": date_in_care.day, "date_in_care(2i)": date_in_care.month, "date_in_care(1i)": date_in_care.year, assigned_volunteer_id: volunteer.id, casa_org_id: organization.id, contact_type_ids: [type1.id], case_assignments_attributes: {"0": {volunteer_id: volunteer.id.to_s}} } end let(:invalid_attributes) { {case_number: nil, birth_month_year_youth: nil} } let(:casa_case) { create(:casa_case, casa_org: organization, case_number: "111") } let(:texts) { ["1-New Court Order Text One", "0-New Court Order Text Two"] } let(:implementation_statuses) { ["unimplemented", nil] } let(:orders_attributes) do { "0" => {text: texts[0], implementation_status: implementation_statuses[0]}, "1" => {text: texts[1], implementation_status: implementation_statuses[1]} } end before { sign_in user } describe "as an admin" do let(:user) { create(:casa_admin, casa_org: organization) } describe "GET /index" do it "renders a successful response" do create(:casa_case) get casa_cases_url expect(response).to be_successful end it "shows all my organization's cases" do volunteer_1 = create(:volunteer, casa_org: user.casa_org) volunteer_2 = create(:volunteer, casa_org: user.casa_org) create(:case_assignment, volunteer: volunteer_1) create(:case_assignment, volunteer: volunteer_2) get casa_cases_url expect(response.body).to include(volunteer_1.casa_cases.first.case_number) expect(response.body).to include(volunteer_2.casa_cases.first.case_number) end it "doesn't show other organizations' cases" do my_case_assignment = build(:case_assignment, casa_org: user.casa_org) different_org = build(:casa_org) not_my_case_assignment = build_stubbed(:case_assignment, casa_org: different_org) get casa_cases_url expect(response.body).to include(my_case_assignment.casa_case.case_number) expect(response.body).not_to include(not_my_case_assignment.casa_case.case_number) end end describe "GET /show" do it "renders a successful response" do get casa_case_url(casa_case) expect(response).to be_successful end it "fails across organizations" do other_org = build(:casa_org) other_case = create(:casa_case, casa_org: other_org) get casa_case_url(other_case) expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end context "when exporting a csv" do subject(:casa_case_show) { get casa_case_path(casa_case, format: :csv) } let(:current_time) { Time.now.strftime("%Y-%m-%d") } it "generates a csv" do casa_case_show expect(response).to have_http_status :ok expect(response.headers["Content-Type"]).to include "text/csv" expect(response.headers["Content-Disposition"]).to include "case-contacts-#{current_time}" end it "adds the correct headers to the csv" do casa_case_show csv_headers = ["Internal Contact Number", "Duration Minutes", "Contact Types", "Contact Made", "Contact Medium", "Occurred At", "Added To System At", "Miles Driven", "Wants Driving Reimbursement", "Casa Case Number", "Creator Email", "Creator Name", "Supervisor Name", "Case Contact Notes"] csv_headers.each { |header| expect(response.body).to include header } end end context "when exporting a xlsx" do subject(:casa_case_show) { get casa_case_path(casa_case, format: :xlsx) } let(:current_time) { Time.now.strftime("%Y-%m-%d") } it "generates a xlsx file" do casa_case_show expect(response).to have_http_status :ok expect(response.headers["Content-Type"]).to include "application/vnd.openxmlformats" expect(response.headers["Content-Disposition"]).to include "case-contacts-#{current_time}" end end end describe "GET /new" do it "renders a successful response" do get new_casa_case_url expect(response).to be_successful end end describe "GET /edit" do it "render a successful response" do get edit_casa_case_url(casa_case) expect(response).to be_successful end it "fails across organizations" do other_org = build(:casa_org) other_case = create(:casa_case, casa_org: other_org) get edit_casa_case_url(other_case) expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "POST /create" do context "with valid parameters" do it "creates a new CasaCase" do expect { post casa_cases_url, params: {casa_case: valid_attributes} }.to change( CasaCase, :count ).by(1) end it "redirects to the created casa_case" do post casa_cases_url, params: {casa_case: valid_attributes} expect(response).to redirect_to(casa_case_url(CasaCase.last)) end it "sets fields correctly" do post casa_cases_url, params: {casa_case: valid_attributes} casa_case = CasaCase.last expect(casa_case.casa_org).to eq organization expect(casa_case.birth_month_year_youth).to eq pre_transition_aged_youth_age expect(casa_case.date_in_care.to_date).to eq date_in_care end it "also responds as json", :aggregate_failures do post casa_cases_url(format: :json), params: {casa_case: valid_attributes} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:created) expect(response.body).to match(valid_attributes[:case_number].to_json) end context "with valid assigned_volunteer_id" do it "creates a case assignment" do expect { post casa_cases_url, params: {casa_case: valid_attributes} }.to change( CaseAssignment, :count ).by(1) end end context "without an assigned_volunteer_id" do let(:valid_attributes) do { case_number: "1234", birth_month_year_youth: pre_transition_aged_youth_age, "date_in_care(3i)": date_in_care.day, "date_in_care(2i)": date_in_care.month, "date_in_care(1i)": date_in_care.year, assigned_volunteer_id: nil, casa_org_id: organization.id, contact_type_ids: [type1.id] } end it "does not create a case assignment" do expect { post casa_cases_url, params: {casa_case: valid_attributes} }.not_to change( CaseAssignment, :count ) end end end it "only creates cases within user's organizations" do other_org = build(:casa_org) attributes = { case_number: "1234", birth_month_year_youth: pre_transition_aged_youth_age, casa_org_id: other_org.id, contact_type_ids: [type1.id] } expect { post casa_cases_url, params: {casa_case: attributes} }.to( change { [organization.casa_cases.count, other_org.casa_cases.count] }.from([0, 0]).to([1, 0]) ) end 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} }.not_to change( CasaCase, :count ) end it "renders an unprocessable entity response (i.e. to display the 'new' template)" do post casa_cases_url, params: {casa_case: invalid_attributes} expect(response).to have_http_status(:unprocessable_content) end it "also respond to json", :aggregate_failures do post casa_cases_url(format: :json), params: {casa_case: invalid_attributes} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expected_response_body = [ "Birth month year youth can't be blank", "Case number can't be blank", "Casa case contact types : At least one contact type must be selected" ].to_json expect(response.body).to eq(expected_response_body) end end context "with case_court_orders_attributes being passed as a parameter" do let(:invalid_params) do attributes = valid_attributes attributes[:case_court_orders_attributes] = orders_attributes {casa_case: attributes} end it "Creates a new CasaCase, but no CaseCourtOrder" 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( CaseCourtOrder, :count ) end it "renders an unprocessable entity response (i.e. to display the 'new' template)" do post casa_cases_url, params: {casa_case: invalid_params} expect(response).to have_http_status(:unprocessable_content) end end end end describe "PATCH /update" do let(:group) { build(:contact_type_group) } let(:type1) { create(:contact_type, contact_type_group: group) } let(:new_attributes) do { case_number: "12345", case_court_orders_attributes: orders_attributes } end let(:new_attributes2) do { case_number: "12345", case_court_orders_attributes: orders_attributes, contact_type_ids: [type1.id] } end context "with valid parameters" do it "updates the requested casa_case" do patch casa_case_url(casa_case), params: {casa_case: new_attributes2} casa_case.reload expect(casa_case.case_number).to eq "12345" expect(casa_case.slug).to eq "12345" expect(casa_case.case_court_orders[0].text).to eq texts[0] expect(casa_case.case_court_orders[0].implementation_status).to eq implementation_statuses[0] expect(casa_case.case_court_orders[1].text).to eq texts[1] expect(casa_case.case_court_orders[1].implementation_status).to eq implementation_statuses[1] end it "redirects to the casa_case" do patch casa_case_url(casa_case), params: {casa_case: new_attributes2} casa_case.reload expect(response).to redirect_to(edit_casa_case_path) end it "displays changed attributes" do patch casa_case_url(casa_case), params: {casa_case: new_attributes2} expect(flash[:notice]).to eq("CASA case was successfully updated.
    • Changed Case number
    • [\"#{type1.name}\"] Contact types added
    • 2 Court orders added or updated
    ") end it "also responds as json", :aggregate_failures do patch casa_case_url(casa_case, format: :json), params: {casa_case: new_attributes2} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).to match(new_attributes2[:case_number].to_json) end end context "with invalid parameters" do it "renders an unprocessable entity response displaying the edit template" do patch casa_case_url(casa_case), params: {casa_case: invalid_attributes} expect(response).to have_http_status(:unprocessable_content) end it "also responds as json", :aggregate_failures do patch casa_case_url(casa_case, format: :json), params: {casa_case: invalid_attributes} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match(["Case number can't be blank"].to_json) end end describe "court orders" do context "when the user tries to make an existing order empty" do let(:orders_updated) do { case_court_orders_attributes: { "0" => { text: "New Court Order Text One Updated", implementation_status: :unimplemented }, "1" => { text: "" } } } end before do patch casa_case_url(casa_case), params: {casa_case: new_attributes2} casa_case.reload orders_updated[:case_court_orders_attributes]["0"][:id] = casa_case.case_court_orders[0].id orders_updated[:case_court_orders_attributes]["1"][:id] = casa_case.case_court_orders[1].id end it "does not update the first court order" do expect { patch casa_case_url(casa_case), params: {casa_case: orders_updated} }.not_to( change { casa_case.reload.case_court_orders[0].text } ) end it "does not update the second court order" do expect { patch casa_case_url(casa_case), params: {casa_case: orders_updated} }.not_to( change { casa_case.reload.case_court_orders[1].text } ) end end end it "does not update across organizations" do other_org = build(:casa_org) other_casa_case = create(:casa_case, case_number: "abc", casa_org: other_org) expect { patch casa_case_url(other_casa_case), params: {casa_case: new_attributes} }.not_to( change { other_casa_case.reload.case_number } ) end end describe "PATCH /casa_cases/:id/deactivate" do let(:casa_case) { create(:casa_case, :active, casa_org: organization, case_number: "111") } let(:params) { {id: casa_case.id} } it "deactivates the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq false end it "redirects to the casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(response).to redirect_to(edit_casa_case_path) end it "flashes success message" do patch deactivate_casa_case_path(casa_case), params: params expect(flash[:notice]).to include("Case #{casa_case.case_number} has been deactivated.") end it "fails across organizations" do other_org = build(:casa_org) other_casa_case = create(:casa_case, casa_org: other_org) patch deactivate_casa_case_path(other_casa_case), params: params expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end it "also responds as json", :aggregate_failures do patch deactivate_casa_case_path(casa_case, format: :json), params: params expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).to match("Case #{casa_case.case_number} has been deactivated.") end context "when deactivation fails" do before do allow_any_instance_of(CasaCase).to receive(:deactivate).and_return(false) end it "does not deactivate the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq true end it "also responds as json", :aggregate_failures do patch deactivate_casa_case_path(casa_case, format: :json), params: params expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match([].to_json) end end end describe "PATCH /casa_cases/:id/reactivate" do let(:casa_case) { create(:casa_case, :inactive, casa_org: organization, case_number: "111") } let(:params) { {id: casa_case.id} } it "reactivates the requested casa_case" do patch reactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq true end it "redirects to the casa_case" do patch reactivate_casa_case_path(casa_case), params: params casa_case.reload expect(response).to redirect_to(edit_casa_case_path) end it "flashes success message" do patch reactivate_casa_case_path(casa_case), params: params expect(flash[:notice]).to include("Case #{casa_case.case_number} has been reactivated.") end it "fails across organizations" do other_org = create(:casa_org) other_casa_case = create(:casa_case, casa_org: other_org) patch reactivate_casa_case_path(other_casa_case), params: params expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end it "also responds as json", :aggregate_failures do patch reactivate_casa_case_path(casa_case, format: :json), params: params expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).to match("Case #{casa_case.case_number} has been reactivated.") end context "when reactivation fails" do before do allow_any_instance_of(CasaCase).to receive(:reactivate).and_return(false) end it "does not reactivate the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq false end it "also responds as json", :aggregate_failures do patch reactivate_casa_case_path(casa_case, format: :json), params: params expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:unprocessable_content) expect(response.body).to match([].to_json) end end end end describe "as a volunteer" do let(:user) { create(:volunteer, casa_org: organization) } let!(:case_assignment) { create(:case_assignment, volunteer: user, casa_case: casa_case) } describe "GET /show" do it "renders a successful response" do get casa_case_url(casa_case) expect(response).to be_successful end it "fails across organizations" do other_org = build(:casa_org) other_case = create(:casa_case, casa_org: other_org) get casa_case_url(other_case) expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "GET /new" do it "denies access and redirects elsewhere" do get new_casa_case_url expect(response).not_to be_successful expect(flash[:notice]).to match(/you are not authorized/) end end describe "POST /create" do context "with valid parameters" do it "denies access" do post casa_cases_url, params: {casa_case: valid_attributes} expect(response).not_to be_successful expect(flash[:notice]).to match(/you are not authorized/) end end end describe "GET /edit" do it "render a successful response" do get edit_casa_case_url(casa_case) expect(response).to be_successful end it "fails across organizations" do other_org = build(:casa_org) other_case = create(:casa_case, casa_org: other_org) get edit_casa_case_url(other_case) expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "PATCH /update" do let(:new_attributes) { { case_number: "12345", court_report_status: :submitted, case_court_orders_attributes: orders_attributes } } context "with valid parameters" do it "updates permitted fields" do patch casa_case_url(casa_case), params: {casa_case: new_attributes} casa_case.reload expect(casa_case.court_report_submitted?).to be_truthy # Not permitted expect(casa_case.case_number).to eq "111" expect(casa_case.case_court_orders.size).to be 2 end it "redirects to the casa_case" do patch casa_case_url(casa_case), params: {casa_case: new_attributes} expect(response).to redirect_to(edit_casa_case_path(casa_case)) end it "also responds as json", :aggregate_failures do patch casa_case_url(casa_case, format: :json), params: {casa_case: new_attributes} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).not_to match(new_attributes[:case_number].to_json) end end it "does not update across organizations" do other_org = build(:casa_org) other_casa_case = create(:casa_case, case_number: "abc", casa_org: other_org) expect { patch casa_case_url(other_casa_case), params: {casa_case: new_attributes} }.not_to( change { other_casa_case.reload.attributes } ) end end describe "GET /index" do it "shows only cases assigned to user" do mine = build(:casa_case, casa_org: organization, case_number: SecureRandom.hex(32)) other = build(:casa_case, casa_org: organization, case_number: SecureRandom.hex(32)) user.casa_cases << mine get casa_cases_url expect(response).to be_successful expect(response.body).to include(mine.case_number) expect(response.body).not_to include(other.case_number) end end describe "PATCH /casa_cases/:id/deactivate" do let(:casa_case) { build(:casa_case, :active, casa_org: organization, case_number: "111") } let(:params) { {id: casa_case.id} } it "does not deactivate the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq true end end describe "PATCH /casa_cases/:id/reactivate" do let(:casa_case) { build(:casa_case, :inactive, casa_org: organization, case_number: "111") } let(:params) { {id: casa_case.id} } it "does not deactivate the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq false end end end describe "as a supervisor" do let(:user) { create(:supervisor, casa_org: organization) } describe "GET /show" do it "renders a successful response" do get casa_case_url(casa_case) expect(response).to be_successful end it "fails across organizations" do other_org = build(:casa_org) other_case = create(:casa_case, casa_org: other_org) get casa_case_url(other_case) expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "GET /new" do it "renders a redirect" do get new_casa_case_url expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "POST /create" do context "with valid parameters" do it "denies access" do post casa_cases_url, params: {casa_case: valid_attributes} expect(response).not_to be_successful expect(flash[:notice]).to match(/you are not authorized/) end end end describe "GET /edit" do it "render a successful response" do get edit_casa_case_url(casa_case) expect(response).to be_successful end it "fails across organizations" do other_org = build(:casa_org) other_case = create(:casa_case, casa_org: other_org) get edit_casa_case_url(other_case) expect(response).to be_redirect expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "PATCH /update" do let(:group) { build(:contact_type_group) } let(:type1) { create(:contact_type, contact_type_group: group) } let(:new_attributes) do { case_number: "12345", court_report_status: :completed, case_court_orders_attributes: orders_attributes } end let(:new_attributes2) do { case_number: "12345", court_report_status: :completed, case_court_orders_attributes: orders_attributes, contact_type_ids: [type1.id] } end context "with valid parameters" do it "updates fields (except case_number)" do patch casa_case_url(casa_case), params: {casa_case: new_attributes2} 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_orders[0].text).to eq texts[0] expect(casa_case.case_court_orders[0].implementation_status).to eq implementation_statuses[0] expect(casa_case.case_court_orders[1].text).to eq texts[1] expect(casa_case.case_court_orders[1].implementation_status).to eq implementation_statuses[1] end it "redirects to the casa_case" do patch casa_case_url(casa_case), params: {casa_case: new_attributes2} expect(response).to redirect_to(edit_casa_case_path(casa_case)) end it "also responds as json", :aggregate_failures do patch casa_case_url(casa_case, format: :json), params: {casa_case: new_attributes2} expect(response.content_type).to eq("application/json; charset=utf-8") expect(response).to have_http_status(:ok) expect(response.body).not_to match(new_attributes[:case_number].to_json) end end it "does not update across organizations" do other_org = build(:casa_org) other_casa_case = create(:casa_case, case_number: "abc", casa_org: other_org) expect { patch casa_case_url(other_casa_case), params: {casa_case: new_attributes} }.not_to( change { other_casa_case.reload.attributes } ) end end describe "GET /index" do it "renders a successful response" do build_stubbed(:casa_case) get casa_cases_url expect(response).to be_successful end end describe "PATCH /casa_cases/:id/deactivate" do let(:casa_case) { create(:casa_case, :active, casa_org: organization, case_number: "111") } let(:params) { {id: casa_case.id} } it "does not deactivate the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq true end end describe "PATCH /casa_cases/:id/reactivate" do let(:casa_case) { create(:casa_case, :inactive, casa_org: organization, case_number: "111") } let(:params) { {id: casa_case.id} } it "does not deactivate the requested casa_case" do patch deactivate_casa_case_path(casa_case), params: params casa_case.reload expect(casa_case.active).to eq false end end end end ================================================ FILE: spec/requests/casa_org_spec.rb ================================================ require "rails_helper" RSpec.describe "CasaOrg", type: :request do let(:casa_org) { build(:casa_org) } let(:casa_case) { build_stubbed(:casa_case, casa_org: casa_org) } before { stub_twilio sign_in create(:casa_admin, casa_org: casa_org) } describe "GET /edit" do subject(:request) do get edit_casa_org_url(casa_org) response end it { is_expected.to be_successful } end describe "PATCH /update" do context "with valid parameters" do subject(:request) do patch casa_org_url(casa_org), params: {casa_org: attributes} response end let(:attributes) do { name: "name", display_name: "display_name", address: "address", twilio_account_sid: "articuno34", twilio_api_key_sid: "Aladdin", twilio_api_key_secret: "open sesame", twilio_phone_number: "+12223334444", show_driving_reimbursement: "1", additional_expenses_enabled: "1" } end it "updates the requested casa_org" do request expect(casa_org.reload.name).to eq "name" expect(casa_org.display_name).to eq "display_name" expect(casa_org.address).to eq "address" expect(casa_org.twilio_phone_number).to eq "+12223334444" expect(casa_org.show_driving_reimbursement).to be true expect(casa_org.additional_expenses_enabled).to be true end describe "on logo update" do subject(:request) do patch casa_org_url(casa_org), params: params response end let(:logo) { fixture_file_upload "company_logo.png", "image/png" } context "with a new logo" do let(:params) { {casa_org: {logo: logo}} } it "uploads the company logo" do expect { request }.to change(ActiveStorage::Attachment, :count).by(1) end end context "with no logo" do let(:params) { {casa_org: {name: "name"}} } it "does not revert logo to default" do casa_org.update(logo: logo) expect { request }.not_to change(ActiveStorage::Attachment, :count) end end end context "and html format" do it { is_expected.to redirect_to(edit_casa_org_url) } it "shows the correct flash message" do request expect(flash[:notice]).to eq("CASA organization was successfully updated.") end end context "and json format" do subject(:request) do patch casa_org_url(casa_org, format: :json), params: {casa_org: attributes} response end it { is_expected.to have_http_status(:ok) } it "returns correct payload", :aggregate_failures do response_data = request.body expect(response_data).to match("display_name".to_json) end end end context "with invalid parameters" do subject(:request) do patch casa_org_url(casa_org), params: params response end let(:params) { {casa_org: {name: nil}} } it "does not update the requested casa_org" do expect { request }.not_to change { casa_org.reload.name } end context "and html format" do it { is_expected.to have_http_status(:unprocessable_content) } it "renders the edit template" do expect(request.body).to match(/error_explanation/) end end context "and json format" do subject(:request) do patch casa_org_url(casa_org, format: :json), params: params response end it { is_expected.to have_http_status(:unprocessable_content) } it "returns correct payload" do response_data = request.body expect(response_data).to match("Name can't be blank".to_json) end end end end end ================================================ FILE: spec/requests/case_assignments_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_assignments", type: :request do let(:casa_org) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let(:volunteer) { create(:volunteer, casa_org: casa_org) } let(:casa_case) { create(:casa_case, casa_org: casa_org) } describe "POST /create" do before { sign_in admin } it "authorizes action" do expect_any_instance_of(CaseAssignmentsController).to receive(:authorize).with(CaseAssignment).and_call_original post case_assignments_url(volunteer_id: volunteer.id), params: {case_assignment: {casa_case_id: casa_case.id}} end context "when the volunteer has been previously assigned to the casa_case" do subject(:request) do post case_assignments_url(casa_case_id: casa_case.id), params: params response end let!(:case_assignment) { create(:case_assignment, active: false, volunteer: volunteer, casa_case: casa_case) } let(:params) { {case_assignment: {volunteer_id: volunteer.id}} } it "reassigns the volunteer to the casa_case" do expect { request }.to change { casa_case.case_assignments.first.active }.from(false).to(true) end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.notice).to eq "Volunteer reassigned to case" end context "when missing params" do let(:params) { {case_assignment: {volunteer_id: ""}} } it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.alert).to match(/Unable to assign volunteer to case/) end end end context "when the case assignment parent is a volunteer" do subject(:request) do post case_assignments_url(volunteer_id: volunteer.id), params: params response end let(:params) { {case_assignment: {casa_case_id: casa_case.id}} } it "creates a new case assignment for the volunteer" do expect { request }.to change(volunteer.casa_cases, :count).by(1) end it { is_expected.to redirect_to edit_volunteer_path(volunteer) } it "sets flash message correctly" do request expect(flash.notice).to eq "Volunteer assigned to case" end context "when missing params" do let(:params) { {case_assignment: {volunteer_id: ""}} } it { is_expected.to redirect_to edit_volunteer_path(volunteer) } it "sets flash message correctly" do request expect(flash.alert).to match(/Unable to assign volunteer to case/) end end end context "when the case assignment parent is a casa_case" do subject(:request) do post case_assignments_url(casa_case_id: casa_case.id), params: params response end let(:params) { {case_assignment: {volunteer_id: volunteer.id}} } it "creates a new case assignment for the casa_case" do expect { request }.to change(casa_case.volunteers, :count).by(1) end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.notice).to eq "Volunteer assigned to case" end context "when missing params" do let(:params) { {case_assignment: {volunteer_id: ""}} } it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.alert).to match(/Unable to assign volunteer to case/) end end end describe "with another org params" do subject(:request) do post url, params: params response end let(:other_org) { build(:casa_org) } context "when the case belongs to another organization" do let(:other_casa_case) { create(:casa_case, casa_org: other_org) } let(:url) { case_assignments_url(casa_case_id: other_casa_case.id) } let(:params) { {case_assignment: {volunteer_id: volunteer.id}} } it "does not create a case assignment" do expect { request }.not_to change(other_casa_case.volunteers, :count) end end context "when the volunteer belongs to another organization" do let(:other_volunteer) { build_stubbed(:volunteer, casa_org: other_org) } let(:url) { case_assignments_url(casa_case_id: casa_case.id) } let(:params) { {case_assignment: {volunteer_id: other_volunteer.id}} } it "does not create a case assignment" do expect { request }.not_to change(casa_case.volunteers, :count) end end end end describe "DELETE /destroy" do before { sign_in admin } let!(:assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } it "authorizes action" do expect_any_instance_of(CaseAssignmentsController).to receive(:authorize).with(assignment).and_call_original delete case_assignment_url(assignment, volunteer_id: volunteer.id) end context "when the case assignment parent is a volunteer" do subject(:request) do delete case_assignment_url(assignment, volunteer_id: volunteer.id) response end it "destroys the case assignment from the volunteer" do expect { request }.to change(volunteer.casa_cases, :count).by(-1) end it { is_expected.to redirect_to edit_volunteer_path(volunteer) } end context "when the case assignment parent is a casa_case" do subject(:request) do delete case_assignment_url(assignment, casa_case_id: casa_case.id) response end it "destroys the case assignment from the casa_case" do expect { request }.to change(casa_case.volunteers, :count).by(-1) end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } end context "when the case belongs to another organization" do subject(:request) do delete case_assignment_url(assignment, casa_case_id: other_casa_case.id) response end let(:other_org) { build(:casa_org) } let(:other_casa_case) { create(:casa_case, casa_org: other_org) } let!(:assignment) { create(:case_assignment, casa_case: other_casa_case) } it "does not destroy the case assignment" do expect { request }.not_to change(other_casa_case.volunteers, :count).from(1) end it { is_expected.to be_not_found } end end describe "PATCH /unassign" do subject(:request) do patch unassign_case_assignment_url(assignment, redirect_to_path: redirect_to_path) response end before { sign_in admin } let(:assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } let(:redirect_to_path) { "" } it "authorizes action" do expect_any_instance_of(CaseAssignmentsController).to( receive(:authorize).with(assignment, :unassign?).and_call_original ) request end it "deactivates the case assignment" do expect { request }.to change { assignment.reload.active? }.to(false) end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.notice).to eq "Volunteer was unassigned from Case #{casa_case.case_number}." end context "when request format is json" do subject(:request) do patch unassign_case_assignment_url(assignment, format: :json) response end it "sets body message correctly" do response_body = request.body expect(response_body).to eq "Volunteer was unassigned from Case #{casa_case.case_number}." end end context "when redirect_to_path is volunteer" do let(:redirect_to_path) { "volunteer" } it { is_expected.to redirect_to edit_volunteer_path(volunteer) } end context "when assignment belongs to another organization" do let(:other_org) { build(:casa_org) } let(:other_casa_case) { create(:casa_case, casa_org: other_org) } let(:assignment) { create(:case_assignment, casa_case: other_casa_case) } it "does not deactivate the case assignment" do expect { request }.not_to change { assignment.reload.active? } end end end describe "PATCH /show_hide_contacts" do subject(:request) do patch show_hide_contacts_case_assignment_path(assignment) response end before { sign_in admin } let(:assignment) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false) } it "authorizes action" do expect_any_instance_of(CaseAssignmentsController).to( receive(:authorize).with(assignment, :show_or_hide_contacts?).and_call_original ) request end context "when case contacts are visible" do it "toggles to hide case contacts" do expect { request }.to change { assignment.reload.hide_old_contacts? } end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.notice).to eq "Old Case Contacts created by #{volunteer.display_name} were successfully hidden." end end context "when case contacts are hidden" do let(:assignment) do create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: false, hide_old_contacts: true) end it "toggles to show case contacts" do expect { request }.to change { assignment.reload.hide_old_contacts? } end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.notice).to eq "Old Case Contacts created by #{volunteer.display_name} are now visible." end end # Note: we don't expect this endpoint to be exposed in the UI if the case is inactive context "when the case_assignment is active" do let(:assignment) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer, active: true) } it "does not toggle contacts visibility" do expect { request }.not_to change { assignment.reload.hide_old_contacts? } end it "redirects to root with an authorization failure message" do request expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to match "not authorized" end end end describe "PATCH /reimbursement" do subject(:request) do patch reimbursement_case_assignment_url(assignment) response end before { sign_in admin } let(:assignment) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer) } it "authorizes action" do expect_any_instance_of(CaseAssignmentsController).to( receive(:authorize).with(assignment, :reimbursement?).and_call_original ) request end it "toggles allow_reimbursement" do expect { request }.to change { assignment.reload.allow_reimbursement } end it { is_expected.to redirect_to edit_casa_case_path(casa_case) } it "sets flash message correctly" do request expect(flash.notice).to eq "Volunteer allow reimbursement changed from Case #{casa_case.case_number}." end end end ================================================ FILE: spec/requests/case_contact_reports_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_contact_reports", type: :request do let!(:case_contact) { build(:case_contact) } before do travel_to Time.zone.local(2020, 1, 1) sign_in user end describe "GET /case_contact_reports" do context "as volunteer" do let(:user) { build(:volunteer) } it "cannot view reports" do get case_contact_reports_url(format: :csv), params: {report: {}} expect(response).to redirect_to root_path end end shared_examples "can view reports" do context "with start_date and end_date" do let(:case_contact_report_params) do { start_date: 1.month.ago, end_date: Date.today } end it "renders a csv file to download" do get case_contact_reports_url(format: :csv), params: {report: {start_date: 1.month.ago, end_date: Date.today}} expect(response).to be_successful expect( response.headers["Content-Disposition"] ).to include 'attachment; filename="case-contacts-report-1577836800.csv' end end context "without start_date and end_date" do it "renders a csv file to download" do get case_contact_reports_url(format: :csv), params: {report: {start_date: "", end_date: ""}} expect(response).to be_successful expect( response.headers["Content-Disposition"] ).to include 'attachment; filename="case-contacts-report-1577836800.csv' end end context "with supervisor_ids filter" do it "renders csv with only the volunteer" do volunteer = create(:volunteer) casa_case = create(:casa_case, casa_org: volunteer.casa_org) contact = create(:case_contact, creator_id: volunteer.id, casa_case: casa_case) build_stubbed(:case_contact, creator_id: user.id, casa_case: casa_case) get case_contact_reports_url(format: :csv), params: {report: {creator_ids: [volunteer.id]}} expect(response).to be_successful expect( response.headers["Content-Disposition"] ).to include 'attachment; filename="case-contacts-report-' expect(response.body).to match(/^#{contact.id},/) expect(response.body.lines.length).to eq(2) end end context "casa_case_ids filter" do let!(:casa_case) { create(:casa_case) } let!(:case_contacts) { create_list(:case_contact, 3, casa_case: casa_case) } before { create_list(:case_contact, 5) } it "returns success with proper headers" do get case_contact_reports_url(format: :csv), params: {report: {casa_case_ids: [casa_case.id]}} expect(response).to be_successful expect( response.headers["Content-Disposition"] ).to include 'attachment; filename="case-contacts-report-' end context "when filter is provided" do it "renders csv with contacts from the casa cases" do get case_contact_reports_url(format: :csv), params: {report: {casa_case_ids: [casa_case.id]}} expect(response.body.lines.length).to eq(4) case_contacts.each do |contact| expect(response.body).to match(/^#{contact.id}/) end end end context "when filter not provided" do it "renders a csv with all case contacts" do get case_contact_reports_url(format: :csv), params: {report: {casa_case_ids: nil}} expect(response.body.lines.length).to eq(9) CaseContact.all.pluck(:id).each do |id| expect(response.body).to match(/^#{id}/) end end end end end context "as supervisor" do it_behaves_like "can view reports" do let(:user) { build(:supervisor) } end end context "as casa_admin" do it_behaves_like "can view reports" do let(:user) { build(:casa_admin) } end let(:user) { build(:casa_admin) } it "passes in casa_org_id to CaseContractReport" do allow(CaseContactReport).to receive(:new).and_return([]) get case_contact_reports_url(format: :csv), params: {report: {creator_ids: [user.id]}} expect(CaseContactReport).to have_received(:new) .with(hash_including(casa_org_id: user.casa_org_id)) end end end def case_contact_report_params { start_date: 1.month.ago, end_date: Date.today } end end ================================================ FILE: spec/requests/case_contacts/case_contacts_new_design_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_contacts_new_design", type: :request do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } before { sign_in admin } context "when new_case_contact_table flag is disabled" do before do allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(false) end describe "GET /index" do it "redirects to case_contacts_path" do get case_contacts_new_design_path expect(response).to redirect_to(case_contacts_path) end it "sets an alert message" do get case_contacts_new_design_path expect(flash[:alert]).to eq("This feature is not available.") end end end context "when new_case_contact_table flag is enabled" do before do allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(true) end describe "GET /index" do subject(:request) do get case_contacts_new_design_path response end let!(:casa_case) { create(:casa_case, casa_org: organization) } let!(:past_contact) { create(:case_contact, :active, casa_case: casa_case, occurred_at: 3.weeks.ago) } let!(:recent_contact) { create(:case_contact, :active, casa_case: casa_case, occurred_at: 3.days.ago) } let!(:draft_contact) { create(:case_contact, casa_case: casa_case, occurred_at: 5.days.ago, status: "started") } it { is_expected.to have_http_status(:success) } it "lists exactly two active contacts and one draft" do doc = Nokogiri::HTML(request.body) case_contact_rows = doc.css('[data-testid="case_contact-row"]') expect(case_contact_rows.size).to eq(3) end it "shows the draft badge exactly once" do doc = Nokogiri::HTML(request.body) expect(doc.css('[data-testid="draft-badge"]').count).to eq(1) end it "orders contacts by occurred_at desc" do body = request.body recent_index = body.index(I18n.l(recent_contact.occurred_at, format: :full)) past_index = body.index(I18n.l(past_contact.occurred_at, format: :full)) expect(recent_index).to be < past_index end end describe "POST /datatable" do let!(:casa_case) { create(:casa_case, casa_org: organization) } let!(:case_contact) { create(:case_contact, :active, casa_case: casa_case, occurred_at: 3.days.ago) } let(:datatable_params) do { draw: "1", start: "0", length: "10", search: {value: ""}, order: {"0" => {column: "0", dir: "desc"}}, columns: { "0" => {name: "occurred_at", orderable: "true"} } } end context "when user is authorized" do it "returns JSON with case contacts data" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json expect(response).to have_http_status(:success) expect(response.content_type).to include("application/json") json = JSON.parse(response.body, symbolize_names: true) expect(json).to have_key(:data) expect(json).to have_key(:recordsTotal) expect(json).to have_key(:recordsFiltered) end it "includes case contact in the data array" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) expect(json[:data]).to be_an(Array) expect(json[:data].first[:id]).to eq(case_contact.id.to_s) end it "handles search parameter" do searchable_contact = create(:case_contact, :active, casa_case: casa_case, creator: create(:volunteer, display_name: "John Doe", casa_org: organization)) search_params = datatable_params.merge(search: {value: "John"}) post datatable_case_contacts_new_design_path, params: search_params, as: :json json = JSON.parse(response.body, symbolize_names: true) ids = json[:data].pluck(:id) expect(ids).to include(searchable_contact.id.to_s) end end context "when user is a volunteer" do let(:volunteer) { create(:volunteer, casa_org: organization) } before { sign_in volunteer } it "allows access to datatable endpoint" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json expect(response).to have_http_status(:success) end it "only returns case contacts created by the volunteer" do volunteer_contact = create(:case_contact, :active, casa_case: casa_case, creator: volunteer) other_volunteer_contact = create(:case_contact, :active, casa_case: casa_case, creator: create(:volunteer, casa_org: organization)) post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) ids = json[:data].pluck(:id) expect(ids).to include(volunteer_contact.id.to_s) expect(ids).not_to include(other_volunteer_contact.id.to_s) end end context "when user is a supervisor" do let(:supervisor) { create(:supervisor, casa_org: organization) } before { sign_in supervisor } it "allows access to datatable endpoint" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json expect(response).to have_http_status(:success) end it "returns all case contacts in the organization" do contact1 = create(:case_contact, :active, casa_case: casa_case, creator: create(:volunteer, casa_org: organization)) contact2 = create(:case_contact, :active, casa_case: casa_case, creator: create(:volunteer, casa_org: organization)) post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) ids = json[:data].pluck(:id) expect(ids).to include(contact1.id.to_s, contact2.id.to_s) end end context "when user is not authenticated" do before { sign_out admin } it "returns unauthorized status" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json expect(response).to have_http_status(:unauthorized) end end context "expanded content fields" do let(:contact_topic) { create(:contact_topic, casa_org: organization) } let(:case_contact_with_details) do create(:case_contact, :active, casa_case: casa_case, notes: "Important follow-up") end before do create(:contact_topic_answer, case_contact: case_contact_with_details, contact_topic: contact_topic, value: "Youth is doing well") end it "includes contact_topic_answers in the response" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s } expect(record[:contact_topic_answers]).to be_an(Array) expect(record[:contact_topic_answers].first[:value]).to eq("Youth is doing well") end it "includes the topic question in contact_topic_answers" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s } expect(record[:contact_topic_answers].first[:question]).to eq(contact_topic.question) end it "includes notes in the response" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s } expect(record[:notes]).to eq("Important follow-up") end it "omits blank topic answer values" do create(:contact_topic_answer, case_contact: case_contact_with_details, contact_topic: contact_topic, value: "") post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s } expect(record[:contact_topic_answers].pluck(:value)).to all(be_present) end it "returns a blank value for notes when notes are empty" do case_contact_without_notes = create(:case_contact, :active, casa_case: casa_case, notes: "") post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_without_notes.id.to_s } expect(record[:notes]).to be_blank end end context "contact_topics field" do let(:contact_topic) { create(:contact_topic, casa_org: organization) } let(:case_contact_with_topics) { create(:case_contact, :active, casa_case: casa_case) } before do case_contact_with_topics.contact_topics << contact_topic end it "returns contact_topics as an array of strings" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_with_topics.id.to_s } expect(record[:contact_topics]).to be_an(Array) end it "includes the topic question in the array" do post datatable_case_contacts_new_design_path, params: datatable_params, as: :json json = JSON.parse(response.body, symbolize_names: true) record = json[:data].find { |d| d[:id] == case_contact_with_topics.id.to_s } expect(record[:contact_topics]).to include(contact_topic.question) end end context "with permission flags and action metadata" do let(:volunteer) { create(:volunteer, casa_org: organization) } let!(:active_contact) { create(:case_contact, :active, casa_case: casa_case, creator: volunteer) } let!(:draft_contact) { create(:case_contact, casa_case: casa_case, creator: volunteer, status: "started") } def post_datatable post datatable_case_contacts_new_design_path, params: datatable_params, as: :json end def row_for(contact_id) json = JSON.parse(response.body, symbolize_names: true) json[:data].find { |row| row[:id] == contact_id.to_s } end context "when signed in as admin" do it "includes can_edit as true" do post_datatable expect(row_for(active_contact.id)[:can_edit]).to eq("true") end it "includes can_destroy as true" do post_datatable expect(row_for(active_contact.id)[:can_destroy]).to eq("true") end it "includes edit_path for the contact" do post_datatable expect(row_for(active_contact.id)[:edit_path]).to eq(edit_case_contact_path(active_contact)) end it "includes followup_id as empty when no followup exists" do post_datatable expect(row_for(active_contact.id)[:followup_id]).to eq("") end it "includes followup_id when a requested followup exists" do followup = create(:followup, case_contact: active_contact, status: :requested, creator: admin) post_datatable expect(row_for(active_contact.id)[:followup_id]).to eq(followup.id.to_s) end end context "when signed in as volunteer" do before { sign_in volunteer } it "includes can_edit as true for their own contact" do post_datatable expect(row_for(active_contact.id)[:can_edit]).to eq("true") end it "includes can_destroy as false for their own active contact" do post_datatable expect(row_for(active_contact.id)[:can_destroy]).to eq("false") end it "includes can_destroy as true for their own draft contact" do post_datatable expect(row_for(draft_contact.id)[:can_destroy]).to eq("true") end end end end end end ================================================ FILE: spec/requests/case_contacts/followups_spec.rb ================================================ require "rails_helper" RSpec.describe "CaseContacts::FollowupsController", type: :request do let(:volunteer) { create(:volunteer) } let(:case_contact) { create(:case_contact) } describe "POST /create" do subject(:request) do post case_contact_followups_path(case_contact), params: params response end let(:notification_double) { double("FollowupNotifier") } let(:params) { {note: "Hello, world!"} } before do sign_in volunteer allow(FollowupNotifier).to receive(:with).and_return(notification_double) allow(notification_double).to receive(:deliver) end it "creates a followup", :aggregate_failures do expect { request }.to change(Followup, :count).by(1) followup = Followup.last expect(followup.note).to eq "Hello, world!" end context "when requested as JSON" do subject(:request) do post case_contact_followups_path(case_contact), params: params, headers: {"Accept" => "application/json"} response end it "returns 204 No Content" do request expect(response).to have_http_status(:no_content) end end it "sends a Followup Notifier to case contact creator" do request followup = Followup.last expect(FollowupNotifier).to( have_received(:with).once.with(followup: followup, created_by: volunteer) ) expect(notification_double).to have_received(:deliver).once.with(case_contact.creator) end context "with invalid case_contact" do it "raises ActiveRecord::RecordNotFound" do expect { post case_contact_followups_path(444444) }.to raise_error(ActiveRecord::RecordNotFound) end end end describe "PATCH /resolve" do let(:notification_double) { double("FollowupResolvedNotifier") } before do sign_in volunteer allow(FollowupResolvedNotifier).to receive(:with).and_return(notification_double) allow(notification_double).to receive(:deliver) end context "followup exists" do subject(:request) do patch resolve_followup_path(followup) response end let(:followup) { create(:followup, case_contact: case_contact, creator: volunteer) } it "marks it as :resolved" do followup expect { request }.to change { followup.reload.resolved? }.from(false).to(true) end context "when requested as JSON" do subject(:request) do patch resolve_followup_path(followup), headers: {"Accept" => "application/json"} response end it "returns 204 No Content" do followup request expect(response).to have_http_status(:no_content) end end it "does not send Followup Notifier" do followup expect(FollowupResolvedNotifier).not_to receive(:with) expect { request }.to change { followup.reload.resolved? }.from(false).to(true) end context "when who resolves the followup is not the followup's creator" do let(:followup) { create(:followup, case_contact: case_contact) } it "sends a Followup Notifier to the creator" do request expect(FollowupResolvedNotifier).to( have_received(:with).once.with(followup: followup, created_by: volunteer) ) expect(notification_double).to have_received(:deliver).once.with(followup.creator) end end end context "followup doesn't exists" do it "raises ActiveRecord::RecordNotFound" do expect { patch resolve_followup_path(444444) }.to raise_error(ActiveRecord::RecordNotFound) end end end end ================================================ FILE: spec/requests/case_contacts/form_spec.rb ================================================ require "rails_helper" RSpec.describe "CaseContacts::Forms", type: :request do let(:casa_org) { build(:casa_org) } let(:contact_topics) { create_list(:contact_topic, 3, casa_org:) } let(:casa_admin) { create(:casa_admin, casa_org:) } let(:supervisor) { create(:supervisor, casa_org:) } let(:volunteer) { create(:volunteer, :with_single_case, casa_org:, supervisor: supervisor) } let(:creator) { volunteer } let(:casa_case) { volunteer.casa_cases.first } let(:user) { volunteer } before { sign_in user } describe "GET /new" do subject(:request) { get new_case_contact_path(casa_case_id: casa_case.id) } it "creates a new case_contact record with user as creator and status 'started'" do expect { request }.to change(CaseContact, :count).by(1) case_contact = CaseContact.last expect(case_contact.status).to eq "started" expect(case_contact.creator).to eq user end it "does not set the contact's casa_case_id" do expect { request }.to change(CaseContact, :count).by(1) case_contact = CaseContact.last expect(case_contact.casa_case_id).to be_nil end it "redirects to show(:details) with the created contact id" do expect { request }.to change(CaseContact, :count).by(1) case_contact = CaseContact.last expect(request).to redirect_to(case_contact_form_path(:details, case_contact_id: case_contact.id)) end end describe "GET /show" do subject(:request) { get case_contact_form_path(:details, case_contact_id: case_contact.id) } let(:case_contact) { create(:case_contact, :started_status, casa_case:, creator:) } it "renders details form with success status" do request expect(response).to have_http_status(:success) expect(response).to render_template(:details) end it "does not change status from 'started'" do expect(case_contact.status).to eq "started" request expect(response).to have_http_status(:success) expect(case_contact.reload.status).to eq "started" end context "when contact created by another casa org volunteer" do let(:other_volunteer) { create(:volunteer, casa_org:) } let(:creator) { other_volunteer } let!(:case_assignment) { create(:case_assignment, volunteer: other_volunteer, casa_case:) } it "redirects to root/sign in" do expect(casa_case.volunteers).to include(volunteer, other_volunteer) expect(case_contact.creator).to eq other_volunteer request expect(response).to redirect_to(root_path) end end context "when user is supervisor" do let(:user) { supervisor } it "allows volunteer's supervisor to view the form" do expect(supervisor.volunteers).to include(volunteer) expect(casa_case.volunteers).to include(volunteer) expect(case_contact.creator).to eq volunteer request expect(response).to have_http_status(:success) expect(response).to render_template(:details) end end context "when user is casa admin" do let(:user) { casa_admin } it "allows admin to view the form" do expect(casa_case.volunteers).to include(volunteer) expect(case_contact.creator).to eq volunteer request expect(response).to have_http_status(:success) expect(response).to render_template(:details) end end context "when step is not :details" do subject(:request) { get case_contact_form_path(:notes, case_contact_id: case_contact.id) } it "raises Wicked::Wizard::InvalidStepError" do expect { request }.to raise_error(Wicked::Wizard::InvalidStepError) end end end describe "PATCH /update" do subject(:request) { patch "/case_contacts/#{case_contact.id}/form/details", params: params } let(:case_contact) { create(:case_contact, :started_status, creator: volunteer) } let(:contact_type_group) { create(:contact_type_group, casa_org:) } let!(:contact_types) { create_list(:contact_type, 2, contact_type_group:) } let(:medium_type) { CaseContact::CONTACT_MEDIUMS.second } let(:contact_type_ids) { [contact_types.first.id] } let(:draft_case_ids) { [casa_case.id] } let(:required_attributes) do { draft_case_ids: draft_case_ids.map(&:to_s), occurred_at: 3.days.ago.to_date, # iso format medium_type: } end let(:valid_attributes) do required_attributes.merge({ contact_type_ids: contact_type_ids.map(&:to_s), contact_made: "1", duration_minutes: 50, duration_hours: 1 }) end let(:invalid_attributes) do {occurred_at: 3.days.from_now, duration_minutes: 50, contact_made: true} end let(:attributes) { valid_attributes } let(:params) { {case_contact: attributes} } it "updates the requested case_contact attributes" do case_contact.update!(duration_minutes: 5, contact_made: false, contact_type_ids: [contact_types.second.id]) submitted_hours = attributes[:duration_hours] submitted_minutes = attributes[:duration_minutes] submitted_minutes += (60 * submitted_hours) if submitted_hours request case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) expect(case_contact.contact_made).to be true expect(case_contact.duration_minutes).to eq submitted_minutes end it "updates the requested contact_type_ids" do expect(case_contact.contact_types).not_to include(contact_types.first) request case_contact.reload expect(case_contact.contact_types).to contain_exactly(contact_types.first) expect(case_contact.contact_types.size).to eq 1 end it "sets the case_contact's casa_case_id and status: 'active'" do expect(case_contact.casa_case_id).to be_nil expect(case_contact.status).to eq "started" request case_contact.reload expect(case_contact.status).to eq "active" expect(case_contact.casa_case_id).to eq casa_case.id end it "changes status to 'active' if it was 'started'" do case_contact.update!(status: "started") request expect(case_contact.reload.status).to eq "active" end it "raises RoutingError if no step in url" do expect { patch "/case_contacts/#{case_contact.id}/form", params: {case_contact: attributes} } .to raise_error(ActionController::RoutingError) end it "redirects to referrer (fallback /case_contacts?success=true)" do request expect(response).to have_http_status :redirect expect(response).to redirect_to case_contacts_path(success: true) end context "with invalid attributes" do let(:attributes) { invalid_attributes } it "does not update the requested case_contact" do original_attributes = case_contact.attributes request expect(case_contact.reload).to have_attributes original_attributes expect(case_contact.duration_minutes).not_to eq(50) expect(case_contact.contact_made).not_to be(true) end it "re-renders the form" do request # this should be a different status, but wicked wizard's 'render' method is a bit different? expect(response).to have_http_status(:success) expect(response).to render_template(:details) end it "does not change the contact status from 'started'" do case_contact.started! expect { request }.not_to change(case_contact, :status) expect(case_contact.reload.status).to eq "started" end end context "with duplicate contact type ids in params" do let(:contact_type_ids) { [contact_types.first.id, contact_types.first.id] } it "dedupes and updates the contact type ids" do expect(case_contact.contact_type_ids).to be_empty request expect(response).to have_http_status(:redirect) expect(case_contact.reload.contact_type_ids).to contain_exactly(contact_types.first.id) end end context "when contact types were previously assigned" do before { case_contact.update!(contact_type_ids: [contact_types.second.id]) } it "changes to contact types in params" do expect(case_contact.contact_type_ids).to contain_exactly(contact_types.second.id) request case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) end it "allows re-assigning the same contact type without uniqueness validation error" do # This test prevents regression of Bugsnag error: # "Validation failed: Case contact has already been taken" case_contact.update!(contact_type_ids: [contact_types.first.id]) expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) # Re-submit form with same contact type (simulates user editing and saving) request case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) end it "handles overlapping contact types (mix of existing and new)" do # Prevent regression: Rails should update associations without destroy_all race condition case_contact.update!(contact_type_ids: [contact_types.first.id]) expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) # Update to include both contact types (one existing, one new) updated_attributes = valid_attributes.merge(contact_type_ids: contact_types.map(&:id).map(&:to_s)) patch "/case_contacts/#{case_contact.id}/form/details", params: {case_contact: updated_attributes} case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id, contact_types.second.id) end it "handles multiple consecutive updates without uniqueness errors" do # Simulate rapid updates that could trigger race conditions in production case_contact.update!(contact_type_ids: [contact_types.first.id]) # First update: change to second contact type updated_attributes = valid_attributes.merge(contact_type_ids: [contact_types.second.id.to_s]) patch "/case_contacts/#{case_contact.id}/form/details", params: {case_contact: updated_attributes} case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.second.id) # Second update: change back to first contact type updated_attributes = valid_attributes.merge(contact_type_ids: [contact_types.first.id.to_s]) patch "/case_contacts/#{case_contact.id}/form/details", params: {case_contact: updated_attributes} case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) # Third update: use both contact types updated_attributes = valid_attributes.merge(contact_type_ids: contact_types.map(&:id).map(&:to_s)) patch "/case_contacts/#{case_contact.id}/form/details", params: {case_contact: updated_attributes} case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id, contact_types.second.id) end it "reduces contact types from multiple to single without errors" do # Start with multiple contact types case_contact.update!(contact_type_ids: contact_types.map(&:id)) expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id, contact_types.second.id) # Update to single contact type request case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) end it "expands contact types from single to multiple without errors" do # Start with single contact type case_contact.update!(contact_type_ids: [contact_types.first.id]) expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) # Update to multiple contact types updated_attributes = valid_attributes.merge(contact_type_ids: contact_types.map(&:id).map(&:to_s)) patch "/case_contacts/#{case_contact.id}/form/details", params: {case_contact: updated_attributes} case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id, contact_types.second.id) end it "replaces all contact types with completely different ones" do # Start with second contact type case_contact.update!(contact_type_ids: [contact_types.second.id]) expect(case_contact.contact_type_ids).to contain_exactly(contact_types.second.id) # Replace with first contact type (no overlap) request case_contact.reload expect(case_contact.contact_type_ids).to contain_exactly(contact_types.first.id) expect(case_contact.contact_type_ids).not_to include(contact_types.second.id) end end context "when contact topic answers in params" do let(:contact_topics) { create_list(:contact_topic, 3, casa_org:) } let(:topic_one) { contact_topics.first } let(:contact_topic_answers_attributes) do { "0" => {contact_topic_id: topic_one.id, value: "Topic 1 Answer"}, "1" => {contact_topic_id: contact_topics.second.id, value: "Topic 2 Answer"}, "2" => {contact_topic_id: contact_topics.third.id, value: "Topic 3 Answer"} } end let(:attributes) { valid_attributes.merge({contact_topic_answers_attributes:}) } it "creates contact topic answers" do expect(attributes[:contact_topic_answers_attributes]).to eq contact_topic_answers_attributes request case_contact.reload expect(case_contact.contact_topic_answers.size).to eq 3 expect(case_contact.contact_topic_answers.pluck(:value)) .to contain_exactly("Topic 1 Answer", "Topic 2 Answer", "Topic 3 Answer") expect(case_contact.contact_topics.flat_map(&:id)).to match_array(contact_topics.collect(&:id)) end context "when answer exists for the same contact topic" do let!(:contact_topic_one_answer) do create(:contact_topic_answer, value: "Original Discussion Topic Answer.", contact_topic: topic_one, case_contact:) end it "overwrites existing answer with id in answer attributes" do contact_topic_answers_attributes["0"][:id] = contact_topic_one_answer.id expect(case_contact.contact_topic_answers.size).to eq 1 request case_contact.reload topic_one_contact_answers = case_contact.contact_topic_answers.where(contact_topic: topic_one) expect(topic_one_contact_answers.size).to eq 1 expect(case_contact.contact_topic_answers.size).to eq 3 expect(topic_one_contact_answers.first.value).to eq "Topic 1 Answer" end end end context "when notes attribute in params" do let(:notes) { "This is a note." } let(:attributes) { valid_attributes.merge({notes:}) } it "updates the requested case_contact" do request case_contact.reload expect(case_contact.notes).to eq "This is a note." end end it "does not send reimbursement email for non-reimbursement case contacts" do expect(attributes[:want_driving_reimbursement]).to be_nil expect { request }.not_to have_enqueued_job(ActionMailer::MailDeliveryJob) end context "when no volunteer address in params" do before { volunteer.create_address!(content: "123 Before St") } it "does not update the volunteer's address" do expect(attributes[:volunteer_address]).to be_nil request expect(case_contact.reload.volunteer_address).to be_nil expect(volunteer.reload.address.content).to eq "123 Before St" end end context "when blank volunteer address in params" do let(:attributes) { valid_attributes.merge({volunteer_address: ""}) } before { volunteer.create_address!(content: "123 Before St") } it "does not update the volunteer's address" do expect(attributes[:volunteer_address]).to eq "" request expect(case_contact.reload.volunteer_address).to be_empty expect(volunteer.reload.address.content).to eq "123 Before St" end end context "when reimbursement info is in params" do let(:attributes) do valid_attributes.merge({ want_driving_reimbursement: true, miles_driven: 60, volunteer_address: "123 Params St" }) end it "updates the case contact with the info" do request case_contact.reload expect(case_contact.want_driving_reimbursement).to be true expect(case_contact.miles_driven).to eq 60 expect(case_contact.volunteer_address).to eq "123 Params St" end it "sends reimbursement email" do expect { request }.to change { have_enqueued_job(ActionMailer::MailDeliveryJob).with("SupervisorMailer", "reimbursement_request_email", volunteer, supervisor) } end it "updates the volunteer's address with the new address" do expect(user).to eq volunteer expect(attributes[:volunteer_address]).to eq "123 Params St" request expect(case_contact.reload.volunteer_address).to eq "123 Params St" expect(volunteer.reload.address.content).to eq "123 Params St" end context "when admin edits volunteer contact" do let(:user) { casa_admin } it "changes the volunteer address, not the admin's" do casa_admin.create_address!(content: "321 Admin Ave") expect(attributes[:volunteer_address]).to eq "123 Params St" request expect(case_contact.reload.volunteer_address).to eq "123 Params St" expect(volunteer.reload.address.content).to eq "123 Params St" expect(casa_admin.reload.address&.content).to eq "321 Admin Ave" end end context "when supervisor edits volunteer contact" do let(:user) { supervisor } it "changes the volunteer address, not the supervisor's" do supervisor.create_address!(content: "321 Super Ave") expect(attributes[:volunteer_address]).to eq "123 Params St" request expect(case_contact.reload.volunteer_address).to eq "123 Params St" expect(volunteer.reload.address.content).to eq "123 Params St" expect(supervisor.reload.address&.content).to eq "321 Super Ave" end end end context "when additional expenses in params" do let(:additional_expenses_attributes) do { "0" => {other_expense_amount: 50, other_expenses_describe: "meal"}, "1" => {other_expense_amount: 100, other_expenses_describe: "hotel"} } end let(:attributes) { valid_attributes.merge({additional_expenses_attributes:}) } it "creates additional expenses for the case contact" do request case_contact.reload expect(case_contact.additional_expenses.first.other_expense_amount).to eq 50 expect(case_contact.additional_expenses.first.other_expenses_describe).to eq "meal" expect(case_contact.additional_expenses.last.other_expense_amount).to eq 100 expect(case_contact.additional_expenses.last.other_expenses_describe).to eq "hotel" end it "succeeds when wants_driving_reimbursement is not true" do case_contact.update!(want_driving_reimbursement: false) attributes[:want_driving_reimbursement] = "0" request expect(case_contact.reload.additional_expenses.size).to eq 2 end end context "when json request (autosave)" do subject(:request) do patch "/case_contacts/#{case_contact.id}/form/details", params:, as: :json response end it { is_expected.to have_http_status(:success) } it "updates with the attributes" do request case_contact.reload expect(case_contact.occurred_at).to eq(attributes[:occurred_at]) expect(case_contact.contact_made).to be true end it "does not change status" do expect(case_contact.status).to eq "started" request expect(case_contact.reload.status).to eq "started" end context "when contact is in details status" do let(:case_contact) { create(:case_contact, :details_status, casa_case:, creator: volunteer) } it "does not change status" do expect(case_contact.status).to eq "details" request expect(case_contact.reload.status).to eq "details" end end context "when contact is in active status" do let(:case_contact) { create(:case_contact, casa_case:, creator: volunteer) } it "does not change the status" do expect(case_contact.status).to eq "active" request expect(case_contact.reload.status).to eq "active" end end context "when attribute is invalid" do let(:attributes) { invalid_attributes } it "does not update the requested case_contact" do expect { request }.not_to change(case_contact.reload, :attributes) end it "responds :unprocessable_content and returns the errors" do request expect(response).to have_http_status(:unprocessable_content) end end end context "when metadata create_another attribute is truthy" do let(:attributes) { valid_attributes.merge({metadata: {create_another: "1"}}) } it "redirects to contact form with the same draft_case_id, ignore_referer" do expect(attributes[:draft_case_ids]).to be_present request expect(response).to have_http_status :redirect expect(response).to redirect_to( new_case_contact_path(draft_case_ids: attributes[:draft_case_ids], ignore_referer: true) ) end end context "with multiple cases selected" do let!(:second_casa_case) { create(:casa_case, casa_org:, volunteers: [volunteer]) } let!(:third_casa_case) { create(:casa_case, casa_org:, volunteers: [volunteer]) } let(:draft_case_ids) { [casa_case.id, second_casa_case.id, third_casa_case.id] } it "copies the contact attributes for each contact" do expect { request }.to change(CaseContact.active, :count).by(3) original_case_contact = case_contact.reload second_case_contact = CaseContact.active.where(casa_case_id: second_casa_case.id).first third_case_contact = CaseContact.active.where(casa_case_id: third_casa_case.id).first unique_columns = ["id", "casa_case_id", "created_at", "updated_at", "draft_case_ids", "metadata"] copied_attrs = original_case_contact.attributes.except(*unique_columns) expect([second_case_contact, third_case_contact]).to all have_attributes copied_attrs end it "sets casa_case and draft_case_ids per contact" do expect { request }.to change(CaseContact.active, :count).by(3) second_case_contact = CaseContact.active.where(casa_case_id: second_casa_case.id).first third_case_contact = CaseContact.active.where(casa_case_id: third_casa_case.id).first expect(case_contact.reload.casa_case_id).to eq draft_case_ids.first expect(case_contact.draft_case_ids).to contain_exactly draft_case_ids.first expect(second_case_contact.casa_case_id).to eq draft_case_ids.second expect(second_case_contact.draft_case_ids).to contain_exactly draft_case_ids.second expect(third_case_contact.casa_case_id).to eq draft_case_ids.third expect(third_case_contact.draft_case_ids).to contain_exactly draft_case_ids.third end it "sets contact_type_ids for all contacts" do expect { request }.to change(CaseContact.active, :count).by(3) contacts = CaseContact.active.last(3) expect(contacts.collect(&:contact_type_ids)).to all match_array(contact_type_ids) end it "copies contact_topic answers for the cases" do case_contact.contact_topic_answers.create!(contact_topic: contact_topics.first, value: "test answer") expect { request }.to change(CaseContact.active, :count).by(3) contacts = CaseContact.active.last(3) contacts.each do |contact| expect(contact.contact_topic_answers.first.contact_topic_id).to eq contact_topics.first.id expect(contact.contact_topic_answers.first.value).to eq "test answer" end end it "redirects to referrer (fallback) page" do request expect(response).to have_http_status :redirect expect(response).to redirect_to case_contacts_path(success: true) end context "when create_another option is truthy" do before { params[:case_contact][:metadata] = {create_another: "1"} } it "redirects to new contact with the same draft_case_ids, :ignore_referer" do request expect(response).to have_http_status :redirect expect(response).to redirect_to new_case_contact_path(draft_case_ids:, ignore_referer: true) end end end end end ================================================ FILE: spec/requests/case_contacts_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_contacts", type: :request do let(:organization) { build(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } before { sign_in admin } describe "GET /index" do subject(:request) do get case_contacts_path(filterrific: filterrific) response end let!(:casa_case) { create(:casa_case, casa_org: organization) } let!(:past_contact) { create(:case_contact, casa_case: casa_case, occurred_at: 3.weeks.ago) } let!(:recent_contact) { create(:case_contact, casa_case: casa_case, occurred_at: 3.days.ago) } let(:filterrific) { {} } it { is_expected.to have_http_status(:success) } it "returns all case contacts" do page = request.parsed_body.to_html expect(page).to include(past_contact.creator.display_name, recent_contact.creator.display_name) end context "with filters applied" do let(:filterrific) { {occurred_starting_at: 1.week.ago} } it "returns all case contacts" do page = request.parsed_body.to_html expect(page).to include(recent_contact.creator.display_name) expect(page).not_to include(past_contact.creator.display_name) end end context "when logged in as a volunteer" do let(:assigned_case) { create(:casa_case, :with_one_case_assignment, casa_org: organization) } let(:unassigned_case) { casa_case } let(:volunteer) { assigned_case.assigned_volunteers.first } let!(:assigned_case_contact) { create(:case_contact, casa_case: assigned_case, creator: volunteer) } let!(:unassigned_case_contact) { create(:case_contact, casa_case: unassigned_case, creator: volunteer, duration_minutes: 180) } before { sign_in volunteer } it "returns only currently assigned cases" do page = request.parsed_body.to_html expect(page).to include("60 minutes") expect(page).not_to include("3 hours") end end end describe "GET /new" do subject(:request) do get new_case_contact_path response end it { is_expected.to have_http_status(:redirect) } it "creates a 'started' status case contact and redirects to the form" do expect { request }.to change(CaseContact, :count).by(1) new_case_contact = CaseContact.last expect(new_case_contact.status).to eq "started" expect(response).to redirect_to(case_contact_form_path(:details, case_contact_id: new_case_contact.id)) end context "when current org has contact topics" do let(:contact_topics) do [build(:contact_topic, active: true, soft_delete: false)] end let(:organization) { create(:casa_org, contact_topics:) } it "does not create contact topic answers" do expect { request } .to change(CaseContact.started, :count).by(1) .and not_change(ContactTopicAnswer, :count) expect(CaseContact.started.last.contact_topic_answers).to be_empty end end end describe "GET /edit" do subject(:request) do get edit_case_contact_url(case_contact) response end let(:case_contact) { create(:case_contact, casa_case: create(:casa_case, :with_case_assignments), notes: "Notes") } it { is_expected.to have_http_status(:redirect) } it "redirects to case contact form" do request expect(response).to redirect_to(case_contact_form_path(:details, case_contact_id: case_contact.id)) end end describe "GET /drafts" do subject(:request) do get case_contacts_drafts_path response end it { is_expected.to have_http_status(:success) } context "when user is volunteer" do before { sign_in volunteer } it { is_expected.to have_http_status(:redirect) } end end describe "DELETE /destroy" do subject(:request) do delete case_contact_path(case_contact), headers: {HTTP_REFERER: case_contacts_path} response end let(:case_contact) { create(:case_contact) } it { is_expected.to redirect_to(case_contacts_path) } it "shows correct flash message" do request expect(flash[:notice]).to eq("Contact is successfully deleted.") end it "soft deletes the case_contact" do expect { request }.to change { case_contact.reload.deleted? }.from(false).to(true) end end describe "GET /restore" do subject(:request) do post restore_case_contact_path(case_contact), headers: {HTTP_REFERER: case_contacts_path} response end let(:case_contact) { create(:case_contact) } before { case_contact.destroy } it { is_expected.to redirect_to(case_contacts_path) } it "shows correct flash message" do request expect(flash[:notice]).to eq("Contact is successfully restored.") end it "soft deletes the case_contact" do expect { request }.to change { case_contact.reload.deleted? }.from(true).to(false) end end xdescribe "GET /leave" do subject(:request) do get leave_case_contact_path response end it { is_expected.to redirect_to(case_contacts_path) } it "redirects back to referer or fallback location" do request expect(response).to redirect_to(case_contacts_path) end end end ================================================ FILE: spec/requests/case_court_orders_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_court_orders", type: :request do let(:user) { build(:casa_admin) } let(:case_court_order) { build(:case_court_order) } before do sign_in user casa_case = create(:casa_case) casa_case.case_court_orders << case_court_order end describe "DELETE /destroy" do subject(:request) do delete case_court_order_url(case_court_order) response end it { is_expected.to be_successful } it "deletes the court order" do expect { request }.to change(CaseCourtOrder, :count).from(1).to(0) end end end ================================================ FILE: spec/requests/case_court_reports_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_court_reports", type: :request do include DownloadHelpers let(:volunteer) { create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor) } before do sign_in volunteer end # case_court_reports#index describe "GET /case_court_reports" do context "as volunteer" do it "can view 'Generate Court Report' page", :aggregate_failures do get case_court_reports_path expect(response).to be_successful expect(assigns(:assigned_cases)).not_to be_empty end end context "as a supervisor" do let(:supervisor) { volunteer.supervisor } before do sign_in supervisor end it "can view the 'Generate Court Report' page", :aggregate_failures do get case_court_reports_path expect(response).to be_successful expect(assigns(:assigned_cases)).not_to be_empty end context "with no cases in the organization" do let(:supervisor) { create(:supervisor, casa_org: create(:casa_org)) } it "can view 'Generate Court Report page", :aggregate_failures do get case_court_reports_path expect(response).to be_successful expect(assigns(:assigned_cases)).to be_empty end end end end # case_court_reports#show describe "GET /case_court_reports/:id" do context "when a valid / existing case is sent" do subject(:request) do get case_court_report_path(casa_case.case_number, format: "docx") response end let(:casa_case) { volunteer.casa_cases.first } before do Tempfile.create do |t| casa_case.court_reports.attach( io: File.open(t.path), filename: "#{casa_case.case_number}.docx" ) end end it "authorizes action" do expect_any_instance_of(CaseCourtReportsController).to receive(:authorize).with(CaseCourtReport).and_call_original request end it "send response as a .DOCX file" do expect(request.content_type).to eq Mime::Type.lookup_by_extension(:docx) end it "send response with a status :ok" do expect(request).to have_http_status(:ok) end end context "when an INVALID / non-existing case is sent" do let(:invalid_casa_case) { build_stubbed(:casa_case) } before do Capybara.current_driver = :selenium_chrome get case_court_report_path(invalid_casa_case.case_number, format: "docx") end it "redirects back to 'Generate Court Report' page", :aggregate_failures, :js do expect(response).to redirect_to(case_court_reports_path) expect(response.content_type).to eq "text/html; charset=utf-8" end it "shows correct flash message" do request expect(flash[:alert]).to eq "Report #{invalid_casa_case.case_number} is not found." end end end # case_court_reports#generate describe "POST /case_court_reports" do subject(:request) do post generate_case_court_reports_path, params: params, headers: {ACCEPT: "application/json"} response end let(:casa_case) { volunteer.casa_cases.first } let(:params) { { case_court_report: { case_number: casa_case.case_number.to_s, start_date: "January 1, 2020", end_date: "January 1, 2021" } } } it "authorizes action" do expect_any_instance_of(CaseCourtReportsController).to receive(:authorize).with(CaseCourtReport).and_call_original request end context "when no custom template is set" do it "sends response as a JSON string", :aggregate_failures do expect(request.content_type).to eq("application/json; charset=utf-8") expect(request.parsed_body).to be_a(ActiveSupport::HashWithIndifferentAccess) end it "has keys ['link', 'status'] in JSON string", :aggregate_failures do body_hash = request.parsed_body expect(body_hash).to have_key "link" expect(body_hash).to have_key "status" end it "sends response with status :ok" do expect(request).to have_http_status(:ok) end it "contains a link ending with .DOCX extension" do expect(request.parsed_body["link"]).to end_with(".docx") end it "uses the default template" do get request.parsed_body["link"] docx_response = Docx::Document.open(StringIO.new(response.body)) expect(header_text(docx_response)).to include("YOUR CASA ORG’S NUMBER") end end context "when a custom template is set" do before do stub_twilio volunteer.casa_org.court_report_template.attach(io: File.new(Rails.root.join("app/documents/templates/montgomery_report_template.docx")), filename: "montgomery_report_template.docx") end it "uses the custom template" do get request.parsed_body["link"] followed_link_response = response docx_response = Docx::Document.open(StringIO.new(followed_link_response.body)) expect(docx_response.paragraphs.map(&:to_s)).to include("Did you forget to enter your court orders?") end end context "with date filtering" do let(:casa_case) { volunteer.casa_cases.first } let!(:contact_in_range) do create(:case_contact, casa_case: casa_case, occurred_at: Date.new(2025, 10, 10)) end let!(:contact_out_of_range) do create(:case_contact, casa_case: casa_case, occurred_at: Date.new(2024, 10, 30)) end let(:params) { { case_court_report: { case_number: casa_case.case_number.to_s, start_date: "2025-10-01", end_date: "2025-10-23" } } } it "includes contacts within the date range in the generated report" do post generate_case_court_reports_path, params: params, headers: {ACCEPT: "application/json"} get response.parsed_body["link"] docx_response = Docx::Document.open(StringIO.new(response.body)) # The contact dates are in table cells, so we need to extract them specifically. table_texts = docx_response.tables.flat_map { |table| table.rows.flat_map { |row| row.cells.map(&:text) } } expect(table_texts.join(" ")).to include(contact_in_range.occurred_at.strftime("%-m/%-d")) end it "does not include contacts outside the date range in the generated report" do post generate_case_court_reports_path, params: params, headers: {ACCEPT: "application/json"} get response.parsed_body["link"] docx_response = Docx::Document.open(StringIO.new(response.body)) table_texts = docx_response.tables.flat_map { |table| table.rows.flat_map { |row| row.cells.map(&:text) } } expect(table_texts.join(" ")).not_to include(contact_out_of_range.occurred_at.strftime("%-m/%-d")) end end context "when user timezone" do let(:server_time) { Time.zone.parse("2020-12-31 23:00:00") } let(:user_different_timezone) do ActiveSupport::TimeZone["Tokyo"] end let(:params) { {case_court_report: {case_number: casa_case.case_number.to_s}, time_zone: "Tokyo"} } before do travel_to server_time end it "is different than server" do get request.parsed_body["link"] followed_link_response = response docx_response = Docx::Document.open(StringIO.new(followed_link_response.body)) expect(docx_response.paragraphs.map(&:to_s)) .to include("Date Written: #{I18n.l(user_different_timezone.at(server_time) .to_date, format: :full, default: nil)}") end end context "when an INVALID / non-existing case is sent" do let(:casa_case) { build_stubbed(:casa_case) } it "sends response as a JSON string", :aggregate_failures do expect(request.content_type).to eq("application/json; charset=utf-8") expect(request.parsed_body).to be_a(ActiveSupport::HashWithIndifferentAccess) end it "has keys ['link','status','error_messages'] in JSON string", :aggregate_failures do body_hash = request.parsed_body expect(body_hash).to have_key "link" expect(body_hash).to have_key "status" expect(body_hash).to have_key "error_messages" end it "sends response with status :not_found" do expect(request).to have_http_status(:not_found) end it "contains a empty link" do expect(request.parsed_body["link"].length).to be 0 end # TODO: Fix controller to have the error message actually get the param with `case_params[:case_number]` it "shows correct error messages" do expect(request.parsed_body["error_messages"]).to include("Report is not found") end end context "when zip report fails" do before do expect_any_instance_of(CaseCourtReportsController).to receive(:save_report).and_raise Zip::Error.new end it { is_expected.to have_http_status(:not_found) } it "shows the correct error message" do expect(request.parsed_body["error_messages"]).to include("Template is not found") end end context "when an unpredictable error occurs" do before do expect_any_instance_of(CaseCourtReportsController).to receive(:save_report).and_raise StandardError.new("Unexpected Error") end it { is_expected.to have_http_status(:unprocessable_content) } it "shows the correct error message" do expect(request.parsed_body["error_messages"]).to include("Unexpected Error") end end end end ================================================ FILE: spec/requests/case_groups_spec.rb ================================================ require "rails_helper" RSpec.describe "/case_groups", type: :request do let(:casa_org) { create :casa_org } let(:supervisor) { create :supervisor, casa_org: } let(:user) { supervisor } let(:casa_cases) { create_list :casa_case, 2, casa_org: } let(:case_group) { create :case_group, casa_org:, casa_cases: } let(:valid_attributes) { attributes_for :case_group, casa_org:, casa_case_ids: casa_cases.map(&:id) } let(:invalid_attributes) do valid_attributes.merge(name: nil, casa_case_ids: []) end before { sign_in user } describe "GET /index" do subject { get case_groups_path } let!(:case_groups) { create_list :case_group, 2, casa_org: } it "renders a successful response" do subject expect(response).to have_http_status(:success) expect(response).to render_template(:index) end it "displays information of the records" do subject expect(response.body).to include(*case_groups.map(&:name)) end end describe "GET /new" do subject { get new_case_group_path } it "renders a successful response" do subject expect(response).to have_http_status(:success) expect(response).to render_template(:new) end end describe "POST /create" do subject { post case_groups_path, params: } let(:params) { {case_group: valid_attributes} } it "creates new record" do expect { subject }.to change(CaseGroup, :count).by(1) end it "redirects to the case group index" do subject expect(response).to redirect_to(case_groups_path) end context "with invalid params" do let(:params) { {case_group: invalid_attributes} } it "does not create a new record" do expect { subject }.not_to change(CaseGroup, :count) end it "renders new template" do subject expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template(:new) end end end describe "GET edit" do subject { get edit_case_group_path(case_group) } let(:case_group) { create :case_group, casa_org: } it "renders a successful response" do subject expect(response).to have_http_status(:success) expect(response).to render_template(:edit) end end describe "PATCH /update" do subject { patch case_group_path(case_group), params: } let(:params) { {case_group: valid_attributes} } it "updates the requested record" do expect(case_group.name).not_to eq(valid_attributes[:name]) subject case_group.reload expect(case_group.name).to eq(valid_attributes[:name]) end it "redirects to the updated record" do subject expect(response).to redirect_to(case_groups_path) end context "with invalid params" do let(:params) { {case_group: invalid_attributes} } it "renders new template" do subject expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template(:edit) end end end describe "DELETE destroy" do subject { delete case_group_path(case_group) } let!(:case_group) { create :case_group, casa_org: } it "destroys the requested record" do expect { subject }.to change(CaseGroup, :count).by(-1) end it "redirects to the case_groups index" do subject expect(response).to redirect_to(case_groups_path) end end end ================================================ FILE: spec/requests/checklist_items_spec.rb ================================================ require "rails_helper" RSpec.describe "ChecklistItems", type: :request do describe "GET new" do context "when logged in as an admin user" do it "the new checklist item page should load successfully" do sign_in_as_admin get new_hearing_type_checklist_item_path(create(:hearing_type)) expect(response).to be_successful end end context "when logged in as a non-admin user" do it "does not allow access to the new checklist item page" do sign_in_as_volunteer get new_hearing_type_checklist_item_path(create(:hearing_type)) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end describe "POST create" do context "when logged in as an admin user" do it "allows for the creation of checklist items" do sign_in_as_admin hearing_type = create(:hearing_type) post hearing_type_checklist_items_path( { hearing_type_id: hearing_type.id, checklist_item: { description: "checklist item description", category: "checklist item category", mandatory: false } } ) expect(response).to redirect_to edit_hearing_type_path(hearing_type) expect(ChecklistItem.count).to eq 1 end end context "when logged in as a non-admin user" do it "does not allow for the creation of checklist items" do sign_in_as_volunteer hearing_type = create(:hearing_type) post hearing_type_checklist_items_path( { hearing_type_id: hearing_type.id, checklist_item: { description: "checklist item description", category: "checklist item category", mandatory: false } } ) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end describe "GET edit" do context "when logged in as an admin user" do it "the edit page should load successfully" do sign_in_as_admin hearing_type = create(:hearing_type) checklist_item = create(:checklist_item) get edit_hearing_type_checklist_item_path(hearing_type, checklist_item) expect(response).to be_successful end end context "when logged in as a non-admin user" do it "does not allow access to the edit page" do sign_in_as_volunteer hearing_type = create(:hearing_type) checklist_item = create(:checklist_item) get edit_hearing_type_checklist_item_path(hearing_type, checklist_item) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end describe "PATCH update" do context "when logged in as an admin user" do it "lets admin users update checklist items" do sign_in_as_admin hearing_type = create(:hearing_type) checklist_item = create(:checklist_item) patch hearing_type_checklist_item_path( { hearing_type_id: hearing_type.id, id: checklist_item.id, checklist_item: { description: "updated checklist item description", category: "updated checklist item category", mandatory: true } } ) expect(response).to redirect_to edit_hearing_type_path(hearing_type) checklist_item.reload expect(checklist_item.description).to eq "updated checklist item description" expect(checklist_item.category).to eq "updated checklist item category" expect(checklist_item.mandatory).to eq true end end context "when logged in as a non-admin user" do it "does not allow updates" do sign_in_as_volunteer hearing_type = create(:hearing_type) checklist_item = create(:checklist_item) patch hearing_type_checklist_item_path( { hearing_type_id: hearing_type.id, id: checklist_item.id, checklist_item: { description: "updated checklist item description", category: "updated checklist item category", mandatory: true } } ) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." checklist_item.reload expect(checklist_item.description).to eq "checklist item description" expect(checklist_item.category).to eq "checklist item category" expect(checklist_item.mandatory).to eq false end end end describe "DELETE destroy" do context "when logged in as an admin user" do it "allows for the deletion of checklist items" do sign_in_as_admin hearing_type = create(:hearing_type) checklist_item = create(:checklist_item) delete hearing_type_checklist_item_path(hearing_type, checklist_item) expect(response).to redirect_to edit_hearing_type_path(hearing_type) expect(ChecklistItem.count).to eq 0 end end context "when logged in as a non-admin user" do it "does not allow for the deletion of checklist items" do sign_in_as_volunteer hearing_type = create(:hearing_type) checklist_item = create(:checklist_item) delete hearing_type_checklist_item_path(hearing_type, checklist_item) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." expect(ChecklistItem.count).to eq 1 end end end end ================================================ FILE: spec/requests/contact_topic_answers_spec.rb ================================================ require "rails_helper" RSpec.describe "/contact_topic_answers", type: :request do let(:casa_org) { create :casa_org } let(:contact_topic) { create :contact_topic, casa_org: } let(:casa_admin) { create :casa_admin, casa_org: } let(:supervisor) { create :supervisor, casa_org: } let(:volunteer) { create :volunteer, :with_single_case, supervisor:, casa_org: } let(:user) { volunteer } let(:casa_case) { volunteer.casa_cases.first } let(:case_contact) { create :case_contact, casa_case:, creator: volunteer } let(:valid_attributes) do attributes_for(:contact_topic_answer) .merge({contact_topic_id: contact_topic.id, case_contact_id: case_contact.id}) end let(:invalid_attributes) { valid_attributes.merge({contact_topic_id: nil, value: "something"}) } before { sign_in user } describe "POST /create" do subject { post contact_topic_answers_path, params:, as: :json } let(:new_attributes) { valid_attributes.except(:value) } let(:params) { {contact_topic_answer: new_attributes} } it "creates a record and responds created" do expect { subject }.to change(ContactTopicAnswer, :count).by(1) expect(response).to have_http_status(:created) end it "returns the record as json" do subject expect(response.content_type).to match(a_string_including("application/json")) answer = ContactTopicAnswer.last expect(response_json[:id]).to eq answer.id expect(response_json.keys) .to contain_exactly(:id, :contact_topic_id, :value, :case_contact_id, :created_at, :updated_at, :selected, :deleted_at) end context "as casa_admin" do let(:user) { casa_admin } it "creates a record and responds created" do expect { subject }.to change(ContactTopicAnswer, :count).by(1) expect(response).to have_http_status(:created) end end context "with invalid parameters" do let(:params) { {contact_topic_answer: invalid_attributes} } it "fails and responds unprocessable_content" do expect { subject }.not_to change(ContactTopicAnswer, :count) expect(response).to have_http_status(:unprocessable_content) end it "returns errors as json" do subject expect(response.content_type).to match(a_string_including("application/json")) expect(response.body).to be_present expect(response_json[:contact_topic]).to include("must be selected") end end context "html request" do subject { post contact_topic_answers_path, params: } it "redirects to referrer/root without creating a contact topic answer" do expect { subject }.to not_change(ContactTopicAnswer, :count) expect(response).to redirect_to(root_url) end end end describe "DELETE /destroy" do subject { delete contact_topic_answer_url(contact_topic_answer), as: :json } let!(:contact_topic_answer) { create :contact_topic_answer, case_contact:, contact_topic: } it "destroys the record and responds no content" do expect { subject } .to change(ContactTopicAnswer, :count).by(-1) expect(response).to have_http_status(:no_content) expect(response.body).to be_empty end context "html request" do subject { delete contact_topic_answer_url(contact_topic_answer) } it "redirects to referrer/root without destroying the contact topic answer" do expect { subject }.to not_change(ContactTopicAnswer, :count) expect(response).to redirect_to(root_url) end end end end ================================================ FILE: spec/requests/contact_topics_spec.rb ================================================ require "rails_helper" RSpec.describe "/contact_topics", type: :request do # This should return the minimal set of attributes required to create a valid # ContactTopic. As you add validations to ContactTopic, be sure to # adjust the attributes here as well. let(:casa_org) { create(:casa_org) } let(:is_active) { nil } let(:contact_topic) { create(:contact_topic, casa_org:) } let(:attributes) { {casa_org_id: casa_org.id} } let(:admin) { create(:casa_admin, casa_org: casa_org) } before { sign_in admin } describe "GET /new" do it "renders a successful response" do get new_contact_topic_url expect(response).to be_successful end end describe "GET /edit" do it "renders a successful response" do get edit_contact_topic_url(contact_topic) expect(response).to be_successful expect(response.body).to include(contact_topic.question) expect(response.body).to include(contact_topic.details) end end describe "POST /create" do context "with valid parameters" do let(:attributes) do { casa_org_id: casa_org.id, question: "test question", details: "test details" } end it "creates a new ContactTopic" do expect do post contact_topics_url, params: {contact_topic: attributes} end.to change(ContactTopic, :count).by(1) topic = ContactTopic.last expect(topic.question).to eq("test question") expect(topic.details).to eq("test details") end it "redirects to the edit casa_org" do post contact_topics_url, params: {contact_topic: attributes} expect(response).to redirect_to(edit_casa_org_path(casa_org)) end it "can set exclude_from_court_report attribute" do attributes[:exclude_from_court_report] = true expect do post contact_topics_url, params: {contact_topic: attributes} end.to change(ContactTopic, :count).by(1) topic = ContactTopic.last expect(topic.exclude_from_court_report).to be true end end context "with invalid parameters" do let(:attributes) do { casa_org_id: casa_org.id, question: "", details: "" } end it "does not create a new ContactTopic" do expect do post contact_topics_url, params: {contact_topic: attributes} end.not_to change(ContactTopic, :count) end it "renders a response with 422 status (i.e. to display the 'new' template)" do post contact_topics_url, params: {contact_topic: attributes} expect(response).to have_http_status(:unprocessable_content) end end end describe "PATCH /update" do context "with valid parameters" do let!(:contact_topic) { create(:contact_topic, casa_org:) } let(:new_attributes) do { casa_org_id: casa_org.id, active: false, question: "test question", details: "test details", soft_delete: true } end it "updates only active, details, question contact_topic" do expect(contact_topic.soft_delete).to eq(false) patch contact_topic_url(contact_topic), params: {contact_topic: new_attributes} contact_topic.reload expect(contact_topic.soft_delete).to eq(false) expect(contact_topic.active).to eq(false) expect(contact_topic.details).to eq("test details") expect(contact_topic.question).to eq("test question") end it "redirects to the casa_org edit" do patch contact_topic_url(contact_topic), params: {contact_topic: new_attributes} expect(response).to redirect_to(edit_casa_org_path(casa_org)) end it "can change exclude_from_court_report" do new_attributes = {exclude_from_court_report: true} expect { patch contact_topic_url(contact_topic), params: {contact_topic: new_attributes} }.to change { contact_topic.reload.exclude_from_court_report }.from(false).to(true) end end context "with invalid parameters" do let(:attributes) { {casa_org_id: 0} } it "renders a response with 422 status (i.e. to display the 'edit' template)" do patch contact_topic_url(contact_topic), params: {contact_topic: attributes} expect(response).to have_http_status(:unprocessable_content) end end end describe "DELETE /soft_delete" do let!(:contact_topic) { create(:contact_topic, casa_org: casa_org) } it "does not destroy the requested contact_topic" do expect do delete soft_delete_contact_topic_url(contact_topic) end.not_to change(ContactTopic, :count) end it "set the requested contact_topic to soft_deleted" do delete soft_delete_contact_topic_url(contact_topic) contact_topic.reload expect(contact_topic.soft_delete).to be true end it "redirects to edit casa_org" do delete soft_delete_contact_topic_url(contact_topic) expect(response).to redirect_to(edit_casa_org_path(casa_org)) end end end ================================================ FILE: spec/requests/contact_type_groups_spec.rb ================================================ require "rails_helper" RSpec.describe "/contact_type_groups", type: :request do describe "GET /contact_type_groups/new" do context "logged in as admin user" do it "can successfully access a contact type group create page" do sign_in_as_admin get new_contact_type_group_path expect(response).to be_successful end end context "logged in as a non-admin user" do it "cannot access a contact type group create page" do sign_in_as_volunteer get new_contact_type_group_path expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a contact type group create page" do get new_contact_type_group_path expect(response).to redirect_to new_user_session_path end end end describe "POST /contact_type_groups" do let(:params) { {contact_type_group: {name: "New Group", active: true}} } context "logged in as admin user" do it "can successfully create a contact type group" do casa_org = build(:casa_org) sign_in build(:casa_admin, casa_org: casa_org) expect { post contact_type_groups_path, params: params }.to change(ContactTypeGroup, :count).by(1) group = ContactTypeGroup.last expect(group.name).to eql "New Group" expect(group.casa_org).to eql casa_org expect(group.active).to be_truthy expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Contact Type Group was successfully created." end end context "logged in as a non-admin user" do it "cannot create a contact type group" do sign_in_as_volunteer post contact_type_groups_path, params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot create a contact type group" do post contact_type_groups_path, params: params expect(response).to redirect_to new_user_session_path end end end describe "GET /contact_type_groups/:id/edit" do context "logged in as admin user" do it "can successfully access a contact type group edit page" do sign_in_as_admin get edit_contact_type_group_path(create(:contact_type_group)) expect(response).to be_successful end end context "logged in as a non-admin user" do it "cannot access a contact type group edit page" do sign_in_as_volunteer get edit_contact_type_group_path(create(:contact_type_group)) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a contact type group edit page" do get edit_contact_type_group_path(create(:contact_type_group)) expect(response).to redirect_to new_user_session_path end end end describe "PUT /contact_type_groups/:id" do let(:params) { {contact_type_group: {name: "New Group Name", active: false}} } context "logged in as admin user" do it "can successfully update a contact type group" do casa_org = build(:casa_org) sign_in build(:casa_admin, casa_org: casa_org) group = create(:contact_type_group, casa_org: casa_org, active: true) put contact_type_group_path(group), params: params group.reload expect(group.name).to eq "New Group Name" expect(group.casa_org).to eq casa_org expect(group.active).to be_falsey expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Contact Type Group was successfully updated." end end context "logged in as a non-admin user" do it "cannot update a update a contact type group" do sign_in_as_volunteer put contact_type_group_path(create(:contact_type_group)), params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot update a update a contact type group" do put contact_type_group_path(create(:contact_type_group)), params: params expect(response).to redirect_to new_user_session_path end end end end ================================================ FILE: spec/requests/contact_types_spec.rb ================================================ require "rails_helper" RSpec.describe "/contact_types", type: :request do let(:group) { create(:contact_type_group) } describe "GET /contact_types/new" do context "logged in as admin user" do it "can successfully access a contact type create page" do sign_in_as_admin get new_contact_type_path expect(response).to be_successful end end context "logged in as a non-admin user" do it "cannot access a contact type create page" do sign_in_as_volunteer get new_contact_type_path expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a contact type create page" do get new_contact_type_path expect(response).to redirect_to new_user_session_path end end end describe "POST /contact_types" do let(:params) { {contact_type: {name: "New Contact", contact_type_group_id: group.id, active: true}} } context "logged in as admin user" do it "can successfully create a contact type" do casa_org = build(:casa_org) sign_in create(:casa_admin, casa_org: casa_org) expect { post contact_types_path, params: params }.to change(ContactType, :count).by(1) contact_type = ContactType.last expect(contact_type.name).to eql "New Contact" expect(contact_type.contact_type_group).to eql group expect(contact_type.active).to be_truthy expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Contact Type was successfully created." end end context "logged in as a non-admin user" do it "cannot create a contact type" do sign_in_as_volunteer post contact_types_path, params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot create a contact type" do post contact_types_path, params: params expect(response).to redirect_to new_user_session_path end end end describe "GET /contact_types/:id/edit" do context "logged in as admin user" do it "can successfully access a contact type edit page" do sign_in_as_admin get edit_contact_type_path(create(:contact_type)) expect(response).to be_successful end end context "logged in as a non-admin user" do it "cannot access a contact type edit page" do sign_in_as_volunteer get edit_contact_type_path(create(:contact_type)) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a contact type edit page" do get edit_contact_type_path(create(:contact_type)) expect(response).to redirect_to new_user_session_path end end end describe "PUT /contact_types/:id" do let(:casa_org) { build(:casa_org) } let(:new_group) { create(:contact_type_group, casa_org: casa_org) } let(:params) { {contact_type: {name: "New Name", contact_type_group_id: new_group.id, active: false}} } context "logged in as admin user" do it "can successfully update a contact type" do sign_in build(:casa_admin, casa_org: casa_org) contact_type = create(:contact_type, contact_type_group: group) put contact_type_path(contact_type), params: params contact_type.reload expect(contact_type.name).to eq "New Name" expect(contact_type.contact_type_group).to eq new_group expect(contact_type.active).to be_falsey expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Contact Type was successfully updated." end end context "logged in as a non-admin user" do it "cannot update a update a contact type" do sign_in_as_volunteer put contact_type_path(create(:contact_type)), params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot update a update a contact type" do put contact_type_path(create(:contact_type)), params: params expect(response).to redirect_to new_user_session_path end end end end ================================================ FILE: spec/requests/court_dates_spec.rb ================================================ require "rails_helper" RSpec.describe "/casa_cases/:casa_case_id/court_dates/:id", type: :request do include DownloadHelpers let(:admin) { create(:casa_admin) } let(:casa_case) { court_date.casa_case } let(:court_date) { create(:court_date) } let(:hearing_type) { create(:hearing_type) } let(:judge) { create(:judge, name: "8`l/UR*|`=Iab'A") } let(:valid_attributes) do { date: Date.yesterday, hearing_type_id: hearing_type.id, judge_id: judge.id } end let(:texts) { ["1-New Order Text One", "0-New Order Text Two"] } let(:implementation_statuses) { ["unimplemented", nil] } let(:orders_attributes) do { "0" => {text: texts[0], implementation_status: implementation_statuses[0], casa_case_id: casa_case.id}, "1" => {text: texts[1], implementation_status: implementation_statuses[1], casa_case_id: casa_case.id} } end let(:invalid_attributes) do { date: nil, hearing_type_id: hearing_type.id, judge_id: judge.id } end before do travel_to Date.new(2021, 1, 1) sign_in admin end describe "GET /show" do subject(:show) { get casa_case_court_date_path(casa_case, court_date) } before do casa_org = court_date.casa_case.casa_org casa_org.court_report_template.attach(io: File.new(Rails.root.join("spec/fixtures/files/default_past_court_date_template.docx")), filename: "test_past_date_template.docx") casa_org.court_report_template.save! show end context "when the request is authenticated" do it { expect(response).to have_http_status(:success) } end context "when the request is unauthenticated" do it "redirects to login page" do sign_out admin get casa_case_court_date_path(casa_case, court_date) expect(response).to redirect_to new_user_session_path end end context "when request format is word document" do subject(:show) { get casa_case_court_date_path(casa_case, court_date), headers: headers } let(:headers) { {accept: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"} } it { expect(response).to be_successful } it "displays the court date" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).to include(/December 25, 2020/) end context "when a judge is attached" do let!(:court_date) { create(:court_date, date: Date.yesterday, judge: judge) } it "includes the judge's name in the document" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{judge.name}/) end end context "without a judge" do let!(:court_date) { create(:court_date, date: Date.yesterday, judge: nil) } it "includes None for the judge's name in the document" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).not_to include(/#{judge.name}/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Judge:/) expect(docx_response.paragraphs.map(&:to_s)).to include(/None/) end end context "with a hearing type" do let!(:court_date) { create(:court_date, date: Date.yesterday, hearing_type: hearing_type) } it "includes the hearing type in the document" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).to include(/#{hearing_type.name}/) end end context "without a hearing type" do let!(:court_date) { create(:court_date, date: Date.yesterday, hearing_type: nil) } it "includes None for the hearing type in the document" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).not_to include(/#{hearing_type.name}/) expect(docx_response.paragraphs.map(&:to_s)).to include(/Hearing Type:/) expect(docx_response.paragraphs.map(&:to_s)).to include(/None/) end end context "with a court order" do let!(:court_date) { create(:court_date, :with_court_order) } it "includes court order info" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).to include(/Court Orders/) expect(table_text(docx_response)).to include(/#{court_date.case_court_orders.first.text}/) expect(table_text(docx_response)).to include(/#{court_date.case_court_orders.first.implementation_status.humanize}/) end end context "without a court order" do let!(:court_date) { create(:court_date) } it "does not include court orders section" do show docx_response = Docx::Document.open(StringIO.new(response.body)) expect(docx_response.paragraphs.map(&:to_s)).not_to include(/Court Orders/) end end end end describe "GET /new" do it "renders a successful response" do get new_casa_case_court_date_path(casa_case) expect(response).to be_successful end end describe "GET /edit" do it "render a successful response" do get edit_casa_case_court_date_path(casa_case, court_date) expect(response).to be_successful end it "fails across organizations" do other_org = create(:casa_org) other_case = create(:casa_case, casa_org: other_org) get edit_casa_case_court_date_path(other_case, court_date) expect(response).to redirect_to(casa_cases_path) expect(response.status).to match 302 expect(flash[:notice]).to eq("Sorry, you are not authorized to perform this action.") end end describe "POST /create" do let(:casa_case) { create(:casa_case) } let(:court_date) { CourtDate.last } context "with valid parameters" do it "creates a new CourtDate" do expect do post casa_case_court_dates_path(casa_case), params: {court_date: valid_attributes} end.to change(CourtDate, :count).by(1) end it "sets the court_report_due_date to be 3 weeks before the court_date" do post casa_case_court_dates_path(casa_case), params: {court_date: valid_attributes} expect(court_date.casa_case.court_dates.last.court_report_due_date).to eq(valid_attributes[:date] - 3.weeks) end it "redirects to the casa_case" do post casa_case_court_dates_path(casa_case), params: {court_date: valid_attributes} expect(response).to redirect_to(casa_case_court_date_path(casa_case, court_date)) end it "sets fields correctly" do post casa_case_court_dates_path(casa_case), params: {court_date: valid_attributes} expect(court_date.casa_case).to eq casa_case expect(court_date.date).to eq Date.yesterday expect(court_date.hearing_type).to eq hearing_type expect(court_date.judge).to eq judge end context "with case_court_orders_attributes being passed as a parameter" do let(:valid_params) do attributes = valid_attributes attributes[:case_court_orders_attributes] = orders_attributes attributes end it "Creates a new CaseCourtOrder" do expect do post casa_case_court_dates_path(casa_case), params: {court_date: valid_params} end.to change(CaseCourtOrder, :count).by(2) end it "sets fields correctly" do post casa_case_court_dates_path(casa_case), params: {court_date: valid_params} expect(court_date.case_court_orders.count).to eq 2 expect(court_date.case_court_orders[0].text).to eq texts[0] expect(court_date.case_court_orders[0].implementation_status).to eq implementation_statuses[0] expect(court_date.case_court_orders[1].text).to eq texts[1] expect(court_date.case_court_orders[1].implementation_status).to eq implementation_statuses[1] end end end context "for a future court date" do let(:valid_attributes) do { date: 10.days.from_now, hearing_type_id: hearing_type.id, judge_id: judge.id } end it "creates a new CourtDate" do expect do post casa_case_court_dates_path(casa_case), params: {court_date: valid_attributes} end.to change(CourtDate, :count).by(1) end end describe "invalid request" do context "with invalid parameters" do it "does not create a new CourtDate" do expect do post casa_case_court_dates_path(casa_case), params: {court_date: invalid_attributes} end.not_to change(CourtDate, :count) end it "renders an unprocessable entity response (i.e. to display the 'new' template)" do post casa_case_court_dates_path(casa_case), params: {court_date: invalid_attributes} expect(response).to have_http_status(:unprocessable_content) expected_errors = [ "Date can't be blank" ].freeze expect(assigns[:court_date].errors.full_messages).to eq expected_errors end end end end describe "PATCH /update" do let(:new_attributes) { { date: 1.week.ago.to_date, hearing_type_id: hearing_type.id, judge_id: judge.id, case_court_orders_attributes: orders_attributes } } context "with valid parameters" do it "updates the requested court_date" do patch casa_case_court_date_path(casa_case, court_date), params: {court_date: new_attributes} court_date.reload expect(court_date.date).to eq 1.week.ago.to_date expect(court_date.hearing_type).to eq hearing_type expect(court_date.judge).to eq judge expect(court_date.case_court_orders[0].text).to eq texts[0] expect(court_date.case_court_orders[0].implementation_status).to eq implementation_statuses[0] expect(court_date.case_court_orders[1].text).to eq texts[1] expect(court_date.case_court_orders[1].implementation_status).to eq implementation_statuses[1] end it "redirects to the court_date" do patch casa_case_court_date_path(casa_case, court_date), params: {court_date: new_attributes} expect(response).to redirect_to casa_case_court_date_path(casa_case, court_date) end end context "with invalid parameters" do it "renders an unprocessable entity response displaying the edit template" do patch casa_case_court_date_path(casa_case, court_date), params: {court_date: invalid_attributes} expect(response).to have_http_status(:unprocessable_content) expected_errors = [ "Date can't be blank" ].freeze expect(assigns[:court_date].errors.full_messages).to eq expected_errors end end describe "court orders" do context "when the user tries to make an existing order empty" do let(:orders_updated) do { case_court_orders_attributes: { "0" => { text: "New Order Text One Updated", implementation_status: :unimplemented }, "1" => { text: "" } } } end before do patch casa_case_court_date_path(casa_case, court_date), params: {court_date: new_attributes} court_date.reload @first_order_id = court_date.case_court_orders[0].id @second_order_id = court_date.case_court_orders[1].id orders_updated[:case_court_orders_attributes]["0"][:id] = @first_order_id orders_updated[:case_court_orders_attributes]["1"][:id] = @second_order_id end it "still updates the first order" do expect do patch casa_case_court_date_path(casa_case, court_date), params: {court_date: orders_updated} end.to( change { court_date.reload.case_court_orders.find(@first_order_id).text } ) end it "does not update the second order" do expect do patch casa_case_court_date_path(casa_case, court_date), params: {court_date: orders_updated} end.not_to( change { court_date.reload.case_court_orders.find(@second_order_id).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) expect do patch casa_case_court_date_path(other_casa_case, court_date), params: {court_date: new_attributes} end.not_to( change { court_date.reload.date } ) end end describe "DELETE /destroy" do subject(:request) do delete casa_case_court_date_path(casa_case, court_date) response end shared_examples "successful deletion" do it "removes court date record" do court_date expect { request }.to change { CourtDate.count }.by(-1) end it { is_expected.to redirect_to(casa_case_path(casa_case)) } it "shows correct flash message" do request expect(flash[:notice]).to match(/Court date was successfully deleted./) end end shared_examples "unsuccessful deletion" do it "does not remove court date record" do court_date expect { request }.not_to change { CourtDate.count } end it { is_expected.to redirect_to(casa_case_court_date_path(casa_case, court_date)) } it "shows correct flash message" do request expect(flash[:notice]).to match(/You can delete only future court dates./) end end context "when the court date is in the past" do it_behaves_like "unsuccessful deletion" end context "when the court date is today" do let(:court_date) { create(:court_date, date: Date.current) } it_behaves_like "unsuccessful deletion" end context "when the court date is in the future" do let(:court_date) { create(:court_date, date: 1.day.from_now) } it_behaves_like "successful deletion" end end end ================================================ FILE: spec/requests/custom_org_links_spec.rb ================================================ require "rails_helper" RSpec.describe "/custom_org_links", type: :request do let(:casa_org) { create(:casa_org) } let(:casa_admin) { create :casa_admin, casa_org: casa_org } let(:volunteer) { create :volunteer, casa_org: casa_org, active: true } describe "GET /custom_org_links/new" do context "when logged in as admin user" do before { sign_in casa_admin } it "can successfully access a custom org link create page" do get new_custom_org_link_path expect(response).to be_successful end end context "when logged in as a non-admin user" do before { sign_in volunteer } it "cannot access a custom org link create page" do get new_custom_org_link_path expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when not logged in" do it "cannot access a custom org link create page" do get new_custom_org_link_path expect(response).to redirect_to new_user_session_path end end end describe "POST /custom_org_links" do let(:params) { {custom_org_link: {text: "New Custom Link", url: "http://www.custom.link", active: true}} } context "when logged in as admin user" do let(:expected_custom_link_attributes) { params[:custom_org_link].merge(casa_org_id: casa_org.id).stringify_keys } before { sign_in casa_admin } it "can successfully create a custom org link" do expect { post custom_org_links_path, params: params }.to change { CustomOrgLink.count }.by(1) expect(CustomOrgLink.last.attributes).to include(**expected_custom_link_attributes) expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Custom link was successfully created." end end context "when logged in as a non-admin user" do before { sign_in volunteer } it "cannot create a custom org link" do post custom_org_links_path, params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when not logged in" do it "cannot create a custom org link" do post custom_org_links_path, params: params expect(response).to redirect_to new_user_session_path end end end describe "GET /custom_org_links/:id/edit" do context "when logged in as admin user" do before { sign_in_as_admin } it "can successfully access a contact type edit page" do get edit_custom_org_link_path(create(:custom_org_link)) expect(response).to be_successful end end context "when logged in as a non-admin user" do before { sign_in_as_volunteer } it "cannot access a contact type edit page" do get edit_custom_org_link_path(create(:custom_org_link)) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when not logged in" do it "cannot access a contact type edit page" do get edit_custom_org_link_path(create(:custom_org_link)) expect(response).to redirect_to new_user_session_path end end end describe "PUT /custom_org_links/:id" do let!(:custom_org_link) { create :custom_org_link, casa_org: casa_org, text: "Existing Link", url: "http://existing.com", active: false } let(:params) { {custom_org_link: {text: "New Custom Link", url: "http://www.custom.link", active: true}} } context "when logged in as admin user" do let(:expected_custom_link_attributes) { params[:custom_org_link].merge(casa_org_id: casa_org.id).stringify_keys } before { sign_in casa_admin } it "can successfully update a custom org link" do expect { put custom_org_link_path(custom_org_link), params: params }.to_not change { CustomOrgLink.count } expect(custom_org_link.reload.attributes).to include(**expected_custom_link_attributes) expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Custom link was successfully updated." end end context "when logged in as a non-admin user" do before { sign_in volunteer } it "cannot update a custom org link" do put custom_org_link_path(custom_org_link), params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when not logged in" do it "cannot update a custom org link" do put custom_org_link_path(custom_org_link), params: params expect(response).to redirect_to new_user_session_path end end end describe "DELETE /custom_org_links/:id" do let!(:custom_org_link) { create :custom_org_link, casa_org: casa_org, text: "Existing Link", url: "http://existing.com", active: false } context "when logged in as admin user" do before { sign_in casa_admin } it "can successfully delete a custom org link" do expect { delete custom_org_link_path(custom_org_link) }.to change { CustomOrgLink.count }.by(-1) expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Custom link was successfully deleted." end end context "when logged in as a non-admin user" do before { sign_in volunteer } it "cannot delete a custom org link" do delete custom_org_link_path(custom_org_link) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when not logged in" do it "cannot delete a custom org link" do delete custom_org_link_path(custom_org_link) expect(response).to redirect_to new_user_session_path end end end end ================================================ FILE: spec/requests/dashboard_spec.rb ================================================ require "rails_helper" RSpec.describe "/dashboard", type: :request do let(:organization) { create(:casa_org) } context "as a volunteer" do let(:volunteer) { create(:volunteer, casa_org: organization) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer) } before do sign_in volunteer end describe "GET /show" do context "with one active case" do it "redirects to the new case contact" do get root_url expect(response).to redirect_to(new_case_contact_path) end end context "more than one active case" do let!(:active_case_assignment) { create :case_assignment, volunteer: volunteer } it "renders a successful response" do get root_url expect(response).to redirect_to(casa_cases_path) end it "shows my cases" do get root_url follow_redirect! expect(response.body).to include(active_case_assignment.casa_case.case_number) expect(response.body).to include(case_assignment.casa_case.case_number) end it "doesn't show other volunteers' cases" do not_logged_in_volunteer = create(:volunteer) create(:case_assignment, volunteer: not_logged_in_volunteer) get root_url follow_redirect! expect(response.body).to include(active_case_assignment.casa_case.case_number) expect(response.body).to include(case_assignment.casa_case.case_number) expect(response.body).not_to include(not_logged_in_volunteer.casa_cases.first.case_number) end it "doesn't show other organizations' cases" do different_org = create(:casa_org) not_my_case_assignment = create(:case_assignment, casa_org: different_org) get root_url follow_redirect! expect(response.body).to include(active_case_assignment.casa_case.case_number) expect(response.body).to include(case_assignment.casa_case.case_number) expect(response.body).not_to include(not_my_case_assignment.casa_case.case_number) end end end end context "as a supervisor" do let(:supervisor) { create(:supervisor, casa_org: organization) } before do sign_in supervisor end describe "GET /show" do it "redirects to the volunteers overview" do get root_url expect(response).to redirect_to(volunteers_url) end end end context "as an admin" do let(:admin) { create(:casa_admin, casa_org: organization) } before do sign_in admin end describe "GET /show" do it "renders a successful response" do get root_url expect(response).to redirect_to(supervisors_path) end end end end ================================================ FILE: spec/requests/emancipation_checklists_spec.rb ================================================ require "rails_helper" RSpec.describe "/emancipation_checklists", type: :request do describe "GET /index" do before { sign_in volunteer } context "when viewing the page as a volunteer" do let(:volunteer) { build(:volunteer) } context "when viewing the page with exactly one transitioning case" do let(:casa_case) { build(:casa_case, casa_org: volunteer.casa_org) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } it "redirects to the emancipation checklist page for that case" do get emancipation_checklists_path expect(response).to redirect_to(casa_case_emancipation_path(casa_case)) end end context "when viewing the page with zero transitioning cases" do it "renders a successful response" do get emancipation_checklists_path expect(response).to be_successful end end context "when viewing the page with more than one transitioning cases" do let(:casa_case_a) { build(:casa_case, casa_org: volunteer.casa_org) } let(:casa_case_b) { build(:casa_case, casa_org: volunteer.casa_org) } let!(:case_assignment_a) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case_a) } let!(:case_assignment_b) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case_b) } it "renders a successful response" do get emancipation_checklists_path expect(response).to be_successful end end end end end ================================================ FILE: spec/requests/emancipations_request_spec.rb ================================================ require "rails_helper" RSpec.describe "/casa_case/:id/emancipation", type: :request do let(:organization) { build(:casa_org) } let(:other_organization) { create(:casa_org) } let(:casa_case) { create(:casa_case, casa_org: organization, birth_month_year_youth: 15.years.ago) } let(:casa_admin) { create(:casa_admin, casa_org: organization) } describe "GET /show" do subject(:request) do get casa_case_emancipation_path(casa_case, :docx) response end before { sign_in casa_admin } it { is_expected.to be_successful } it "authorizes casa case" do expect_any_instance_of(EmancipationsController).to receive(:authorize).with(casa_case).and_call_original request end it "populates and sends correct emancipation template" do sablon_template = double("Sablon::Template") allow(Sablon).to( receive(:template).with( File.expand_path("app/documents/templates/emancipation_checklist_template.docx") ).and_return(sablon_template) ) allow(Sablon).to receive(:content).and_return([]) expect(EmancipationChecklistDownloadHtml).to receive(:new).with(casa_case, []).and_call_original expected_context = {case_number: casa_case.case_number, emancipation_checklist: []} expect(sablon_template).to( receive(:render_to_string).with(expected_context, type: :docx).and_return("rendered context") ) expect_any_instance_of(EmancipationsController).to( receive(:send_data).with( "rendered context", filename: "#{casa_case.case_number} Emancipation Checklist.docx" ).and_call_original ) request end context "when request is not .docx" do subject(:request) do get casa_case_emancipation_path(casa_case) response end it { is_expected.to be_successful } it "does not send any data" do expect_any_instance_of(EmancipationsController).not_to receive(:send_data) request end end end describe "POST /save" do subject(:request) do post save_casa_case_emancipation_path(casa_case), params: params response end before { sign_in casa_admin } let(:category) { create(:emancipation_category) } let(:option_a) { create(:emancipation_option, emancipation_category_id: category.id, name: "A") } let(:params) { {check_item_action: "add_option", check_item_id: option_a.id} } it { is_expected.to be_successful } it "authorizes save_emancipation?" do expect_any_instance_of(EmancipationsController).to( receive(:authorize).with(CasaCase, :save_emancipation?).and_call_original ) expect_any_instance_of(EmancipationsController).to( receive(:authorize).with(casa_case, :update_emancipation_option?).and_call_original ) request end context "when check_item_id is invalid" do let(:params) { {check_item_action: "add_option", check_item_id: -1} } it { is_expected.not_to be_successful } it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "Tried to destroy an association that does not exist"}) end end context "when check_item_action is invalid" do let(:params) { {check_item_action: "invalid", check_item_id: option_a.id} } it { is_expected.not_to be_successful } it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "Check item action: invalid is not a supported action"}) end end context "when casa_case is not transitioning" do let(:params) { {check_item_action: "add_option", check_item_id: option_a.id} } let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_options: [], emancipation_categories: [], birth_month_year_youth: 13.years.ago) end it { is_expected.not_to be_successful } it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "The current case is not marked as transitioning"}) end end describe "each check_item_action" do context "with the add_category action" do let(:params) { {check_item_action: "add_category", check_item_id: category.id} } it { is_expected.to be_successful } it "adds the category" do expect { request }.to change { casa_case.emancipation_categories.count }.by(1) end context "when the category is already added to the case" do let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_categories: [category]) end it { is_expected.not_to be_successful } it "does not add the category" do expect { request }.not_to change { casa_case.emancipation_categories.count } end it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "The record already exists as an association on the case"}) end end end context "with the add_option action" do let(:params) { {check_item_action: "add_option", check_item_id: option_a.id} } it { is_expected.to be_successful } it "adds the option" do expect { request }.to change { casa_case.emancipation_options.count }.by(1) end context "when the option is already added to the case" do let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_options: [option_a]) end it { is_expected.not_to be_successful } it "does not add the option" do expect { request }.not_to change { casa_case.emancipation_options.count } end it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "The record already exists as an association on the case"}) end end end context "with the delete_category action" do let(:params) { {check_item_action: "delete_category", check_item_id: category.id} } let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_categories: [category], emancipation_options: [option_a]) end it { is_expected.to be_successful } it "removes the category" do expect { request }.to change { casa_case.emancipation_categories.count }.by(-1) end it "removes all options associated with the category" do expect { request }.to change { casa_case.emancipation_options.count }.by(-1) end context "when the category is not added to the case" do let(:casa_case) { create(:casa_case, casa_org: organization, emancipation_categories: []) } it { is_expected.not_to be_successful } it "does not remove anything" do expect { request }.not_to change { casa_case.emancipation_categories.count } end it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "Tried to destroy an association that does not exist"}) end end end context "with the delete_option action" do let(:params) { {check_item_action: "delete_option", check_item_id: option_a.id} } let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_options: [option_a]) end it { is_expected.to be_successful } it "removes the option" do expect { request }.to change { casa_case.emancipation_options.count }.by(-1) end context "when the option is not added to the case" do let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_options: []) end it { is_expected.not_to be_successful } it "does not remove anything" do expect { request }.not_to change { casa_case.emancipation_options.count } end it "shows correct error message" do body = request.parsed_body expect(body).to eq({"error" => "Tried to destroy an association that does not exist"}) end end end context "with the set_option action" do let(:other_category) { create(:emancipation_category) } let(:options) { create_list(:emancipation_option, 3, emancipation_category_id: other_category.id) } let(:option) { options.first } let(:params) { {check_item_action: "set_option", check_item_id: option.id} } let(:casa_case) do create(:casa_case, casa_org: organization, emancipation_options: [option_a, *options]) end it { is_expected.to be_successful } it "sets the option according to the right category" do request expect(casa_case.reload.emancipation_options).to contain_exactly(option_a, option) end end end end end ================================================ FILE: spec/requests/error_spec.rb ================================================ require "rails_helper" RSpec.describe "/error", type: :request do describe "GET /error" do it "renders the error test page" do get error_path expect(response).to be_successful end end describe "POST /error" do it "raises an error causing an internal server error" do expect { post error_path }.to raise_error(StandardError, /This is an intentional test exception/) end end end ================================================ FILE: spec/requests/followup_reports_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "FollowupReports", type: :request do describe "GET /index" do context "when the user has access" do let(:admin) { build(:casa_admin) } it "returns the CSV report" do sign_in admin get followup_reports_path(format: :csv) expect(response).to have_http_status(:success) expect(response.header["Content-Type"]).to eq("text/csv") expect(response.headers["Content-Disposition"]).to( match("followup-report-#{Time.current.strftime("%Y-%m-%d")}.csv") ) end it "adds the correct headers to the csv" do sign_in admin get followup_reports_path(format: :csv) csv_headers = [ "Case Number", "Volunteer Name(s)", "Note Creator Name", "Note" ] csv_headers.each { |header| expect(response.body).to include(header) } end end context "when the user is not authorized to access" do it "redirects to root and displays an unauthorized message" do volunteer = build(:volunteer) sign_in volunteer get followup_reports_path(format: :csv) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end end ================================================ FILE: spec/requests/fund_requests_spec.rb ================================================ require "rails_helper" RSpec.describe FundRequestsController, type: :request do describe "GET /casa_cases/:casa_id/fund_request/new" do context "when volunteer" do context "when casa_case is within organization" do it "is successful" do volunteer = create(:volunteer, :with_casa_cases) casa_case = volunteer.casa_cases.first sign_in volunteer get new_casa_case_fund_request_path(casa_case) expect(response).to be_successful end end context "when casa case is not within organization" do it "redirects to root" do volunteer = create(:volunteer) casa_case = create(:casa_case, casa_org: create(:casa_org)) sign_in volunteer get new_casa_case_fund_request_path(casa_case) expect(response).to redirect_to root_path end end end context "when supervisor" do context "when casa_case is within organization" do it "is successful" do org = create(:casa_org) supervisor = create(:supervisor, casa_org: org) casa_case = create(:casa_case, casa_org: org) sign_in supervisor get new_casa_case_fund_request_path(casa_case) expect(response).to be_successful end end context "when casa_case is not within organization" do it "redirects to root" do supervisor = create(:supervisor) casa_case = create(:casa_case, casa_org: create(:casa_org)) sign_in supervisor get new_casa_case_fund_request_path(casa_case) expect(response).to redirect_to root_path end end end context "when admin" do context "when casa_case is within organization" do it "is successful" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) casa_case = create(:casa_case, casa_org: org) sign_in admin get new_casa_case_fund_request_path(casa_case) expect(response).to be_successful end end context "when casa_case is not within organization" do it "redirects to root" do admin = create(:casa_admin) casa_case = create(:casa_case, casa_org: create(:casa_org)) sign_in admin get new_casa_case_fund_request_path(casa_case) expect(response).to redirect_to root_path end end end end describe "POST /casa_cases/:casa_id/fund_request" do let(:params) do { fund_request: { submitter_email: "submitter@example.com", youth_name: "CINA-123", payment_amount: "$10.00", deadline: "2022-12-31", request_purpose: "something noble", payee_name: "Minnie Mouse", requested_by_and_relationship: "Favorite Volunteer", other_funding_source_sought: "Some other agency", impact: "Great", extra_information: "foo bar" } } end context "when volunteer" do context "when casa_case is within organization" do context "with valid params" do it "creates fund request, calls mailer, and redirects to casa case" do volunteer = create(:volunteer, :with_casa_cases) casa_case = volunteer.casa_cases.first stub_const("ENV", {"FUND_REQUEST_RECIPIENT_EMAIL" => "recipient@example.com"}) sign_in volunteer expect { post casa_case_fund_request_path(casa_case), params: params }.to change(FundRequest, :count).by(1) .and change(ActionMailer::Base.deliveries, :count).by(1) fr = FundRequest.last aggregate_failures do expect(fr.submitter_email).to eq "submitter@example.com" expect(fr.youth_name).to eq "CINA-123" expect(fr.payment_amount).to eq "$10.00" expect(fr.deadline).to eq "2022-12-31" expect(fr.request_purpose).to eq "something noble" expect(fr.payee_name).to eq "Minnie Mouse" expect(fr.requested_by_and_relationship).to eq "Favorite Volunteer" expect(fr.other_funding_source_sought).to eq "Some other agency" expect(fr.impact).to eq "Great" expect(fr.extra_information).to eq "foo bar" expect(response).to redirect_to casa_case end mail = ActionMailer::Base.deliveries.last aggregate_failures do expect(mail.subject).to eq("Fund request from submitter@example.com") expect(mail.to).to contain_exactly("recipient@example.com", "submitter@example.com") expect(mail.body.encoded).to include("Youth name") expect(mail.body.encoded).to include("CINA-123") expect(mail.body.encoded).to include("Payment amount") expect(mail.body.encoded).to include("$10.00") expect(mail.body.encoded).to include("Deadline") expect(mail.body.encoded).to include("2022-12-31") expect(mail.body.encoded).to include("Request purpose") expect(mail.body.encoded).to include("something noble") expect(mail.body.encoded).to include("Payee name") expect(mail.body.encoded).to include("Minnie Mouse") expect(mail.body.encoded).to include("Requested by and relationship") expect(mail.body.encoded).to include("Favorite Volunteer") expect(mail.body.encoded).to include("Other funding source sought") expect(mail.body.encoded).to include("Some other agency") expect(mail.body.encoded).to include("Impact") expect(mail.body.encoded).to include("Great") expect(mail.body.encoded).to include("Extra information") expect(mail.body.encoded).to include("foo bar") end end end context "with invalid params" do it "does not create fund request or call mailer" do volunteer = create(:volunteer, :with_casa_cases) casa_case = volunteer.casa_cases.first allow_any_instance_of(FundRequest).to receive(:save).and_return(false) sign_in volunteer expect(FundRequestMailer).not_to receive(:send_request) expect { post casa_case_fund_request_path(casa_case), params: params }.not_to change(FundRequest, :count) expect(response).to have_http_status(:unprocessable_content) end end end context "when casa_case is not within organization" do it "does not create fund request or call mailer" do volunteer = create(:volunteer, :with_casa_cases) casa_case = create(:casa_case, casa_org: create(:casa_org)) sign_in volunteer expect(FundRequestMailer).not_to receive(:send_request) expect { post casa_case_fund_request_path(casa_case), params: params }.not_to change(FundRequest, :count) expect(response).to redirect_to root_path end end end context "when supervisor" do it "creates fund request, calls mailer, and redirects to casa case" do supervisor = create(:supervisor) casa_case = create(:casa_case) mailer_mock = double("mailer", deliver: nil) sign_in supervisor expect(FundRequestMailer).to receive(:send_request).with(nil, instance_of(FundRequest)).and_return(mailer_mock) expect(mailer_mock).to receive(:deliver) expect { post casa_case_fund_request_path(casa_case), params: params }.to change(FundRequest, :count).by(1) expect(response).to redirect_to casa_case end end context "when admin" do it "creates fund request, calls mailer, and redirects to casa case" do admin = create(:casa_admin) casa_case = create(:casa_case) mailer_mock = double("mailer", deliver: nil) sign_in admin expect(FundRequestMailer).to receive(:send_request).with(nil, instance_of(FundRequest)).and_return(mailer_mock) expect(mailer_mock).to receive(:deliver) expect { post casa_case_fund_request_path(casa_case), params: params }.to change(FundRequest, :count).by(1) expect(response).to redirect_to casa_case end end end end ================================================ FILE: spec/requests/health_spec.rb ================================================ require "rails_helper" RSpec.describe "Health", type: :request do before do Casa::Application.load_tasks Rake::Task["after_party:store_deploy_time"].invoke end describe "GET /health" do before do get "/health" end it "renders an html file" do # delete this test when there are more specific tests about the page expect(response.header["Content-Type"]).to include("text/html") end end describe "GET /health.json" do before do get "/health.json" end it "renders a json file" do expect(response.header["Content-Type"]).to include("application/json") end it "has key latest_deploy_time" do hash_body = nil # This is here for the linter expect { hash_body = JSON.parse(response.body).with_indifferent_access }.not_to raise_exception expect(hash_body.keys).to contain_exactly("latest_deploy_time") end end describe "GET #case_contacts_creation_times_in_last_week" do it "returns timestamps of case contacts created in the last week" do case_contact1 = create(:case_contact, created_at: 6.days.ago) case_contact2 = create(:case_contact, created_at: 2.weeks.ago) get case_contacts_creation_times_in_last_week_health_index_path expect(response).to have_http_status(:ok) expect(response.content_type).to include("application/json") timestamps = JSON.parse(response.body)["timestamps"] expect(timestamps).to include(case_contact1.created_at.to_i) expect(timestamps).not_to include(case_contact2.created_at.to_i) end end describe "GET #monthly_line_graph_data" do def setup # Create case contacts for testing create(:case_contact, notes: "Test Notes", created_at: 11.months.ago) create(:case_contact, notes: "", created_at: 11.months.ago) create(:case_contact, created_at: 10.months.ago) create(:case_contact, created_at: 9.months.ago) # Create associated contact_topic_answers create(:contact_topic_answer, case_contact: CaseContact.first) create(:contact_topic_answer, case_contact: CaseContact.last) end it "returns case contacts creation times in the last year" do travel_to Time.zone.local(2024, 5, 2) setup get monthly_line_graph_data_health_index_path expect(response).to have_http_status(:ok) expect(response.content_type).to include("application/json") chart_data = JSON.parse(response.body) expect(chart_data).to be_an(Array) expect(chart_data.length).to eq(12) expect(chart_data[0]).to eq([11.months.ago.strftime("%b %Y"), 2, 1, 2]) expect(chart_data[1]).to eq([10.months.ago.strftime("%b %Y"), 1, 0, 1]) expect(chart_data[2]).to eq([9.months.ago.strftime("%b %Y"), 1, 1, 1]) expect(chart_data[3]).to eq([8.months.ago.strftime("%b %Y"), 0, 0, 0]) end it "returns case contacts creation times in the last year (on the first of the month)" do travel_to Time.zone.local(2024, 5, 1) setup get monthly_line_graph_data_health_index_path expect(response).to have_http_status(:ok) expect(response.content_type).to include("application/json") chart_data = JSON.parse(response.body) expect(chart_data).to be_an(Array) expect(chart_data.length).to eq(12) expect(chart_data[0]).to eq([11.months.ago.strftime("%b %Y"), 2, 1, 2]) expect(chart_data[1]).to eq([10.months.ago.strftime("%b %Y"), 1, 0, 1]) expect(chart_data[2]).to eq([9.months.ago.strftime("%b %Y"), 1, 1, 1]) expect(chart_data[3]).to eq([8.months.ago.strftime("%b %Y"), 0, 0, 0]) end end describe "GET #monthly_unique_users_graph_data" do def setup volunteer1 = create(:user, type: "Volunteer") volunteer2 = create(:user, type: "Volunteer") supervisor = create(:user, type: "Supervisor") casa_admin = create(:user, type: "CasaAdmin") supervisor_volunteer1 = create(:supervisor_volunteer, is_active: true) supervisor_volunteer2 = create(:supervisor_volunteer, is_active: true) create(:login_activity, user: volunteer1, created_at: 11.months.ago, success: true) create(:login_activity, user: volunteer2, created_at: 11.months.ago, success: true) create(:login_activity, user: supervisor, created_at: 11.months.ago, success: true) create(:login_activity, user: casa_admin, created_at: 11.months.ago, success: true) create(:login_activity, user: volunteer1, created_at: 10.months.ago, success: true) create(:login_activity, user: volunteer2, created_at: 9.months.ago, success: true) create(:login_activity, user: supervisor, created_at: 9.months.ago, success: true) create(:login_activity, user: casa_admin, created_at: 9.months.ago, success: true) create(:case_contact, creator_id: supervisor_volunteer1.volunteer_id, created_at: 11.months.ago) create(:case_contact, creator_id: supervisor_volunteer1.volunteer_id, created_at: 11.months.ago) create(:case_contact, creator_id: supervisor_volunteer2.volunteer_id, created_at: 11.months.ago) create(:case_contact, creator_id: supervisor_volunteer1.volunteer_id, created_at: 10.months.ago) end it "returns monthly unique users data for volunteers, supervisors, and admins in the last year" do travel_to Time.zone.local(2024, 5, 2) setup get monthly_unique_users_graph_data_health_index_path expect(response).to have_http_status(:ok) expect(response.content_type).to include("application/json") chart_data = JSON.parse(response.body) expect(chart_data).to be_an(Array) expect(chart_data.length).to eq(12) expect(chart_data[0]).to eq([11.months.ago.strftime("%b %Y"), 2, 1, 1, 2]) expect(chart_data[1]).to eq([10.months.ago.strftime("%b %Y"), 1, 0, 0, 1]) expect(chart_data[2]).to eq([9.months.ago.strftime("%b %Y"), 1, 1, 1, 0]) end it "returns monthly unique users data for volunteers, supervisors, and admins in the last year (on the first of the month)" do travel_to Time.zone.local(2024, 5, 1) setup get monthly_unique_users_graph_data_health_index_path expect(response).to have_http_status(:ok) expect(response.content_type).to include("application/json") chart_data = JSON.parse(response.body) expect(chart_data).to be_an(Array) expect(chart_data.length).to eq(12) expect(chart_data[0]).to eq([11.months.ago.strftime("%b %Y"), 2, 1, 1, 2]) expect(chart_data[1]).to eq([10.months.ago.strftime("%b %Y"), 1, 0, 0, 1]) expect(chart_data[2]).to eq([9.months.ago.strftime("%b %Y"), 1, 1, 1, 0]) end end end ================================================ FILE: spec/requests/hearing_types_spec.rb ================================================ require "rails_helper" RSpec.describe "/hearing_types", type: :request do describe "GET /hearing_types/new" do context "when logged in as admin user" do it "allows access to hearing type create page" do sign_in_as_admin get new_hearing_type_path expect(response).to be_successful end end context "when logged in as a non-admin user" do it "does not allow access to hearing type create page" do sign_in_as_volunteer get new_hearing_type_path expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when an unauthenticated request is made" do it "does not allow access to hearing type create page" do get new_hearing_type_path expect(response).to redirect_to new_user_session_path end end end describe "POST /hearing_types" do let(:params) { {hearing_type: {name: "New Hearing", active: true}} } context "when logged in as admin user" do it "successfully create a hearing type" do casa_org = create(:casa_org) sign_in create(:casa_admin, casa_org: casa_org) expect { post hearing_types_path, params: params }.to change(HearingType, :count).by(1) hearing_type = HearingType.last expect(hearing_type.name).to eql "New Hearing" expect(hearing_type.active).to be_truthy expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Hearing Type was successfully created." end end context "when logged in as a non-admin user" do it "does not create a hearing type" do sign_in_as_volunteer post hearing_types_path, params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when an unauthenticated request is made" do it "does not create a hearing type" do post hearing_types_path, params: params expect(response).to redirect_to new_user_session_path end end end describe "GET /hearing_types/:id/edit" do context "when logged in as admin user" do it "allows access to hearing type edit page" do sign_in_as_admin get edit_hearing_type_path(create(:hearing_type)) expect(response).to be_successful end end context "when logged in as a non-admin user" do it "does not allow access to hearing type edit page" do sign_in_as_volunteer get edit_hearing_type_path(create(:hearing_type)) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when an unauthenticated request is made" do it "does not allow access to hearing type edit page" do get edit_hearing_type_path(create(:hearing_type)) expect(response).to redirect_to new_user_session_path end end end describe "PUT /hearing_types/:id" do let(:casa_org) { create(:casa_org) } let(:params) { {hearing_type: {name: "New Name", active: true}} } context "when logged in as admin user" do it "successfully update hearing type with active status" do sign_in create(:casa_admin, casa_org: casa_org) hearing_type = create(:hearing_type) put hearing_type_path(hearing_type), params: params hearing_type.reload expect(hearing_type.name).to eq "New Name" expect(hearing_type.active).to be_truthy expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Hearing Type was successfully updated." end it "successfully update hearing type with inactive status" do sign_in create(:casa_admin, casa_org: casa_org) hearing_type = create(:hearing_type) put hearing_type_path(hearing_type), params: params hearing_type.update(active: false) hearing_type.reload expect(hearing_type.name).to eq "New Name" expect(hearing_type.active).to be_falsey expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Hearing Type was successfully updated." end end context "when logged in as a non-admin user" do it "does not update hearing type" do sign_in_as_volunteer put hearing_type_path(create(:hearing_type)), params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "when an unauthenticated request is made" do it "does not update hearing type" do put hearing_type_path(create(:hearing_type)), params: params expect(response).to redirect_to new_user_session_path end end end end ================================================ FILE: spec/requests/imports_spec.rb ================================================ require "rails_helper" RSpec.describe "/imports", type: :request do let(:volunteer_file) { fixture_file_upload "volunteers.csv", "text/csv" } let(:supervisor_file) { fixture_file_upload "supervisors.csv", "text/csv" } let(:case_file) { fixture_file_upload "casa_cases.csv", "text/csv" } let(:existing_case_file) { fixture_file_upload "existing_casa_case.csv", "text/csv" } let(:supervisor_volunteers_file) { fixture_file_upload "supervisor_volunteers.csv", "text/csv" } let(:casa_admin) { build(:casa_admin) } let(:pre_transition_aged_youth_age) { Date.current - 14.years } before do # next_court_date in casa_cases.csv needs to be a future date travel_to Date.parse("Sept 15 2022") end describe "GET /index" do it "renders an unsuccessful response when the user is not an admin" do sign_in create(:volunteer) get imports_url expect(response).not_to be_successful end it "renders a successful response when the user is an admin" do sign_in casa_admin get imports_url expect(response).to be_successful end it "validates volunteers CSV header" do sign_in casa_admin post imports_url, params: { import_type: "volunteer", file: supervisor_file, sms_opt_in: "1" } expect(request.session[:import_error]).to include("Expected", VolunteerImporter::IMPORT_HEADER.join(", ")) expect(response).to redirect_to(imports_url(import_type: "volunteer")) end it "validates supervisors CSV header" do sign_in casa_admin post imports_url, params: { import_type: "supervisor", file: volunteer_file, sms_opt_in: "1" } expect(request.session[:import_error]).to include("Expected", SupervisorImporter::IMPORT_HEADER.join(", ")) expect(response).to redirect_to(imports_url(import_type: "supervisor")) end it "validates cases CSV header" do sign_in casa_admin post imports_url, params: { import_type: "casa_case", file: supervisor_file, sms_opt_in: "1" } expect(request.session[:import_error]).to include("Expected", CaseImporter::IMPORT_HEADER.join(", ")) expect(response).to redirect_to(imports_url(import_type: "casa_case")) end it "creates volunteers in volunteer CSV imports" do sign_in casa_admin expect(Volunteer.count).to eq(0) expect { post imports_url, params: { import_type: "volunteer", file: volunteer_file, sms_opt_in: "1" } }.to change(Volunteer, :count).by(3) expect(response).to redirect_to(imports_url(import_type: "volunteer")) end it "creates supervisors and adds volunteers in supervisor CSV imports" do sign_in casa_admin # make sure appropriate volunteers exist VolunteerImporter.import_volunteers(volunteer_file, casa_admin.casa_org_id) expect(Supervisor.count).to eq(0) expect { post imports_url, params: { import_type: "supervisor", file: supervisor_file, sms_opt_in: "1" } }.to change(Supervisor, :count).by(3) expect(Supervisor.find_by(email: "supervisor1@example.net").volunteers.size).to eq(1) expect(Supervisor.find_by(email: "supervisor2@example.net").volunteers.size).to eq(2) expect(Supervisor.find_by(email: "supervisor3@example.net").volunteers.size).to eq(0) expect(response).to redirect_to(imports_url(import_type: "supervisor")) end it "creates supervisors and assigns the volunteer if not already assigned" do sign_in casa_admin # make sure appropriate volunteers exist VolunteerImporter.new(volunteer_file, casa_admin.casa_org_id).import_volunteers expect(Supervisor.count).to eq(0) expect { post imports_url, params: { import_type: "supervisor", file: supervisor_volunteers_file, sms_opt_in: "1" } }.to change(Supervisor, :count).by(2) expect(Supervisor.find_by(email: "s5@example.com").volunteers.size).to eq(1) expect(Supervisor.find_by(email: "s6@example.com").volunteers.size).to eq(0) expect(response).to redirect_to(imports_url(import_type: "supervisor")) end it "creates case in cases CSV imports and adds next court date" do sign_in casa_admin expect(CasaCase.count).to eq(0) expect { post imports_url, params: { import_type: "casa_case", file: case_file, sms_opt_in: "1" } }.to change(CasaCase, :count).by(3) expect(CasaCase.first.next_court_date).not_to be_nil expect(CasaCase.last.next_court_date).to be_nil expect(response).to redirect_to(imports_url(import_type: "casa_case")) end it "produces an error when a deactivated case already exists in cases CSV imports" do sign_in casa_admin create(:casa_case, case_number: "CINA-00-0000", birth_month_year_youth: pre_transition_aged_youth_age, active: "false") expect(CasaCase.count).to eq(1) expect { post imports_url, params: { import_type: "casa_case", file: existing_case_file } }.not_to change(CasaCase, :count) expect(request.session[:import_error]).to include("Not all rows were imported.") expect(response).to redirect_to(imports_url(import_type: "casa_case")) failed_csv_path = FailedImportCsvService.new(import_type: :casa_case, user: casa_admin).csv_filepath expect(File.exist?(failed_csv_path)).to be true file_contents = File.read(failed_csv_path) expect(file_contents).to include("Case CINA-00-0000 already exists, but is inactive. Reactivate the CASA case instead.") FileUtils.rm_f(failed_csv_path) end it "calls FailedImportCsvService#store when there are failed rows from the import" do sign_in casa_admin csv_service_double = instance_double(FailedImportCsvService) allow(csv_service_double).to receive(:failed_rows=) allow(csv_service_double).to receive(:store) allow(csv_service_double).to receive(:cleanup) allow(FailedImportCsvService).to receive(:new).and_return(csv_service_double) allow(CaseImporter).to receive(:import_cases).and_return({ message: "Some cases were not imported.", exported_rows: "Case CINA-00-0000 already exists, but is inactive. Reactivate the CASA case instead.", type: :error }) expect(csv_service_double).to receive(:failed_rows=).with("Case CINA-00-0000 already exists, but is inactive. Reactivate the CASA case instead.") expect(csv_service_double).to receive(:store) expect(csv_service_double).to receive(:cleanup) post imports_url, params: { import_type: "casa_case", file: fixture_file_upload("existing_casa_case.csv", "text/csv") } expect(request.session[:import_error]).to include("Click here to download failed rows.") expect(response).to redirect_to(imports_url(import_type: "casa_case")) end it "writes a fallback message when exported rows exceed max size" do sign_in casa_admin large_exported_content = "a" * (FailedImportCsvService::MAX_FILE_SIZE_BYTES + 1) allow(CaseImporter).to receive(:import_cases).and_return({ message: "Some rows were too large.", exported_rows: large_exported_content, type: :error }) post imports_url, params: { import_type: "casa_case", file: case_file } failed_csv_path = FailedImportCsvService.new(import_type: :casa_case, user: casa_admin).csv_filepath expect(File.exist?(failed_csv_path)).to be true written_content = File.read(failed_csv_path) expect(written_content).to include("The file was too large to save") expect(request.session[:import_error]).to include("Click here to download failed rows.") FileUtils.rm_f(failed_csv_path) end it "shows a fallback error message when the failed import CSV was never created" do sign_in casa_admin get download_failed_imports_path(import_type: "casa_case") expect(response.body).to include("Please upload a CASA Case CSV") expect(response.headers["Content-Disposition"]).to include("attachment; filename=\"failed_rows.csv\"") end it "shows a fallback error message when the failed import CSV expired" do sign_in casa_admin service = FailedImportCsvService.new( import_type: :casa_case, user: casa_admin, failed_rows: "some content", filepath: path ) service.store File.utime(2.days.ago.to_time, 2.days.ago.to_time, service.csv_filepath) get download_failed_imports_path(import_type: "casa_case") expect(response.body).to include("Please upload a CASA Case CSV") expect(response.headers["Content-Disposition"]).to include("attachment; filename=\"failed_rows.csv\"") expect(File.exist?(path)).to be_falsey end end end ================================================ FILE: spec/requests/judges_spec.rb ================================================ require "rails_helper" RSpec.describe "/judges", type: :request do describe "GET /judges/new" do context "logged in as admin user" do it "can successfully access a judge create page" do sign_in_as_admin get new_judge_path expect(response).to be_successful end end context "logged in as a non-admin user" do it "cannot access a judge create page" do sign_in_as_volunteer get new_judge_path expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a judge create page" do get new_judge_path expect(response).to redirect_to new_user_session_path end end end describe "POST /judges" do let(:params) { {judge: {name: "Joe Judge", active: true}} } context "logged in as admin user" do it "can successfully create a judge" do casa_org = build(:casa_org) sign_in build(:casa_admin, casa_org: casa_org) expect { post judges_path, params: params }.to change(Judge, :count).by(1) judge = Judge.last expect(judge.name).to eql "Joe Judge" expect(judge.casa_org).to eql casa_org expect(judge.active).to be_truthy expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Judge was successfully created." end end context "logged in as a non-admin user" do it "cannot create a judge" do sign_in_as_volunteer post judges_path, params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot create a judge" do post judges_path, params: params expect(response).to redirect_to new_user_session_path end end end describe "GET /judges/:id/edit" do let(:judge) { create(:judge) } context "logged in as admin user" do it "can successfully access a judge edit page" do sign_in_as_admin get edit_judge_path(judge) expect(response).to be_successful end end context "logged in as a non-admin user" do it "cannot access a judge edit page" do sign_in_as_volunteer get edit_judge_path(judge) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot access a judge edit page" do get edit_judge_path(judge) expect(response).to redirect_to new_user_session_path end end end describe "PUT /judges/:id" do let(:judge) { create(:judge) } let(:params) { {judge: {name: "New Name", judge_id: judge.id, active: false}} } context "logged in as admin user" do it "can successfully update a judge" do casa_org = build(:casa_org) sign_in build(:casa_admin, casa_org: casa_org) put judge_path(judge), params: params judge.reload expect(judge.name).to eq "New Name" expect(judge.active).to be_falsey expect(response).to redirect_to edit_casa_org_path(casa_org) expect(response.request.flash[:notice]).to eq "Judge was successfully updated." end end context "logged in as a non-admin user" do it "cannot update a judge" do sign_in_as_volunteer put judge_path(judge), params: params expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end context "unauthenticated request" do it "cannot update a judge" do put judge_path(judge), params: params expect(response).to redirect_to new_user_session_path end end end end ================================================ FILE: spec/requests/languages_spec.rb ================================================ require "rails_helper" RSpec.describe LanguagesController, type: :request do describe "POST /create" do context "when request params are valid" do it "creates a new language" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) sign_in admin post languages_path, params: { language: { name: "Spanish" } } expect(response.status).to eq 302 expect(response).to redirect_to(edit_casa_org_path(organization.id)) expect(flash[:notice]).to eq "Language was successfully created." end end end describe "PATCH /update" do context "when request params are valid" do it "creates a new language" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) language = create(:language, casa_org: organization) sign_in admin patch language_path(language), params: { language: { name: "Spanishes" } } expect(response.status).to eq 302 expect(response).to redirect_to(edit_casa_org_path(organization.id)) expect(flash[:notice]).to eq "Language was successfully updated." end end end end ================================================ FILE: spec/requests/learning_hour_topics_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "LearningHourTopics", type: :request do let(:casa_org) { create(:casa_org) } let(:admin) { build(:casa_admin, casa_org:) } before do sign_in admin end describe "GET /new" do it "returns a successful response" do get new_learning_hour_topic_path expect(response).to have_http_status(:success) end end describe "GET /edit" do it "returns a successful response" do learning_hour_topic = create(:learning_hour_topic, casa_org:) get edit_learning_hour_topic_path(learning_hour_topic) expect(response).to have_http_status(:success) end end describe "POST /create" do context "when the params are valid" do it "creates the learning hour topic successfully and redirects to the organization's edit page" do params = { learning_hour_topic: { name: "Social Science" } } expect do post learning_hour_topics_path, params: params end.to change(LearningHourTopic, :count).by(1) expect(response).to have_http_status(:redirect) expect(flash[:notice]).to match(/learning topic was successfully created/i) expect(response).to redirect_to(edit_casa_org_path(casa_org)) end end context "when the params are not valid" do it "returns an unprocessable_content response" do params = { learning_hour_topic: { name: nil } } expect do post learning_hour_topics_path, params: params end.not_to change(LearningHourTopic, :count) expect(response).to have_http_status(:unprocessable_content) end end end describe "PATCH /update" do context "when the params are valid" do it "updates the learning hour type successfully and redirects to the organization's edit page" do learning_hour_topic = create(:learning_hour_topic, casa_org:, name: "Homeschooling") params = {learning_hour_topic: {name: "Remote"}} patch learning_hour_topic_path(learning_hour_topic), params: params expect(response).to redirect_to(edit_casa_org_path(casa_org)) expect(learning_hour_topic.reload.name).to eq("Remote") expect(flash[:notice]).to match(/learning topic was successfully updated/i) end end context "when the params are invalid" do it "returns an unprocessable_content response" do learning_hour_topic = create(:learning_hour_topic, casa_org:, name: "Homeschooling") params = {learning_hour_topic: {name: nil}} patch learning_hour_topic_path(learning_hour_topic), params: params expect(response).to have_http_status(:unprocessable_content) end end end end ================================================ FILE: spec/requests/learning_hour_types_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "LearningHourTypes", type: :request do let(:casa_org) { create(:casa_org) } let(:admin) { build(:casa_admin, casa_org:) } before do sign_in admin end describe "GET /new" do it "returns a successful response" do get new_learning_hour_type_path expect(response).to have_http_status(:success) end end describe "GET /edit" do it "returns a successful response" do learning_hour_type = create(:learning_hour_type, casa_org:) get edit_learning_hour_type_path(learning_hour_type) expect(response).to have_http_status(:success) end end describe "POST /create" do context "when the params are valid" do it "creates the learning hour type successfully and redirects to the organization's edit page" do params = { learning_hour_type: { name: "Homeschooling", active: true } } expect do post learning_hour_types_path, params: params end.to change(LearningHourType, :count).by(1) expect(response).to have_http_status(:redirect) expect(flash[:notice]).to match(/learning type was successfully created/i) expect(response).to redirect_to(edit_casa_org_path(casa_org)) end end context "when the params are not valid" do it "returns an unprocessable_content response" do params = { learning_hour_type: { active: true } } expect do post learning_hour_types_path, params: params end.not_to change(LearningHourType, :count) expect(response).to have_http_status(:unprocessable_content) end end end describe "PATCH /update" do context "when the params are valid" do it "updates the learning hour type successfully and redirects to the organization's edit page" do learning_hour_type = create(:learning_hour_type, casa_org:, name: "Homeschooling") params = {learning_hour_type: {name: "Remote"}} patch learning_hour_type_path(learning_hour_type), params: params expect(response).to redirect_to(edit_casa_org_path(casa_org)) expect(learning_hour_type.reload.name).to eq("Remote") expect(flash[:notice]).to match(/learning type was successfully updated/i) end end context "when the params are invalid" do it "returns an unprocessable_content response" do learning_hour_type = create(:learning_hour_type, casa_org:, name: "Homeschooling") params = {learning_hour_type: {name: nil}} patch learning_hour_type_path(learning_hour_type), params: params expect(response).to have_http_status(:unprocessable_content) end end end end ================================================ FILE: spec/requests/learning_hours_reports_spec.rb ================================================ require "rails_helper" RSpec.describe "LearningHoursReports", type: :request do let(:organization) { create(:casa_org) } let(:admin) { build(:casa_admin, casa_org: organization) } describe "GET /index" do subject(:request) do get "#{learning_hours_reports_url}.csv" response end before do sign_in admin allow(LearningHoursReport).to receive(:new).and_call_original end it { is_expected.to be_successful } it "triggers report generation correctly" do request expect(LearningHoursReport).to have_received(:new).once.with(organization.id) end it "sends downloadable data correctly", :aggregate_failures do response_headers = request.headers expect(response_headers["Content-Type"]).to match("text/csv") expect(response_headers["Content-Disposition"]).to( match("learning-hours-report-#{Time.current.strftime("%Y-%m-%d")}.csv") ) end end end ================================================ FILE: spec/requests/learning_hours_spec.rb ================================================ require "rails_helper" RSpec.describe "LearningHours", type: :request do let(:volunteer) { create(:volunteer) } context "as a volunteer user" do before { sign_in volunteer } describe "GET /index" do it "succeeds" do get learning_hours_path expect(response).to have_http_status(:success) end it "displays the Learning Topic column if learning_topic_active is true" do volunteer.casa_org.update(learning_topic_active: true) get learning_hours_path expect(response.body).to include("Learning Topic") end it "does not display the Learning Topic column if learning_topic_active is false" do volunteer.casa_org.update(learning_topic_active: false) get learning_hours_path expect(response.body).not_to include("Learning Topic") end end end context "as a supervisor user" do let(:supervisor) { create(:supervisor) } before { sign_in supervisor } describe "GET /index" do it "succeeds" do get learning_hours_path expect(response).to have_http_status(:success) end it "displays the time completed column" do get learning_hours_path expect(response.body).to include("Time Completed YTD") end end end context "as an admin user" do let(:admin) { create(:casa_admin) } before { sign_in admin } describe "GET /index" do it "succeeds" do get learning_hours_path expect(response).to have_http_status(:success) end it "displays the time completed column" do get learning_hours_path expect(response.body).to include("Time Completed YTD") end end end end ================================================ FILE: spec/requests/mileage_rates_spec.rb ================================================ require "rails_helper" RSpec.describe "/mileage_rates", type: :request do let(:casa_org) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } describe "GET /index" do subject(:request) do get mileage_rates_path response end let!(:mileage_rate) { create(:mileage_rate, effective_date: Date.new(2023, 1, 1), casa_org: casa_org) } let!(:other_mileage_rate) do create(:mileage_rate, effective_date: Date.new(2023, 2, 1), casa_org: casa_org) end let!(:other_casa_org_mileage_rate) do create(:mileage_rate, effective_date: Date.new(2023, 2, 1)) end before { sign_in admin } it { is_expected.to be_successful } it "shows mileage rates correctly", :aggregate_failures do page = request.body expect(page).to match(/#{mileage_rate_path(mileage_rate)}.*#{mileage_rate_path(other_mileage_rate)}/m) expect(page).not_to include(mileage_rate_path(other_casa_org_mileage_rate)) end end describe "GET /new" do it "renders a successful response only for admin user" do sign_in admin get new_mileage_rate_path expect(response).to be_successful end end describe "POST /create" do let(:mileage_rate) { MileageRate.last } before do sign_in admin end context "with valid params" do let(:params) do { mileage_rate: { casa_org_id: admin.casa_org_id, effective_date: DateTime.current, amount: "22.87" } } end it "creates a new mileage rate" do expect { post mileage_rates_path, params: params }.to change(MileageRate, :count).by(1) expect(response).to have_http_status(:redirect) expect(mileage_rate[:casa_org_id]).to eq(admin.casa_org_id) expect(mileage_rate[:effective_date]).to eq(params[:mileage_rate][:effective_date].to_date) expect(mileage_rate[:amount]).to eq(params[:mileage_rate][:amount].to_f) expect(response).to redirect_to mileage_rates_path end end context "with invalid parameters" do let(:params) do { mileage_rate: { casa_org_id: admin.casa_org_id, effective_date: DateTime.current, amount: "" } } end it "does not create a mileage rate" do expect { post mileage_rates_path, params: params }.not_to change { MileageRate.count } expect(response).to have_http_status(:unprocessable_content) end end context "when a previous mileage rate exists for the effective date" do let(:date) { DateTime.current } let(:params) do { mileage_rate: { casa_org_id: admin.casa_org_id, effective_date: date, amount: "" } } end before do create(:mileage_rate, effective_date: date) end it "must not create a new mileage rate" do expect { post mileage_rates_path, params: params }.not_to change { MileageRate.count } expect(response).to have_http_status(:unprocessable_content) end end end describe "PATCH /update" do let(:mileage_rate) { create(:mileage_rate, amount: 10.11, effective_date: DateTime.parse("01-01-2021")) } before { sign_in admin } context "with valid params" do it "updates the mileage_rate" do patch mileage_rate_path(mileage_rate), params: { mileage_rate: { amount: "22.87" } } expect(response).to have_http_status(:redirect) mileage_rate.reload expect(mileage_rate.amount).to eq 22.87 end end context "with invalid parameters" do let(:params) do { mileage_rate: { amount: "" } } end it "does not update a mileage rate" do patch mileage_rate_path(mileage_rate), params: params expect(response).to have_http_status(:unprocessable_content) mileage_rate.reload expect(mileage_rate.amount).to eq(10.11) end end context "when updating the mileage rate effective date and already exists one" do let(:another_mileage_rate) { create(:mileage_rate) } let(:params) do { mileage_rate: { effective_date: DateTime.parse("01-01-2021") } } end it "does not update a mileage rate" do expect { patch mileage_rate_path(another_mileage_rate), params: params }.not_to change { another_mileage_rate } end end end end ================================================ FILE: spec/requests/mileage_reports_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "MileageReports", type: :request do describe "GET /index" do context "when the user has access" do let(:admin) { build(:casa_admin) } it "returns the CSV report" do sign_in admin get mileage_reports_path(format: :csv) expect(response).to have_http_status(:success) expect(response.header["Content-Type"]).to eq("text/csv") expect(response.headers["Content-Disposition"]).to( match("mileage-report-#{Time.current.strftime("%Y-%m-%d")}.csv") ) end it "adds the correct headers to the csv" do sign_in admin get mileage_reports_path(format: :csv) csv_headers = [ "Contact Types", "Occurred At", "Miles Driven", "Casa Case Number", "Creator Name", "Supervisor Name", "Volunteer Address", "Reimbursed" ] csv_headers.each { |header| expect(response.body).to include(header) } end end context "when the user is not authorized to access" do it "redirects to root and displays an unauthorized message" do volunteer = build(:volunteer) sign_in volunteer get mileage_reports_path(format: :csv) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end end ================================================ FILE: spec/requests/missing_data_reports_spec.rb ================================================ require "rails_helper" RSpec.describe MissingDataReportsController, type: :request do let(:admin) { create(:casa_admin) } context "as an admin user" do describe "GET /index" do before do sign_in admin get missing_data_reports_path(format: :csv) end it "returns a successful response" do expect(response).to be_successful expect(response.header["Content-Type"]).to eq("text/csv") end end end context "without authenctication" do describe "GET /index" do before { get missing_data_reports_path(format: :csv) } it "return unauthorized" do expect(response).to have_http_status(:unauthorized) end end end end ================================================ FILE: spec/requests/notes_spec.rb ================================================ require "rails_helper" RSpec.describe "/volunteers/notes", type: :request do describe "POST /create" do context "when logged in as admin" do it "can create a note for volunteer in same organization" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) sign_in admin expect { post volunteer_notes_path(volunteer), params: {note: {content: "Very nice!"}} }.to change(Note, :count).by(1) expect(response).to redirect_to edit_volunteer_path(volunteer) expect(Note.last.content).to eq "Very nice!" end it "cannot create a note for volunteer in different organization" do organization = create(:casa_org) other_organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) sign_in admin expect { post volunteer_notes_path(volunteer), params: {note: {content: "Very nice!"}} }.not_to change(Note, :count) expect(response).to redirect_to root_path end end context "when logged in as a supervisor" do it "can create a note for volunteer in same organization" do organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) sign_in supervisor expect { post volunteer_notes_path(volunteer), params: {note: {content: "Very nice!"}} }.to change(Note, :count).by(1) expect(response).to redirect_to edit_volunteer_path(volunteer) expect(Note.last.content).to eq "Very nice!" end it "cannot create a note for volunteer in different organization" do organization = create(:casa_org) other_organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) sign_in supervisor expect { post volunteer_notes_path(volunteer), params: {note: {content: "Very nice!"}} }.not_to change(Note, :count) expect(response).to redirect_to root_path end end context "when logged in as volunteer" do it "cannot create a note" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) sign_in volunteer expect { post volunteer_notes_path(volunteer), params: {note: {content: "Very nice!"}} }.not_to change(Note, :count) expect(response).to redirect_to root_path end end end describe "GET /edit" do context "when logged in as admin" do context "when volunteer in same organization" do it "is successful if note belongs to volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer) sign_in admin get edit_volunteer_note_path(volunteer, note) expect(response).to be_successful end it "redirects to root path if note does not belong to volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) other_volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: other_volunteer) sign_in admin get edit_volunteer_note_path(volunteer, note) expect(response).to redirect_to root_path end end context "when volunteer in different organization" do it "redirects to root path" do organization = create(:casa_org) other_organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) note = create(:note, notable: volunteer) sign_in admin get edit_volunteer_note_path(volunteer, note) expect(response).to redirect_to root_path end end end context "when logged in as supervisor" do context "when volunteer in same organization" do it "is successful if note belongs to volunteer" do organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer) sign_in supervisor get edit_volunteer_note_path(volunteer, note) expect(response).to be_successful end it "redirects to root path if note does not belong to volunteer" do organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) other_volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: other_volunteer) sign_in supervisor get edit_volunteer_note_path(volunteer, note) expect(response).to redirect_to root_path end end context "when volunteer in different organization" do it "redirects to root path" do organization = create(:casa_org) other_organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) note = create(:note, notable: volunteer) sign_in supervisor get edit_volunteer_note_path(volunteer, note) expect(response).to redirect_to root_path end end end context "when logged in as volunteer" do context "when note belongs to volunteer" do it "redirects to root path" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer) sign_in volunteer get edit_volunteer_note_path(volunteer, note) expect(response).to redirect_to root_path end end end end describe "PATCH /update" do context "when logged in as an admin" do context "when volunteer in same org" do it "updates note and redirects to edit volunteer page" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer, creator: admin, content: "Good job.") sign_in admin patch volunteer_note_path(volunteer, note), params: {note: {content: "Very nice!"}} expect(response).to redirect_to(edit_volunteer_path(volunteer)) expect(note.reload.content).to eq "Very nice!" end end context "when volunteer in different org" do it "does not update note and redirects to root path" do organization = create(:casa_org) other_organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) note = create(:note, notable: volunteer, content: "Good job.") sign_in admin patch volunteer_note_path(volunteer, note), params: {note: {content: "Very nice!"}} expect(response).to redirect_to root_path expect(note.reload.content).to eq "Good job." end end end context "when logged in as a supervisor" do context "when volunteer in same org" do it "updates note and redirects to edit volunteer page" do organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer, content: "Good job.") sign_in supervisor patch volunteer_note_path(volunteer, note), params: {note: {content: "Very nice!"}} expect(response).to redirect_to(edit_volunteer_path(volunteer)) expect(note.reload.content).to eq "Very nice!" end end context "when volunteer in different org" do it "does not update note and redirects to root path" do organization = create(:casa_org) other_organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) note = create(:note, notable: volunteer, content: "Good job.") sign_in supervisor patch volunteer_note_path(volunteer, note), params: {note: {content: "Very nice!"}} expect(response).to redirect_to root_path expect(note.reload.content).to eq "Good job." end end end context "when logged in as a volunteer" do context "when updating note belonging to volunteer" do it "does not update note and redirects to root path" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) note = create(:note, notable: volunteer, content: "Good job.") sign_in volunteer patch volunteer_note_path(volunteer, note), params: {note: {content: "Very nice!"}} expect(response).to redirect_to root_path expect(note.reload.content).to eq "Good job." end end end end describe "DELETE /destroy" do context "when logged in as an admin" do it "can delete notes about a volunteer in same organization" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer) sign_in admin expect { delete volunteer_note_path(volunteer, note) }.to change(Note, :count).by(-1) expect(response).to redirect_to edit_volunteer_path(volunteer) end it "cannot delete notes about a volunteer in different organization" do organization = create(:casa_org) other_organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) note = create(:note, notable: volunteer) sign_in admin expect { delete volunteer_note_path(volunteer, note) }.not_to change(Note, :count) expect(response).to redirect_to root_path end end context "when logged in as a supervisor" do it "can delete notes about a volunteer in same organization" do organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) note = create(:note, notable: volunteer) sign_in supervisor expect { delete volunteer_note_path(volunteer, note) }.to change(Note, :count).by(-1) expect(response).to redirect_to edit_volunteer_path(volunteer) end it "cannot delete notes about a volunteer in different organization" do organization = create(:casa_org) other_organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, casa_org: other_organization) note = create(:note, notable: volunteer) sign_in supervisor expect { delete volunteer_note_path(volunteer, note) }.not_to change(Note, :count) expect(response).to redirect_to root_path end end context "when logged in as a volunteer" do it "cannot delete notes" do volunteer = create(:volunteer, :with_assigned_supervisor) note = create(:note, notable: volunteer) sign_in volunteer expect { delete volunteer_note_path(volunteer, note) }.not_to change(Note, :count) expect(response).to redirect_to root_path end end end end ================================================ FILE: spec/requests/notifications_spec.rb ================================================ require "rails_helper" RSpec.describe "/notifications", type: :request do before do travel_to Date.new(2021, 1, 1) end describe "GET /index" do context "when there are no patch notes" do context "when logged in as an admin" do let(:admin) { create(:casa_admin) } before do sign_in admin end it "shows the no notification message" do get notifications_url expect(response.body).to include("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") end context "when there is a deploy date" do before do Health.instance.update_attribute(:latest_deploy_time, Date.today) end it "does not show the patch notes section" do get notifications_url queryable_html = Nokogiri.HTML5(response.body) expect(queryable_html.css("h3").text).not_to include("Patch Notes") end end end end context "when there are patch notes" do let(:patch_note_group_all_users) { create(:patch_note_group, :all_users) } let(:patch_note_group_no_volunteers) { create(:patch_note_group, :only_supervisors_and_admins) } let(:patch_note_type_a) { create(:patch_note_type, name: "patch_note_type_a") } let(:patch_note_type_b) { create(:patch_note_type, name: "patch_note_type_b") } let(:patch_note_1) { create(:patch_note, note: "Patch Note 1", patch_note_type: patch_note_type_a) } let(:patch_note_2) { create(:patch_note, note: "Patch Note B", patch_note_type: patch_note_type_b) } before do patch_note_1.update(created_at: Date.new(2020, 12, 31), patch_note_group: patch_note_group_all_users) patch_note_2.update(created_at: Date.new(2020, 12, 31), patch_note_group: patch_note_group_no_volunteers) end context "when logged in as an admin" do let(:admin) { create(:casa_admin) } before do sign_in admin end context "when there is no deploy date" do it "shows the no notification message" do get notifications_url expect(response.body).to include("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") end it "does not show the patch notes section" do get notifications_url queryable_html = Nokogiri.HTML5(response.body) expect(queryable_html.css("h3").text).not_to include("Patch Notes") end end context "when there is a deploy date" do before do Health.instance.update_attribute(:latest_deploy_time, Date.new(2021, 1, 1)) end it "does not show the no notification message" do get notifications_url expect(response.body).not_to include("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") end it "does not show patch notes made after the deploy date" do patch_note_1.update_attribute(:created_at, Date.new(2021, 1, 2)) patch_note_2.update_attribute(:created_at, Date.new(2020, 12, 31)) get notifications_url expect(response.body).not_to include(CGI.escapeHTML(patch_note_1.note)) expect(response.body).to include(CGI.escapeHTML(patch_note_2.note)) end end end context "when logged in as volunteer" do let(:volunteer) { create(:volunteer) } before do sign_in volunteer Health.instance.update_attribute(:latest_deploy_time, Date.new(2021, 1, 1)) patch_note_1.update(created_at: Date.new(2020, 12, 31), patch_note_group: patch_note_group_all_users) patch_note_2.update(created_at: Date.new(2020, 12, 31), patch_note_group: patch_note_group_no_volunteers) end it "shows only the patch notes available to their user group" do get notifications_url expect(response.body).to include(CGI.escapeHTML(patch_note_1.note)) expect(response.body).not_to include(CGI.escapeHTML(patch_note_2.note)) end end end end describe "POST #mark_as_read" do let(:user) { create(:volunteer) } let(:notification) { create(:notification, :followup_with_note, recipient: user, read_at: nil) } before { sign_in user } context "when user is authorized" do it "marks the notification as read" do post mark_as_read_notification_path(notification) expect(notification.reload.read_at).not_to be_nil end it "redirects to the notification event URL" do post mark_as_read_notification_path(notification) case_contact_url = edit_case_contact_path(CaseContact.last) expect(response).to redirect_to(case_contact_url) end end context "when user is not authorized" do let(:other_user) { create(:volunteer) } before { sign_in other_user } it "does not mark the notification as read" do post mark_as_read_notification_path(notification) expect(notification.reload.read_at).to be_nil end it "redirects to root" do post mark_as_read_notification_path(notification) expect(response).to redirect_to(root_path) end end it "does not mark the notification as read if it is already read" do notification = create(:notification, :followup_read, recipient: user) expect { post mark_as_read_notification_path(notification) }.not_to(change { notification.reload.read_at }) end end end ================================================ FILE: spec/requests/other_duties_spec.rb ================================================ require "rails_helper" RSpec.describe "/other_duties", type: :request do describe "GET /new" do context "when volunteer" do it "is successful" do volunteer = create(:volunteer) sign_in volunteer get new_other_duty_path expect(response).to be_successful end end context "when supervisor" do it "redirects to root path" do supervisor = create(:supervisor) sign_in supervisor get new_other_duty_path expect(response).to redirect_to root_path end end context "when admin" do it "redirects to root path" do admin = create(:casa_admin) sign_in admin get new_other_duty_path expect(response).to redirect_to root_path end end end describe "POST /create" do context "when volunteer" do context "with valid parameters" do it "creates one new Duty and returns to other duties page" do volunteer = create(:volunteer) sign_in volunteer expect { post other_duties_path, params: {other_duty: attributes_for(:other_duty)} }.to change(OtherDuty, :count).by(1) expect(response).to redirect_to(other_duties_path) end end context "with invalid parameters" do it "does not create a new Duty and renders new page" do volunteer = create(:volunteer) sign_in volunteer expect { post other_duties_path, params: {other_duty: attributes_for(:other_duty, notes: "")} }.not_to change(OtherDuty, :count) expect(response).to have_http_status(:unprocessable_content) end end end context "when supervisor" do it "does not create record and redirects to root" do supervisor = create(:supervisor) sign_in supervisor expect { post other_duties_path, params: {other_duty: attributes_for(:other_duty)} }.not_to change(OtherDuty, :count) expect(response).to redirect_to root_path end end context "when admin" do it "does not create record and redirects to root" do admin = create(:casa_admin) sign_in admin expect { post other_duties_path, params: {other_duty: attributes_for(:other_duty)} }.not_to change(OtherDuty, :count) expect(response).to redirect_to root_path end end end describe "GET /edit" do context "when volunteer" do context "when viewing own record" do it "is successful" do volunteer = create(:volunteer) duty = create(:other_duty, creator: volunteer) sign_in volunteer get edit_other_duty_path(duty) expect(response).to be_successful end end context "when viewing other's record" do it "redirects to root path" do volunteer = create(:volunteer) duty = create(:other_duty) sign_in volunteer get edit_other_duty_path(duty) expect(response).to redirect_to root_path end end end context "when supervisor" do it "redirects to root path" do supervisor = create(:supervisor) duty = create(:other_duty) sign_in supervisor get edit_other_duty_path(duty) expect(response).to redirect_to root_path end end context "when admin" do it "redirects to root path" do admin = create(:casa_admin) duty = create(:other_duty) sign_in admin get edit_other_duty_path(duty) expect(response).to redirect_to root_path end end end describe "PATCH /update" do context "when volunteer updating own duty" do context "with valid parameters" do it "updates the duty and redirects to other duties page" do volunteer = create(:volunteer) other_duty = create(:other_duty, notes: "Test 1", creator: volunteer) sign_in volunteer patch other_duty_path(other_duty), params: {other_duty: {notes: "Test 2"}} expect(other_duty.reload.notes).to eq("Test 2") expect(response).to redirect_to other_duties_path end end context "with invalid parameters" do it "does not update and re-renders edit page" do volunteer = create(:volunteer) other_duty = create(:other_duty, notes: "Test 1", creator: volunteer) sign_in volunteer patch other_duty_path(other_duty), params: {other_duty: {notes: ""}} expect(other_duty.reload.notes).to eq "Test 1" expect(response).to have_http_status(:unprocessable_content) end end end context "when volunteer updating other person's record" do it "does not update the duty and redirects to root path" do volunteer = create(:volunteer) other_duty = create(:other_duty, notes: "Test 1") sign_in volunteer patch other_duty_path(other_duty), params: {other_duty: {notes: "Test 2"}} expect(other_duty.reload.notes).to eq("Test 1") expect(response).to redirect_to root_path end end context "when supervisor" do it "does not update the duty and redirects to root path" do supervisor = create(:supervisor) other_duty = create(:other_duty, notes: "Test 1") sign_in supervisor patch other_duty_path(other_duty), params: {other_duty: {notes: "Test 2"}} expect(other_duty.reload.notes).to eq("Test 1") expect(response).to redirect_to root_path end end context "when admin" do it "does not update the duty and redirects to root path" do admin = create(:casa_admin) other_duty = create(:other_duty, notes: "Test 1") sign_in admin patch other_duty_path(other_duty), params: {other_duty: {notes: "Test 2"}} expect(other_duty.reload.notes).to eq("Test 1") expect(response).to redirect_to root_path end end end describe "GET /index" do context "when admin" do it "can see volunteer's other duties from own organization" do volunteer = create(:volunteer) duties = create_pair(:other_duty, creator: volunteer) admin = create(:casa_admin, casa_org: volunteer.casa_org) other_org_duty = create(:other_duty) sign_in admin get other_duties_path expect(response.body).to include(volunteer.display_name) expect(response.body).to include(duties.first.decorate.truncate_notes) expect(response.body).to include(duties.second.decorate.truncate_notes) expect(response.body).not_to include(other_org_duty.decorate.truncate_notes) end end context "when supervisor" do it "can see own active volunteer's other duties from own organization" do supervisor = create(:supervisor, :with_volunteers) volunteer1 = supervisor.volunteers.first volunteer2 = supervisor.volunteers.last SupervisorVolunteer.find_by(supervisor: supervisor, volunteer: volunteer2).update!(is_active: false) duties = create_pair(:other_duty, creator: volunteer1) inactive_duty = create(:other_duty, creator: volunteer2) volunteer_other_sup = create(:volunteer, casa_org: volunteer1.casa_org) other_sup_duty = create(:other_duty, creator: volunteer_other_sup) other_org = create(:casa_org) volunteer_other_org = create(:volunteer, casa_org: other_org) other_org_duty = create(:other_duty, creator: volunteer_other_org) sign_in supervisor get other_duties_path expect(response.body).to include(volunteer1.display_name) expect(response.body).to include(duties.first.decorate.truncate_notes) expect(response.body).to include(duties.second.decorate.truncate_notes) expect(response.body).not_to include(volunteer2.display_name) expect(response.body).not_to include(inactive_duty.decorate.truncate_notes) expect(response.body).not_to include(volunteer_other_sup.display_name) expect(response.body).not_to include(other_sup_duty.decorate.truncate_notes) expect(response.body).not_to include(volunteer_other_org.display_name) expect(response.body).not_to include(other_org_duty.decorate.truncate_notes) end end context "when volunteer" do it "shows only duties from the volunteer" do volunteer = create(:volunteer) mine = build(:other_duty) other = build(:other_duty) volunteer.other_duties << mine sign_in volunteer get other_duties_url expect(response).to be_successful expect(response.body).to include(mine.decorate.truncate_notes) expect(response.body).not_to include(other.decorate.truncate_notes) end end end end ================================================ FILE: spec/requests/placement_reports_spec.rb ================================================ require "rails_helper" RSpec.describe "/placement_reports", type: :request do let(:admin) { create(:casa_admin) } describe "GET /index" do it "renders a successful response" do sign_in admin get placement_reports_path, headers: {"ACCEPT" => "*/*"} expect(response).to be_successful end end end ================================================ FILE: spec/requests/placement_types_spec.rb ================================================ require "rails_helper" RSpec.describe "PlacementTypes", type: :request do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:placement_type) { create(:placement_type, casa_org: organization) } describe "as an admin" do before do sign_in admin end describe "GET /edit" do it "returns http success" do get edit_placement_type_path(placement_type) expect(response).to have_http_status(:success) end end describe "GET /new" do it "returns http success" do get "/placement_types/new" expect(response).to have_http_status(:success) end end describe "POST /create" do it "returns http success" do expect { post "/placement_types", params: {placement_type: {name: "New Placement Type"}} }.to change(PlacementType, :count).by(1) expect(response).to redirect_to(edit_casa_org_path(organization)) end end describe "PATCH /update" do it "returns http success" do patch placement_type_path(placement_type), params: {placement_type: {name: "Updated Placement Type"}} expect(response).to redirect_to(edit_casa_org_path(organization)) expect(placement_type.reload.name).to eq("Updated Placement Type") end end end end ================================================ FILE: spec/requests/placements_spec.rb ================================================ require "rails_helper" RSpec.describe "Placements", type: :request do let(:casa_org) { build(:casa_org) } let(:admin) { create(:casa_admin, casa_org:) } let(:casa_case) { create(:casa_case, casa_org:) } before do sign_in admin end describe "GET /index" do it "displays the placement information" do get casa_case_placements_path(casa_case) expect(response).to have_http_status(:success) expect(response.body).to include("Placement History") expect(response.body).to include(casa_case.case_number) end end describe "GET /show" do it "displays the placement details" do placement_type = build(:placement_type, casa_org:, name: "Reunification") placement = create(:placement, casa_case:, placement_type:) get casa_case_placement_path(casa_case, placement) expect(response).to have_http_status(:success) expect(response.body).to include("Placement") expect(response.body).to include("Reunification") expect(response.body).to include(casa_case.case_number) end end describe "GET /new" do it "returns a successful response" do get new_casa_case_placement_path(casa_case) expect(response).to have_http_status(:success) end end describe "GET /edit" do it "returns a successful response" do placement = create(:placement, casa_case:) get edit_casa_case_placement_path(casa_case, placement) expect(response).to have_http_status(:success) end end describe "POST /create" do context "when the params are valid" do it "creates the placement successfully and redirects to the placement" do placement_type = create(:placement_type, casa_org:, name: "Adoption by relative") params = { placement: { placement_started_at: Date.new(2026, 2, 1), placement_type_id: placement_type.id } } expect do post casa_case_placements_path(casa_case), params: params end.to change(Placement, :count).by(1) expect(response).to have_http_status(:redirect) expect(flash[:notice]).to match(/placement was successfully created/i) follow_redirect! expect(response.body).to include("Placement") expect(response.body).to include("Adoption by relative") expect(response.body).to include(casa_case.case_number) end end end describe "PATCH /update" do context "when the params are valid" do it "updates the placement successfully" do placement = create(:placement, casa_case:, placement_started_at: Date.new(2026, 4, 1)) params = {placement: {placement_started_at: Date.new(2026, 1, 1)}} patch casa_case_placement_path(casa_case, placement), params: params expect(response).to redirect_to(casa_case_placements_path(casa_case)) expect(placement.reload.placement_started_at).to eq(Date.new(2026, 1, 1)) expect(flash[:notice]).to match(/placement was successfully updated/i) end end context "when the params are invalid" do it "returns an unprocessable_content response" do placement = create(:placement, casa_case:, placement_started_at: Date.new(2026, 4, 1)) params = {placement: {placement_started_at: 1000.years.ago}} patch casa_case_placement_path(casa_case, placement), params: params expect(response).to have_http_status(:unprocessable_content) end end end describe "DELETE /destroy" do it "deletes the placement successfully" do placement = create(:placement, casa_case:) expect do delete casa_case_placement_path(casa_case, placement) end.to change(Placement, :count).by(-1) expect(response).to have_http_status(:redirect) expect(flash[:notice]).to match(/placement was successfully deleted/i) follow_redirect! expect(response.body).to include("Placement History") expect(response.body).to include(casa_case.case_number) end end end ================================================ FILE: spec/requests/preference_sets_spec.rb ================================================ require "rails_helper" RSpec.describe "PreferenceSets", type: :request do let!(:supervisor) { create(:supervisor) } let!(:preference_set) { supervisor.preference_set } let!(:table_state) { {"columns" => [{"visible" => "false"}, {"visible" => "true"}, {"visible" => "false"}, {"visible" => "true"}]} } describe "GET /preference_sets/table_state/volunteers_table" do subject { get "/preference_sets/table_state/volunteers_table" } before do sign_in supervisor supervisor.preference_set.table_state["volunteers_table"] = table_state supervisor.preference_set.save! end it "returns the table state" do subject expect(response.body).to eq(table_state.to_json) end end describe "POST /preference_sets/table_state_update/volunteers_table" do subject { post "/preference_sets/table_state_update/volunteers_table", params: {table_name: "volunteers_table", table_state: table_state} } before do sign_in supervisor end it "updates the table state" do subject preference_set.reload expect(preference_set.table_state["volunteers_table"]).to eq(table_state) end it "returns a 200 OK status" do subject expect(response).to have_http_status(:ok) end end end ================================================ FILE: spec/requests/reimbursements_spec.rb ================================================ require "rails_helper" RSpec.describe ReimbursementsController, type: :request do let(:admin) { create(:casa_admin) } let(:casa_org) { admin.casa_org } let(:case_contact) { create(:case_contact) } let(:notification_double) { double("ReimbursementCompleteNotifier") } before do sign_in(admin) allow(ReimbursementCompleteNotifier).to receive(:with).and_return(notification_double) allow(notification_double).to receive(:deliver) end describe "GET /index" do it "calls ReimbursementPolicy::Scope to filter reimbursements" do contact_relation = double(CaseContact) allow(contact_relation).to receive_message_chain( :want_driving_reimbursement, :created_max_ago, :filter_by_reimbursement_status ).and_return([]) allow(ReimbursementPolicy::Scope).to receive(:new) .with(admin, CaseContact.joins(:casa_case)) .and_return(double(resolve: contact_relation)) expect(contact_relation).to receive_message_chain( :want_driving_reimbursement, :created_max_ago, :filter_by_reimbursement_status ) get reimbursements_url expect(ReimbursementPolicy::Scope).to have_received(:new) .with(admin, CaseContact.joins(:casa_case)) end end describe "PATCH /mark_as_complete" do it "changes reimbursement status to complete" do patch reimbursement_mark_as_complete_url(case_contact, case_contact: {reimbursement_complete: true}) expect(ReimbursementCompleteNotifier).to(have_received(:with).once.with(case_contact: case_contact)) expect(response).to redirect_to(reimbursements_path) expect(response).to have_http_status(:redirect) expect(case_contact.reload.reimbursement_complete).to be_truthy end end describe "PATCH /mark_as_needs_review" do before { case_contact.update(reimbursement_complete: true) } it "changes reimbursement status to needs review" do patch reimbursement_mark_as_needs_review_url(case_contact, case_contact: {reimbursement_complete: false}) expect(response).to redirect_to(reimbursements_path) expect(response).to have_http_status(:redirect) expect(case_contact.reload.reimbursement_complete).to be_falsey end end end ================================================ FILE: spec/requests/reports_spec.rb ================================================ require "rails_helper" RSpec.describe "/reports", type: :request do describe "GET #index" do subject do get reports_url response end context "while signed in as an admin" do before do sign_in build(:casa_admin) end it { is_expected.to be_successful } end context "while signed in as a supervisor" do before do sign_in build(:supervisor) end it { is_expected.to be_successful } end context "while signed in as a volunteer" do before do sign_in build(:volunteer) end it { is_expected.not_to be_successful } end end describe "GET #export_emails" do before do sign_in build(:casa_admin) end it "renders a csv file to download" do get export_emails_url(format: :csv) expect(response).to be_successful expect( response.headers["Content-Disposition"] ).to include "attachment; filename=\"volunteers-emails-#{Time.current.strftime("%Y-%m-%d")}.csv" end end end ================================================ FILE: spec/requests/static_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "/", type: :request do describe "GET /" do subject(:request) do get root_path response end it { is_expected.to be_successful } end end ================================================ FILE: spec/requests/supervisor_volunteers_spec.rb ================================================ require "rails_helper" RSpec.describe "/supervisor_volunteers", type: :request do let!(:casa_org) { build(:casa_org) } let!(:admin) { build(:casa_admin, casa_org: casa_org) } let!(:supervisor) { create(:supervisor, casa_org: casa_org) } describe "POST /create" do let!(:volunteer) { create(:volunteer, casa_org: casa_org) } context "when no pre-existing association between supervisor and volunteer exists" do it "creates a new supervisor_volunteers association" do valid_parameters = { supervisor_volunteer: {volunteer_id: volunteer.id}, supervisor_id: supervisor.id } sign_in(admin) expect { post supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: edit_volunteer_path(volunteer).to_s} }.to change(SupervisorVolunteer, :count).by(1) expect(response).to redirect_to edit_volunteer_path(volunteer) end end context "when an inactive association between supervisor and volunteer exists" do let!(:association) do create( :supervisor_volunteer, supervisor: supervisor, volunteer: volunteer, is_active: false ) end it "sets that association to active" do valid_parameters = { supervisor_volunteer: {volunteer_id: volunteer.id}, supervisor_id: supervisor.id } sign_in(admin) expect { post supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: edit_volunteer_path(volunteer).to_s} }.not_to change(SupervisorVolunteer, :count) expect(response).to redirect_to edit_volunteer_path(volunteer) association.reload expect(association.is_active?).to be(true) end end context "when an inactive association between the volunteer and a different supervisor exists" do let!(:other_supervisor) { build(:supervisor, casa_org: casa_org) } let!(:previous_association) do create( :supervisor_volunteer, supervisor: other_supervisor, volunteer: volunteer, is_active: false ) end it "does not remove association" do valid_parameters = { supervisor_volunteer: {volunteer_id: volunteer.id}, supervisor_id: supervisor.id } sign_in(admin) expect { post supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: edit_volunteer_path(volunteer).to_s} }.to change(SupervisorVolunteer, :count).by(1) expect(response).to redirect_to edit_volunteer_path(volunteer) expect(previous_association.reload.is_active?).to be(false) expect(SupervisorVolunteer.where(supervisor: supervisor, volunteer: volunteer).exists?).to be(true) end end context "when passing the supervisor_id as the supervisor_volunteer_params" do let!(:association) do create( :supervisor_volunteer, supervisor: supervisor, volunteer: volunteer, is_active: false ) end it "stills set the association as active" do valid_parameters = { supervisor_volunteer: {supervisor_id: supervisor.id, volunteer_id: volunteer.id} } sign_in(admin) expect { post supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: edit_volunteer_path(volunteer).to_s} }.not_to change(SupervisorVolunteer, :count) expect(response).to redirect_to edit_volunteer_path(volunteer) association.reload expect(association.is_active?).to be(true) end end end describe "PATCH /unassign" do let!(:volunteer) { create(:volunteer, casa_org: casa_org) } let!(:association) do create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer) end context "when the logged in user is an admin" do it "sets the is_active flag for assignment of a volunteer to a supervisor to false" do sign_in admin expect { patch unassign_supervisor_volunteer_path(volunteer), headers: {HTTP_REFERER: edit_volunteer_path(volunteer).to_s} }.to change(supervisor.volunteers, :count) association.reload expect(association.is_active?).to be(false) expect(response).to redirect_to edit_volunteer_path(volunteer) end end context "when the logged in user is a supervisor" do it "sets the is_active flag for assignment of a volunteer to a supervisor to false" do sign_in supervisor expect { patch unassign_supervisor_volunteer_path(volunteer), headers: {HTTP_REFERER: edit_volunteer_path(volunteer).to_s} }.to change(supervisor.volunteers, :count) association.reload expect(association.is_active?).to be(false) expect(response).to redirect_to edit_volunteer_path(volunteer) end end context "when the logged in user is not an admin or supervisor" do let!(:association) do create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer) end it "does not set the is_active flag on the association to false" do sign_in volunteer expect { patch unassign_supervisor_volunteer_path(volunteer) }.not_to change(supervisor.volunteers, :count) association.reload expect(association.is_active?).to be(true) end end end describe "POST /bulk_assignment" do subject do post bulk_assignment_supervisor_volunteers_url, params: params end let!(:volunteer_1) { create(:volunteer, casa_org: casa_org) } let!(:volunteer_2) { create(:volunteer, casa_org: casa_org) } let!(:volunteer_3) { create(:volunteer, casa_org: casa_org) } let(:supervisor_id) { supervisor.id } let(:params) do { supervisor_volunteer: { supervisor_id: supervisor_id, volunteer_ids: [volunteer_1.id, volunteer_2.id, volunteer_3.id] } } end it "creates an association for each volunteer" do sign_in(admin) expect { subject }.to change(SupervisorVolunteer, :count).by(3) end context "when association already exists" do let!(:associations) do [ create(:supervisor_volunteer, :inactive, supervisor: supervisor, volunteer: volunteer_1), create(:supervisor_volunteer, :inactive, supervisor: supervisor, volunteer: volunteer_2), create(:supervisor_volunteer, :inactive, supervisor: supervisor, volunteer: volunteer_3) ] end it "sets to active" do sign_in(admin) subject associations.each do |association| association.reload expect(association.is_active?).to be true end end it "does not create new associations" do sign_in(admin) expect { subject }.not_to change(SupervisorVolunteer, :count) end end context "when none passed as supervisor" do let(:supervisor_id) { "" } let!(:associations) do [ create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer_1), create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer_2), create(:supervisor_volunteer, supervisor: supervisor, volunteer: volunteer_3) ] end it "sets associations to inactive" do sign_in(admin) subject associations.each do |association| association.reload expect(association.is_active?).to be false end end it "does not remove associations" do sign_in(admin) expect { subject }.not_to change(SupervisorVolunteer, :count) end end end end ================================================ FILE: spec/requests/supervisors_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe "/supervisors", type: :request do let(:org) { create(:casa_org) } let(:admin) { build(:casa_admin, casa_org: org) } let(:supervisor) { create(:supervisor, casa_org: org) } let(:update_supervisor_params) do {supervisor: {display_name: "New Name", phone_number: "+14163218092"}} end describe "GET /index" do it "returns http status ok" do sign_in admin get supervisors_path expect(response).to have_http_status(:ok) end context "when casa case has court_dates" do let!(:casa_case) { create(:casa_case, casa_org: org, court_dates: [court_date]) } let(:court_date) { create(:court_date) } it "does not return casa case" do sign_in admin get supervisors_path expect(response.body).not_to include(casa_case.case_number) end end context "when casa case does not have court_dates" do let!(:casa_case) { create(:casa_case, casa_org: org, court_dates: []) } it "does not return casa case" do sign_in admin get supervisors_path expect(response.body).to include(casa_case.case_number) end end end describe "GET /new" do it "admin can view the new supervisor page" do sign_in admin get new_supervisor_url expect(response).to be_successful end it "supervisors can not view the new supervisor page" do sign_in supervisor get new_supervisor_url expect(response).not_to be_successful end end describe "GET /edit" do context "same org" do it "admin can view the edit supervisor page" do sign_in admin get edit_supervisor_url(supervisor) expect(response).to be_successful end it "supervisor can view the edit supervisor page" do sign_in supervisor get edit_supervisor_url(supervisor) expect(response).to be_successful end it "other supervisor can view the edit supervisor page" do sign_in build(:supervisor, casa_org: org) get edit_supervisor_url(supervisor) expect(response).to be_successful end it "returns volunteers ever assigned if include_unassigned param is present" do sign_in admin get edit_supervisor_url(supervisor), params: {include_unassigned: true} expect(response).to be_successful expect(assigns(:all_volunteers_ever_assigned)).not_to be_nil end it "returns no volunteers ever assigned if include_unassigned param is false" do sign_in admin get edit_supervisor_url(supervisor), params: {include_unassigned: false} expect(response).to be_successful expect(assigns(:all_volunteers_ever_assigned)).to be_nil end end context "different org" do let(:diff_org) { create(:casa_org) } let(:supervisor_diff_org) { create(:supervisor, casa_org: diff_org) } it "admin cannot view the edit supervisor page" do sign_in_as_admin get edit_supervisor_url(supervisor_diff_org) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end it "supervisor cannot view the edit supervisor page" do sign_in_as_supervisor get edit_supervisor_url(supervisor_diff_org) expect(response).to redirect_to root_path expect(response.request.flash[:notice]).to eq "Sorry, you are not authorized to perform this action." end end end describe "PATCH /update" do context "while signed in as an admin" do before do sign_in admin end it "admin updates the supervisor" do patch supervisor_path(supervisor), params: update_supervisor_params supervisor.reload expect(supervisor.display_name).to eq "New Name" expect(supervisor.phone_number).to eq "+14163218092" end it "updates supervisor email and sends a confirmation email" do patch supervisor_path(supervisor), params: { supervisor: {email: "newemail@gmail.com"} } supervisor.reload expect(response).to have_http_status(:redirect) expect(supervisor.unconfirmed_email).to eq("newemail@gmail.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "can set the supervisor to be inactive" do patch supervisor_path(supervisor), params: {supervisor: {active: false}} supervisor.reload expect(supervisor).not_to be_active end context "when the email exists already and the supervisor has volunteers assigned" do let(:other_supervisor) { create(:supervisor) } let(:supervisor) { create(:supervisor, :with_volunteers) } it "gracefully fails" do patch supervisor_path(supervisor), params: {supervisor: {email: other_supervisor.email}} expect(response).to have_http_status(:unprocessable_content) end end end context "while signed in as a supervisor" do before do sign_in supervisor end it "supervisor updates their own name" do patch supervisor_path(supervisor), params: update_supervisor_params supervisor.reload expect(supervisor.display_name).to eq "New Name" expect(supervisor).to be_active end it "supervisor updates their own email and receives a confirmation email" do patch supervisor_path(supervisor), params: { supervisor: {email: "newemail@gmail.com"} } supervisor.reload expect(response).to have_http_status(:redirect) expect(supervisor.unconfirmed_email).to eq("newemail@gmail.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "cannot change its own type" do patch supervisor_path(supervisor), params: update_supervisor_params.merge(type: "casa_admin") supervisor.reload expect(supervisor).not_to be_casa_admin end it "cannot set itself to be inactive" do patch supervisor_path(supervisor), params: update_supervisor_params.merge(active: false) supervisor.reload expect(supervisor).to be_active end it "supervisor cannot update another supervisor" do supervisor2 = create(:supervisor, display_name: "Old Name", email: "oldemail@gmail.com") patch supervisor_path(supervisor2), params: update_supervisor_params supervisor2.reload expect(supervisor2.display_name).to eq "Old Name" expect(supervisor2.email).to eq "oldemail@gmail.com" expect(response).to redirect_to(root_url) end end end describe "POST /create" do let(:params) do { supervisor: { display_name: "Display Name", email: "displayname@example.com" } } end it "sends an invitation email" do org = create(:casa_org, twilio_enabled: true) admin = build(:casa_admin, casa_org: org) sign_in admin post supervisors_url, params: params expect(Devise.mailer.deliveries.count).to eq(1) expect(Devise.mailer.deliveries.first.text_part.body.to_s).to include(admin.casa_org.display_name) expect(Devise.mailer.deliveries.first.text_part.body.to_s).to include("This is the first step to accessing your new Supervisor account.") end it "sends a SMS when a phone number exists" do org = create(:casa_org, twilio_enabled: true) admin = build(:casa_admin, casa_org: org) twilio_activation_success_stub = WebMockHelper.twilio_activation_success_stub("supervisor") WebMockHelper.twilio_activation_error_stub("supervisor") short_io_stub = WebMockHelper.short_io_stub_sms params[:supervisor][:phone_number] = "+12222222222" sign_in admin post supervisors_url, params: params expect(short_io_stub).to have_been_requested.times(2) expect(twilio_activation_success_stub).to have_been_requested.times(1) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New supervisor created successfully. SMS has been sent!/) end it "does not send a SMS if phone number not given" do org = create(:casa_org, twilio_enabled: true) admin = build(:casa_admin, casa_org: org) twilio_activation_success_stub = WebMockHelper.twilio_activation_success_stub("supervisor") twilio_activation_error_stub = WebMockHelper.twilio_activation_error_stub("supervisor") short_io_stub = WebMockHelper.short_io_stub_sms sign_in admin post supervisors_url, params: params expect(short_io_stub).to have_been_requested.times(0) expect(twilio_activation_success_stub).to have_been_requested.times(0) expect(twilio_activation_error_stub).to have_been_requested.times(0) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New supervisor created successfully./) end it "does not send a SMS if Twilio has an error" do # ex. credentials entered wrong org = create(:casa_org, twilio_account_sid: "articuno31", twilio_enabled: true) admin = create(:casa_admin, casa_org: org) WebMockHelper.twilio_activation_success_stub("supervisor") twilio_activation_error_stub = WebMockHelper.twilio_activation_error_stub("supervisor") short_io_stub = WebMockHelper.short_io_stub_sms sign_in admin params[:supervisor][:phone_number] = "+12222222222" post supervisors_url, params: params expect(short_io_stub).to have_been_requested.times(2) # TODO: why is this called at all? expect(twilio_activation_error_stub).to have_been_requested.times(1) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New supervisor created successfully. SMS not sent. Error: ./) end it "does not send a SMS if the casa_org does not have Twilio enabled" do org = create(:casa_org, twilio_enabled: false) admin = build(:casa_admin, casa_org: org) short_io_stub = WebMockHelper.short_io_stub_sms sign_in admin params[:supervisor][:phone_number] = "+12222222222" post supervisors_url, params: params expect(short_io_stub).to have_been_requested.times(2) # TODO: why is this called at all? expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New supervisor created successfully./) end end describe "PATCH /activate" do let(:inactive_supervisor) { create(:supervisor, :inactive) } before { sign_in admin } it "activates an inactive supervisor" do patch activate_supervisor_path(inactive_supervisor) expect(flash[:notice]).to eq("Supervisor was activated. They have been sent an email.") inactive_supervisor.reload expect(inactive_supervisor.active).to be true end it "sends an activation mail" do expect { patch activate_supervisor_path(inactive_supervisor) }.to change { ActionMailer::Base.deliveries.count }.by(1) end end describe "PATCH /deactivate" do before { sign_in admin } it "deactivates an active supervisor" do patch deactivate_supervisor_path(supervisor) supervisor.reload expect(supervisor.active).to be false end it "doesn't send an deactivation email" do expect { patch deactivate_supervisor_path(supervisor) }.not_to change { ActionMailer::Base.deliveries.count } end end describe "PATCH /resend_invitation" do before { sign_in admin } it "resends an invitation email" do expect(supervisor.invitation_created_at.present?).to eq(false) patch resend_invitation_supervisor_path(supervisor) supervisor.reload expect(supervisor.invitation_created_at.present?).to eq(true) expect(Devise.mailer.deliveries.count).to eq(1) expect(Devise.mailer.deliveries.first.subject).to eq(I18n.t("devise.mailer.invitation_instructions.subject")) expect(response).to redirect_to(edit_supervisor_path(supervisor)) end end describe "PATCH /change_to_admin" do let(:user) { User.find(supervisor.id) } # find the user after their type has changed context "when signed in as an admin" do before do sign_in admin patch change_to_admin_supervisor_path(supervisor) end it "changes the supervisor to an admin" do expect(user).not_to be_supervisor expect(user).to be_casa_admin end it "redirects to the edit page for an admin" do expect(response).to redirect_to(edit_casa_admin_path(supervisor)) end end context "when signed in as a supervisor" do before do sign_in supervisor patch change_to_admin_supervisor_path(supervisor) end it "does not changes the supervisor to an admin" do expect(user).to be_supervisor expect(user).not_to be_casa_admin end end end end ================================================ FILE: spec/requests/users/invitations_spec.rb ================================================ require "rails_helper" RSpec.describe "Users::InvitationsController", type: :request do let(:casa_org) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let(:volunteer) { create(:volunteer, casa_org: casa_org) } describe "GET /users/invitation/accept" do context "with valid invitation token" do let(:invitation_token) do volunteer.invite!(admin) volunteer.raw_invitation_token end it "renders the invitation acceptance form" do get accept_user_invitation_path(invitation_token: invitation_token) expect(response).to have_http_status(:success) expect(response.body).to include("Set my password") end it "sets the invitation_token on the resource" do get accept_user_invitation_path(invitation_token: invitation_token) # Check that the hidden field contains the token expect(response.body).to include('name="user[invitation_token]"') expect(response.body).to include("value=\"#{invitation_token}\"") end end context "without invitation token" do it "redirects to root path" do get accept_user_invitation_path expect(response).to redirect_to(root_path) end end end describe "PUT /users/invitation" do let(:invitation_token) do volunteer.invite!(admin) volunteer.raw_invitation_token end context "with valid password" do let(:params) do { user: { invitation_token: invitation_token, password: "SecurePassword123!", password_confirmation: "SecurePassword123!" } } end it "accepts the invitation" do put user_invitation_path, params: params volunteer.reload expect(volunteer.invitation_accepted_at).not_to be_nil end it "redirects to the dashboard" do put user_invitation_path, params: params expect(response).to redirect_to(root_path) end it "signs in the user" do put user_invitation_path, params: params # Follow redirects until we reach the final authenticated page follow_redirect! while response.status == 302 # User should be on an authenticated page expect(response).to have_http_status(:success) end end context "with mismatched passwords" do let(:params) do { user: { invitation_token: invitation_token, password: "SecurePassword123!", password_confirmation: "DifferentPassword456!" } } end it "does not accept the invitation" do put user_invitation_path, params: params volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end it "renders the edit page with errors" do put user_invitation_path, params: params expect(response).to have_http_status(:ok) expect(response.body).to include("Password confirmation doesn't match") end end context "with password too short" do let(:params) do { user: { invitation_token: invitation_token, password: "short", password_confirmation: "short" } } end it "does not accept the invitation" do put user_invitation_path, params: params volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end it "renders the edit page with errors" do put user_invitation_path, params: params expect(response).to have_http_status(:ok) expect(response.body).to include("Password is too short") end end context "with blank password" do let(:params) do { user: { invitation_token: invitation_token, password: "", password_confirmation: "" } } end it "does not accept the invitation" do put user_invitation_path, params: params volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end it "renders the edit page with errors" do put user_invitation_path, params: params expect(response).to have_http_status(:ok) expect(response.body).to include("can't be blank") end end context "without invitation token" do let(:params) do { user: { password: "SecurePassword123!", password_confirmation: "SecurePassword123!" } } end it "does not accept the invitation" do put user_invitation_path, params: params volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end it "renders the edit page with errors" do put user_invitation_path, params: params expect(response).to have_http_status(:ok) expect(response.body).to include("Invitation token can't be blank") end end end end ================================================ FILE: spec/requests/users/passwords_spec.rb ================================================ require "rails_helper" RSpec.describe "Users::PasswordsController", type: :request do let!(:org) { create(:casa_org) } let!(:user) { create(:user, phone_number: "+12222222222", casa_org: org) } let!(:twillio_service_double) { instance_double(TwilioService) } let!(:short_url_service_double) { instance_double(ShortUrlService) } before do allow(TwilioService).to( receive(:new).with( org ).and_return(twillio_service_double) ) allow(twillio_service_double).to receive(:send_sms) allow(ShortUrlService).to receive(:new).and_return(short_url_service_double) allow(short_url_service_double).to( receive(:create_short_url).with(a_string_matching(edit_user_password_path)) ) allow(short_url_service_double).to receive(:short_url).and_return("reset_url") end describe "POST /create" do subject(:request) do post user_password_url, params: params response end context "with valid parameters" do let(:params) { {user: {email: user.email, phone_number: user.phone_number}} } it "sends a password reset SMS to existing user" do request expect(twillio_service_double).to have_received(:send_sms).once.with( {From: org.twilio_phone_number, Body: a_string_matching("reset_url"), To: user.phone_number} ) end it "sends a password reset email to existing user" do expect_any_instance_of(User).to receive(:send_reset_password_instructions).once request end it { is_expected.to redirect_to(user_session_url) } it "shows the correct flash message" do request expect(flash[:notice]).to( eq("If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes.") ) end describe "(email only)" do let(:params) { {user: {email: user.email, phone_number: ""}} } it "sends a password reset email to existing user" do expect_any_instance_of(User).to receive(:send_reset_password_instructions).once request end it "does not send sms with reset password" do request expect(twillio_service_double).not_to have_received(:send_sms) end end describe "(phone_number only)" do let(:params) { {user: {email: "", phone_number: user.phone_number}} } it "sends a password reset SMS to existing user" do request expect(twillio_service_double).to have_received(:send_sms).once.with( {From: org.twilio_phone_number, Body: a_string_matching("reset_url"), To: user.phone_number} ) end it "does not send email with reset password" do expect_any_instance_of(User).not_to receive(:send_reset_password_instructions) request end end end context "with invalid parameters" do let(:params) { {user: {email: "", phone_number: ""}} } it "sets errors correctly" do request expect(request.parsed_body.to_html).to include("Please enter at least one field.") end end context "with wrong parameters" do let(:params) { {user: {phone_number: "13333333333"}} } it "sets errors correctly" do request expect(flash[:notice]).to( eq("If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes.") ) end end context "when twilio is disabled" do let(:params) { {user: {email: user.email, phone_number: user.phone_number}} } before do org.update(twilio_enabled: false) end it "does not send an sms, only an email" do expect_any_instance_of(User).to receive(:send_reset_password_instructions).once request expect(flash[:notice]).to( eq("If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes.") ) end end end describe "PUT /update" do let(:token) do raw_token, enc_token = Devise.token_generator.generate(User, :reset_password_token) user.update!(reset_password_token: enc_token, reset_password_sent_at: Time.current) raw_token end let(:params) do { user: { reset_password_token: token, password: "newpassword123!", password_confirmation: "newpassword123!" } } end subject(:submit_reset) { put user_password_path, params: params } it "successfully resets the password" do submit_reset expect(response).to redirect_to(new_user_session_path) expect(flash[:notice]).to eq("Your password has been changed successfully.") end end end ================================================ FILE: spec/requests/users_spec.rb ================================================ require "rails_helper" RSpec.describe "/users", type: :request do before { sms_notification_event = SmsNotificationEvent.new(name: "test", user_type: Volunteer) sms_notification_event.save } describe "GET /edit" do context "with a volunteer signed in" do it "renders a successful response" do volunteer = create(:volunteer) sign_in volunteer get edit_users_path expect(response).to be_successful expect(response.body).to include(volunteer.email) end end context "with an admin signed in" do it "renders a successful response" do admin = build(:casa_admin) sign_in admin get edit_users_path expect(response).to be_successful expect(response.body).to include(admin.email) end end end describe "PATCH /update" do it "updates the user" do volunteer = build(:volunteer) sign_in volunteer patch users_path, params: {user: {display_name: "New Name", address_attributes: {content: "some address"}, phone_number: "+12223334444", date_of_birth: Date.new(1958, 12, 1), sms_notification_event_ids: [SmsNotificationEvent.first.id]}} expect(volunteer.address.content).to eq "some address" expect(volunteer.display_name).to eq "New Name" expect(volunteer.phone_number).to eq "+12223334444" expect(volunteer.date_of_birth).to eq Date.new(1958, 12, 1) expect(volunteer.sms_notification_event_ids).to include SmsNotificationEvent.first.id expect(UserSmsNotificationEvent.count).to eq 1 end end describe "PATCH /update_password" do subject do patch update_password_users_path(user), params: { user: { current_password: "12345678", password: "new_pass", password_confirmation: "new_pass" } } end before { sign_in user } context "when volunteer" do let(:user) { create(:volunteer) } context "when successfully" do it "updates the user password" do subject expect(user.valid_password?("new_pass")).to be_truthy end it "calls UserMailer to reminder the user that password has changed" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(user).and_return(mailer) expect(mailer).to receive(:deliver) subject end end context "when failure" do subject do patch update_password_users_path(user), params: { user: { password: "", password_confirmation: "wrong" } } end it "does not update the user password", :aggregate_failures do subject expect(user.valid_password?("wrong")).to be_falsey expect(user.valid_password?("")).to be_falsey end it "does not call UserMailer to reminder the user that password has changed" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(user).and_return(mailer) expect(mailer).not_to receive(:deliver) subject end end end context "when supervisor" do let(:user) { create(:supervisor) } context "when successfully" do it "updates the user password" do subject expect(user.valid_password?("new_pass")).to be_truthy end it "calls UserMailer to reminder the user that password has changed" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(user).and_return(mailer) expect(mailer).to receive(:deliver) subject end it "bypasses sign in if the current user is the true user" do expect_any_instance_of(UsersController).to receive(:bypass_sign_in).with(user) subject end it "does not bypass sign in when the current user is not the true user" do allow_any_instance_of(UsersController).to receive(:true_user).and_return(User.new) expect_any_instance_of(UsersController).not_to receive(:bypass_sign_in).with(user) subject end end context "when failure" do subject do patch update_password_users_path(user), params: { user: { password: "", password_confirmation: "wrong" } } end it "does not update the user password", :aggregate_failures do subject expect(user.valid_password?("wrong")).to be_falsey expect(user.valid_password?("")).to be_falsey end it "does not call UserMailer to reminder the user that password has changed" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(user).and_return(mailer) expect(mailer).not_to receive(:deliver) subject end end end context "when casa_admin" do let(:user) { create(:casa_admin) } context "when successfully" do it "updates the user password" do subject expect(user.valid_password?("new_pass")).to be_truthy end it "calls UserMailer to reminder the user that password has changed" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(user).and_return(mailer) expect(mailer).to receive(:deliver) subject end it "bypasses sign in if the current user is the true user" do expect_any_instance_of(UsersController).to receive(:bypass_sign_in).with(user) subject end it "does not bypass sign in when the current user is not the true user" do allow_any_instance_of(UsersController).to receive(:true_user).and_return(User.new) expect_any_instance_of(UsersController).not_to receive(:bypass_sign_in).with(user) subject end end context "when failure" do subject do patch update_password_users_path(user), params: { user: { password: "", password_confirmation: "wrong" } } end it "does not update the user password", :aggregate_failures do subject expect(user.valid_password?("wrong")).to be_falsey expect(user.valid_password?("")).to be_falsey end it "does not call UserMailer to reminder the user that password has changed" do mailer = double(UserMailer, deliver: nil) allow(UserMailer).to receive(:password_changed_reminder).with(user).and_return(mailer) expect(mailer).not_to receive(:deliver) subject end end end end describe "PATCH /update_email" do subject do patch update_email_users_path(user), params: { user: { current_password: "12345678", email: "newemail@example.com" } } end before { sign_in user } context "when volunteer" do let(:user) { create(:volunteer, email: "old_email@example.com") } context "when successfully" do it "updates the user email" do subject user.confirm expect(user.valid_password?("12345678")).to be_truthy expect(user.email).to eq("newemail@example.com") expect(user.old_emails).to contain_exactly("old_email@example.com") end it "send an alert and a confirmation email" do subject expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.body.encoded) .to match("Click here to confirm your email") end it "strips whitespace from email" do patch update_email_users_path(user), params: { user: { current_password: "12345678", email: " newemail@example.com " } } user.confirm expect(user.email).to eq("newemail@example.com") end end context "when failure" do subject do patch update_email_users_path(user), params: { user: { current_password: "wrongpassword", email: "wrong@example.com" } } end it "does not update the user email", :aggregate_failures do subject expect(user.valid_password?("wrongpassword")).to be_falsey expect(user.valid_password?("")).to be_falsey expect(user.email).not_to eq("wrong@example.com") end it "does not call UserMailer to reminder the user that password has changed" do subject expect(ActionMailer::Base.deliveries.count).to eq(0) subject end end end context "when supervisor" do let(:user) { create(:supervisor) } context "when successfully" do it "updates the user email" do subject user.confirm expect(user.valid_password?("12345678")).to be_truthy expect(user.email).to eq("newemail@example.com") end it "calls DeviseMailer to remind the user that email has changed along with a confirmation link" do subject expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.body.encoded) .to match("Click here to confirm your email") end it "bypasses sign in if the current user is the true user" do expect_any_instance_of(UsersController).to receive(:bypass_sign_in).with(user) subject end it "does not bypass sign in when the current user is not the true user" do allow_any_instance_of(UsersController).to receive(:true_user).and_return(User.new) expect_any_instance_of(UsersController).not_to receive(:bypass_sign_in).with(user) subject end end context "when failure" do subject do patch update_password_users_path(user), params: { user: { password: "wrong", email: "wrong@example.com" } } end it "does not update the user password", :aggregate_failures do subject expect(user.valid_password?("wrong")).to be_falsey expect(user.valid_password?("")).to be_falsey expect(user.email).not_to eq("wrong@example.com") end it "does not call UserMailer to reminder the user that password has changed" do expect(ActionMailer::Base.deliveries.count).to eq(0) subject end end end context "when casa_admin" do let(:user) { create(:casa_admin) } context "when successfully" do it "updates the user email" do subject user.confirm expect(user.valid_password?("12345678")).to be_truthy expect(user.email).to eq("newemail@example.com") end it "calls DeviseMailer to remind the user that email has changed along with a confirmation link" do subject expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.body.encoded) .to match("Click here to confirm your email") end it "bypasses sign in if the current user is the true user" do expect_any_instance_of(UsersController).to receive(:bypass_sign_in).with(user) subject end it "does not bypass sign in when the current user is not the true user" do allow_any_instance_of(UsersController).to receive(:true_user).and_return(User.new) expect_any_instance_of(UsersController).not_to receive(:bypass_sign_in).with(user) subject end end context "when failure" do subject do patch update_password_users_path(user), params: { user: { password: "", email: "wrong@example.com" } } end it "does not update the user email", :aggregate_failures do subject expect(user.valid_password?("wrong")).to be_falsey expect(user.valid_password?("")).to be_falsey expect(user.email).not_to eq("wrong@example.com") end it "does not call UserMailer to reminder the user that password has changed" do expect(ActionMailer::Base.deliveries.count).to eq(0) end end end end describe "PATCH /add_language" do let(:volunteer) { create(:volunteer) } before { sign_in volunteer } context "when request params are valid" do let(:language) { create(:language) } before do patch add_language_users_path(volunteer), params: { language_id: language.id } end it "adds language to current user" do expect(volunteer.languages).to include(language) end it "notifies the user that the language has been added" do expect(response).to redirect_to(edit_users_path) expect(flash[:notice]).to eq "#{language.name} was added to your languages list." end end context "when request params are invalid" do it "displays an error message when the Language id is empty" do patch add_language_users_path(volunteer), params: { language_id: "" } expect(response).to have_http_status(:unprocessable_content) expect(response.body).to include("Please select a language before adding.") end end context "when the user tries to add the same language again" do let(:language) { create(:language) } before do # Add the language once patch add_language_users_path(volunteer), params: { language_id: language.id } # Try to add the same language again patch add_language_users_path(volunteer), params: { language_id: language.id } end it "does not add the language again" do expect(volunteer.languages.count).to eq(1) # Ensure the language count remains the same end it "notifies the user that the language is already in their list" do expect(response).to have_http_status(:unprocessable_content) expect(response.body).to include("#{language.name} is already in your languages list.") end end end describe "DELETE /remove_language" do let(:volunteer) { create(:volunteer) } before { sign_in volunteer } context "when request params are valid" do let(:language) { create(:language) } before do patch add_language_users_path(volunteer), params: { language_id: language.id } end it "removes a language from a volunteer languages list" do delete remove_language_users_path(language_id: language.id) expect(response.status).to eq 302 expect(response).to redirect_to(edit_users_path) expect(flash[:notice]).to eq "#{language.name} was removed from your languages list." expect(volunteer.languages).not_to include language end end context "when request params are invalid" do let(:language) { create(:language) } before do patch add_language_users_path(volunteer), params: { language_id: language.id } end it "raises error when Language do not exist" do expect { delete remove_language_users_path(999) }.to raise_error(ActiveRecord::RecordNotFound) end end end end ================================================ FILE: spec/requests/volunteers_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe "/volunteers", type: :request do let(:organization) { create(:casa_org) } let(:admin) { build(:casa_admin, casa_org: organization) } let(:supervisor) { create(:supervisor, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } describe "GET /index" do it "renders a successful response" do sign_in admin get volunteers_path expect(response).to be_successful end end describe "GET /show" do it "renders a successful response" do sign_in admin get volunteer_path(volunteer.id) expect(response).to redirect_to(edit_volunteer_path(volunteer.id)) end context "with admin from different organization" do let(:other_org_admin) { build(:casa_admin, casa_org: create(:casa_org)) } it "does not show" do sign_in other_org_admin get volunteer_path(volunteer.id) expect(response).to redirect_to("/") end end end describe "POST /datatable" do let(:data) { {recordsTotal: 51, recordsFiltered: 10, data: 10.times.map { {} }} } before do allow(VolunteerDatatable).to receive(:new).and_return double "datatable", to_json: data.to_json end it "is successful" do sign_in admin post datatable_volunteers_path expect(response).to be_successful end it "renders json data" do sign_in admin post datatable_volunteers_path expect(response.body).to eq data.to_json end end describe "GET /new" do it "renders a successful response for admin user" do sign_in admin get new_volunteer_path expect(response).to be_successful end it "renders a successful response for supervisor user" do sign_in supervisor get new_volunteer_path expect(response).to be_successful end it "does not render for volunteers" do sign_in volunteer get new_volunteer_path expect(response).not_to be_successful end end describe "GET /edit" do subject(:request) do get edit_volunteer_url(volunteer) response end before { sign_in admin } it { is_expected.to be_successful } it "shows correct volunteer", :aggregate_failures do create(:volunteer, casa_org: organization) page = request.parsed_body.to_html expect(page).to include(volunteer.email) expect(page).to include(volunteer.display_name) expect(page).to include(volunteer.phone_number) end it "shows correct supervisor options", :aggregate_failures do supervisors = create_list(:supervisor, 3, casa_org: organization) supervisors.append(create(:supervisor, casa_org: organization, display_name: "O'Hara")) # test for HTML escaping page = Nokogiri::HTML(subject.body) names = page.css("#supervisor_volunteer_supervisor_id option").map(&:text) expect(supervisors.map(&:display_name)).to match_array(names) end end describe "POST /create" do context "with valid params" do let(:params) do { volunteer: { display_name: "Example", email: "volunteer1@example.com" } } end it "creates a new volunteer and sends account_setup email" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) sign_in admin expect { post volunteers_url, params: params }.to change { ActionMailer::Base.deliveries.count }.by(1) expect(response).to have_http_status(:redirect) volunteer = Volunteer.last expect(volunteer.email).to eq("volunteer1@example.com") expect(volunteer.display_name).to eq("Example") expect(volunteer.casa_org).to eq(admin.casa_org) expect(response).to redirect_to edit_volunteer_path(volunteer) end it "sends a SMS when phone number exists" do organization = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: organization) params[:volunteer][:phone_number] = "+12222222222" twilio_activation_success_stub = WebMockHelper.twilio_activation_success_stub("volunteer") short_io_stub = WebMockHelper.short_io_stub_sms sign_in admin post volunteers_url, params: params expect(short_io_stub).to have_been_requested.times(2) expect(twilio_activation_success_stub).to have_been_requested.times(1) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New volunteer created successfully. SMS has been sent!/) end it "does not send a SMS when phone number is not provided" do organization = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: organization) sign_in admin post volunteers_url, params: params expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New volunteer created successfully./) end it "does not send a SMS when Twilio API has an error" do org = create(:casa_org, twilio_account_sid: "articuno31", twilio_enabled: true) admin = create(:casa_admin, casa_org: org) twilio_activation_error_stub = WebMockHelper.twilio_activation_error_stub("volunteer") short_io_stub = WebMockHelper.short_io_stub_sms params[:volunteer][:phone_number] = "+12222222222" sign_in admin post volunteers_url, params: params expect(short_io_stub).to have_been_requested.times(2) # TODO: why is this called at all? expect(twilio_activation_error_stub).to have_been_requested.times(1) expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New volunteer created successfully. SMS not sent. Error: ./) end it "does not send a SMS if the casa_org does not have Twilio enabled" do org = create(:casa_org, twilio_enabled: false) admin = build(:casa_admin, casa_org: org) params[:volunteer][:phone_number] = "+12222222222" short_io_stub = WebMockHelper.short_io_stub_sms sign_in admin post volunteers_url, params: params expect(short_io_stub).to have_been_requested.times(2) # TODO: why is this called at all? expect(response).to have_http_status(:redirect) follow_redirect! expect(flash[:notice]).to match(/New volunteer created successfully./) end end context "with invalid parameters" do let(:params) do { volunteer: { display_name: "", email: "volunteer1@example.com" } } end it "does not create a new volunteer" do org = create(:casa_org, twilio_enabled: false) admin = build(:casa_admin, casa_org: org) sign_in admin expect { post volunteers_url, params: params }.to change(Volunteer, :count).by(0) .and change(ActionMailer::Base.deliveries, :count).by(0) expect(response).to have_http_status(:unprocessable_content) end end end describe "PATCH /update" do before { sign_in admin } context "with valid params" do it "updates the volunteer" do patch volunteer_path(volunteer), params: { volunteer: {display_name: "New Name", phone_number: "+15463457898"} } expect(response).to have_http_status(:redirect) volunteer.reload expect(volunteer.display_name).to eq "New Name" expect(volunteer.phone_number).to eq "15463457898" end it "sends the volunteer a confirmation email upon email change" do patch volunteer_path(volunteer), params: { volunteer: {email: "newemail@gmail.com"} } expect(response).to have_http_status(:redirect) volunteer.reload expect(volunteer.unconfirmed_email).to eq("newemail@gmail.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end end context "with invalid params" do let!(:other_volunteer) { create(:volunteer) } it "does not update the volunteer" do volunteer.supervisor = build(:supervisor) patch volunteer_path(volunteer), params: { volunteer: {email: other_volunteer.email, display_name: "New Name", phone_number: "+15463457898"} } expect(response).to have_http_status(:unprocessable_content) volunteer.reload expect(volunteer.display_name).not_to eq "New Name" expect(volunteer.email).not_to eq other_volunteer.email expect(volunteer.phone_number).not_to eq "+15463457898" end end # Activation/deactivation must be done separately through /activate and # /deactivate, respectively it "cannot change the active state" do patch volunteer_path(volunteer), params: { volunteer: {active: false} } volunteer.reload expect(volunteer.active).to eq(true) end end describe "PATCH /activate" do let(:volunteer) { create(:volunteer, :inactive, casa_org: organization) } let(:volunteer_with_cases) { create(:volunteer, :with_cases_and_contacts, casa_org: organization) } let(:case_number) { volunteer_with_cases.casa_cases.first.case_number.parameterize } it "activates an inactive volunteer" do sign_in admin patch activate_volunteer_path(volunteer) volunteer.reload expect(volunteer.active).to eq(true) end it "sends an activation email" do sign_in admin expect { patch activate_volunteer_path(volunteer) }.to change { ActionMailer::Base.deliveries.count }.by(1) end context "activated volunteer without cases" do it "shows a flash messages indicating the volunteer has been activated and sent an email" do sign_in admin patch activate_volunteer_path(volunteer) expect(response).to redirect_to(edit_volunteer_path(volunteer)) follow_redirect! expect(flash[:notice]).to match(/Volunteer was activated. They have been sent an email./) end end context "activated volunteer with cases" do it "shows a flash message indicating the volunteer has been activated and sent an email" do sign_in admin patch activate_volunteer_path(id: volunteer_with_cases, redirect_to_path: "casa_case", casa_case_id: case_number) expect(response).to redirect_to(edit_casa_case_path(case_number)) follow_redirect! expect(flash[:notice]).to match(/Volunteer was activated. They have been sent an email./) end end end describe "PATCH /deactivate" do subject(:request) do patch deactivate_volunteer_path(volunteer) response end before { sign_in admin } it { is_expected.to redirect_to(edit_volunteer_path(volunteer)) } it "shows the correct flash message" do request expect(flash[:notice]).to eq("Volunteer was deactivated.") end it "deactivates an active volunteer" do request expect(volunteer.reload.active).to eq(false) end it "doesn't send a deactivation email" do expect { request }.not_to change { ActionMailer::Base.deliveries.count } end end describe "PATCH /resend_invitation" do it "resends an invitation email as an admin" do sign_in admin expect(volunteer.invitation_created_at.present?).to eq(false) get resend_invitation_volunteer_path(volunteer) volunteer.reload expect(volunteer.invitation_created_at.present?).to eq(true) expect(Devise.mailer.deliveries.count).to eq(1) expect(Devise.mailer.deliveries.first.subject).to eq(I18n.t("devise.mailer.invitation_instructions.subject")) expect(response).to redirect_to(edit_volunteer_path(volunteer)) end it "resends an invitation email as a supervisor" do sign_in supervisor expect(volunteer.invitation_created_at.present?).to eq(false) get resend_invitation_volunteer_path(volunteer) volunteer.reload expect(volunteer.invitation_created_at.present?).to eq(true) expect(Devise.mailer.deliveries.count).to eq(1) expect(Devise.mailer.deliveries.first.subject).to eq(I18n.t("devise.mailer.invitation_instructions.subject")) expect(response).to redirect_to(edit_volunteer_path(volunteer)) end end describe "PATCH /reminder" do describe "as admin" do it "emails the volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) supervisor = build(:supervisor, casa_org: organization) volunteer = create(:volunteer, supervisor: supervisor, casa_org_id: organization.id) sign_in admin patch reminder_volunteer_path(volunteer) email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil expect(email.to).to eq [volunteer.email] expect(email.subject).to eq("Reminder to input case contacts") end it "cc's their supervisor and admin when the 'with_cc` param is present" do admin = create(:casa_admin, casa_org_id: organization.id) supervisor = build(:supervisor, casa_org: organization) volunteer = create(:volunteer, supervisor: supervisor, casa_org_id: organization.id) sign_in admin patch reminder_volunteer_path(volunteer), params: { with_cc: true } email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil expect(email.to).to eq [volunteer.email] expect(email.subject).to eq("Reminder to input case contacts") expect(email.cc).to include(volunteer.supervisor.email) expect(email.cc).to include(admin.email) end end describe "as supervisor" do it "emails the volunteer" do organization = create(:casa_org) supervisor = build(:supervisor, casa_org: organization) volunteer = create(:volunteer, supervisor: supervisor, casa_org_id: organization.id) sign_in supervisor patch reminder_volunteer_path(volunteer) email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil expect(email.to).to eq [volunteer.email] expect(email.subject).to eq("Reminder to input case contacts") end it "cc's their supervisor when the 'with_cc` param is present" do organization = create(:casa_org) supervisor = build(:supervisor, casa_org: organization) volunteer = create(:volunteer, supervisor: supervisor, casa_org_id: organization.id) sign_in supervisor patch reminder_volunteer_path(volunteer), params: { with_cc: true } email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil expect(email.to).to eq [volunteer.email] expect(email.subject).to eq("Reminder to input case contacts") expect(email.cc).to eq([supervisor.email]) end end it "emails the volunteer without a supervisor" do organization = create(:casa_org) volunteer_without_supervisor = create(:volunteer) supervisor = build(:supervisor, casa_org: organization) sign_in supervisor patch reminder_volunteer_path(volunteer_without_supervisor), params: { with_cc: true } email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil expect(email.to).to eq [volunteer_without_supervisor.email] expect(email.subject).to eq("Reminder to input case contacts") expect(email.cc).to be_empty end end describe "POST /send_reactivation_alert" do before do sign_in admin @short_io_stub = WebMockHelper.twilio_activation_success_stub end it "sends an reactivation SMS" do get send_reactivation_alert_volunteer_path(volunteer) expect(response).to redirect_to(edit_volunteer_path(volunteer)) expect(response.status).to match 302 end it "does not send a reactivation SMS when Casa Org has Twilio disabled" do org = create(:casa_org, twilio_enabled: false) adm = create(:casa_admin, casa_org: org) vol = create(:volunteer, casa_org: org) sign_in adm get send_reactivation_alert_volunteer_path(vol) expect(response).to redirect_to(edit_volunteer_path(vol)) expect(flash[:notice]).to match(/Volunteer reactivation alert not sent. Twilio is disabled for #{org.name}/) end end describe "GET /impersonate" do let!(:other_volunteer) { create(:volunteer, casa_org: organization) } let!(:supervisor) { create(:supervisor, casa_org: organization) } it "can impersonate a volunteer as an admin" do sign_in admin get impersonate_volunteer_path(volunteer) expect(response).to redirect_to(root_path) expect(controller.current_user).to eq(volunteer) end it "can impersonate a volunteer as a supervisor" do sign_in supervisor get impersonate_volunteer_path(volunteer) expect(response).to redirect_to(root_path) expect(controller.current_user).to eq(volunteer) end it "can not impersonate as a volunteer" do sign_in volunteer get impersonate_volunteer_path(other_volunteer) expect(response).to redirect_to(root_path) expect(controller.current_user).to eq(volunteer) follow_redirect! expect(flash[:notice]).to match(/Sorry, you are not authorized to perform this action./) end end end ================================================ FILE: spec/routing/all_casa_admins/patch_notes_routing_spec.rb ================================================ require "rails_helper" RSpec.describe AllCasaAdmins::PatchNotesController, type: :routing do describe "routing" do it "routes to #index" do expect(get: "/all_casa_admins/patch_notes").to route_to("all_casa_admins/patch_notes#index") end it "routes to #create" do expect(post: "/all_casa_admins/patch_notes").to route_to("all_casa_admins/patch_notes#create") end it "routes to #update via PUT" do expect(put: "/all_casa_admins/patch_notes/1").to route_to("all_casa_admins/patch_notes#update", id: "1") end it "routes to #update via PATCH" do expect(patch: "/all_casa_admins/patch_notes/1").to route_to("all_casa_admins/patch_notes#update", id: "1") end it "routes to #destroy" do expect(delete: "/all_casa_admins/patch_notes/1").to route_to("all_casa_admins/patch_notes#destroy", id: "1") end end end ================================================ FILE: spec/seeds/seeds_spec.rb ================================================ require "rails_helper" require "rake" def empty_ar_classes ar_classes = [ AllCasaAdmin, CasaAdmin, CasaCase, Judge, CasaOrg, CaseAssignment, CaseContact, ContactType, ContactTypeGroup, Supervisor, SupervisorVolunteer, User, LearningHour, HearingType, Volunteer, CaseCourtOrder ] ar_classes.select { |klass| klass.count == 0 }.map(&:name) end RSpec.describe "Seeds" do describe "test development DB" do before do Rails.application.load_tasks allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("test")) end it "successfully populates all necessary tables" do ActiveRecord::Tasks::DatabaseTasks.load_seed expect(empty_ar_classes).to eq([]) end end end ================================================ FILE: spec/services/additional_expense_params_service_spec.rb ================================================ require "rails_helper" RSpec.describe AdditionalExpenseParamsService do subject { described_class.new(params).calculate } context "single existing additional expense" do let(:params) { ActionController::Parameters.new(case_contact: {additional_expenses_attributes: {"0": {other_expense_amount: 10, other_expenses_describe: "hi", id: 1}}}) } it "calculates" do expect(subject.to_json).to eq("[{\"other_expense_amount\":10,\"other_expenses_describe\":\"hi\",\"id\":1}]") end end context "multiple new additional expense" do let(:params) { ActionController::Parameters.new(case_contact: { additional_expenses_attributes: { "0": {other_expense_amount: 10, other_expenses_describe: "new expense 0"}, "1": {other_expense_amount: 20, other_expenses_describe: "new expense 1"} } }) } it "calculates" do expect(subject.length).to eq(2) expect(subject[0]["other_expense_amount"]).to eq(10) expect(subject[0]["other_expenses_describe"]).to eq("new expense 0") expect(subject[1]["other_expense_amount"]).to eq(20) expect(subject[1]["other_expenses_describe"]).to eq("new expense 1") end end end ================================================ FILE: spec/services/backfill_followupable_service_spec.rb ================================================ require "rails_helper" RSpec.describe BackfillFollowupableService do include ActiveJob::TestHelper let(:run_backfill) { described_class.new.fill_followup_id_and_type } after do clear_enqueued_jobs end describe "backfilling followup polymorphic columns" do let(:case_contact) { create(:case_contact) } let!(:followup) { create(:followup, :without_dual_writing, case_contact: case_contact) } it "updates followupable_id and followupable_type correctly" do expect { run_backfill }.to change { followup.reload.followupable_id }.from(nil).to(case_contact.id) .and change { followup.reload.followupable_type }.from(nil).to("CaseContact") end context "when an error occurs during update" do before do allow_any_instance_of(Followup).to receive(:update_columns).and_raise(StandardError.new("Update failed")) end it "logs the error and notifies Bugsnag" do expect(Bugsnag).to receive(:notify).with(instance_of(StandardError)) expect(Rails.logger).to receive(:error).with(/Failed to update Followup/) expect { run_backfill }.not_to change { followup.reload.followupable_id } end end end end ================================================ FILE: spec/services/casa_case_change_service_spec.rb ================================================ require "rails_helper" RSpec.describe CasaCaseChangeService do subject { described_class.new(original, changed).changed_attributes_messages } context "with same original and changed" do let(:original) { create(:casa_case).full_attributes_hash } let(:changed) { original } it "does not show diff" do expect(subject).to eq(nil) end end context "with different original and changed" do let(:original) { create(:casa_case).full_attributes_hash } let(:changed) { create(:casa_case, :with_case_assignments, :with_one_court_order, :active, :with_case_contacts).full_attributes_hash } it "shows useful diff" do expect(subject).to contain_exactly("Changed Id", "Changed Case number", "Changed Created at", "Changed Birth month year youth", "1 Court order added or updated") end end end ================================================ FILE: spec/services/case_contacts_contact_dates_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactsContactDates do before do travel_to Date.new(2021, 6, 1) end describe "#contact_dates_details" do subject { described_class.new(interviewees).contact_dates_details } context "without interviewees" do let(:interviewees) { [] } it "returns an empty array" do expect(subject).to eq([]) end end context "with interviewees" do let(:contact_type_1) { create(:contact_type, name: "Mental therapist") } let(:contact_type_2) { create(:contact_type, name: "Physical therapist") } let(:contact_type_3) { create(:contact_type, name: "Aunt") } let(:ccct_1) { create(:case_contact_contact_type, contact_type: contact_type_1) } let(:ccct_2) { create(:case_contact_contact_type, contact_type: contact_type_2) } let(:ccct_3) { create(:case_contact_contact_type, contact_type: contact_type_2, case_contact: create(:case_contact, occurred_at: 1.month.ago)) } let(:ccct_4) do create( :case_contact_contact_type, contact_type: contact_type_2, case_contact: create( :case_contact, occurred_at: 2.months.ago, medium_type: CaseContact::TEXT_EMAIL ) ) end let(:ccct_5) { create(:case_contact_contact_type, contact_type: contact_type_3, case_contact: create(:case_contact, occurred_at: 2.months.ago)) } let(:interviewees) { [ccct_1, ccct_2, ccct_3, ccct_4, ccct_5] } it "returns formatted data" do expect(subject).to eq([ {dates: "6/01*", dates_by_medium_type: {"in-person" => "6/01*"}, name: "Names of persons involved, starting with the child's name", type: "Mental therapist"}, {dates: "4/01*, 5/01*, 6/01*", dates_by_medium_type: {"in-person" => "5/01*, 6/01*", "text/email" => "4/01*"}, name: "Names of persons involved, starting with the child's name", type: "Physical therapist"}, {dates: "4/01*", dates_by_medium_type: {"in-person" => "4/01*"}, name: "Names of persons involved, starting with the child's name", type: "Aunt"} ]) end end end end ================================================ FILE: spec/services/case_contacts_export_csv_service_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe CaseContactsExportCsvService, type: :service do describe "#perform" do it "exports the case contacts without the court topics header by default" do casa_case = create(:casa_case) create(:case_contact, casa_case: casa_case, medium_type: "text/email", occurred_at: Date.new(2026, 1, 8)) case_contacts = casa_case.decorate.case_contacts_ordered_by_occurred_at csv = CaseContactsExportCsvService.new(case_contacts, filtered_columns).perform parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.count).to eq(1) expect(parsed_csv.headers).to eq(expected_headers) expect(csv).to match(%r{text/email}) expect(csv).to match(/January 8, 2026/) expect(parsed_csv.headers).not_to include("Court Topics") end context "when there are no case contacts" do it "exports only the headers" do casa_case = build(:casa_case) case_contacts = casa_case.decorate.case_contacts_ordered_by_occurred_at csv = CaseContactsExportCsvService.new(case_contacts, filtered_columns).perform parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.count).to eq(0) expect(parsed_csv.headers).to eq(expected_headers) end end context "when the filtered columns includes court topics" do it "exports the case contacts with the CaseContactReport::COLUMNS with the contact topics" do casa_case = create(:casa_case) case_contact = build(:case_contact, casa_case:, medium_type: "text/email", occurred_at: Date.new(2026, 1, 8)) create(:case_contact, casa_case:, medium_type: "in-person", occurred_at: Date.new(2026, 3, 16)) contact_topic = build(:contact_topic, question: "A Topic") create(:contact_topic_answer, case_contact:, contact_topic:, value: "An answer") case_contacts = casa_case.decorate.case_contacts_ordered_by_occurred_at csv = CaseContactsExportCsvService.new(case_contacts, filtered_columns).perform parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.count).to eq(2) expect(parsed_csv.headers).to eq(expected_headers + ["A Topic"]) expect(csv).to match(/in-person/) expect(csv).to match(/March 16, 2026/) expect(csv).to match(%r{text/email}) expect(csv).to match(/January 8, 2026/) expect(csv).to match(/a topic/i) expect(csv).to match(/an answer/i) end it "does not include topics that don't have any answers" do casa_case = create(:casa_case) case_contact = build(:case_contact, casa_case: casa_case, medium_type: "text/email", occurred_at: Date.new(2026, 1, 8)) contact_topic = build(:contact_topic, question: "A Topic with an Answer") create(:contact_topic_answer, contact_topic:, case_contact:, value: "An answer") build(:contact_topic, question: "Nothing to show") case_contacts = casa_case.decorate.case_contacts_ordered_by_occurred_at csv = CaseContactsExportCsvService.new(case_contacts, filtered_columns).perform parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.count).to eq(1) expect(parsed_csv.headers).to eq(expected_headers + ["A Topic with an Answer"]) expect(csv).to match(%r{text/email}) expect(csv).to match(/January 8, 2026/) expect(csv).to include("An answer") expect(csv).not_to include("Nothing to show") end context "when there are multiple answers to a case contact's court topic" do it "exports the case contact including only the latest contact topic answer" do casa_case = create(:casa_case) case_contact = build(:case_contact, casa_case: casa_case, medium_type: "text/email", occurred_at: Date.new(2026, 1, 8)) contact_topic = build(:contact_topic, question: "A Topic") create(:contact_topic_answer, case_contact:, contact_topic:, value: "First answer") create(:contact_topic_answer, case_contact:, contact_topic:, value: "Second answer") case_contacts = casa_case.decorate.case_contacts_ordered_by_occurred_at csv = CaseContactsExportCsvService.new(case_contacts, filtered_columns).perform parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.count).to eq(1) expect(parsed_csv.headers).to eq(expected_headers + ["A Topic"]) expect(csv).to match(%r{text/email}) expect(csv).to match(/January 8, 2026/) expect(csv).to match(/a topic/i) expect(csv).to include("Second answer") expect(csv).not_to include("First answer") end end end context "when court topics are filtered out" do it "exports the case contacts with the CaseContactReport::COLUMNS without the Court topics entries" do casa_case = create(:casa_case) case_contact = build(:case_contact, casa_case:, medium_type: "text/email", occurred_at: Date.new(2026, 1, 8)) create(:case_contact, casa_case:, medium_type: "in-person", occurred_at: Date.new(2026, 3, 16)) contact_topic = build(:contact_topic, question: "Another Topic") create(:contact_topic_answer, case_contact:, contact_topic:, value: "Another answer") case_contacts = casa_case.decorate.case_contacts_ordered_by_occurred_at filtered_columns = CaseContactReport::COLUMNS - [:court_topics] csv = CaseContactsExportCsvService.new(case_contacts, filtered_columns).perform parsed_csv = CSV.parse(csv, headers: true) expect(parsed_csv.count).to eq(2) expect(parsed_csv.headers).to eq(expected_headers - ["A Topic"]) expect(csv).to match(/in-person/) expect(csv).to match(/March 16, 2026/) expect(csv).to match(%r{text/email}) expect(csv).to match(/January 8, 2026/) expect(csv).not_to include("Another Topic") expect(csv).not_to include("Another answer") end end end def filtered_columns CaseContactReport::COLUMNS end def expected_headers [ "Internal Contact Number", "Duration Minutes", "Contact Types", "Contact Made", "Contact Medium", "Occurred At", "Added To System At", "Miles Driven", "Wants Driving Reimbursement", "Casa Case Number", "Creator Email", "Creator Name", "Supervisor Name", "Case Contact Notes" ] end end ================================================ FILE: spec/services/court_report_due_sms_reminder_service_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe CourtReportDueSmsReminderService do include SmsBodyHelper describe "court report due sms reminder service" do let(:org) { create(:casa_org, twilio_enabled: true) } let!(:volunteer) { create(:volunteer, casa_org: org, receive_sms_notifications: true, phone_number: "+12223334444") } let!(:report_due_date) { Date.current + 7.days } before do WebMockHelper.short_io_court_report_due_date_stub WebMockHelper.twilio_court_report_due_date_stub end context "when sending sms reminder" do it "sends a SMS with a short url successfully" do response = CourtReportDueSmsReminderService.court_report_reminder(volunteer, report_due_date) expect(response.error_code).to match nil expect(response.status).to match "sent" expect(response.body).to match court_report_due_msg(report_due_date, "https://42ni.short.gy/jzTwdF") end end context "when volunteer is not opted into sms notifications" do let(:volunteer) { create(:volunteer, receive_sms_notifications: false) } it "does not send a SMS" do response = CourtReportDueSmsReminderService.court_report_reminder(volunteer, report_due_date) expect(response).to be_nil end end context "when volunteer does not have a valid phone number" do let(:volunteer) { create(:volunteer, phone_number: nil) } it "does not send a SMS" do response = CourtReportDueSmsReminderService.court_report_reminder(volunteer, report_due_date) expect(response).to be_nil end end context "when volunteer's casa_org does not have twilio enabled" do let(:org) { create(:casa_org, twilio_enabled: false) } let(:volunteer_2) { create(:volunteer, casa_org: org) } it "does not send a SMS" do response = CourtReportDueSmsReminderService.court_report_reminder(volunteer_2, report_due_date) expect(response).to be_nil end end end end ================================================ FILE: spec/services/court_report_format_contact_date_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe CourtReportFormatContactDate, type: :service do describe "#format" do context "when there has been a successful contact" do it "returns the day and month of when the Case Contact's occcurred" do case_contact = build( :case_contact, occurred_at: Date.new(2026, 4, 16), contact_made: true ) contact_date = CourtReportFormatContactDate.new(case_contact).format expect(contact_date).to eq("4/16") end end context "when there has been no contact made" do it "returns the day and month of when the Case Contact's occcurred with a suffix" do case_contact = build(:case_contact, occurred_at: Date.new(2026, 3, 16)) contact_date = CourtReportFormatContactDate.new(case_contact).format expect(contact_date).to eq("3/16*") end end end describe "#format_long" do it "returns the day, month and year of when the Case Contact's in the long format" do case_contact = build(:case_contact, occurred_at: Date.new(2026, 2, 16)) contact_date = CourtReportFormatContactDate.new(case_contact).format_long expect(contact_date).to eq("02/16/26") end end end ================================================ FILE: spec/services/create_all_casa_admin_service_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe CreateAllCasaAdminService, type: :service do let(:user) { build(:user) } let(:params) do ActionController::Parameters.new( { all_casa_admin: { email: "casa_admin23@example.com" } } ).permit! end describe "#build" do it "initializes an AllCasaAdmin with the given params and a password" do allow(SecureRandom).to receive(:hex).with(10).and_return("12345678910") admin = CreateAllCasaAdminService.new(params, user) all_casa_admin = admin.build expect(all_casa_admin).to be_instance_of(AllCasaAdmin) expect(all_casa_admin).not_to be_persisted expect(all_casa_admin).to have_attributes( email: params[:all_casa_admin][:email], password: "12345678910" ) end end describe "#create!" do it "creates an AllCasaAdmin with the given params and sends an invite" do admin = CreateAllCasaAdminService.new(params, user) admin.build expect do admin.create! end.to change(AllCasaAdmin, :count).by(1) casa_admin = AllCasaAdmin.last expect(casa_admin.invited_by_id).to eq(user.id) expect(casa_admin.invited_by_type).to eq("User") expect(casa_admin).to have_attributes( email: params[:all_casa_admin][:email] ) end context "when there are errors" do it "does not create an AllCasaAdmin and returns the errors" do params = ActionController::Parameters.new( { all_casa_admin: { email: "invalid_email_format" } } ).permit! admin = CreateAllCasaAdminService.new(params, user) admin.build expect do admin.create! end.to raise_error(ActiveRecord::RecordInvalid, /email is invalid/i) end end end end ================================================ FILE: spec/services/create_casa_admin_service_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe CreateCasaAdminService, type: :service do let(:organization) { create(:casa_org) } let(:user) { build(:user) } let(:params) do ActionController::Parameters.new( { casa_admin: { email: "casa_admin23@example.com", display_name: "Bob Cat", phone_number: "+16306149615", date_of_birth: Date.new(1990, 1, 1), receive_reimbursement_email: "1", monthly_learning_hours_report: "1" } } ).permit! end describe "#build" do it "initializes a CasaAdmin with the given params" do admin = CreateCasaAdminService.new(organization, params, user) casa_admin = admin.build expect(casa_admin).to be_instance_of(CasaAdmin) expect(casa_admin).not_to be_persisted expect(casa_admin).to have_attributes( display_name: params[:casa_admin][:display_name], phone_number: params[:casa_admin][:phone_number], email: params[:casa_admin][:email], date_of_birth: params[:casa_admin][:date_of_birth], receive_reimbursement_email: true, monthly_learning_hours_report: true ) end it "initializes a CasaAdmin with custom fields" do admin = CreateCasaAdminService.new(organization, params, user) casa_admin = admin.build expect(casa_admin).to have_attributes( active: true, casa_org_id: organization.id, type: "CasaAdmin" ) expect(casa_admin.password).to be_present end end describe "#create!" do it "creates a CasaAdmin with the given params" do admin = CreateCasaAdminService.new(organization, params, user) admin.build expect do admin.create! end.to change(CasaAdmin, :count).by(1) casa_admin = CasaAdmin.last expect(casa_admin).to have_attributes( display_name: params[:casa_admin][:display_name], phone_number: params[:casa_admin][:phone_number], email: params[:casa_admin][:email], date_of_birth: params[:casa_admin][:date_of_birth], receive_reimbursement_email: true, monthly_learning_hours_report: true, active: true, casa_org_id: organization.id, password: nil ) end it "sends an invite from the user" do admin = CreateCasaAdminService.new(organization, params, user) admin.build casa_admin = admin.create! expect(casa_admin.invited_by_id).to eq(user.id) expect(casa_admin.invited_by_type).to eq("User") end context "when there are errors" do it "does not create the CasaAdmin and returns the errors" do params = ActionController::Parameters.new( { casa_admin: { email: "invalid_email_format", display_name: "Bob Cat" } } ).permit! admin = CreateCasaAdminService.new(organization, params, user) admin.build expect do admin.create! end.to raise_error(ActiveRecord::RecordInvalid, /email is invalid/i) end end end end ================================================ FILE: spec/services/deployment/backfill_case_contact_started_metadata_service_spec.rb ================================================ require "rails_helper" RSpec.describe Deployment::BackfillCaseContactStartedMetadataService do let(:past) { Date.new(2020, 1, 1).in_time_zone } let(:parsed_past) { past.as_json } let(:present) { Date.new(2024, 1, 1).in_time_zone } let(:parsed_present) { present.as_json } before { travel_to present } context "when a case contact has status metadata" do let(:case_contact) { create(:case_contact) } context "when a case contact has status started metadata" do let!(:case_contact) { create(:case_contact, :started, created_at: past) } it "does not change metadata" do described_class.new.backfill_metadata expect(case_contact.reload.metadata.dig("status", "started")).to eq(parsed_past) end end context "when a case contact has other status metadata" do let!(:case_contact) { create(:case_contact, created_at: past, metadata: {"status" => {"details" => parsed_past}}) } it "does not change status details" do described_class.new.backfill_metadata expect(case_contact.reload.metadata.dig("status", "started")).to eq(parsed_past) end it "sets status started" do described_class.new.backfill_metadata expect(case_contact.reload.metadata.dig("status", "started")).to eq(parsed_past) end end end context "when a case contact has no metadata" do let!(:case_contact) { create(:case_contact, created_at: past, metadata: {}) } it "does not change metadata" do described_class.new.backfill_metadata expect(case_contact.reload.metadata.dig("status", "started")).to be_nil end end end ================================================ FILE: spec/services/emancipation_checklist_download_html_spec.rb ================================================ require "rails_helper" RSpec.describe EmancipationChecklistDownloadHtml do describe "#call" do it "renders the form correctly" do ec1_option_a = create(:emancipation_option, name: "With friend") ec1_option_b = create(:emancipation_option, name: "With relative") ec1 = create(:emancipation_category, name: "Youth has housing", emancipation_options: [ec1_option_a, ec1_option_b]) ec2 = create(:emancipation_category, name: "Youth has completed a budget") create(:emancipation_category, name: "Youth is employed") current_case = create(:casa_case, emancipation_categories: [ec1, ec2], emancipation_options: [ec1_option_a]) emancipation_form_data = EmancipationCategory.all service = described_class.new(current_case, emancipation_form_data) str = service.call expect(str).to match "With friend" expect(str).to match "With relative" expect(str).to match "Youth has housing" expect(str).to match "Youth has completed a budget" expect(str).to match "Youth is employed" end end end ================================================ FILE: spec/services/emancipation_checklist_reminder_service_spec.rb ================================================ require "rails_helper" RSpec.describe EmancipationChecklistReminderService do include ActiveJob::TestHelper let(:send_reminders) { described_class.new.send_reminders } before do travel_to Date.new(2022, 10, 1) end after do clear_enqueued_jobs end context "with only two eligible cases" do subject(:task) { described_class.new } let!(:eligible_case1) { create(:case_assignment) } let!(:eligible_case2) { create(:case_assignment) } let!(:ineligible_case1) { create(:case_assignment, pre_transition: true) } let!(:inactive_case) { create(:case_assignment, :inactive) } it "#initialize correctly captures the eligible cases" do expect(CasaCase.count).to eq(4) expect(task.cases).not_to be_empty expect(task.cases.length).to eq(2) end end context "volunteer with transition age youth case" do let!(:casa_case) { create(:casa_case, :with_one_case_assignment) } it "sends notification" do expect { send_reminders }.to change { casa_case.case_assignments.first.volunteer.notifications.count }.by(1) end end context "volunteer with multiple transition age youth cases" do let!(:volunteer) { create(:volunteer, :with_casa_cases) } it "sends notification for each case" do expect { send_reminders }.to change { volunteer.notifications.count }.by(2) end end context "volunteer without transition age youth case" do let!(:casa_case) { create(:casa_case, :with_one_case_assignment, birth_month_year_youth: 13.years.ago) } it "does not send notification" do expect { send_reminders }.not_to change { casa_case.case_assignments.first.volunteer.notifications.count } end end context "when the case assignment is inactive" do let!(:case_assignment) { create(:case_assignment, :inactive) } it "does not send notification" do expect { send_reminders }.not_to change { case_assignment.volunteer.notifications.count } end end context "when there are no case assignments" do it "does not raise error" do expect { send_reminders }.not_to raise_error end end end ================================================ FILE: spec/services/failed_import_csv_service_spec.rb ================================================ require "rails_helper" require "fileutils" require "csv" RSpec.describe FailedImportCsvService do let(:import_type) { "casa_case" } let(:user) { create(:casa_admin) } let(:csv_string) { "case_number,birth_month_year_youth\n12345,2001-04\n" } let(:user_id_hash) { Digest::SHA256.hexdigest(user.id.to_s)[0..15] } let(:csv_path) { Rails.root.join("tmp", import_type, "failed_rows_userid_#{user_id_hash}.csv") } subject(:service) { described_class.new(failed_rows: failed_rows, import_type: import_type, user: user) } before { FileUtils.rm_f(csv_path) } after { FileUtils.rm_f(csv_path) } def create_file(content: csv_string, mtime: Time.current) FileUtils.mkdir_p(File.dirname(csv_path)) File.write(csv_path, content) File.utime(mtime.to_time, mtime.to_time, csv_path) end describe "#store" do context "when file is within size limit" do let(:failed_rows) { csv_string } it "writes the CSV content to the tmp file" do service.store expect(File.exist?(csv_path)).to be true expect(File.read(csv_path)).to eq csv_string end end context "when file exceeds size limit" do let(:failed_rows) { "a" * (described_class::MAX_FILE_SIZE_BYTES + 1) } it "logs a warning and stores a warning" do expect(Rails.logger).to receive(:warn).with(/CSV too large to save for user/) service.store expect(File.read(csv_path)).to match(/The file was too large to save/) end end end describe "#read" do let(:failed_rows) { "" } context "when file exists and has not expired" do before { create_file } it "returns the contents" do expect(service.read).to eq csv_string end end context "when file is expired" do let(:failed_rows) { "The failed import file has expired. Please upload a new CSV." } before { create_file(mtime: 2.days.ago.to_time) } it "deletes the file and returns fallback message" do expect(File.exist?(csv_path)).to be true expect(service.read).to include("The failed import file has expired") expect(File.exist?(csv_path)).to be false end end context "when file never existed" do it "returns fallback message" do expect(service.read).to include("No failed import file found") end end end describe "#cleanup" do let(:failed_rows) { "" } context "when file exists" do before { create_file } it "removes the file" do expect(File.exist?(csv_path)).to be true expect(Rails.logger).to receive(:info).with(/Removing old failed rows CSV/) service.cleanup expect(File.exist?(csv_path)).to be false end end context "when file does not exist" do it "does nothing" do expect { service.cleanup }.not_to raise_error end end end end ================================================ FILE: spec/services/fdf_inputs_service_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe FdfInputsService, type: :service do describe ".clean" do context "when the string is nil" do it "returns nil" do expect(FdfInputsService.clean("")).to be_nil expect(FdfInputsService.clean(" ")).to be_nil expect(FdfInputsService.clean(nil)).to be_nil end end it "returns the escaped string" do expect(FdfInputsService.clean("hello world")).to eq("hello world") expect(FdfInputsService.clean("(test)")).to eq("\\(test\\)") expect(FdfInputsService.clean("path\\to\\file")).to eq("path\\\\to\\\\file") expect(FdfInputsService.clean("(a\\b)")).to eq("\\(a\\\\b\\)") expect(FdfInputsService.clean("((a))")).to eq("\\(\\(a\\)\\)") expect(FdfInputsService.clean("\\(test\\)")).to eq("\\\\\\(test\\\\\\)") end end describe "#write_to_file" do it "calls PdfForms with the given arguments and returns a file" do inputs = {name: "Bob Cat"} pdf_template_path = "/path/to/template.pdf" basename = "test_file" fake_pdf_forms = double("PdfForms") allow(PdfForms).to receive(:new).and_return(fake_pdf_forms) allow(fake_pdf_forms).to receive(:fill_form_with_fdf) service = FdfInputsService.new( inputs: inputs, pdf_template_path: pdf_template_path, basename: basename ) result = service.write_to_file expect(fake_pdf_forms).to have_received(:fill_form_with_fdf) do |template, output_path, fdf_path, flatten| expect(template).to eq(pdf_template_path) expect(output_path).to be_a(String) expect(File.exist?(output_path)).to be(true) expect(fdf_path).to be_a(String) expect(File.exist?(fdf_path)).to be(true) expect(flatten).to eq(flatten: true) end expect(result).to be_a(Tempfile) expect(File.exist?(result.path)).to be(true) end end end ================================================ FILE: spec/services/followup_export_csv_service_spec.rb ================================================ require "rails_helper" RSpec.describe FollowupExportCsvService do subject { described_class.new(casa_case.casa_org) } let!(:casa_case) { create(:casa_case) } let!(:creator) { create(:user, display_name: "Craig") } let!(:alice) { create(:volunteer, display_name: "Alice", casa_org: casa_case.casa_org) } let!(:bob) { create(:volunteer, display_name: "Bob", casa_org: casa_case.casa_org) } let!(:case_contact) { create(:case_contact, casa_case: casa_case) } let!(:followup) { create(:followup, creator: creator, case_contact: case_contact, note: "hello, this is the thing, ") } before do create(:case_assignment, casa_case: casa_case, volunteer: alice) create(:case_assignment, casa_case: casa_case, volunteer: bob) end describe "#perform" do it "Exports case contact followup data" do results = subject.perform.split("\n") expect(results.count).to eq(2) expect(results[0].split(",")).to eq(["Case Number", "Volunteer Name(s)", "Note Creator Name", "Note"]) expect(results[1]).to eq %(#{case_contact.casa_case.case_number},Alice and Bob,Craig,"#{followup.note}") end end end ================================================ FILE: spec/services/followup_service_spec.rb ================================================ require "rails_helper" RSpec.describe FollowupService do describe ".create_followup" do let(:case_contact) { create(:case_contact) } let(:creator) { create(:volunteer) } let(:note) { "This is a test note." } let(:notification_double) { double("FollowupNotifier") } before do allow(FollowupNotifier).to receive(:with).and_return(notification_double) allow(notification_double).to receive(:deliver) end it "successfully creates a followup and sends notification" do expect { FollowupService.create_followup(case_contact, creator, note) }.to change(Followup, :count).by(1) followup = Followup.last expect(followup.note).to eq(note) expect(followup.creator).to eq(creator) expect(followup.followupable).to eq(case_contact) expect(FollowupNotifier).to have_received(:with).with( followup: followup, created_by: creator ) expect(notification_double).to have_received(:deliver) end context "when followup fails to save" do before do allow_any_instance_of(Followup).to receive(:save).and_return(false) end it "does not send a notification" do expect(FollowupService.create_followup(case_contact, creator, note)).to be_a_new(Followup) expect(FollowupNotifier).not_to have_received(:with) end end end end ================================================ FILE: spec/services/inactive_messages_service_spec.rb ================================================ require "rails_helper" RSpec.describe InactiveMessagesService do describe "#inactive_messages" do subject { described_class.new(supervisor).inactive_messages } let(:supervisor) { create :supervisor } it "has messages" do v1 = create(:supervisor_volunteer, supervisor: supervisor).volunteer create(:case_assignment, :inactive, volunteer: v1, casa_case: create(:casa_case, case_number: "ABC")) create(:case_assignment, :inactive, volunteer: v1, casa_case: create(:casa_case, case_number: "DEF")) create(:case_assignment, volunteer: v1, casa_case: create(:casa_case, case_number: "active-case")) create(:supervisor_volunteer, supervisor: supervisor) expect(subject.count).to eq(2) expect(subject.first).to match(/Case .* marked inactive this week./) expect(subject.first).to match(/Case .* marked inactive this week./) end it "has no messages" do expect(subject).to eq([]) end end end ================================================ FILE: spec/services/learning_hours_export_csv_service_spec.rb ================================================ require "rails_helper" RSpec.describe LearningHoursExportCsvService do let!(:user) { create(:user) } let!(:learning_hour_type) { create(:learning_hour_type) } let!(:learning_hour) do create(:learning_hour, duration_hours: 2, duration_minutes: 30, occurred_at: "2022-06-20", learning_hour_type: learning_hour_type) end describe "#perform" do let(:result) { described_class.new(LearningHour.all).perform } it "returns a csv as a string starting with the learning hours headers" do csv_headers = "Volunteer Name,Learning Hours Title,Learning Hours Type,Duration,Date Of Learning\n" expect(result).to start_with(csv_headers) end it "returns a csv as a string ending with the learning hours values" do csv_values = "#{user.display_name},#{learning_hour.name},#{learning_hour.learning_hour_type.name}," \ "2:30,2022-06-20\n" if learning_hour.name.include? "," csv_values.gsub!(learning_hour.name, '"' + learning_hour.name + '"') end expect(result).to end_with(csv_values) end end end ================================================ FILE: spec/services/mileage_export_csv_service_spec.rb ================================================ require "rails_helper" RSpec.describe MileageExportCsvService do subject { described_class.new(case_contacts).perform } let(:case_contacts) { CaseContact.where(id: case_contact) } let(:case_contact) { create(:case_contact) } it "creates CSV" do results = subject.split("\n") expect(results.count).to eq(2) expect(results[0].split(",")).to eq([ "Contact Types", "Occurred At", "Miles Driven", "Casa Case Number", "Creator Name", "Supervisor Name", "Volunteer Address", "Reimbursed" ]) expect(results[1].split(",").count).to eq(9) end end ================================================ FILE: spec/services/missing_data_export_csv_service_spec.rb ================================================ require "rails_helper" RSpec.describe MissingDataExportCsvService do let!(:casa_cases) { create_list(:casa_case, 3) } let(:result) { described_class.new(CasaCase.all).perform } describe "#perform" do it "returns a string formatted as csv" do expect(result).to match("Casa Case Number,Youth Birth Month And Year,Upcoming Hearing Date,Court Orders\n") expect(result).to match("#{casa_cases[0].case_number},OK,MISSING,MISSING\n") expect(result).to match("#{casa_cases[1].case_number},OK,MISSING,MISSING\n") expect(result).to match("#{casa_cases[2].case_number},OK,MISSING,MISSING\n") end end end ================================================ FILE: spec/services/no_contact_made_sms_reminder_service_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe NoContactMadeSmsReminderService do include SmsBodyHelper describe "court report due sms reminder service" do let(:org) { create(:casa_org, twilio_enabled: true) } let!(:volunteer) { create(:volunteer, receive_sms_notifications: true, phone_number: "+12222222222", casa_org: org) } let!(:contact_type) { "test" } before do WebMockHelper.short_io_stub_localhost WebMockHelper.twilio_no_contact_made_stub end context "when sending sms reminder" do it "sends a SMS with a short url successfully" do response = NoContactMadeSmsReminderService.no_contact_made_reminder(volunteer, contact_type) expect(response.error_code).to match nil expect(response.status).to match "sent" expect(response.body).to match no_contact_made_msg(contact_type, "https://42ni.short.gy/jzTwdF") end end context "when volunteer is not opted into sms notifications" do let(:volunteer) { create(:volunteer, receive_sms_notifications: false) } it "does not send a SMS" do response = NoContactMadeSmsReminderService.no_contact_made_reminder(volunteer, contact_type) expect(response).to be_nil end end context "when volunteer does not have a valid phone number" do let(:volunteer) { create(:volunteer, phone_number: nil) } it "does not send a SMS" do response = NoContactMadeSmsReminderService.no_contact_made_reminder(volunteer, contact_type) expect(response).to be_nil end end context "when volunteer's casa_org does not have twilio enabled" do let(:casa_org) { create(:casa_org, twilio_enabled: false) } let(:volunteer) { create(:volunteer, casa_org: casa_org) } it "does not send a SMS" do response = NoContactMadeSmsReminderService.no_contact_made_reminder(volunteer, contact_type) expect(response).to be_nil end end end end ================================================ FILE: spec/services/placement_export_csv_service_spec.rb ================================================ require "rails_helper" require "factory_bot_rails" RSpec.describe PlacementExportCsvService do it "creates a Placements CSV with placement headers" do casa_org = create(:casa_org, name: "Fake Name", display_name: "Fake Display Name") placement_type = create(:placement_type, casa_org: casa_org) creator = create(:user) create(:placement, creator: creator, placement_type: placement_type) csv_headers = "Casa Org,Casa Case Number,Placement Type,Placement Started At,Created At,Creator Name\n" result = PlacementExportCsvService.new(casa_org: casa_org).perform expect(result).to start_with(csv_headers) end end ================================================ FILE: spec/services/preference_set_table_state_service_spec.rb ================================================ require "rails_helper" RSpec.describe PreferenceSetTableStateService do subject { described_class.new(user_id: user.id) } let!(:user) { create(:user) } let(:preference_set) { user.preference_set } let!(:table_state) { {"volunteers_table" => {"columns" => [{"visible" => true}]}} } let(:table_state2) { {"columns" => [{"visible" => false}]} } let(:table_name) { "volunteers_table" } describe "#update!" do context "when the update is successful" do it "updates the table state" do expect { subject.update!(table_state: table_state2, table_name: table_name) }.to change { preference_set.reload.table_state }.from({}).to({table_name => table_state2}) end end context "when the update fails" do before do allow_any_instance_of(PreferenceSet).to receive(:save!).and_raise(ActiveRecord::RecordNotSaved) end it "raises an error" do expect { subject.update!(table_state: table_state2, table_name: table_name) }.to raise_error(PreferenceSetTableStateService::TableStateUpdateFailed, "Failed to update table state for '#{table_name}'") end end end describe "#table_state" do context "when the preference set exists" do before do table_state = {"columns" => [{"visible" => true}]} user.preference_set.table_state["volunteers_table"] = table_state user.preference_set.save! end it "returns the table state" do expect(subject.table_state(table_name: "volunteers_table")).to eq(table_state["volunteers_table"]) end end context "when there is no data for that table name" do before do preference_set.table_state = {} preference_set.save end it "returns nil" do expect(subject.table_state(table_name: table_name)).to eq(nil) end end end end ================================================ FILE: spec/services/short_url_service_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe ShortUrlService do let!(:original_url) { "https://www.google.com" } let!(:notification_object) { ShortUrlService.new } let!(:short_io_domain) { Rails.application.credentials[:SHORT_IO_DOMAIN] } describe "short.io API" do before do WebMockHelper.short_io_stub end it "returns a successful response with correct http request" do response = notification_object.create_short_url(original_url) expect(a_request(:post, "https://api.short.io/links") .with(body: {originalURL: original_url, domain: short_io_domain}.to_json, headers: {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => "1337"})) .to have_been_made.once expect(response.code).to match 200 expect(response.body).to match "{\"shortURL\":\"https://42ni.short.gy/jzTwdF\"}" end it "returns a short url" do notification_object.create_short_url(original_url) short_url = notification_object.short_url expect(short_url).to be_an_instance_of(String) expect(short_url).to match "https://42ni.short.gy/jzTwdF" end end end ================================================ FILE: spec/services/sms_reminder_service_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe SmsReminderService do describe "court report due sms reminder service" do let(:org) { create(:casa_org, twilio_enabled: true) } let!(:volunteer) { create(:volunteer, casa_org: org, receive_sms_notifications: true, phone_number: "+12222222222") } let!(:message) { "It's been two weeks since you've tried reaching 'test'. Try again! https://42ni.short.gy/jzTwdF" } before do WebMockHelper.short_io_stub_localhost WebMockHelper.twilio_no_contact_made_stub end context "when sending sms reminder" do it "sends a SMS with a short url successfully" do response = SmsReminderService.send_reminder(volunteer, message) expect(response.error_code).to match nil expect(response.status).to match "sent" expect(response.body).to match "It's been two weeks since you've tried reaching 'test'. Try again! https://42ni.short.gy/jzTwdF" end end context "when volunteer is not opted into sms notifications" do let(:volunteer) { create(:volunteer, receive_sms_notifications: false) } it "does not send a SMS" do response = SmsReminderService.send_reminder(volunteer, message) expect(response).to be_nil end end context "when volunteer does not have a valid phone number" do let(:volunteer) { create(:volunteer, phone_number: nil) } it "does not send a SMS" do response = SmsReminderService.send_reminder(volunteer, message) expect(response).to be_nil end end context "when a volunteer's casa_org does not have twilio enabled" do let(:casa_org_twilio_disabled) { create(:casa_org, twilio_enabled: false) } let(:volunteer_twilio_disabled) { create(:volunteer, casa_org: casa_org_twilio_disabled) } it "does not send a SMS" do response = SmsReminderService.send_reminder(volunteer_twilio_disabled, message) expect(response).to be_nil end end end end ================================================ FILE: spec/services/svg_sanitizer_service_spec.rb ================================================ require "rails_helper" RSpec.describe SvgSanitizerService do let(:result) { described_class.sanitize(file) } describe "when receiving a svg file" do let(:file) { fixture_file_upload("unsafe_svg.svg", "image/svg+xml") } it "removes script tags" do expect(result.read).not_to match("script") end end describe "when not receiving a svg file" do let(:file) { fixture_file_upload("company_logo.png", "image/png") } it "returns the file without changes" do expect(result).to eq(file) end end describe "when receiving nil" do let(:file) { nil } it "returns nil" do expect(result).to be_nil end end end ================================================ FILE: spec/services/twilio_service_spec.rb ================================================ require "rails_helper" require "support/stubbed_requests/webmock_helper" RSpec.describe TwilioService do describe "twilio API" do context "SMS messaging" do let(:short_url) { ShortUrlService.new } let!(:casa_org) do create( :casa_org, twilio_phone_number: "+15555555555", twilio_account_sid: "articuno34", twilio_api_key_sid: "Aladdin", twilio_api_key_secret: "open sesame", twilio_enabled: true ) end before do WebMockHelper.short_io_stub_sms WebMockHelper.twilio_success_stub end it "can send a SMS with a short url successfully" do twilio = TwilioService.new(casa_org) short_url.create_short_url("https://www.google.com") params = { From: "+15555555555", Body: "Execute Order 66 - ", To: "+12222222222", URL: short_url.short_url } # response is a Twilio API obj response = twilio.send_sms(params) expect(response.error_code).to match nil expect(response.status).to match "sent" expect(response.body).to match "Execute Order 66 - https://42ni.short.gy/jzTwdF" end end context "when twilio is disabled" do let!(:casa_org_twilio_disabled) do create( :casa_org, twilio_phone_number: "+15555555553", twilio_account_sid: "zapdos43", twilio_api_key_sid: "Jasmine", twilio_api_key_secret: "hakuna matata", twilio_enabled: false ) end before do WebMockHelper.short_io_stub_sms WebMockHelper.twilio_success_stub end it "returns nil" do short_url = ShortUrlService.new twilio = TwilioService.new(casa_org_twilio_disabled) short_url.create_short_url("https://www.google.com") params = { From: "+15555555555", Body: "Execute Order 66 - ", To: "+12222222222", URL: short_url.short_url } response = twilio.send_sms(params) expect(response).to eq nil end end end end ================================================ FILE: spec/services/volunteer_birthday_reminder_service_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerBirthdayReminderService do include ActiveJob::TestHelper let(:send_reminders) { described_class.new.send_reminders } let(:now) { Date.new(2022, 10, 15) } let(:this_month) { now.month } let(:this_month_15th) { Date.new(now.year, now.month, 15) } let(:next_month) { Date.new(1988, this_month + 1, 1) } let(:not_next_month) { Date.new(1998, this_month - 1, 1) } before do travel_to now end after do clear_enqueued_jobs end context "there is a volunteer with a birthday next month" do let!(:volunteer) do create(:volunteer, :with_assigned_supervisor, date_of_birth: next_month) end it "creates a notification" do expect { send_reminders }.to change { volunteer.supervisor.notifications.count }.by(1) end end context "there are multiple volunteers with birthdays next month" do let(:supervisor) { create(:supervisor) } let!(:volunteer) do create_list(:volunteer, 4, :with_assigned_supervisor, date_of_birth: next_month, supervisor: supervisor) end it "creates multiple notifications" do expect { send_reminders }.to change { Noticed::Notification.count }.by(4) end end context "there is an unsupervised volunteer with a birthday next month" do let!(:volunteer) do create(:volunteer, date_of_birth: next_month) end it "does not create a notification" do expect { send_reminders }.not_to change { Noticed::Notification.count } end end context "there is a volunteer with no date_of_birth" do let!(:volunteer) do create(:volunteer, :with_assigned_supervisor, date_of_birth: nil) end it "does not create a notification" do expect { send_reminders }.not_to change { volunteer.supervisor.notifications.count } end end context "there is a volunteer with a birthday that is not next month" do let!(:volunteer) do create(:volunteer, :with_assigned_supervisor, date_of_birth: not_next_month) end it "does not create a notification" do expect { send_reminders }.not_to change { volunteer.supervisor.notifications.count } end end context "when today is the 15th" do before { travel_to(this_month_15th) } let!(:volunteer) do create(:volunteer, :with_assigned_supervisor, date_of_birth: next_month) end it "runs the rake task" do expect { send_reminders }.to change { volunteer.supervisor.notifications.count }.by(1) end end context "when today is not the 15th" do before { travel_to(this_month_15th + 2.days) } it "skips the rake task" do expect { send_reminders }.not_to change { Noticed::Notification.count } end end end ================================================ FILE: spec/services/volunteers_emails_export_csv_service_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteersEmailsExportCsvService do describe "#call" do it "Exports correct data from volunteers" do casa_org = create(:casa_org) other_casa_org = create(:casa_org) active_volunteer = create(:volunteer, :with_casa_cases, casa_org: casa_org) inactive_volunteer = create(:volunteer, :inactive, casa_org: casa_org) other_org_volunteer = create(:volunteer, casa_org: other_casa_org) active_volunteer_cases = active_volunteer.casa_cases.active.map { |c| [c.case_number, c.in_transition_age?] }.to_h csv = described_class.new(casa_org).call results = csv.split("\n") expect(results.count).to eq(2) expect(results[0].split(",")).to eq(["Email", "Old Emails", "Case Number", "Volunteer Name", "Case Transition Aged Status"]) expect(results[1]).to eq("#{active_volunteer.email},No Old Emails,\"#{active_volunteer_cases.keys.join(", ")}\",#{active_volunteer.display_name},\"#{active_volunteer_cases.values.join(", ")}\"") expect(csv).to match(/#{active_volunteer.email}/) expect(csv).not_to match(/#{inactive_volunteer.email}/) expect(csv).not_to match(/#{other_org_volunteer.email}/) end it "Exports correct data from volunteers, including old emails" do casa_org_2 = create(:casa_org) volunteer_with_old_emails = create(:volunteer, old_emails: ["old_email@example.com"], casa_org: casa_org_2) csv = described_class.new(casa_org_2).call results = csv.split("\n") expect(results.count).to eq(2) expect(results[0].split(",")).to eq(["Email", "Old Emails", "Case Number", "Volunteer Name", "Case Transition Aged Status"]) expect(results[1]).to eq("#{volunteer_with_old_emails.email},#{volunteer_with_old_emails.old_emails.join(", ")},\"\",#{volunteer_with_old_emails.display_name},\"\"") end end end ================================================ FILE: spec/spec_helper.rb ================================================ # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause # this file to always be loaded, without a need to explicitly require it in any # files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an # individual file that may not need all of that loaded. Instead, consider making # a separate helper file that requires the additional dependencies and performs # the additional setup, and require it from the spec files that actually need # it. if ENV["RUN_SIMPLECOV"] require "simplecov" SimpleCov.start do command_name "Job #{ENV["TEST_ENV_NUMBER"]}" if ENV["TEST_ENV_NUMBER"] add_filter "/spec/" add_filter "/lib/tasks/auto_annotate_models.rake" add_group "Models", "/app/models" add_group "Controllers", "/app/controllers" add_group "Channels", "/app/channels" add_group "Decorators", "/app/decorators" add_group "Helpers", "/app/helpers" add_group "Jobs", "/app/jobs" add_group "Importers", "/app/lib/importers" add_group "Mailers", "/app/mailers" add_group "Policies", "/app/policies" add_group "Values", "/app/values" add_group "Tasks", "/lib/tasks" add_group "Config", "/config" add_group "Database", "/db" end # https://github.com/simplecov-ruby/simplecov?tab=readme-ov-file#want-to-use-spring-with-simplecov # Rails.application.eager_load! end # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: # be_bigger_than(2).and_smaller_than(4).description # # => "be bigger than 2 and smaller than 4" # ...rather than: # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to # `true` in RSpec 4. mocks.verify_partial_doubles = true end # This option will default to `:apply_to_host_groups` in RSpec 4 (and will # have no way to turn it off -- the option exists only for backwards # compatibility in RSpec 3). It causes shared context metadata to be # inherited by the metadata hash of host groups and examples, rather than # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. # This allows you to limit a spec run to individual examples or groups # you care about by tagging them with `:focus` metadata. When nothing # is tagged with `:focus`, all examples get run. RSpec also provides # aliases for `it`, `describe`, and `context` that include `:focus` # metadata: `fit`, `fdescribe` and `fcontext`, respectively. config.filter_run_when_matching :focus # Allows RSpec to persist some state between runs in order to support # the `--only-failures` and `--next-failure` CLI options. We recommend # you configure your source control system to ignore this file. config.example_status_persistence_file_path = "tmp/persistent_examples.txt" # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ config.disable_monkey_patching! # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. if config.files_to_run.one? # Use the documentation formatter for detailed output, # unless a formatter has already been configured # (e.g. via a command-line flag). config.default_formatter = "doc" end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. # config.profile_examples = 10 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed end ================================================ FILE: spec/support/api_helper.rb ================================================ module ApiHelper include Rack::Test::Methods def app Rails.application end end RSpec.configure do |config| config.include ApiHelper, type: :api # apply to all spec for apis folder end ================================================ FILE: spec/support/capybara.rb ================================================ require "capybara/rails" require "capybara/rspec" require "capybara-screenshot/rspec" require "selenium/webdriver" Capybara.default_max_wait_time = ENV.fetch("CAPYBARA_WAIT_TIME", "10").to_i # not used unless you swap it out for selenium_chrome_headless_in_container to watch tests running in docker Capybara.register_driver :selenium_chrome_in_container do |app| Capybara::Selenium::Driver.new app, browser: :remote, url: "http://selenium_chrome:4444/wd/hub", capabilities: [:chrome] end # disable CSS transitions and js animations Capybara.disable_animation = true Capybara::Screenshot.autosave_on_failure = true Capybara.save_path = Rails.root.join("tmp", "screenshots#{ENV["GROUPS_UNDERSCORE"]}") options = Selenium::WebDriver::Chrome::Options.new options.add_argument("--disable-gpu") options.add_argument("--ignore-certificate-errors") options.add_argument("--window-size=1280,1900") options.add_preference(:browser, set_download_behavior: {behavior: "allow"}) # used in docker Capybara.register_driver :selenium_chrome_headless_in_container do |app| options.add_argument("--headless") options.add_preference(:download, prompt_for_download: false, default_directory: "/home/seluser/Downloads") options.add_argument("--disable-dev-shm-usage") Capybara::Selenium::Driver.new app, browser: :remote, url: "http://selenium_chrome:4444/wd/hub", options: options end # used without docker Capybara.register_driver :selenium_chrome_headless do |app| options.add_argument("--headless") options.add_argument("--disable-site-isolation-trials") options.add_preference(:download, prompt_for_download: false, default_directory: DownloadHelpers::PATH.to_s) Capybara::Selenium::Driver.new app, browser: :chrome, options: options end RSpec.configure do |config| config.before(:each, type: :system) do driven_by :rack_test end config.before(:each, :js, type: :system) do config.include DownloadHelpers clear_downloads if ENV["DOCKER"] driven_by :selenium_chrome_headless_in_container Capybara.server_host = "0.0.0.0" Capybara.server_port = 4000 Capybara.app_host = "http://web:4000" else driven_by :selenium_chrome_headless end end config.before(:each, :debug, type: :system) do config.include DownloadHelpers clear_downloads if ENV["DOCKER"] driven_by :selenium_chrome_in_container Capybara.server_host = "0.0.0.0" Capybara.server_port = 4000 Capybara.app_host = "http://web:4000" else driven_by :selenium_chrome end end end ================================================ FILE: spec/support/case_court_report_helpers.rb ================================================ # frozen_string_literal: true # Helper methods for case court reports system specs module CaseCourtReportHelpers # Finds the 'Generate Report' button, clicks it, and confirms the modal is visible. # Assumes the user is already on the case_court_reports_path. def open_court_report_modal find('[data-bs-target="#generate-docx-report-modal"]').click expect(page).to have_selector("#generate-docx-report-modal", visible: :visible) end # Opens the Select2 dropdown within the (already open) report modal. # Confirms the dropdown options are visible. def open_case_select2_dropdown # Wait for the Select2 container to be visible expect(page).to have_css("#case_select_body .selection", visible: :visible) # Click the container to open the dropdown find("#case_select_body .selection").click # Wait for the dropdown to appear expect(page).to have_css(".select2-dropdown", visible: :visible) end end ================================================ FILE: spec/support/datatable_helper.rb ================================================ module DatatableHelper def datatable_params(order_by:, additional_filters: {}, order_direction: "ASC", page: nil, per_page: nil, search_term: nil) if page.present? raise ":per_page argument required when :page present" if per_page.blank? start = [page - 1, 0].max * per_page end { additional_filters: additional_filters, columns: {"0" => {name: order_by}}, length: per_page, order: {"0" => {column: "0", dir: order_direction}}, search: {value: search_term}, start: start } end def described_class class_name = self.class.name.split("::")[2] class_name.constantize rescue NameError # TODO warning log to bugsnag here ? end def escaped(value) ERB::Util.html_escape value end end ================================================ FILE: spec/support/download_helpers.rb ================================================ module DownloadHelpers TIMEOUT = 10 PATH = Rails.root.join("tmp/downloads#{ENV["TEST_ENV_NUMBER"]}") def downloads Dir[PATH.join("*")] end def download downloads.first end def download_content wait_for_download File.read(download) end def download_docx wait_for_download Docx::Document.open(download) end def header_text(download_docx) zip = download_docx.zip files = zip.glob("word/header*.xml").map { |h| h.name } filename_and_contents_pairs = files.map do |file| simple_file_name = file.sub(/^word\//, "").sub(/\.xml$/, "") [simple_file_name, Nokogiri::XML(zip.read(file))] end filename_and_contents_pairs.map { |name, doc| doc.text }.join("\n") end def table_text(download_docx) download_docx.tables.map { |t| t.rows.map(&:cells).flatten.map(&:to_s) }.flatten end def download_file_name File.basename(download) end def wait_for_download Timeout.timeout(TIMEOUT) do sleep 0.1 until downloaded? end end def downloaded? !downloading? && downloads.any? end def downloading? downloads.grep(/\.crdownload$/).any? end def clear_downloads FileUtils.rm_f(downloads) end end ================================================ FILE: spec/support/factory_bot.rb ================================================ # frozen_string_literal: true def described_class_factory described_class.name.gsub("::", "").underscore end RSpec.configure do |config| config.include FactoryBot::Syntax::Methods # Any factory that takes more than .5 seconds to create will show in the # console when running the tests. config.before(:suite) do ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload| execution_time_in_seconds = finish - start if execution_time_in_seconds >= 0.5 Rails.logger.warn { "Slow factory: #{payload[:name]} takes #{execution_time_in_seconds} seconds using strategy #{payload[:strategy]}" } end end end # This will output records as they are created. Handy for debugging but very # noisy. # config.before(:each) do # ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload| # $stderr.puts "FactoryBot: #{payload[:strategy]}(:#{payload[:name]})" # end # end # This will output total database records being created. if ENV.fetch("SPEC_OUTPUT_FACTORY_BOT_OBJECT_CREATION_STATS", false) factory_bot_results = {} config.before(:suite) do ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload| factory_name = payload[:name] strategy_name = payload[:strategy] factory_bot_results[factory_name] ||= {} factory_bot_results[factory_name][:total] ||= 0 factory_bot_results[factory_name][:total] += 1 factory_bot_results[factory_name][strategy_name] ||= 0 factory_bot_results[factory_name][strategy_name] += 1 end end config.after(:suite) do puts "How many objects did factory_bot create? (probably too many- let's tune some factories...)" pp factory_bot_results end end end ================================================ FILE: spec/support/fill_in_case_contact_fields.rb ================================================ module FillInCaseContactFields DETAILS_ID = "#contact-form-details" NOTES_ID = "#contact-form-notes" TOPIC_VALUE_CLASS = ".contact-topic-answer-input" TOPIC_SELECT_CLASS = ".contact-topic-id-select" REIMBURSEMENT_ID = "#contact-form-reimbursement" EXPENSE_AMOUNT_CLASS = ".expense-amount-input" EXPENSE_DESCRIBE_CLASS = ".expense-describe-input" def fill_in_contact_details(case_numbers: [], contact_types: [], contact_made: true, medium: "In Person", occurred_on: Time.zone.today, hours: nil, minutes: nil) within DETAILS_ID do within "#draft-case-id-selector" do if case_numbers.nil? all(:element, "a", title: "Remove this item").each(&:click) end if case_numbers.present? find(".ts-control").click Array.wrap(case_numbers).each_with_index do |case_number, index| checkbox_for_case_number = first("span", text: case_number).sibling("input") checkbox_for_case_number.click unless checkbox_for_case_number.checked? end find(".ts-control").click end end fill_in "case_contact_occurred_at", with: occurred_on if occurred_on if contact_types.present? contact_types.each do |contact_type| check contact_type end elsif !contact_types.nil? contact_type = ContactType.first check contact_type.name end choose medium if medium if contact_made check "Contact was made" else uncheck "Contact was made" end fill_in "case_contact_duration_hours", with: hours if hours fill_in "case_contact_duration_minutes", with: minutes if minutes end end alias_method :complete_details_page, :fill_in_contact_details def fill_in_notes(notes: nil, contact_topic_answers_attrs: []) within NOTES_ID do Array.wrap(contact_topic_answers_attrs).each_with_index do |attributes, index| click_on "Add Another Discussion Topic" if index > 0 answer_topic_unscoped attributes[:question], attributes[:answer] end if notes.present? fill_in "Additional Notes", with: notes end end end alias_method :complete_notes_page, :fill_in_notes # @param miles [Integer] # @param want_reimbursement [Boolean] # @param address [String] def fill_in_mileage(miles: 0, want_reimbursement: false, address: nil) within REIMBURSEMENT_ID do check_reimbursement(want_reimbursement) return unless want_reimbursement fill_in "case_contact_miles_driven", with: miles if miles.present? fill_in "case_contact_volunteer_address", with: address if address end end alias_method :fill_in_expenses_page, :fill_in_mileage def choose_medium(medium) choose medium if medium end def check_reimbursement(want_reimbursement = true) if want_reimbursement check "Request travel or other reimbursement" else uncheck "Request travel or other reimbursement" end end def fill_expense_fields(amount, describe, index: nil) within REIMBURSEMENT_ID do amount_field = index.present? ? all(EXPENSE_AMOUNT_CLASS)[index] : all(EXPENSE_AMOUNT_CLASS).last describe_field = index.present? ? all(EXPENSE_DESCRIBE_CLASS)[index] : all(EXPENSE_DESCRIBE_CLASS).last amount_field.fill_in(with: amount) if amount describe_field.fill_in(with: describe) if describe end end def answer_topic(question, answer, index: nil) within NOTES_ID do answer_topic_unscoped(question, answer, index:) end end private # use when already 'within' notes div def answer_topic_unscoped(question, answer, index: nil) topic_select = index.nil? ? all(TOPIC_SELECT_CLASS).last : all(TOPIC_SELECT_CLASS)[index] answer_field = index.nil? ? all(TOPIC_VALUE_CLASS).last : all(TOPIC_VALUE_CLASS)[index] topic_select.select(question) if question answer_field.fill_in(with: answer) if answer end end RSpec.configure do |config| config.include FillInCaseContactFields, type: :system end ================================================ FILE: spec/support/flipper_helper.rb ================================================ RSpec.configure do |config| config.before(:each, :flipper) do allow(Flipper).to receive(:enabled?) end end ================================================ FILE: spec/support/pretender_context.rb ================================================ module PretenderContext def true_user end end ================================================ FILE: spec/support/prosopite.rb ================================================ # frozen_string_literal: true return unless defined?(Prosopite) # Test configuration — this file owns all Prosopite settings for the test env Prosopite.enabled = true Prosopite.raise = true Prosopite.rails_logger = true Prosopite.prosopite_logger = true # Allowlist for known acceptable N+1 patterns (e.g., test matchers) Prosopite.allow_stack_paths = [ "shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb", "shoulda/matchers/active_model/validate_presence_of_matcher.rb", "shoulda/matchers/active_model/validate_inclusion_of_matcher.rb", "shoulda/matchers/active_model/allow_value_matcher.rb" ] # Load ignore list from file for gradual rollout — directories listed in # .prosopite_ignore are scanned but won't raise, only log. PROSOPITE_IGNORE = if File.exist?("spec/.prosopite_ignore") File.read("spec/.prosopite_ignore") .lines .map(&:chomp) .reject { |line| line.empty? || line.start_with?("#") } else [] end RSpec.configure do |config| # Pause Prosopite during factory creation to prevent false positives # from factory callbacks and associations config.before(:suite) do if defined?(FactoryBot) FactoryBot::SyntaxRunner.class_eval do alias_method :original_create, :create def create(*args, **kwargs, &block) if defined?(Prosopite) && Prosopite.enabled? Prosopite.pause { original_create(*args, **kwargs, &block) } else original_create(*args, **kwargs, &block) end end end end end config.around do |example| if use_prosopite?(example) Prosopite.scan { example.run } else original_enabled = Prosopite.enabled? Prosopite.enabled = false begin example.run ensure Prosopite.enabled = original_enabled end end end end def use_prosopite?(example) # Explicit metadata takes precedence return false if example.metadata[:disable_prosopite] return true if example.metadata[:enable_prosopite] # Check against ignore list PROSOPITE_IGNORE.none? do |path| File.fnmatch?("./#{path}/*", example.metadata[:rerun_file_path].to_s) end end ================================================ FILE: spec/support/pundit_helper.rb ================================================ module PunditHelper def enable_pundit(view, user) without_partial_double_verification do allow(view).to receive(:policy) do |record| Pundit.policy(user, record) end end end end ================================================ FILE: spec/support/rack_attack.rb ================================================ RSpec.configure do |config| config.before do Rack::Attack.enabled = false end end ================================================ FILE: spec/support/request_helpers.rb ================================================ module Support module RequestHelpers def response_json @response_json ||= JSON.parse(response.body, symbolize_names: true) end end end ================================================ FILE: spec/support/rspec_retry.rb ================================================ # spec/spec_helper.rb require "rspec/retry" RSpec.configure do |config| # show retry status in spec process config.verbose_retry = true # show exception that triggers a retry if verbose_retry is set to true config.display_try_failure_messages = true if ENV["CI"] == "true" # run retry only on features config.around :each, :js do |ex| ex.run_with_retry retry: 3 end end end ================================================ FILE: spec/support/session_helper.rb ================================================ module SessionHelper def sign_in_as_admin sign_in(CasaAdmin.first || create(:casa_admin)) end def sign_in_as_volunteer sign_in(Volunteer.first || create(:volunteer)) end # TODO use this more def sign_in_as_supervisor sign_in(Supervisor.first || create(:supervisor)) end # TODO use this more def sign_in_as_all_casa_admin sign_in(AllCasaAdmin.first || create(:all_casa_admin)) end end ================================================ FILE: spec/support/shared_examples/shows_court_dates_links.rb ================================================ RSpec.shared_examples_for "shows court dates links" do before do travel_to Date.new(2020, 1, 2) _newest_pcd = create(:court_date, date: DateTime.current - 5.days, casa_case: casa_case) _oldest_pcd = create(:court_date, date: DateTime.current - 10.days, casa_case: casa_case) hearing_type = create(:hearing_type, name: "Some Hearing Name") _court_date_with_details = create(:court_date, :with_court_details, casa_case: casa_case, hearing_type: hearing_type) end it "shows court orders" do visit edit_casa_case_path(casa_case) expect(page).to have_link("December 23, 2019") expect(page).to have_link("December 26, 2019") expect(page).to have_link("December 26, 2019 - Some Hearing Name") end it "past court dates are ordered" do visit casa_case_path(casa_case) expect(page).to have_text("December 23, 2019") expect(page).to have_text(/December 23, 2019.*December 28, 2019/m) end end ================================================ FILE: spec/support/shared_examples/shows_error_for_invalid_phone_numbers.rb ================================================ RSpec.shared_examples_for "shows error for invalid phone numbers" do it "shows error message for phone number < 12 digits" do (role == "admin" || role == "user") ? fill_in("Phone number", with: "+141632489") : fill_in("#{role}_phone_number", with: "+141632489") (role == "user") ? click_on("Update Profile") : click_on("Submit") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for phone number > 12 digits" do (role == "admin" || role == "user") ? fill_in("Phone number", with: "+141632180923") : fill_in("#{role}_phone_number", with: "+141632180923") (role == "user") ? click_on("Update Profile") : click_on("Submit") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for bad phone number" do (role == "admin" || role == "user") ? fill_in("Phone number", with: "+141632u809o") : fill_in("#{role}_phone_number", with: "+141632u809o") (role == "user") ? click_on("Update Profile") : click_on("Submit") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for phone number without country code" do (role == "admin" || role == "user") ? fill_in("Phone number", with: "+24163218092") : fill_in("#{role}_phone_number", with: "+24163218092") (role == "user") ? click_on("Update Profile") : click_on("Submit") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end end ================================================ FILE: spec/support/shared_examples/soft_deleted_model.rb ================================================ RSpec.shared_examples_for "a soft-deleted model" do |skip_ignores_deleted_records_in_validations_check: false, skip_deleted_at_index_check: false| # for usage with acts_as_paranoid models it { is_expected.to have_db_column(:deleted_at) } unless skip_deleted_at_index_check it { is_expected.to have_db_index(:deleted_at) } end it "cannot be found, by default" do model ||= create(described_class_factory) model.destroy! expect(described_class.find_by(id: model.id)).to be_nil end it "returned when unscoped" do model ||= create(described_class_factory) model.destroy! expect(described_class.unscoped.find_by(id: model.id)).to be_present end context "uniqueness" do it "ignores deleted records in validations" do unless skip_ignores_deleted_records_in_validations_check obj = create(described_class_factory) new_obj = obj.dup expect(new_obj).not_to be_valid obj.destroy! expect(new_obj).to be_valid expect { new_obj.save! }.not_to raise_exception end end end end ================================================ FILE: spec/support/stubbed_requests/short_io_api.rb ================================================ module ShortIOAPI def short_io_stub(base_url = "https://www.google.com") WebMock.stub_request(:post, "https://api.short.io/links") .with( body: {originalURL: base_url, domain: "42ni.short.gy"}.to_json, headers: { "Accept" => "application/json", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Authorization" => "1337", "Content-Type" => "application/json", "User-Agent" => "Ruby" } ) .to_return(status: 200, body: "{\"shortURL\":\"https://42ni.short.gy/jzTwdF\"}", headers: {}) end def short_io_stub_sms WebMock.stub_request(:post, "https://api.short.io/links") .with( headers: { "Accept" => "application/json", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Authorization" => "1337", "Content-Type" => "application/json", "User-Agent" => "Ruby" } ) .to_return(status: 200, body: "{\"shortURL\":\"https://42ni.short.gy/jzTwdF\"}", headers: {}) end def short_io_error_stub WebMock.stub_request(:post, "https://api.short.io/links") .with( body: {originalURL: "www.badrequest.com", domain: "42ni.short.gy"}.to_json ) .to_return(status: 401, body: "{\"shortURL\":\"https://42ni.short.gy/jzTwdF\"}", headers: {}) end def short_io_stub_localhost(base_url = "http://localhost:3000/case_contacts/new") WebMock.stub_request(:post, "https://api.short.io/links") .with( body: {originalURL: base_url, domain: "42ni.short.gy"}.to_json, headers: { "Accept" => "application/json", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Authorization" => "1337", "Content-Type" => "application/json", "User-Agent" => "Ruby" } ) .to_return(status: 200, body: "{\"shortURL\":\"https://42ni.short.gy/jzTwdF\"}", headers: {}) end def short_io_court_report_due_date_stub(base_url = "http://localhost:3000/case_court_reports") WebMock.stub_request(:post, "https://api.short.io/links") .with( body: {originalURL: base_url, domain: "42ni.short.gy"}.to_json, headers: { "Accept" => "application/json", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Authorization" => "1337", "Content-Type" => "application/json", "User-Agent" => "Ruby" } ) .to_return(status: 200, body: "{\"shortURL\":\"https://42ni.short.gy/jzTwdF\"}", headers: {}) end end ================================================ FILE: spec/support/stubbed_requests/twilio_api.rb ================================================ module TwilioAPI def twilio_success_stub_messages_60_days WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno34/Messages.json").with( headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ).to_return( {body: "{\"body\":\"It's been 60 days or more since you've reached out to these members of your youth's network:\"}"}, {body: "{\"body\":\"• test\"}"}, {body: "{\"body\":\"If you have made contact with them in the past 60 days, remember to log it: https://42ni.short.gy/jzTwdF\"}"} ) end def twilio_success_stub WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno34/Messages.json") .with( body: {From: "+15555555555", Body: "Execute Order 66 - https://42ni.short.gy/jzTwdF", To: "+12222222222"}, headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ) .to_return(body: "{\"error_code\":null, \"status\":\"sent\", \"body\":\"Execute Order 66 - https://42ni.short.gy/jzTwdF\"}") end def twilio_activation_success_stub(resource = "") WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno34/Messages.json") .with( headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ) .to_return(body: "{\"error_code\":null, \"status\":\"sent\", \"body\":\"Execute Order 66 - https://42ni.short.gy/jzTwdF\"}") end def twilio_activation_error_stub(resource = "") WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno31/Messages.json") .with( headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ) .to_return(body: "{\"error_code\":\"42\", \"status\":\"failed\", \"body\":\"My tea's gone cold I wonder why\"}") end def twilio_court_report_due_date_stub(resource = "") court_due_date = Date.current + 7.days WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno34/Messages.json") .with( body: {"Body" => "Your court report is due on #{court_due_date}. Generate a court report to complete & submit here: https://42ni.short.gy/jzTwdF", "From" => "+15555555555", "To" => "+12223334444"}, headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ) .to_return(body: "{\"error_code\":null, \"status\":\"sent\", \"body\":\"Your court report is due on #{court_due_date}. Generate a court report to complete & submit here: https://42ni.short.gy/jzTwdF\"}") end def twilio_no_contact_made_stub(resource = "") WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno34/Messages.json") .with( body: {"Body" => "It's been two weeks since you've tried reaching 'test'. Try again! https://42ni.short.gy/jzTwdF", "From" => "+15555555555", "To" => "+12222222222"}, headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ) .to_return(body: "{\"error_code\":null, \"status\":\"sent\", \"body\":\"It's been two weeks since you've tried reaching 'test'. Try again! https://42ni.short.gy/jzTwdF\"}") end def twilio_password_reset_stub(resource) WebMock.stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/articuno34/Messages.json") .with( body: {From: "+15555555555", Body: "Hi #{resource.display_name}, click here to reset your password: https://42ni.short.gy/jzTwdF", To: "+12222222222"}, headers: { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" } ) .to_return(body: "{\"error_code\":null, \"status\":\"sent\", \"body\":\"Execute Order 66 - https://42ni.short.gy/jzTwdF\"}") end end ================================================ FILE: spec/support/stubbed_requests/webmock_helper.rb ================================================ require "support/stubbed_requests/short_io_api" require "support/stubbed_requests/twilio_api" class WebMockHelper extend ShortIOAPI extend TwilioAPI end ================================================ FILE: spec/support/twilio_helper.rb ================================================ module TwilioHelper def stub_twilio twilio_client = instance_double(Twilio::REST::Client) messages = instance_double(Twilio::REST::Api::V2010::AccountContext::MessageList) allow(Twilio::REST::Client).to receive(:new).with("Aladdin", "open sesame", "articuno34").and_return(twilio_client) allow(twilio_client).to receive(:messages).and_return(messages) allow(messages).to receive(:list).and_return([]) end end ================================================ FILE: spec/support/user_input_helpers.rb ================================================ class UserInputHelpers DANGEROUS_STRINGS = [ "مرحبا بالعالم هذا اسم من ترجمة جوجل", "שלום עולם זה שם מגוגל תרגם", '"1\'; DROP TABLE users-- 1"', '<', "Dr. Jane Smith, MS; MST; Esq." ].freeze end ================================================ FILE: spec/swagger_helper.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated # NOTE: If you're using the rswag-api to serve API descriptions, you'll need # to ensure that it's configured to serve Swagger from the same folder config.openapi_root = Rails.root.join("swagger").to_s # Define one or more Swagger documents and provide global metadata for each one # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will # be generated at the provided relative path under openapi_root # By default, the operations defined in spec files are added to the first # document below. You can override this behavior by adding a swagger_doc tag to the # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' config.openapi_specs = { "v1/swagger.yaml" => { openapi: "3.0.1", info: { title: "API V1", version: "v1" }, components: { schemas: { login_success: { type: :object, properties: { api_token: {type: :string}, refresh_token: {type: :string}, user: { id: {type: :integer}, display_name: {type: :string}, email: {type: :string}, token_expires_at: {type: :datetime}, refresh_token_expires_at: {type: :datetime} } } }, login_failure: { type: :object, properties: { message: {type: :string} } }, sign_out: { type: :object, properties: { message: {type: :string} } } } }, paths: {}, servers: [ { url: "https://{defaultHost}", variables: { defaultHost: { default: "www.example.com" } } } ] } } # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. # The swagger_docs configuration option has the filename including format in # the key, this may want to be changed to avoid putting yaml in json files. # Defaults to json. Accepts ':json' and ':yaml'. config.openapi_format = :yaml end ================================================ FILE: spec/system/all_casa_admins/all_casa_admin_spec.rb ================================================ require "rails_helper" RSpec.describe "all_casa_admins/casa_orgs/casa_admins/new", type: :system do it "requires login" do visit new_all_casa_admins_casa_org_path expect(page).to have_content "You need to sign in before continuing." expect(page).to have_current_path "/all_casa_admins/sign_in", ignore_query: true visit all_casa_admins_casa_org_path(id: create(:casa_org).id) expect(page).to have_current_path "/all_casa_admins/sign_in", ignore_query: true expect(page).to have_content "You need to sign in before continuing." end it "does not allow logged in non all casa admin" do casa_admin = create(:casa_admin) sign_in casa_admin visit new_all_casa_admins_casa_org_path expect(page).to have_current_path "/all_casa_admins/sign_in", ignore_query: true expect(page).to have_text "You need to sign in before continuing." visit "/" expect(page).to have_current_path "/supervisors", ignore_query: true expect(page).to have_text "Sign Out" end it "login and create new CasaOrg and new CasaAdmin for CasaOrg" do all_casa_admin = create(:all_casa_admin) sign_in all_casa_admin visit "/" expect(page).to have_text "All CASA Admin" expect(page).to have_text "New CASA Organization" expect(page).to have_text "New All CASA Admin" expect(page).to have_text "CASA Organizations" # left sidebar expect(page).to have_text "Patch Notes" expect(page).to have_text "Edit Profile" expect(page).to have_text "Feature Flags" expect(page).to have_text "Log Out" # footer expect(page).to have_link("Ruby For Good", href: "https://rubyforgood.org/") expect(page).to have_link("Report a site issue", href: "https://form.typeform.com/to/iXY4BubB") expect(page).to have_link("SMS Terms & Conditions", href: "/sms-terms-conditions.html") # create new org click_on "New CASA Organization" expect(page).to have_current_path "/all_casa_admins/casa_orgs/new", ignore_query: true expect(page).to have_text "Create a new CASA Organization" fill_in "Name", with: "Cool Org Name" fill_in "Display name", with: "display name" fill_in "Address", with: "123 Main St" click_on "Create CASA Organization" expect(page).to have_text "CASA Organization was successfully created." expect(page).to have_text "Cool Org Name" expect(page).to have_current_path(%r{/all_casa_admins/casa_orgs/\d+}, ignore_query: true) expect(page).to have_content "Administrators" expect(page).to have_content "Details" expect(page).to have_content "Number of admins: 0" expect(page).to have_content "Number of supervisors: 0" expect(page).to have_content "Number of active volunteers: 0" expect(page).to have_content "Number of inactive volunteers: 0" expect(page).to have_content "Number of active cases: 0" expect(page).to have_content "Number of inactive cases: 0" expect(page).to have_content "Number of all case contacts including inactives: 0" expect(page).to have_content "Number of active supervisor to volunteer assignments: 0" expect(page).to have_content "Number of active case assignments: 0" # create new admin click_on "New CASA Admin" expect(page).to have_content "New CASA Admin for Cool Org Name" click_button "Submit" expect(page).to have_content "2 errors prohibited this Casa admin from being saved:" expect(page).to have_content "Email can't be blank" expect(page).to have_content "Display name can't be blank" fill_in "Email", with: "invalid email" fill_in "Display name", with: "Freddy" click_button "Submit" expect(page).to have_content "1 error prohibited this Casa admin from being saved:" expect(page).to have_content "Email is invalid" fill_in "Email", with: "valid@example.com" fill_in "Display name", with: "Freddy Valid" click_button "Submit" expect(page).to have_content "New admin created successfully" expect(page).to have_content "valid@example.com" click_on "New CASA Admin" fill_in "Email", with: "valid@example.com" fill_in "Display name", with: "Freddy Valid" click_button "Submit" expect(page).to have_content "Email has already been taken" expect(CasaAdmin.find_by(email: "valid@example.com").invitation_created_at).not_to be_nil end it "edits all casa admins" do admin = create(:all_casa_admin) other_admin = create(:all_casa_admin) sign_in admin visit edit_all_casa_admins_path # validate email uniqueness fill_in "all_casa_admin_email", with: other_admin.email click_on "Update Profile" expect(page).to have_text "already been taken" # update email fill_in "all_casa_admin_email", with: "newemail@example.com" click_on "Update Profile" expect(page).to have_text "successfully updated" expect(ActionMailer::Base.deliveries.last.body.encoded).to match(">Your CASA account's email has been updated to newemail@example.com") # change password click_on "Change Password" fill_in "all_casa_admin_password", with: "newpassword" fill_in "all_casa_admin_password_confirmation", with: "badmatch" click_on "Update Password" expect(page).to have_text "confirmation doesn't match" click_on "Change Password" fill_in "all_casa_admin_password", with: "newpassword" fill_in "all_casa_admin_password_confirmation", with: "newpassword" click_on "Update Password" expect(page).to have_text "Password was successfully updated." expect(ActionMailer::Base.deliveries.last.body.encoded).to match("Your CASA password has been changed.") end it "admin invitations expire" do all_casa_admin = AllCasaAdmin.invite!(email: "valid@email.com") raw_token = all_casa_admin.raw_invitation_token # Invitation is valid within 1 week travel 2.days visit accept_all_casa_admin_invitation_path(invitation_token: raw_token) expect(page).to have_text "Set my password" # Invitation expires after 1 week travel 8.days visit accept_all_casa_admin_invitation_path(invitation_token: raw_token) expect(page).to have_text "The invitation token provided is not valid!" end end ================================================ FILE: spec/system/all_casa_admins/edit_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "AllCasaAdmin edit page", type: :system do let(:admin) { create(:all_casa_admin) } before do sign_in admin visit edit_all_casa_admins_path end it "shows the password section only after clicking 'Change Password'", :aggregate_failures, :js do expect_password_section_hidden # Click the Change Password button click_button "Change Password" # Password section should now be visible expect_password_section_visible end private def expect_password_section_hidden expect(page).to have_selector("#collapseOne.collapse:not(.show)", visible: :all) end def expect_password_section_visible expect(page).to have_selector("#collapseOne.collapse.show") expect(page).to have_field("all_casa_admin[password]") expect(page).to have_field("all_casa_admin[password_confirmation]") expect(page).to have_button("Update Password") end end ================================================ FILE: spec/system/all_casa_admins/password_change_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "AllCasaAdmin password change", type: :system do let(:admin) { create(:all_casa_admin, email: "all_casa_admin1@example.com", password: "12345678") } before do sign_in admin visit edit_all_casa_admins_path click_button "Change Password" end it "shows error when password fields are blank", :aggregate_failures, :js do click_button "Update Password" expect(page).to have_selector("#error_explanation.alert") expect(page).to have_text("Password can't be blank") end it "shows error when password confirmation doesn't match", :aggregate_failures, :js do fill_in "all_casa_admin[password]", with: "newpassword" fill_in "all_casa_admin[password_confirmation]", with: "wrongconfirmation" click_button "Update Password" expect(page).to have_selector("#error_explanation.alert") expect(page).to have_text("Password confirmation doesn't match Password") end it "shows success flash when password is updated", :aggregate_failures, :js do fill_in "all_casa_admin[password]", with: "newpassword" fill_in "all_casa_admin[password_confirmation]", with: "newpassword" click_button "Update Password" expect(page).to have_selector(".header-flash") expect(page).to have_text("Password was successfully updated.") end end ================================================ FILE: spec/system/all_casa_admins/patch_notes/index_spec.rb ================================================ require "rails_helper" RSpec.describe "all_casa_admins/patch_notes/index", type: :system do context "the new patch note form" do let(:all_casa_admin) { create(:all_casa_admin) } context "when the new patch note form's textarea is blank" do it "displays a warning after trying to create", :js do sign_in all_casa_admin visit all_casa_admins_patch_notes_path within "#new-patch-note" do click_on "Create" end expect(page).to have_selector(".warning-notification", text: "Cannot save an empty patch note") end end context "when the patch note form is filled out" do let(:patch_note_text) { "/6cg0lad1P/NFtV" } let!(:patch_note_group) { create(:patch_note_group, :all_users) } let!(:patch_note_type) { create(:patch_note_type, name: "5[1ht=d\\%*^qRON") } it "displays a the new patch note text on the page", :js do sign_in all_casa_admin visit all_casa_admins_patch_notes_path expect(page).to have_text("Patch Notes") within "#new-patch-note" do text_area = first(:css, "textarea").native text_area.send_keys(patch_note_text) click_on "Create" end # wait_for_ajax # Failure/Error: window_handles.slice(1..).each { |win| close_window(win) } # # NoMethodError: # undefined method `slice' for nil:NilClass expect(page).to have_text("Patch Notes") expect(page.find(".patch-note-list-item.new textarea")&.value).to eq(patch_note_text) end end end end ================================================ FILE: spec/system/all_casa_admins/sessions/new_spec.rb ================================================ require "rails_helper" RSpec.describe "all_casa_admins/sessions/new", type: :system do let(:all_casa_admin) { create(:all_casa_admin) } let(:volunteer) { build_stubbed(:volunteer) } context "when authenticated user" do before { sign_in all_casa_admin } it "renders AllCasaAdmin dashboard page" do visit "/" expect(page).to have_text "All CASA Admin" end it "allows sign out" do visit "/" find("#all-casa-log-out").click expect(page).not_to have_text "sign in before continuing" expect(page).to have_text "Signed out successfully" expect(page).to have_text "All CASA Log In" end it "allows access to flipper" do visit "/flipper" expect(page).to have_text "Flipper" end end context "when unauthenticated" do it "shows sign in page" do visit "/all_casa_admins/sign_in" expect(page).to have_text "All CASA Log In" end it "allows sign in" do visit "/all_casa_admins/sign_in" fill_in "Email", with: all_casa_admin.email fill_in "Password", with: "12345678" within ".actions" do click_on "Log in" end expect(page).to have_text "All CASA Admin" end it "prevents User sign in" do visit "/all_casa_admins/sign_in" fill_in "Email", with: volunteer.email fill_in "Password", with: "12345678" within ".actions" do click_on "Log in" end expect(page).to have_text(/invalid email or password/i) end it "denies access to flipper" do original = Rails.application.env_config["action_dispatch.show_exceptions"] Rails.application.env_config["action_dispatch.show_exceptions"] = :rescuable visit "/flipper" expect(page).to have_text "No route matches [GET] \"/flipper\"" ensure Rails.application.env_config["action_dispatch.show_exceptions"] = original end end end ================================================ FILE: spec/system/application/timeout_warning_spec.rb ================================================ require "rails_helper" RSpec.describe "/*", type: :system do context "when user is signed in" do let(:user) { create(:volunteer) } before do sign_in user end it "renders the seconds before logout as a javascript variable" do visit "/" parsed_page = Nokogiri::HTML(page.html) expect(parsed_page.at("script").text.strip).to include(user.timeout_in.in_seconds.to_s) end end end ================================================ FILE: spec/system/banners/dismiss_spec.rb ================================================ require "rails_helper" RSpec.describe "banners/dismiss", :js, type: :system do let!(:casa_org) { create(:casa_org) } let!(:active_banner) { create(:banner, casa_org: casa_org) } let(:volunteer) { create(:volunteer, casa_org: casa_org) } context "when user dismisses a banner" do it "hides banner" do sign_in volunteer visit root_path expect(page).to have_text("Please fill out this survey") click_on "Dismiss" expect(page).not_to have_text("Please fill out this survey") end end end ================================================ FILE: spec/system/banners/new_spec.rb ================================================ require "rails_helper" RSpec.describe "Banners", :js, type: :system do include ActionText::SystemTestHelper let(:admin) { create(:casa_admin) } let(:organization) { admin.casa_org } it "adds a banner" do sign_in admin visit banners_path click_on "New Banner" fill_in "Name", with: "Volunteer Survey Announcement" check "Active?" fill_in_rich_text_area "banner_content", with: "Please fill out this survey." click_on "Submit" visit banners_path expect(page).to have_text("Volunteer Survey Announcement") within "#banners" do click_on "Edit", match: :first end fill_in "Name", with: "Better Volunteer Survey Announcement" click_on "Submit" expect(page).to have_text("Better Volunteer Survey Announcement") visit root_path expect(page).to have_text("Please fill out this survey.") end it "lets you create banner with expiration time and edit it" do sign_in admin visit banners_path click_on "New Banner" fill_in "Name", with: "Expiring Announcement" check "Active?" find("#banner_expires_at").execute_script("this.value = arguments[0]", 7.days.from_now.strftime("%Y-%m-%dT%H:%M")) fill_in_rich_text_area "banner_content", with: "Please fill out this survey." click_on "Submit" expect(page).to have_text("Expiring Announcement Yes in 7 days") within "#banners" do click_on "Edit", match: :first end find("#banner_expires_at").execute_script("this.value = arguments[0]", 3.days.from_now.strftime("%Y-%m-%dT%H:%M")) click_on "Submit" expect(page).to have_text("Expiring Announcement Yes in 3 days") visit root_path expect(page).to have_text("Please fill out this survey.") end it "does not allow creation of banner with an expiration time set in the past" do sign_in admin visit banners_path click_on "New Banner" fill_in "Name", with: "Announcement not created" find("#banner_expires_at").execute_script("this.value = arguments[0]", 1.week.ago.strftime("%Y-%m-%dT%H:%M")) fill_in_rich_text_area "banner_content", with: "Please fill out this survey." click_on "Submit" visit banners_path expect(page).not_to have_text("Announcement not created") end describe "when an organization has an active banner" do let(:admin) { create(:casa_admin) } let(:organization) { create(:casa_org) } let(:active_banner) { create(:banner, casa_org: organization) } context "when a banner is submitted as active" do it "deactivates and replaces the current active banner" do active_banner sign_in admin visit banners_path expect(page).to have_text(active_banner.content.body.to_plain_text) click_on "New Banner" fill_in "Name", with: "New active banner name" check "Active?" fill_in_rich_text_area "banner_content", with: "New active banner content." click_on "Submit" # We're still on the banners page expect(page).to have_current_path(banners_path) within("table#banners") do already_existing_banner_row = find("tr", text: active_banner.name) expect(already_existing_banner_row).to have_selector("td.min-width", text: "No") end expect(page).to have_text("New active banner content.") end end context "when a banner is submitted as inactive" do it "does not deactivate the current active banner" do active_banner sign_in admin visit banners_path expect(page).to have_text(active_banner.content.body.to_plain_text) click_on "New Banner" fill_in "Name", with: "New active banner name" fill_in_rich_text_area "banner_content", with: "New active banner content." click_on "Submit" visit banners_path within("table#banners") do already_existing_banner_row = find("tr", text: active_banner.name) expect(already_existing_banner_row).to have_selector("td.min-width", text: "Yes") end expect(page).to have_text(active_banner.content.body.to_plain_text) end end end end ================================================ FILE: spec/system/bulk_court_dates/new_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "bulk_court_dates/new", type: :system do let(:now) { Date.new(2021, 1, 1) } let(:casa_org) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let!(:casa_case) { create(:casa_case, casa_org: casa_org) } let!(:court_date) { create(:court_date, :with_court_details, casa_case: casa_case, date: now - 1.week) } let!(:judge) { create(:judge) } let!(:hearing_type) { create(:hearing_type) } let(:court_order_text) { Faker::Lorem.paragraph(sentence_count: 2) } it "is successful", :js do case_group = build(:case_group, casa_org: casa_org) case_group.case_group_memberships.first.casa_case = casa_case case_group.save! travel_to now sign_in admin visit casa_cases_path click_on "New Bulk Court Date" select case_group.name, from: "Case Group" fill_in "court_date_date", with: :now fill_in "court_date_court_report_due_date", with: :now select judge.name, from: "Judge" select hearing_type.name, from: "Hearing type" click_on "Add a court order" text_area = first(:css, "textarea").native text_area.send_keys(court_order_text) page.find("select.implementation-status").find(:option, text: "Partially implemented").select_option within ".top-page-actions" do click_on "Create" end visit casa_case_path(casa_case) expect(page).to have_content(hearing_type.name) expect(page).to have_content(court_order_text) travel_back end end ================================================ FILE: spec/system/casa_admins/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_admins/edit", type: :system do let(:admin) { create :casa_admin, monthly_learning_hours_report: false } before { sign_in admin } context "with valid data" do it "can successfully edit user display name and phone number" do expected_display_name = "Root Admin" expected_phone_number = "+14398761234" expected_date_of_birth = "1997-04-16" visit edit_casa_admin_path(admin) fill_in "Display name", with: expected_display_name fill_in "Phone number", with: expected_phone_number fill_in "Date of birth", with: expected_date_of_birth check "Receive Monthly Learning Hours Report" click_on "Submit" expect(page).to have_text "Casa Admin was successfully updated." expect(page).to have_field "Display name", with: expected_display_name expect(page).to have_field "Phone number", with: expected_phone_number expect(page).to have_field "Date of birth", with: expected_date_of_birth expect(page).to have_checked_field("Receive Monthly Learning Hours Report") end end context "with valid email data" do before do visit edit_casa_admin_path(admin) @old_email = admin.email fill_in "Email", with: "new_admin_email@example.com" click_on "Submit" admin.reload end it "sends a confirmation email upon submission and does not change the user's displayed email" do expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") expect(page).to have_text "Admin was successfully updated. Confirmation Email Sent." expect(page).to have_field("Email", with: @old_email) admin.reload expect(admin.unconfirmed_email).to eq("new_admin_email@example.com") end it "succesfully updates the user email once the user confirms the changes" do admin.confirm visit edit_casa_admin_path(admin) expect(page).to have_field("Email", with: "new_admin_email@example.com") admin.reload expect(admin.old_emails).to eq([@old_email]) end end context "with invalid data" do let(:role) { "admin" } before do visit edit_casa_admin_path(admin) fill_in "Email", with: "newemail@example.com" fill_in "Display name", with: "Kaedehara Kazuha" end it_behaves_like "shows error for invalid phone numbers" it "shows error message for invalid date" do fill_in "Date of birth", with: 8.days.from_now.strftime("%Y/%m/%d") click_on "Submit" expect(page).to have_text "Date of birth must be in the past." end it "shows error message for empty email" do fill_in "Email", with: "" fill_in "Display name", with: "" click_on "Submit" expect(page).to have_text "Email can't be blank" expect(page).to have_text "Display name can't be blank" end end it "can successfully deactivate", :js do another = create(:casa_admin) visit edit_casa_admin_path(another) dismiss_confirm do click_on "Deactivate" end expect(page).not_to have_text("Admin was deactivated.") accept_confirm do click_on "Deactivate" end expect(page).to have_text("Admin was deactivated.") expect(another.reload.active).to be_falsey end it "can resend invitation to a another admin", :js do another = create(:casa_admin) visit edit_casa_admin_path(another) click_on "Resend Invitation" expect(page).to have_content("Invitation sent") end it "can convert the admin to a supervisor", :js do visit edit_casa_admin_path(admin) click_on "Change to Supervisor" expect(page).to have_text("Admin was changed to Supervisor.") expect(User.find(admin.id)).to be_supervisor end it "is not able to edit last sign in" do visit edit_casa_admin_path(admin) expect(page).to have_text "Added to system " expect(page).to have_text "Invitation email sent never" expect(page).to have_text "Last logged in" expect(page).to have_text "Invitation accepted never" expect(page).to have_text "Password reset last sent never" end end ================================================ FILE: spec/system/casa_admins/index_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_admins/index", type: :system do let(:organization) { build(:casa_org) } let(:admin) { build(:casa_admin, casa_org: organization) } it "displays other admins within the same CASA organization" do admin2 = create(:casa_admin, email: "Jon@org.com", casa_org: organization) admin3 = create(:casa_admin, email: "Bon@org.com", casa_org: organization) different_org_admin = build_stubbed(:casa_admin, email: "Jovi@something.else", casa_org: build_stubbed(:casa_org)) supervisor = build(:supervisor, email: "super@visor.com", casa_org: organization) volunteer = build(:volunteer, email: "volun@tear.com", casa_org: organization) sign_in admin visit casa_admins_path within "#admins" do expect(page).to have_content(admin2.email) expect(page).to have_content(admin3.email) expect(page).to have_no_content(different_org_admin.email) expect(page).to have_no_content(supervisor.email) expect(page).to have_no_content(volunteer.email) end end it "displays a deactivated tag for inactive admins" do inactive_admin = create(:casa_admin, :inactive, casa_org: organization) sign_in admin visit casa_admins_path within "#admins" do expect(page).to have_content(inactive_admin.email) expect(page).to have_content("Deactivated") end end end ================================================ FILE: spec/system/casa_admins/new_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_admins/new", type: :system do let(:admin) { create :casa_admin } it "validates and creates new admin" do visit casa_admins_path expect(page).to have_content "You need to sign in before continuing." sign_in admin visit casa_admins_path click_on "New Admin" expect(page).to have_content "Create New Casa Admin" click_button "Submit" expect(page).to have_content "2 errors prohibited this Casa admin from being saved:" expect(page).to have_content "Email can't be blank" expect(page).to have_content "Display name can't be blank" fill_in "Email", with: "invalid email" fill_in "Display name", with: "Freddy" click_button "Submit" expect(page).to have_content "1 error prohibited this Casa admin from being saved:" expect(page).to have_content "Email is invalid" fill_in "Email", with: "valid@example.com" fill_in "Display name", with: "Freddy Valid" click_button "Submit" expect(page).to have_content "New admin created successfully" expect(page).to have_content "valid@example.com" last_email = ActionMailer::Base.deliveries.last expect(last_email.to).to eq ["valid@example.com"] expect(last_email.subject).to have_text "CASA Console invitation instructions" expect(last_email.html_part.body.encoded).to have_text "your new CasaAdmin account." click_on "New Admin" fill_in "Email", with: "valid@example.com" fill_in "Display name", with: "Freddy Valid" click_button "Submit" expect(page).to have_content "Email has already been taken" expect(CasaAdmin.find_by(email: "valid@example.com").invitation_created_at).not_to be_nil end end ================================================ FILE: spec/system/casa_cases/additional_index_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/index", type: :system do # TODO combine with other casa cases index system spec let(:user) { build_stubbed :casa_admin } let(:organization) { create(:casa_org) } let(:volunteer) { build :volunteer, casa_org: organization } let(:admin) { create(:casa_admin, casa_org: organization) } context "logged in as admin" do before do sign_in admin visit casa_cases_path end it "has content" do expect(page).to have_text("Cases") expect(page).to have_link("New Case", href: new_casa_case_path) expect(page).to have_selector("button", text: "Casa Case Prefix") expect(page).to have_selector("th", text: "Case Number") expect(page).to have_selector("th", text: "Hearing Type") expect(page).to have_selector("th", text: "Judge") expect(page).to have_selector("th", text: "Status") expect(page).to have_selector("th", text: "Transition Aged Youth") expect(page).to have_selector("th", text: "Assigned To") end it "filters active/inactive", :js do active_case = build(:casa_case, active: true, casa_org: organization) active_case1 = build(:casa_case, active: true, casa_org: organization) inactive_case = build(:casa_case, active: false, casa_org: organization) create(:case_assignment, volunteer: volunteer, casa_case: active_case) create(:case_assignment, volunteer: volunteer, casa_case: active_case1) create(:case_assignment, volunteer: volunteer, casa_case: inactive_case) visit casa_cases_path expect(page).to have_selector(".casa-case-filters") # by default, only active casa cases are shown expect(page.all("table#casa-cases tbody tr").count).to eq [active_case, active_case1].size click_on "Status" find(:css, 'input[data-value="Active"]').click expect(page).to have_text("No matching records found") find(:css, 'input[data-value="Inactive"]').click expect(page.all("table#casa-cases tbody tr").count).to eq [inactive_case].size end it "Only displays cases belonging to user's org" do org_cases = create_list :casa_case, 3, active: true, casa_org: organization new_org = create :casa_org other_org_cases = create_list :casa_case, 3, active: true, casa_org: new_org visit casa_cases_path org_cases.each { |casa_case| expect(page).to have_content casa_case.case_number } other_org_cases.each { |casa_case| expect(page).not_to have_content casa_case.case_number } end end context "logged in as volunteer" do before do sign_in volunteer visit casa_cases_path end it "hides filters" do expect(page).not_to have_text("Assigned to Volunteer") expect(page).not_to have_text("Assigned to more than 1 Volunteer") expect(page).not_to have_text("Assigned to Transition Aged Youth") expect(page).not_to have_text("Casa Case Prefix") expect(page).not_to have_text("Select columns") expect(page).not_to have_selector(".casa-case-filters") end end end ================================================ FILE: spec/system/casa_cases/edit_spec.rb ================================================ require "rails_helper" require "stringio" RSpec.describe "Edit CASA Case", type: :system do context "logged in as admin" do let(:organization) { build(:casa_org) } let(:other_organization) { build(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:other_org_admin) { create(:casa_admin, casa_org: other_organization) } let(:casa_case) { create(:casa_case, :with_one_court_order, casa_org: organization) } let(:other_org_casa_case) { create(:casa_case, :with_one_court_order, casa_org: other_organization) } let(:contact_type_group) { create(:contact_type_group, casa_org: organization) } let(:other_org_contact_type_group) { create(:contact_type_group, casa_org: other_organization) } let!(:contact_type) { create(:contact_type, contact_type_group: contact_type_group) } let!(:another_contact_type) { create(:contact_type, contact_type_group: contact_type_group) } let!(:saved_case_contact_type) { create(:casa_case_contact_type, casa_case: casa_case, contact_type: contact_type) } let!(:other_org_contact_type) { create(:contact_type, contact_type_group: other_org_contact_type_group) } let!(:siblings_casa_cases) do create(:casa_case, :with_one_court_order, casa_org: organization) organization.casa_cases.excluding(casa_case) end let!(:other_org_siblings_casa_cases) do create(:casa_case, :with_one_court_order, casa_org: other_organization) other_organization.casa_cases.excluding(other_org_casa_case) end before { sign_in admin } it_behaves_like "shows court dates links" it "shows court orders" do visit edit_casa_case_path(casa_case) court_order = casa_case.case_court_orders.first expect(page).to have_text(court_order.text) expect(page).to have_text(court_order.implementation_status.humanize) end it "edits case", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" select "Submitted", from: "casa_case_court_report_status" find(".ts-control").click page.all(".ts-dropdown-content input") select_all_el = page.find("span[data-test=select-all-input]") # uncheck all contact type options select_all_el.click within ".ts-dropdown-content" do expect(page).not_to have_css(".form-check-input--checked") expect(page).to have_css(".form-check-input--unchecked", count: 3) end # check all contact type options select_all_el.click within ".ts-dropdown-content" do expect(page).not_to have_css("input.form-check-input--unchecked") expect(page).to have_css("input.form-check-input--checked", count: 3) end # unselect contact_type from dropdown find("span", text: contact_type.name).click page.find('button[data-action="court-order-form#add"]').click find("#court-orders-list-container").first("textarea").send_keys("Court Order Text One") within ".top-page-actions" do click_on "Update CASA Case" end expect(page).to have_text("Submitted") expect(page).to have_text("Court Date") expect(page).not_to have_text("Court Report Due Date") expect(page).not_to have_field("Court Report Due Date") expect(page).to have_text("Youth's Date in Care") expect(page).to have_text("Court Order Text One") expect(page).not_to have_text("Deactivate Case") expect(casa_case.contact_types).to eq [another_contact_type] has_checked_field? contact_type.name end it "does not display anything when not part of the organization", :js do visit casa_case_path(other_org_casa_case.id) expect(page).to have_text("Sorry, you are not authorized to perform this action.") end it "deactivates a case when part of the same organization", :js do visit edit_casa_case_path(casa_case) click_on "Deactivate CASA Case" click_on "Yes, deactivate" expect(page).to have_text("Case #{casa_case.case_number} has been deactivated") expect(page).to have_text("Case was deactivated on: #{I18n.l(casa_case.updated_at, format: :standard, default: nil)}") expect(page).to have_text("Reactivate CASA Case") expect(page).not_to have_text("Court Date") expect(page).not_to have_text("Court Report Due Date") expect(page).not_to have_field("Court Report Due Date") end it "does not allow an admin to deactivate a case if not in an organization" do visit edit_casa_case_path(other_org_casa_case) expect(page).to have_text("Sorry, you are not authorized to perform this action.") end it "reactivates a case", :js do visit edit_casa_case_path(casa_case) click_on "Deactivate CASA Case" click_on "Yes, deactivate" click_link("Reactivate CASA Case") expect(page).to have_text("Case #{casa_case.case_number} has been reactivated.") expect(page).to have_text("Deactivate CASA Case") expect(page).to have_text("Court Date") expect(page).not_to have_text("Court Report Due Date") expect(page).not_to have_field("Court Report Due Date") end context "when trying to assign a volunteer to a case" do it "is able to assign volunteers if in the same organization", :js do visit edit_casa_case_path(casa_case) expect(page).to have_content("Manage Volunteers") expect(page).to have_css("#volunteer-assignment") end it "errors if trying to assign volunteers for another organization" do visit edit_casa_case_path(other_org_casa_case) expect(page).to have_text("Sorry, you are not authorized to perform this action.") end end context "Copy all court orders from a case" do it "does not allow access to cases not within the organization" do visit edit_casa_case_path(other_org_casa_case) expect(page).to have_text("Sorry, you are not authorized to perform this action.") end it "copy button should be disabled when no case is selected", :js do visit edit_casa_case_path(casa_case) expect(page).to have_button("copy-court-button", disabled: true) end it "copy button should be enabled when a case is selected", :js do visit edit_casa_case_path(casa_case) select siblings_casa_cases.first.case_number, from: "casa_case_siblings_casa_cases" expect(page).to have_button("copy-court-button", disabled: false) end it "containses all case from organization except current case", :js do visit edit_casa_case_path(casa_case) within "#casa_case_siblings_casa_cases" do siblings_casa_cases.each do |scc| expect(page).to have_selector("option", text: scc.case_number) end expect(page).not_to have_selector("option", text: casa_case.case_number) end end it "copies all court orders from selected case", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" selected_case = siblings_casa_cases.first select selected_case.case_number, from: "casa_case_siblings_casa_cases" click_on "Copy" within ".swal2-popup" do expect(page).to have_text("Copy all orders from case ##{selected_case.case_number}?") click_on "Copy" end expect(page).to have_text("Court orders have been copied") casa_case.reload court_orders_text = casa_case.case_court_orders.map(&:text) court_orders_status = casa_case.case_court_orders.map(&:implementation_status) selected_case.case_court_orders.each do |orders| expect(court_orders_text).to include orders.text expect(court_orders_status).to include orders.implementation_status end end it "does not overwrite existing court orders", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" selected_case = siblings_casa_cases.first current_orders = casa_case.case_court_orders.each(&:dup) select selected_case.case_number, from: "casa_case_siblings_casa_cases" click_on "Copy" within ".swal2-popup" do expect(page).to have_text("Copy all orders from case ##{selected_case.case_number}?") click_on "Copy" end expect(page).to have_text("Court orders have been copied") casa_case.reload current_orders.each do |orders| expect(casa_case.case_court_orders.map(&:text)).to include orders.text end expect(casa_case.case_court_orders.count).to be >= current_orders.count end it "does not move court orders from one case to another", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" selected_case = siblings_casa_cases.first select selected_case.case_number, from: "casa_case_siblings_casa_cases" click_on "Copy" within ".swal2-popup" do expect(page).to have_text("Copy all orders from case ##{selected_case.case_number}?") click_on "Copy" end expect(page).to have_text("Court orders have been copied") casa_case.reload expect(selected_case.case_court_orders.count).to be > 0 end end end context "logged in as supervisor" do let(:casa_org) { build(:casa_org) } let(:supervisor) { create(:supervisor, casa_org: casa_org) } let(:casa_case) { create(:casa_case, :with_one_court_order, casa_org: casa_org) } let!(:contact_type_group) { build(:contact_type_group, casa_org: casa_org) } let!(:contact_type_1) { create(:contact_type, name: "Youth", contact_type_group: contact_type_group) } let!(:contact_type_2) { build(:contact_type, name: "Supervisor", contact_type_group: contact_type_group) } let!(:next_year) { (Date.today.year + 1).to_s } before { sign_in supervisor } it_behaves_like "shows court dates links" it "edits case", :js do stub_twilio visit casa_case_path(casa_case) expect(page).to have_text("Court Report Status: Not submitted") visit edit_casa_case_path(casa_case) select "Submitted", from: "casa_case_court_report_status" scroll_to('button[data-action="court-order-form#add"]').click find("#court-orders-list-container").first("textarea").send_keys("Court Order Text One") select "Partially implemented", from: "casa_case[case_court_orders_attributes][0][implementation_status]" expect(page).to have_text("Set Implementation Status") find(".ts-control").click select_all_el = page.find("span[data-test=select-all-input]") # uncheck all contact type options select_all_el.click within ".ts-dropdown-content" do expect(page).not_to have_css(".form-check-input--checked") expect(page).to have_css(".form-check-input--unchecked", count: 2) end # check all contact type options select_all_el.click within ".ts-dropdown-content" do expect(page).not_to have_css("input.form-check-input--unchecked") expect(page).to have_css("input.form-check-input--checked", count: 2) end # since all contact type options checked, don't need to select one within ".top-page-actions" do click_on "Update CASA Case" end has_checked_field? "Youth" has_no_checked_field? "Supervisor" expect(page).to have_text("Court Date") expect(page).not_to have_text("Court Report Due Date") expect(page).not_to have_field("Court Report Due Date") expect(page).not_to have_field("Court Report Due Date", with: "#{next_year}-09-08") expect(page).to have_text("Youth's Date in Care") expect(page).to have_text("Court Order Text One") expect(page).to have_text("Partially implemented") visit casa_case_path(casa_case) expect(page).to have_text("Court Report Status: Submitted") expect(page).not_to have_text("8-SEP-#{next_year}") end it "views deactivated case" do casa_case.deactivate visit edit_casa_case_path(casa_case) expect(page).to have_text("Case was deactivated on: #{I18n.l(casa_case.updated_at, format: :standard, default: nil)}") expect(page).not_to have_text("Court Date") expect(page).not_to have_text("Court Report Due Date") expect(page).not_to have_text("Youth's Date in Care") expect(page).not_to have_text("Day") expect(page).not_to have_text("Month") expect(page).not_to have_text("Year") expect(page).not_to have_text("Reactivate Case") expect(page).not_to have_text("Update Casa Case") end it "shows court orders" do visit edit_casa_case_path(casa_case) court_order = casa_case.case_court_orders.first expect(page).to have_text(court_order.text) expect(page).to have_text(court_order.implementation_status.humanize) end describe "assign and unassign a volunteer to a case" do let(:organization) { build(:casa_org) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:supervisor1) { build(:supervisor, casa_org: organization) } let!(:volunteer) { create(:volunteer, supervisor: supervisor1, casa_org: organization) } def sign_in_and_assign_volunteer sign_in supervisor1 visit casa_case_path(casa_case.id) click_on "Edit Case Details" select volunteer.display_name, from: "case_assignment[volunteer_id]" click_on "Assign Volunteer" end before do travel_to Time.zone.local(2020, 8, 29, 4, 5, 6) end context "when a volunteer is assigned to a case" do it "marks the volunteer as assigned and shows the start date of the assignment", :js do sign_in_and_assign_volunteer expect(page).to have_content("Volunteer assigned to case") expect(casa_case.case_assignments.count).to eq 1 unassign_button = page.find("button.btn-outline-danger") expect(unassign_button.text).to eq "Unassign Volunteer" assign_badge = page.find("span.bg-success") expect(assign_badge.text).to eq "ASSIGNED" end it "shows an assignment start date and no assignment end date" do sign_in_and_assign_volunteer assignment_start = page.find("td[data-test=assignment-start]").text assignment_end = page.find("td[data-test=assignment-end]").text expect(assignment_start).to eq("August 29, 2020") expect(assignment_end).to be_empty end end context "when a volunteer is unassigned from a case" do it "marks the volunteer as unassigned and shows assignment start/end dates", :js do sign_in_and_assign_volunteer unassign_button = page.find("button.btn-outline-danger") expect(unassign_button.text).to eq "Unassign Volunteer" click_on "Unassign Volunteer" assign_badge = page.find("span.bg-danger") expect(assign_badge.text).to eq "UNASSIGNED" expected_start_and_end_date = "August 29, 2020" assignment_start = page.find("td[data-test=assignment-start]").text assignment_end = page.find("td[data-test=assignment-end]").text expect(assignment_start).to eq(expected_start_and_end_date) expect(assignment_end).to eq(expected_start_and_end_date) end end context "when supervisor other than volunteer's supervisor" do before { volunteer.update(supervisor: build(:supervisor)) } it "unassigns volunteer", :js do sign_in_and_assign_volunteer unassign_button = page.find("button.btn-outline-danger") expect(unassign_button.text).to eq "Unassign Volunteer" click_on "Unassign Volunteer" assign_badge = page.find("span.bg-danger") expect(assign_badge.text).to eq "UNASSIGNED" end end it "when can assign only active volunteer to a case" do create(:volunteer, casa_org: organization) build_stubbed(:volunteer, :inactive, casa_org: organization) sign_in_and_assign_volunteer expect(find("select[name='case_assignment[volunteer_id]']").all("option").count { |option| option[:value].present? }).to eq 1 end end describe "case assigned to multiple volunteers" do let(:organization) { build(:casa_org) } let(:supervisor) { create(:casa_admin, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } let!(:volunteer_1) { create(:volunteer, display_name: "AAA", casa_org: organization) } let!(:volunteer_2) { create(:volunteer, display_name: "BBB", casa_org: organization) } it "supervisor assigns multiple volunteers to the same case" do sign_in supervisor visit edit_casa_case_path(casa_case.id) select volunteer_1.display_name, from: "Select a Volunteer" click_on "Assign Volunteer" expect(page).to have_text("Volunteer assigned to case") expect(page).to have_text(volunteer_1.display_name) # Attempt to assign a second volunteer without selecting one click_on "Assign Volunteer" expect(page).to have_text("Unable to assign volunteer to case: Volunteer must exist. Volunteer can't be blank.") select volunteer_2.display_name, from: "Select a Volunteer" click_on "Assign Volunteer" expect(page).to have_text("Volunteer assigned to case") expect(page).to have_text(volunteer_2.display_name) end end describe "form behavior" do it "displays 'Please select volunteer' in the dropdown" do sign_in supervisor visit edit_casa_case_path(casa_case.id) select_element = find("#case_assignment_casa_case_id") # Check if the default option exists and has the expected text expect(select_element).to have_selector("option[value='']", text: "Please Select Volunteer") end end context "deleting court orders", :js do let(:casa_case) { create(:casa_case, :with_one_court_order, :with_casa_case_contact_types) } let(:text) { casa_case.case_court_orders.first.text } it "can delete a court order" do visit edit_casa_case_path(casa_case.case_number.parameterize) expect(page).to have_text(text) find('button[data-action="click->court-order-form#remove"]').click expect(page).to have_text("Are you sure you want to remove this court order? Doing so will delete all records of it unless it was included in a previous court report.") find("button.swal2-confirm").click expect(page).not_to have_text(text) within ".actions-cc" do click_on "Update CASA Case" end expect(page).not_to have_text(text) end end context "a casa case with contact type" do let(:organization) { build(:casa_org) } let(:casa_case_with_contact_type) { create(:casa_case, :with_casa_case_contact_types, casa_org: organization) } it "has contact type checked" do contact_types = casa_case_with_contact_type.contact_types.map(&:id) visit edit_casa_case_path(casa_case_with_contact_type) all("input[type=checkbox][class~=case-contact-contact-type]").each do |checkbox| if contact_types.include? checkbox.value expect(checkbox).to be_checked else expect(checkbox).not_to be_checked end end end end context "when trying to assign a volunteer to a case" do it "is able to assign volunteers", :js do visit edit_casa_case_path(casa_case) expect(page).to have_content("Manage Volunteers") expect(page).to have_css("#volunteer-assignment") end end end context "logged in as volunteer" do let(:volunteer) { build(:volunteer) } let(:casa_case) { create(:casa_case, :with_one_court_order, casa_org: volunteer.casa_org) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } let!(:court_dates) do [10, 30, 31, 90].map { |n| create(:court_date, casa_case: casa_case, date: n.days.ago) } end let!(:reports) do [5, 11, 23, 44, 91].map do |n| path_to_template = "app/documents/templates/default_report_template.docx" args = { volunteer_id: volunteer.id, case_id: casa_case.id, path_to_template: path_to_template } context = CaseCourtReportContext.new(args).context report = CaseCourtReport.new(path_to_template: path_to_template, context: context) casa_case.court_reports.attach(io: StringIO.new(report.generate_to_string), filename: "report#{n}.docx") attached_report = casa_case.latest_court_report attached_report.created_at = n.days.ago attached_report.save! attached_report end end let!(:siblings_casa_cases) do organization = volunteer.casa_org casa_case2 = create(:casa_case, :with_one_court_order, casa_org: organization) create(:case_assignment, volunteer: volunteer, casa_case: casa_case2) organization.casa_cases.excluding(casa_case) end before { sign_in volunteer } it_behaves_like "shows court dates links" it "views attached court reports" do visit edit_casa_case_path(casa_case) # test court dates with reports get the correct ones [[0, 1], [2, 3], [3, 4]].each do |di, ri| expect(page).to have_link("(Attached Report)", href: rails_blob_path(reports[ri], disposition: "attachment")) expect(page).to have_link(I18n.l(court_dates[di].date, format: :full, default: nil)) end # and that the one with no report still gets one expect(page).to have_link(I18n.l(court_dates[1].date, format: :full, default: nil)) expect(page).to have_text(I18n.l(court_dates[1].date, format: :full, default: nil)) end it "shows court orders" do visit edit_casa_case_path(casa_case) court_order = casa_case.case_court_orders.first expect(page).to have_text(court_order.text) expect(page).to have_text(court_order.implementation_status.humanize) end it "edits case" do visit casa_case_path(casa_case) expect(page).to have_text("Court Report Status: Not submitted") visit edit_casa_case_path(casa_case) select "Submitted", from: "casa_case_court_report_status" within ".actions-cc" do click_on "Update CASA Case" end expect(page).not_to have_field("Court Report Due Date") expect(page).not_to have_text("Youth's Date in Care") expect(page).not_to have_text("Deactivate Case") expect(page).to have_css('button[data-action="court-order-form#add"]') visit casa_case_path(casa_case) expect(page).to have_text("Court Report Status: Submitted") end it "adds a standard court order", :js do visit edit_casa_case_path(casa_case) select("Family therapy", from: "Court Order Type") click_button("Add a court order") textarea = all("textarea.court-order-text-entry").last expect(textarea.value).to eq("Family therapy") end it "adds a custom court order", :js do visit edit_casa_case_path(casa_case) click_button("Add a court order") textarea = all("textarea.court-order-text-entry").last expect(textarea.value).to eq("") end context "Copy all court orders from a case" do it "copy button should be disabled when no case is selected", :js do visit edit_casa_case_path(casa_case) expect(page).to have_button("copy-court-button", disabled: true) end it "copy button should be enabled when a case is selected", :js do visit edit_casa_case_path(casa_case) select siblings_casa_cases.first.case_number, from: "casa_case_siblings_casa_cases" expect(page).to have_button("copy-court-button", disabled: false) end it "copy button and select shouldn't be visible when a volunteer only has one case", :js do volunteer = build(:volunteer) casa_case = create(:casa_case, :with_one_court_order, casa_org: volunteer.casa_org) create(:case_assignment, volunteer: volunteer, casa_case: casa_case) visit edit_casa_case_path(casa_case) expect(page).not_to have_button("copy-court-button") expect(page).not_to have_selector("casa_case_siblings_casa_cases") end it "containses all cases associated to current volunteer except current case", :js do visit edit_casa_case_path(casa_case) within "#casa_case_siblings_casa_cases" do siblings_casa_cases.each do |scc| expect(page).to have_selector("option", text: scc.case_number) end expect(page).not_to have_selector("option", text: casa_case.case_number) end end it "copies all court orders from selected case", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" selected_case = siblings_casa_cases.first select selected_case.case_number, from: "casa_case_siblings_casa_cases" click_on "Copy" within ".swal2-popup" do expect(page).to have_text("Copy all orders from case ##{selected_case.case_number}?") click_on "Copy" end expect(page).to have_text("Court orders have been copied") casa_case.reload court_orders_text = casa_case.case_court_orders.map(&:text) court_orders_status = casa_case.case_court_orders.map(&:implementation_status) selected_case.case_court_orders.each do |orders| expect(court_orders_text).to include orders.text expect(court_orders_status).to include orders.implementation_status end end it "does not overwrite existing court orders", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" selected_case = siblings_casa_cases.first current_orders = casa_case.case_court_orders.each(&:dup) select selected_case.case_number, from: "casa_case_siblings_casa_cases" click_on "Copy" within ".swal2-popup" do expect(page).to have_text("Copy all orders from case ##{selected_case.case_number}?") click_on "Copy" end expect(page).to have_text("Court orders have been copied") casa_case.reload current_orders.each do |orders| expect(casa_case.case_court_orders.map(&:text)).to include orders.text end expect(casa_case.case_court_orders.count).to be >= current_orders.count end it "does not move court orders from one case to another", :js do visit casa_case_path(casa_case.id) click_on "Edit Case Details" selected_case = siblings_casa_cases.first select selected_case.case_number, from: "casa_case_siblings_casa_cases" click_on "Copy" within ".swal2-popup" do expect(page).to have_text("Copy all orders from case ##{selected_case.case_number}?") click_on "Copy" end expect(page).to have_text("Court orders have been copied") casa_case.reload expect(selected_case.case_court_orders.count).to be > 0 end end end end ================================================ FILE: spec/system/casa_cases/emancipation/show_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/show", type: :system do let(:organization) { build(:casa_org) } let(:volunteer) { build(:volunteer, casa_org: organization) } let(:casa_case) { build(:casa_case, casa_org: organization) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } let!(:emancipation_category) { build(:emancipation_category, mutually_exclusive: true) } let!(:emancipation_option) { create(:emancipation_option, emancipation_category: emancipation_category) } let(:supervisor) { create(:supervisor, casa_org: organization) } before do sign_in user visit casa_case_emancipation_path(casa_case.id) end context "volunteer user", :js do let(:user) { volunteer } it "has a title" do expect(page).to have_content("Emancipation Checklist") expect(page).to have_content(emancipation_category.name) end it "opens through main input, selects an option, and unselects option through main input" do emancipation_category = page.find(".emancipation-category") find(".emacipation-category-input-label-pair").click expect(page).to have_content(emancipation_option.name) expect(emancipation_category["data-is-open"]).to match(/true/) find(".check-item").click find(".emacipation-category-input-label-pair").click expect(page).to have_css(".success-notification", text: "Unchecked #{emancipation_option.name}") expect(emancipation_category["data-is-open"]).to match(/true/) end it "shows and hides the options through collapse icon" do emancipation_category = page.find(".emancipation-category") find(".category-collapse-icon").click expect(emancipation_category["data-is-open"]).to match(/true/) find(".category-collapse-icon").click expect(emancipation_category["data-is-open"]).to match(/false/) end end end ================================================ FILE: spec/system/casa_cases/fund_requests/new_spec.rb ================================================ require "rails_helper" RSpec.describe "fund_requests/new", type: :system do let(:org) { create(:casa_org) } let(:volunteer) { create(:volunteer, :with_casa_cases, casa_org: org) } let(:casa_case) { volunteer.casa_cases.first } before do sign_in volunteer visit new_casa_case_fund_request_path(casa_case) end it "creates a fund request for the casa case" do aggregate_failures do expect(page).to have_field "Your email", with: volunteer.email expect(page).to have_field "Name or case number of youth", with: casa_case.case_number expect(page).to have_field "Requested by & relationship to youth", with: "#{volunteer.display_name} CASA Volunteer" end fill_in "Amount of payment", with: "100" fill_in "Deadline", with: "2022-12-31" fill_in "Request is for", with: "Fun outing" fill_in "Name of payee", with: "Minnie Mouse" fill_in "Other source of funding", with: "some other agency" fill_in "How will this funding positively impact", with: "provide support" fill_in "Please use this space", with: "foo bar" expect { click_on "Submit Fund Request" }.to change(FundRequest, :count).by(1) expect(page).to have_text "Fund Request was sent for case #{casa_case.case_number}" fr = FundRequest.last aggregate_failures do expect(fr.deadline).to eq "2022-12-31" expect(fr.extra_information).to eq "foo bar" expect(fr.impact).to eq "provide support" expect(fr.other_funding_source_sought).to eq "some other agency" expect(fr.payee_name).to eq "Minnie Mouse" expect(fr.payment_amount).to eq "100" expect(fr.request_purpose).to eq "Fun outing" expect(fr.requested_by_and_relationship).to eq "#{volunteer.display_name} CASA Volunteer" expect(fr.submitter_email).to eq volunteer.email expect(fr.youth_name).to eq casa_case.case_number end end it "shows error when submitter email is blank" do fill_in "Your email", with: "" fill_in "Amount of payment", with: "100" fill_in "Deadline", with: "2022-12-31" fill_in "Request is for", with: "Fun outing" fill_in "Name of payee", with: "Minnie Mouse" expect { click_on "Submit Fund Request" }.not_to change(FundRequest, :count) expect(page).to have_content("Submitter email can't be blank") end end ================================================ FILE: spec/system/casa_cases/index_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/index", type: :system do let(:organization) { create(:casa_org) } let(:volunteer) { create(:volunteer, display_name: "Bob Loblaw", casa_org: organization) } let(:case_number) { "CINA-1" } it "is filterable and linkable", :js do organization = build(:casa_org) admin = build(:casa_admin, casa_org: organization) volunteer = build(:volunteer, display_name: "Cool Volunteer", casa_org: organization) cina = build(:casa_case, active: true, casa_org: organization, case_number: case_number) tpr = create(:casa_case, active: true, casa_org: organization, case_number: "TPR-100") no_prefix = create(:casa_case, active: true, casa_org: organization, case_number: "123-12-123") create(:case_assignment, volunteer: volunteer, casa_case: cina) sign_in admin visit casa_cases_path expect(page).to have_link("Cool Volunteer", href: "/volunteers/#{volunteer.id}/edit") expect(page).to have_link("CINA-1", href: "/casa_cases/#{cina.case_number.parameterize}") expect(page).to have_link("TPR-100", href: "/casa_cases/#{tpr.case_number.parameterize}") expect(page).to have_link("123-12-123", href: "/casa_cases/#{no_prefix.case_number.parameterize}") click_on "Status" click_on "Assigned to Volunteer" click_on "Assigned to more than 1 Volunteer" click_on "Assigned to Transition Aged Youth" click_on "Casa Case Prefix" end it "has a usable dropdown in sidebar" do cina = build(:casa_case, active: true, casa_org: organization, case_number: case_number) create(:case_assignment, volunteer: volunteer, casa_case: cina) sign_in volunteer visit root_path click_on "My Cases" within "#ddmenu_my-cases" do click_on case_number end expect(page).to have_text("CASA Case Details") expect(page).to have_text("Case number: CINA-1") end end ================================================ FILE: spec/system/casa_cases/new_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/new", type: :system do context "when signed in as a Casa Org Admin" do context "when all fields are filled" do let(:casa_org) { build(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let(:contact_type_group) { create(:contact_type_group, casa_org: casa_org) } let!(:contact_type) { create(:contact_type, contact_type_group: contact_type_group) } let(:volunteer_display_name) { "Test User" } let!(:supervisor) { create(:supervisor, casa_org: casa_org) } let!(:volunteer) { create(:volunteer, display_name: volunteer_display_name, supervisor: supervisor, casa_org: casa_org) } it "is successful", :js do case_number = "12345" sign_in admin visit root_path click_on "Cases" click_on "New Case" travel_to Time.zone.local(2020, 12, 1) do court_date = 21.days.from_now fourteen_years = (Date.today.year - 14).to_s fill_in "Case number", with: case_number fill_in "Court Date", with: court_date select "March", from: "casa_case_birth_month_year_youth_2i" select fourteen_years, from: "casa_case_birth_month_year_youth_1i" select "Submitted", from: "casa_case_court_report_status" find(".ts-control").click select_all_el = page.find("span[data-test=select-all-input]") # uncheck all contact type options select_all_el.click within ".ts-dropdown-content" do expect(page).not_to have_css(".form-check-input--checked") expect(page).to have_css(".form-check-input--unchecked", count: 2) end # check all contact type options select_all_el.click within ".ts-dropdown-content" do expect(page).not_to have_css("input.form-check-input--unchecked") expect(page).to have_css("input.form-check-input--checked", count: 2) end select "Test User", from: "casa_case[case_assignments_attributes][0][volunteer_id]" within ".top-page-actions" do click_on "Create CASA Case" end expect(page).to have_content(case_number) expect(page).to have_content(I18n.l(court_date, format: :day_and_date)) expect(page).to have_content("CASA case was successfully created.") expect(page).not_to have_content("Court Report Due Date: Thursday, 1-APR-2021") # accurate for frozen time expect(page).to have_content("Transition Aged Youth: Yes") expect(page).to have_content(volunteer_display_name) end end end context "when non-mandatory fields are not filled" do it "is successful", :js do casa_org = build(:casa_org) admin = create(:casa_admin, casa_org: casa_org) contact_type_group = create(:contact_type_group, casa_org: casa_org) create(:contact_type, contact_type_group: contact_type_group) case_number = "12345" sign_in admin visit new_casa_case_path fill_in "Case number", with: case_number fill_in "Next Court Date", with: DateTime.now.next_month five_years = (Date.today.year - 5).to_s select "March", from: "casa_case_birth_month_year_youth_2i" select five_years, from: "casa_case_birth_month_year_youth_1i" within ".actions-cc" do click_on "Create CASA Case" end expect(page).to have_content(case_number) expect(page).to have_content("CASA case was successfully created.") expect(page).to have_content("Next Court Date: ") expect(page).not_to have_content("Court Report Due Date:") expect(page).to have_content("Transition Aged Youth: No") end end context "when the case number field is not filled" do it "does not create a new case" do casa_org = build(:casa_org) admin = create(:casa_admin, casa_org: casa_org) sign_in admin visit new_casa_case_path check "casa_case_empty_court_date" within ".actions-cc" do click_on "Create CASA Case" end expect(find("#casa_case_empty_court_date")).to be_checked expect(page).to have_current_path(casa_cases_path, ignore_query: true) expect(page).to have_content("Case number can't be blank") end end context "when the court date field is not filled" do context "when empty court date checkbox is checked" do it "creates a new case", :js do casa_org = build(:casa_org) admin = create(:casa_admin, casa_org: casa_org) contact_type_group = create(:contact_type_group, casa_org: casa_org) create(:contact_type, contact_type_group: contact_type_group) case_number = "12345" sign_in admin visit new_casa_case_path fill_in "Case number", with: case_number five_years = (Date.today.year - 5).to_s select "March", from: "casa_case_birth_month_year_youth_2i" select five_years, from: "casa_case_birth_month_year_youth_1i" check "casa_case_empty_court_date" within ".actions-cc" do click_on "Create CASA Case" end expect(page).to have_content(case_number) expect(page).to have_content("CASA case was successfully created.") expect(page).to have_content("Next Court Date:") expect(page).not_to have_content("Court Report Due Date:") expect(page).to have_content("Transition Aged Youth: No") end end context "when empty court date checkbox is not checked" do it "does not create a new case", :js do casa_org = build(:casa_org) admin = create(:casa_admin, casa_org: casa_org) contact_type_group = create(:contact_type_group, casa_org: casa_org) contact_type = create(:contact_type, contact_type_group: contact_type_group) case_number = "12345" sign_in admin visit new_casa_case_path fill_in "Case number", with: case_number five_years = (Date.today.year - 5).to_s select "March", from: "casa_case_birth_month_year_youth_2i" select five_years, from: "casa_case_birth_month_year_youth_1i" # 2/14/2025 - by default, all contact types are selected on page load so don't need to manually select within ".actions-cc" do click_on "Create CASA Case" end selected_contact_type = find(".ts-control .item").text expect(selected_contact_type).to eq(contact_type.name) expect(page).to have_current_path(casa_cases_path, ignore_query: true) expect(page).to have_content("Court dates date can't be blank") end end end context "when the case number already exists in the organization" do it "does not create a new case" do casa_org = build(:casa_org) admin = create(:casa_admin, casa_org: casa_org) case_number = "12345" _existing_casa_case = create(:casa_case, case_number: case_number, casa_org: casa_org) sign_in admin visit new_casa_case_path fill_in "Case number", with: case_number within ".actions-cc" do click_on "Create CASA Case" end expect(page).to have_current_path(casa_cases_path, ignore_query: true) expect(page).to have_content("Case number has already been taken") end end end end ================================================ FILE: spec/system/casa_cases/show_more_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/show", :js, type: :system do let(:user) { build_stubbed :casa_admin } let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:supervisor) { create(:supervisor, casa_org: organization) } let(:volunteer) { create :volunteer, display_name: "Andy Dwyer", casa_org: organization } let!(:case_assignment) { create(:case_assignment, casa_case: casa_case, volunteer: volunteer) } let(:casa_case) { create(:casa_case, casa_org: organization) } context "user is an admin" do it "redirects to edit volunteer page when volunteer name clicked" do sign_in admin visit casa_case_path(casa_case.id) expect(page).to have_text("Assigned Volunteers:\nAndy Dwyer") expect(page).to have_link("Andy Dwyer") click_on "Andy Dwyer" expect(page).to have_text("Editing Volunteer") end it "sends reminder to volunteer" do sign_in admin visit casa_case_path(casa_case.id) expect(page).to have_button("Send Reminder") expect(page).to have_text("Send CC to Supervisor and Admin") click_on "Send Reminder" expect(page).to have_current_path(edit_volunteer_path(volunteer)) expect(page).to have_text("Reminder sent to volunteer") end end context "user is a supervisor" do it "sends reminder to volunteer" do sign_in supervisor visit casa_case_path(casa_case.id) expect(page).to have_button("Send Reminder") expect(page).to have_text(/Send CC to Supervisor and Admin$/) click_on "Send Reminder" expect(page).to have_current_path(edit_volunteer_path(volunteer)) expect(page).to have_text("Reminder sent to volunteer") end end context "user is a volunteer" do it "does not render a link to edit volunteer page" do sign_in volunteer visit casa_case_path(casa_case.id) expect(page).to have_text("Assigned Volunteers:\nAndy Dwyer") expect(page).to have_no_link("Andy Dwyer", href: volunteer_path(volunteer.id)) end end end ================================================ FILE: spec/system/casa_cases/show_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/show", type: :system do include ActionView::Helpers::DateHelper let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:volunteer) { build(:volunteer, display_name: "Bob Loblaw", casa_org: organization) } let(:casa_case) { create(:casa_case, :with_one_court_order, casa_org: organization, case_number: "CINA-1", date_in_care: date_in_care) } let!(:court_date) { create(:court_date, court_report_due_date: 1.month.from_now) } let(:date_in_care) { 6.years.ago } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } let!(:case_contact) { create(:case_contact, creator: volunteer, casa_case: casa_case) } let!(:emancipation_categories) { create_list(:emancipation_category, 3) } let!(:future_court_date) { create(:court_date, date: 21.days.from_now, casa_case: casa_case) } before do sign_in user visit casa_case_path(casa_case.id) end shared_examples "shows emancipation checklist link" do context "when youth is in transition age" do it "sees link to emancipation" do expect(page).to have_link("Emancipation 0 / #{emancipation_categories.size}") end end context "when youth is not in transition age" do before do casa_case.update!(birth_month_year_youth: DateTime.current) visit casa_case_path(casa_case) end it "does not see a link to emancipation checklist" do expect(page).not_to have_link("Emancipation 0 / #{emancipation_categories.size}") end end end describe "Report Generation", :js do let(:modal_selector) { '[data-bs-target="#generate-docx-report-modal"]' } let(:user) { volunteer } before do travel_to Date.new(2021, 1, 1) sign_in user visit casa_case_path(casa_case.id) end context "when first arriving to 'Generate Court Report' page" do it "generation modal hidden" do expect(page).to have_selector "#btnGenerateReport", text: "Generate Report", visible: false expect(page).not_to have_selector ".select2" end end context "after opening 'Download Court Report' modal" do before do page.find(modal_selector).click end # putting all this in the same system test shaves 3 seconds off the test suite it "modal has correct contents" do start_date = page.find("#start_date").value expect(start_date).to eq("January 01, 2021") # default date end_date = page.find("#end_date").value expect(end_date).to eq("January 01, 2021") # default date expect(page).to have_selector "#btnGenerateReport", text: "Generate Report", visible: true expect(page).not_to have_selector ".select2" expect(page).to have_selector("#btnGenerateReport .lni-download", visible: true) expect(page).not_to have_selector("#btnGenerateReport[disabled]") expect(page).to have_selector("#spinner", visible: :hidden) within("#generate-docx-report-modal") do expect(page).to have_content(casa_case.case_number) # when choosing the prompt option (value is empty) and click on 'Generate Report' button, nothing should happen" # should have disabled generate button, download icon and no spinner click_button "Generate Report" end wait_for_download expect(download_file_name).to match(/#{casa_case.case_number}.docx/) end end end context "admin user" do let(:user) { admin } it_behaves_like "shows court dates links" it_behaves_like "shows emancipation checklist link" it "can see case creator in table" do expect(page).to have_text("Bob Loblaw") end it "can navigate to edit volunteer page" do expect(page).to have_link("Bob Loblaw", href: "/volunteers/#{volunteer.id}/edit") end it "sees link to profile page" do expect(page).to have_link(href: "/users/edit") end it "can see court orders" do expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content(casa_case.case_court_orders[0].implementation_status_symbol) end it "can see next court date", :js do expect(page).to have_content( "Next Court Date: #{I18n.l(future_court_date.date, format: :day_and_date)}" ) end it "can see the youth's Date In Care", :js do expect(page).to have_content( "Youth's Date in Care: #{I18n.l(date_in_care, format: :youth_date_of_birth)}" ) end it "can see the time since the youth's Date In Care", :js do expect(page).to have_content("#{time_ago_in_words(date_in_care)} ago") end it "can see Add to Calendar buttons", :js do expect(page).to have_content("Add to Calendar") end context "court report download link visibility" do it "does not show download link to admin when report status is not submitted" do fixture = Rails.root.join("spec/fixtures/files/sample_report.docx") casa_case.court_reports.attach( io: File.open(fixture), filename: "sample_report.docx", content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) casa_case.update!(court_report_status: :in_review) visit casa_case_path(casa_case.id) expect(page).not_to have_link("Click to download") end it "shows download link to admin when report status is submitted" do fixture = Rails.root.join("spec/fixtures/files/sample_report.docx") casa_case.court_reports.attach( io: File.open(fixture), filename: "sample_report.docx", content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) casa_case.update!(court_report_status: :submitted) visit casa_case_path(casa_case.id) expect(page).to have_link("Click to download") end end context "when there is no future court date or court report due date" do before do casa_case = create(:casa_case, casa_org: organization) visit casa_case_path(casa_case.id) end it "can not see Add to Calendar buttons", :js do expect(page).not_to have_content("Add to Calendar") end end context "when old case contacts are hidden" do it "displays all case contacts to admin", :js do casa_case = create(:casa_case, casa_org: organization) volunteer_1 = create(:volunteer, display_name: "Volunteer 1", casa_org: casa_case.casa_org) volunteer_2 = create(:volunteer, display_name: "Volunteer 2", casa_org: casa_case.casa_org) create(:case_assignment, casa_case: casa_case, volunteer: volunteer_1) create(:case_assignment, casa_case: casa_case, volunteer: volunteer_2, active: false, hide_old_contacts: true) create(:case_contact, contact_made: true, casa_case: casa_case, creator: volunteer_1, occurred_at: DateTime.now - 1) create(:case_contact, contact_made: true, casa_case: casa_case, creator: volunteer_2, occurred_at: DateTime.now - 1) visit casa_case_path(casa_case.id) expect(page).to have_css("#case_contacts_list .card-content", count: 2) end end end context "supervisor user" do let(:user) { create(:supervisor, casa_org: organization) } let!(:case_contact) { create(:case_contact, creator: user, casa_case: casa_case) } it_behaves_like "shows emancipation checklist link" it "sees link to own edit page" do expect(page).to have_link(href: "/supervisors/#{user.id}/edit") end context "case contact by another supervisor" do let(:other_supervisor) { create(:supervisor, casa_org: organization) } let!(:case_contact) { create(:case_contact, creator: other_supervisor, casa_case: casa_case) } it "sees link to other supervisor" do expect(page).to have_link(href: "/supervisors/#{other_supervisor.id}/edit") end end it "can see court orders" do expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content(casa_case.case_court_orders[0].implementation_status_symbol) end context "court report download link visibility" do it "does not show download link to supervisor when report status is not submitted" do fixture = Rails.root.join("spec/fixtures/files/sample_report.docx") casa_case.court_reports.attach( io: File.open(fixture), filename: "sample_report.docx", content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) casa_case.update!(court_report_status: :in_review) visit casa_case_path(casa_case.id) expect(page).not_to have_link("Click to download") end it "shows download link to supervisor when report status is submitted" do fixture = Rails.root.join("spec/fixtures/files/sample_report.docx") casa_case.court_reports.attach( io: File.open(fixture), filename: "sample_report.docx", content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) casa_case.update!(court_report_status: :submitted) visit casa_case_path(casa_case.id) expect(page).to have_link("Click to download") end end context "when old case contacts are hidden" do it "displays all case contacts to supervisor", :js do casa_case = create(:casa_case, casa_org: organization) volunteer_1 = create(:volunteer, display_name: "Volunteer 1", casa_org: casa_case.casa_org) volunteer_2 = create(:volunteer, display_name: "Volunteer 2", casa_org: casa_case.casa_org) create(:case_assignment, casa_case: casa_case, volunteer: volunteer_1) create(:case_assignment, casa_case: casa_case, volunteer: volunteer_2, active: false, hide_old_contacts: true) create(:case_contact, contact_made: true, casa_case: casa_case, creator: volunteer_1, occurred_at: DateTime.now - 1) create(:case_contact, contact_made: true, casa_case: casa_case, creator: volunteer_2, occurred_at: DateTime.now - 1) visit casa_case_path(casa_case.id) expect(page).to have_css("#case_contacts_list .card-content", count: 2) end end end context "volunteer user" do let(:user) { volunteer } it_behaves_like "shows emancipation checklist link" it "can see court orders" do expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content(casa_case.case_court_orders[0].implementation_status_symbol) end context "when old case contacts are hidden" do before do volunteer_2 = create(:volunteer, display_name: "Volunteer 2", casa_org: casa_case.casa_org) create(:case_assignment, casa_case: casa_case, volunteer: volunteer_2, active: false, hide_old_contacts: true) create(:case_contact, contact_made: true, casa_case: casa_case, creator: volunteer_2, occurred_at: DateTime.now - 1) end it "displays only visible cases to volunteer", :js do visit casa_case_path(casa_case.id) expect(page).to have_css("#case_contacts_list .card-content", count: 1) end end context "when old case contacts are displayed" do before do volunteer_2 = create(:volunteer, display_name: "Volunteer 2", casa_org: casa_case.casa_org) create(:case_assignment, casa_case: casa_case, volunteer: volunteer_2, active: false, hide_old_contacts: false) create(:case_contact, contact_made: true, casa_case: casa_case, creator: volunteer_2, occurred_at: DateTime.now - 1) end it "displays all cases to the volunteer" do visit casa_case_path(casa_case.id) expect(page).to have_css("#case_contacts_list .card-content", count: 2) end end end context "court order - implementation status symbol" do let(:user) { admin } it "when implemented" do casa_case.case_court_orders[0].update(implementation_status: :implemented) visit casa_case_path(casa_case) expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content("✅") end it "when not implemented" do casa_case.case_court_orders[0].update(implementation_status: :unimplemented) visit casa_case_path(casa_case) expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content("❌") end it "when partial implemented" do casa_case.case_court_orders[0].update(implementation_status: :partially_implemented) visit casa_case_path(casa_case) expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content("🕗") end it "when not specified" do casa_case.case_court_orders[0].update(implementation_status: nil) visit casa_case_path(casa_case) expect(page).to have_content("Court Orders") expect(page).to have_content(casa_case.case_court_orders[0].text) expect(page).to have_content("❌") end end end ================================================ FILE: spec/system/casa_org/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_org/edit", type: :system do it "can update show_driving_reimbursement flag" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) sign_in admin visit edit_casa_org_path(organization) uncheck "Show driving reimbursement" click_on "Submit" expect(page).not_to have_checked_field("Show driving reimbursement") check "Show driving reimbursement" click_on "Submit" expect(page).to have_checked_field("Show driving reimbursement") end it "can upload a logo image" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) stub_twilio sign_in admin visit edit_casa_org_path(organization) page.attach_file("Logo", file_fixture("company_logo.png"), visible: :visible) click_on "Submit" expect(page).to have_content("CASA organization was successfully updated.") end it "hides Twilio Form if twilio is not enabled", :js do organization = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_casa_org_path(organization) uncheck "Enable Twilio" expect(page).to have_selector("#casa_org_twilio_account_sid", visible: :hidden) expect(page).to have_selector("#casa_org_twilio_api_key_sid", visible: :hidden) expect(page).to have_selector("#casa_org_twilio_api_key_secret", visible: :hidden) expect(page).to have_selector("#casa_org_twilio_phone_number", visible: :hidden) end it "displays Twilio Form when Enable Twilio is checked" do organization = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_casa_org_path(organization) expect(page).to have_text("Enable Twilio") expect(page).to have_selector("#casa_org_twilio_account_sid", visible: :visible) expect(page).to have_selector("#casa_org_twilio_api_key_sid", visible: :visible) expect(page).to have_selector("#casa_org_twilio_api_key_secret", visible: :visible) expect(page).to have_selector("#casa_org_twilio_phone_number", visible: :visible) end it "requires Twilio Form to be filled in correctly", :js do organization = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_casa_org_path(organization) fill_in "Twilio Phone Number", with: "" click_on "Submit" expect(page).to have_css("#casa_org_twilio_phone_number:invalid") end end ================================================ FILE: spec/system/case_contacts/additional_expenses_spec.rb ================================================ require "rails_helper" RSpec.describe "CaseContact AdditionalExpenses Form", :flipper, type: :system do subject do visit new_case_contact_path(casa_case) fill_in_contact_details(contact_types: [contact_type.name]) fill_in_mileage want_reimbursement: true, miles: 50, address: "123 Params St" end let(:casa_org) { build :casa_org, :all_reimbursements_enabled } let(:volunteer) { create :volunteer, :with_single_case, casa_org: } let(:casa_case) { volunteer.casa_cases.first } let!(:contact_type) { create :contact_type, casa_org: } before do allow(Flipper).to receive(:enabled?).with(:show_additional_expenses).and_return(true) sign_in volunteer end it "is not rendered when casa org expenses disabled" do casa_org.update! additional_expenses_enabled: false subject check "Request travel or other reimbursement" expect(page).to have_no_field(class: "expense-amount-input", visible: :all) expect(page).to have_no_field(class: "expense-describe-input", visible: :all) expect(page).to have_no_button("Add Another Expense", visible: :all) end it "is not shown until Reimbursement is checked and Add Another Expense clicked", :js do sign_in volunteer visit new_case_contact_path casa_case fill_in_contact_details expect(page).to have_no_button "Add Another Expense" check "Request travel or other reimbursement" expect(page).to have_no_field(class: "expense-amount-input", visible: :all) expect(page).to have_no_field(class: "expense-describe-input", visible: :all) click_on "Add Another Expense" expect(page).to have_field(class: "expense-describe-input") expect(page).to have_field(class: "expense-amount-input") end it "does not submit values if reimbursement is cancelled (unchecked)", :js do subject click_on "Add Another Expense" fill_expense_fields 5.34, "Lunch" uncheck "Request travel or other reimbursement" click_on "Submit" expect(page).to have_text("Case contact successfully created") expect(CaseContact.active.count).to eq(1) case_contact = CaseContact.active.last expect(case_contact.additional_expenses).to be_empty expect(case_contact.miles_driven).to be_zero expect(case_contact.want_driving_reimbursement).to be false end it "can remove an expense", :js do subject fill_in_contact_details check "Request travel or other reimbursement" fill_in "case_contact_miles_driven", with: 50 fill_in "case_contact_volunteer_address", with: "123 Params St" click_on "Add Another Expense" fill_expense_fields 1.50, "1st meal" click_on "Add Another Expense" fill_expense_fields 2.50, "2nd meal" click_on "Add Another Expense" fill_expense_fields 2.00, "3rd meal" within "#contact-form-expenses" do click_on "Delete", match: :first end expect(page).to have_field(class: "expense-amount-input", count: 2) click_on "Submit" expect(page).to have_text("Case contact successfully created") case_contact = CaseContact.active.last expect(case_contact.additional_expenses.size).to eq(2) expect(CaseContact.count).to eq(1) expect(AdditionalExpense.count).to eq(2) end it "requires a description for each additional expense", :js do subject click_on "Add Another Expense" fill_expense_fields 5.34, nil click_on "Submit" expect(page).to have_text("Other Expense Details can't be blank") expect(CaseContact.active.count).to eq(0) expect(AdditionalExpense.count).to eq(1) end context "when editing existing case contact expenses" do subject { visit edit_case_contact_path case_contact } let(:case_contact) { create :case_contact, :wants_reimbursement, casa_case:, creator: volunteer, contact_types: [contact_type] } let!(:additional_expenses) do [ create(:additional_expense, case_contact:, other_expense_amount: 1.11, other_expenses_describe: "First Expense"), create(:additional_expense, case_contact:, other_expense_amount: 2.22, other_expenses_describe: "Second Expense") ] end it "shows existing expenses in the form" do subject expect(page).to have_field(class: "expense-amount-input", count: additional_expenses.size) expect(page).to have_field(class: "expense-describe-input", count: additional_expenses.size) expect(page).to have_field(class: "expense-amount-input", with: "1.11") expect(page).to have_field(class: "expense-describe-input", with: "First Expense") expect(page).to have_field(class: "expense-amount-input", with: "2.22") expect(page).to have_field(class: "expense-describe-input", with: "Second Expense") expect(page).to have_button "Add Another Expense" end it "allows removing expenses", :js do subject expect(page).to have_css(".expense-amount-input", count: 2) expect(page).to have_css(".expense-describe-input", count: 2) within "#contact-form-expenses" do click_on "Delete", match: :first end expect(page).to have_css(".expense-amount-input", count: 1) expect(page).to have_css(".expense-describe-input", count: 1) click_on "Submit" expect(page).to have_text(/Case contact .* was successfully updated./) expect(CaseContact.active.count).to eq(1) expect(AdditionalExpense.count).to eq(1) expect(case_contact.reload.additional_expenses.size).to eq(1) end it "can add an expense", :js do subject click_on "Add Another Expense" fill_expense_fields 11.50, "Gas" click_on "Submit" expect(page).to have_text(/Case contact .* was successfully updated./) expect(case_contact.reload.additional_expenses.size).to eq(3) expect(AdditionalExpense.count).to eq(3) end end end ================================================ FILE: spec/system/case_contacts/case_contacts_new_design_spec.rb ================================================ require "rails_helper" RSpec.describe "Case contacts new design", type: :system, js: true do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:contact_topic) { create(:contact_topic, casa_org: organization, question: "What was discussed?") } let!(:case_contact) do create(:case_contact, :active, casa_case: casa_case, notes: "Important follow-up needed") end before do create(:contact_topic_answer, case_contact: case_contact, contact_topic: contact_topic, value: "Youth is doing well in school") allow(Flipper).to receive(:enabled?).and_call_original allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(true) end shared_context "signed in as admin" do before do sign_in admin visit case_contacts_new_design_path end end describe "New Case Contact button" do include_context "signed in as admin" it "is visible to an admin" do expect(page).to have_link("New Case Contact", href: new_case_contact_path) end it "navigates to the new case contact form when clicked as an admin" do click_link "New Case Contact" expect(page).to have_current_path(%r{/case_contacts/\d+/form/details}) end context "when signed in as a volunteer" do let(:volunteer) { create(:volunteer, casa_org: organization) } before do sign_in volunteer visit case_contacts_new_design_path end it "is visible to a volunteer" do expect(page).to have_link("New Case Contact", href: new_case_contact_path) end it "navigates to the new case contact form when clicked as a volunteer" do click_link "New Case Contact" expect(page).to have_current_path(%r{/case_contacts/\d+/form/details}) end end end describe "row expansion" do include_context "signed in as admin" it "shows the expanded content after clicking the chevron" do find(".expand-toggle").click expect(page).to have_content("What was discussed?") expect(page).to have_content("Youth is doing well in school") end it "shows notes in the expanded content" do find(".expand-toggle").click expect(page).to have_content("Additional Notes") expect(page).to have_content("Important follow-up needed") end it "hides the expanded content after clicking the chevron again" do find(".expand-toggle").click expect(page).to have_content("Youth is doing well in school") find(".expand-toggle").click expect(page).to have_no_content("Youth is doing well in school") end end describe "action menu" do include_context "signed in as admin" it "opens the dropdown when the ellipsis button is clicked" do find(".cc-ellipsis-toggle").click expect(page).to have_css(".dropdown-menu.show") end it "shows Edit in the menu" do find(".cc-ellipsis-toggle").click expect(page).to have_text("Edit") end it "shows Delete in the menu" do find(".cc-ellipsis-toggle").click expect(page).to have_text("Delete") end it "shows Set Reminder when no followup exists" do find(".cc-ellipsis-toggle").click expect(page).to have_text("Set Reminder") expect(page).to have_no_text("Resolve Reminder") end it "shows Resolve Reminder when a requested followup exists" do create(:followup, case_contact: case_contact, status: :requested, creator: admin) visit case_contacts_new_design_path find(".cc-ellipsis-toggle").click expect(page).to have_text("Resolve Reminder") expect(page).to have_no_text("Set Reminder") end it "closes the dropdown when clicking outside" do find(".cc-ellipsis-toggle").click expect(page).to have_css(".dropdown-menu.show") find("h1").click expect(page).to have_no_css(".dropdown-menu.show") end end describe "Edit action" do include_context "signed in as admin" it "navigates to the edit form when Edit is clicked" do find(".cc-ellipsis-toggle").click click_link "Edit" expect(page).to have_current_path(/case_contacts\/#{case_contact.id}\/form/) end end describe "Delete action" do include_context "signed in as admin" let(:occurred_at_text) { I18n.l(case_contact.occurred_at, format: :full) } it "removes the row after confirming the delete dialog" do expect(page).to have_text(occurred_at_text) find(".cc-ellipsis-toggle").click find(".cc-delete-action").click click_button "Delete" expect(page).to have_no_text(occurred_at_text) end it "leaves the row in place when the delete dialog is cancelled" do expect(page).to have_text(occurred_at_text) find(".cc-ellipsis-toggle").click find(".cc-delete-action").click click_button "Cancel" expect(page).to have_text(occurred_at_text) end end describe "Set Reminder action" do include_context "signed in as admin" it "creates a followup and shows Resolve Reminder in the menu after confirming" do find(".cc-ellipsis-toggle").click find(".cc-set-reminder-action").click click_button "Confirm" expect(page).to have_css("i.fas.fa-bell:not([style])") find(".cc-ellipsis-toggle").click expect(page).to have_text("Resolve Reminder") expect(page).to have_no_text("Set Reminder") end it "does not create a followup when cancelled" do find(".cc-ellipsis-toggle").click find(".cc-set-reminder-action").click click_button "Cancel" expect(case_contact.followups.reload).to be_empty end end describe "Resolve Reminder action" do include_context "signed in as admin" let!(:followup) { create(:followup, case_contact: case_contact, status: :requested, creator: admin) } before { visit case_contacts_new_design_path } it "resolves the followup and shows Set Reminder in the menu afterwards" do find(".cc-ellipsis-toggle").click find(".cc-resolve-reminder-action").click expect(page).to have_css("i.fas.fa-bell[style*='opacity']") find(".cc-ellipsis-toggle").click expect(page).to have_text("Set Reminder") expect(page).to have_no_text("Resolve Reminder") end it "marks the followup as resolved" do find(".cc-ellipsis-toggle").click find(".cc-resolve-reminder-action").click # Wait for reload to confirm the AJAX completed before checking DB expect(page).to have_css("i.fas.fa-bell[style*='opacity']") expect(followup.reload.status).to eq("resolved") end end describe "permission states" do let(:volunteer) { create(:volunteer, casa_org: organization) } let(:casa_case_for_volunteer) { create(:casa_case, casa_org: organization) } let!(:active_contact) { create(:case_contact, :active, casa_case: casa_case_for_volunteer, creator: volunteer, occurred_at: 5.days.ago) } let!(:draft_contact) { create(:case_contact, casa_case: casa_case_for_volunteer, creator: volunteer, status: "started", occurred_at: 10.days.ago) } before do sign_in volunteer visit case_contacts_new_design_path end it "shows Delete as disabled for an active contact" do find("#cc-actions-btn-#{active_contact.id}").click expect(page).to have_css(".dropdown-menu[aria-labelledby='cc-actions-btn-#{active_contact.id}'].show") expect(page).to have_css(".dropdown-menu[aria-labelledby='cc-actions-btn-#{active_contact.id}'] button.dropdown-item.disabled", text: "Delete") end it "shows Delete as enabled for a draft contact" do find("#cc-actions-btn-#{draft_contact.id}").click expect(page).to have_css(".dropdown-menu[aria-labelledby='cc-actions-btn-#{draft_contact.id}'].show") expect(page).to have_css(".dropdown-menu[aria-labelledby='cc-actions-btn-#{draft_contact.id}'] button.cc-delete-action", text: "Delete") end end end ================================================ FILE: spec/system/case_contacts/contact_topic_answers_spec.rb ================================================ require "rails_helper" RSpec.describe "CaseContact form ContactTopicAnswers and notes", type: :system do subject do sign_in user visit new_case_contact_path(casa_case) end let(:casa_org) { create :casa_org, :all_reimbursements_enabled } let(:casa_case) { volunteer.casa_cases.first } let(:volunteer) { create :volunteer, :with_single_case, casa_org: } let(:user) { volunteer } let!(:contact_type) { create :contact_type, casa_org: } let!(:contact_topics) { create_list :contact_topic, 2, casa_org: } let(:contact_topic_questions) { contact_topics.map(&:question) } let(:select_options) { contact_topic_questions + ["Select a discussion topic"] } let(:topic_select_class) { "contact-topic-id-select" } let(:topic_answer_input_class) { "contact-topic-answer-input" } let(:autosave_alert_div) { "#contact-form-notes" } let(:autosave_alert_css) { 'small[role="alert"]' } let(:autosave_alert_text) { "Saved!" } def notes_section page.find_by_id("contact-form-notes") end it "shows a topic form when page is loaded and lists all contact topics" do subject expect(notes_section).to have_field(class: topic_select_class) expect(notes_section).to have_field(class: topic_answer_input_class) expect(notes_section).to have_select(class: topic_select_class, with_options: contact_topic_questions) end it "adds contact answers for the topics", :js do subject fill_in_contact_details(contact_types: [contact_type.name]) topic_one = contact_topics.first topic_two = contact_topics.last fill_in_notes(contact_topic_answers_attrs: [ {question: topic_one.question, answer: "First discussion topic answer."}, {question: topic_two.question, answer: "Second discussion topic answer."} ]) click_on "Submit" expect(page).to have_content("Case contact successfully created.") expect(CaseContact.active.size).to eq 1 case_contact = CaseContact.active.last expect(case_contact.reload.contact_topic_answers).to be_present expect(case_contact.reload.contact_topic_answers.size).to eq 2 first_topic_answer = case_contact.contact_topic_answers.find_by(contact_topic_id: topic_one.id) second_topic_answer = case_contact.contact_topic_answers.find_by(contact_topic_id: topic_two.id) expect(first_topic_answer.value).to eq "First discussion topic answer." expect(second_topic_answer.value).to eq "Second discussion topic answer." end it "does not add multiple records for the same answer due to autosave", :js do subject fill_in_contact_details(contact_types: [contact_type.name]) answer_topic contact_topics.first.question, "First discussion topic answer." within notes_section do expect(page).to have_text "Saved" # autosave success alert expect(page).to have_no_text "Saved" # wait for clearing of alert end answer_topic contact_topics.first.question, "Changing the first topic answer." within notes_section { expect(page).to have_text "Saved" } click_on "Submit" expect(page).to have_content("Case contact successfully created.") expect(CaseContact.active.count).to eq(1) expect(ContactTopicAnswer.count).to eq(1) case_contact = CaseContact.active.last created_answer = ContactTopicAnswer.last expect(created_answer.contact_topic).to eq(contact_topics.first) expect(created_answer.value).to eq "Changing the first topic answer." expect(case_contact.contact_topic_answers.size).to eq 1 expect(case_contact.contact_topic_answers).to include created_answer end it "prevents adding more answers than topics", :js do subject (contact_topics.size - 1).times do click_on "Add Another Discussion Topic" end expect(notes_section).to have_button("Add Another Discussion Topic", disabled: true) end it "disables contact topics that are already selected", :js do subject topic_one_question = contact_topics.first.question answer_topic topic_one_question, "First discussion topic answer." expect(notes_section).to have_select(class: topic_select_class, count: 1) expect(notes_section).to have_no_select(class: topic_select_class, disabled_options: [topic_one_question]) click_on "Add Another Discussion Topic" expect(notes_section).to have_select(class: topic_select_class, count: 2) expect(notes_section).to have_select(class: topic_select_class, disabled_options: [topic_one_question], count: 1) end context "when casa org has no contact topics" do let(:contact_topics) { [] } it "displays a field for contact.notes", :js do subject expect(page).to have_no_button "Add Another Discussion Topic" expect(notes_section).to have_field "Additional Notes" fill_in_contact_details fill_in "Additional Notes", with: "This is a note." click_on "Submit" expect(page).to have_content("Case contact successfully created.") contact = CaseContact.active.last expect(CaseContact.active.count).to eq(1) expect(contact.contact_topic_answers).to be_empty expect(contact.notes).to eq "This is a note." end it "saves 'Additional Notes' answer as contact.notes", :js do subject fill_in_contact_details(contact_types: [contact_type.name]) fill_in "Additional Notes", with: "This is a fake a topic answer." click_on "Submit" expect(page).to have_text("Case contact successfully created") contact = CaseContact.active.last expect(CaseContact.active.count).to eq(1) expect(contact.contact_topic_answers).to be_empty expect(contact.notes).to eq "This is a fake a topic answer." end end context "when editing an existing case contact" do subject do sign_in user visit edit_case_contact_path(case_contact) end let(:case_contact) { create :case_contact, casa_case:, creator: user } context "when there are existing contact topic answers" do let(:topic_one) { contact_topics.first } let!(:answer_one) { create :contact_topic_answer, contact_topic: topic_one, case_contact: } let(:topic_two) { contact_topics.second } let!(:answer_two) { create :contact_topic_answer, contact_topic: topic_two, case_contact: } it "fills inputs with the answers" do subject expect(notes_section).to have_select(class: topic_select_class, count: 2) expect(notes_section).to have_field(class: topic_answer_input_class, count: 2) expect(notes_section).to have_field(class: topic_answer_input_class, with: answer_one.value) expect(notes_section).to have_field(class: topic_answer_input_class, with: answer_two.value) expect(notes_section).to have_select( class: topic_select_class, selected: topic_one.question, options: select_options ) expect(notes_section).to have_select( class: topic_select_class, selected: topic_two.question, options: select_options ) end it "can remove an existing answer", :js do subject fill_in_contact_details expect(notes_section).to have_select(class: topic_select_class, count: 2) accept_confirm do notes_section.find_button(text: "Delete", match: :first).click end expect(notes_section).to have_select(class: topic_select_class, count: 1, visible: :all) click_on "Submit" expect(page).to have_content(/Case contact .* was successfully updated./) case_contact.reload expect(ContactTopicAnswer.count).to eq(1) expect(case_contact.contact_topic_answers.size).to eq(1) end end it "autosaves form with answer inputs", :js do subject fill_in_contact_details( contact_made: false, medium: "In Person", occurred_on: 1.day.ago.to_date, hours: 1, minutes: 5 ) click_on "Add Another Discussion Topic" answer_topic contact_topics.first.question, "Topic One answer." within autosave_alert_div do find(autosave_alert_css, text: autosave_alert_text, wait: 3) end expect(page).to have_content("Editing Existing Case Contact") expect(CaseContact.count).to eq(1) case_contact = CaseContact.last expect(case_contact.casa_case).to eq casa_case expect(ContactTopicAnswer.count).to eq(1) expect(case_contact.contact_topic_answers.size).to eq(1) expect(case_contact.contact_topic_answers.last.value).to eq "Topic One answer." expect(case_contact.contact_made).to be false expect(case_contact.medium_type).to eq "in-person" expect(case_contact.duration_minutes).to eq 65 expect(case_contact.occurred_at).to eq 1.day.ago.to_date end context "when contact notes exist" do let(:notes) { "This was previously saved as 'case_contact.notes'." } before { case_contact.update! notes: } it "presents an 'Additional Notes' field" do subject expect(notes_section).to have_field("Additional Notes", with: case_contact.notes) end end end end ================================================ FILE: spec/system/case_contacts/drafts_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contacts/drafts", type: :system do let(:organization) { build(:casa_org) } let(:admin) { build(:casa_admin, casa_org: organization) } context "with case contacts" do let!(:casa_case) { build(:casa_case, casa_org: organization) } let!(:other_org_case) { build(:case_contact, notes: "NOTE_A") } let!(:past_contact) { build(:case_contact, casa_case: casa_case, occurred_at: 3.weeks.ago, notes: "NOTE_B") } let!(:past_contact_draft) { create(:case_contact, :started_status, casa_case: casa_case, occurred_at: 3.weeks.ago, notes: "NOTE_C") } let!(:recent_contact) { build(:case_contact, casa_case: casa_case, occurred_at: 3.days.ago, notes: "NOTE_D") } let!(:recent_contact_draft) { create(:case_contact, :started_status, casa_case: casa_case, occurred_at: 3.days.ago, notes: "NOTE_E") } it "shows only same orgs drafts" do sign_in admin visit case_contacts_drafts_path expect(page).not_to have_content("NOTE_A") expect(page).not_to have_content("NOTE_B") expect(page).to have_content("NOTE_C") expect(page).not_to have_content("NOTE_D") expect(page).to have_content("NOTE_E") end end end ================================================ FILE: spec/system/case_contacts/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contacts/edit", type: :system do let(:organization) { build(:casa_org, :all_reimbursements_enabled) } let(:volunteer) { create(:volunteer, :with_single_case, casa_org: organization) } let(:casa_case) { volunteer.casa_cases.first } let(:contact_types) { create_list(:contact_type, 2, casa_org: organization) } let(:case_contact) do create(:case_contact, duration_minutes: 105, casa_case:, creator: volunteer, contact_types: [contact_types.first]) end let(:user) { volunteer } before { sign_in user } context "when admin" do let(:admin) { create(:casa_admin, casa_org: organization) } let(:user) { admin } it "successfully edits case contact", :js do visit edit_case_contact_path(case_contact) complete_details_page(case_numbers: [], contact_types: [], contact_made: true, medium: "Letter") complete_notes_page fill_in_expenses_page click_on "Submit" expect(page).to have_text "Case contact created at #{case_contact.created_at.strftime("%-I:%-M %p on %m-%e-%Y")}, was successfully updated." case_contact.reload expect(case_contact.casa_case_id).to eq casa_case.id expect(case_contact.duration_minutes).to eq 105 expect(case_contact.medium_type).to eq "letter" expect(case_contact.contact_made).to be true end it "successfully edits case contact with mileage reimbursement", :js do visit edit_case_contact_path(case_contact) complete_details_page(case_numbers: [], contact_types: [], contact_made: true, medium: "In Person", hours: 1, minutes: 45, occurred_on: Date.new(2020, 4, 4)) complete_notes_page fill_in_expenses_page(miles: 10, want_reimbursement: true, address: "123 str") click_on "Submit" expect(page).to have_text "Case contact created at #{case_contact.created_at.strftime("%-I:%-M %p on %m-%e-%Y")}, was successfully updated." case_contact.reload volunteer.address&.reload expect(case_contact.casa_case.volunteers[0]).to eq volunteer expect(volunteer.address&.content).to eq "123 str" expect(case_contact.casa_case_id).to eq casa_case.id expect(case_contact.duration_minutes).to eq 105 expect(case_contact.medium_type).to eq "in-person" expect(case_contact.contact_made).to be true end it "does not allow volunteer address edit for case contact with ambiguous volunteer" do create(:case_assignment, casa_case:, volunteer: build(:volunteer, casa_org: organization)) case_contact.update!(creator: admin) expect(casa_case.volunteers).not_to include case_contact.creator expect(casa_case.volunteers.size).to be > 1 visit edit_case_contact_path(case_contact) complete_details_page(case_numbers: [], contact_types: [], contact_made: true, medium: "In Person", hours: 1, minutes: 45, occurred_on: "04/04/2020") check "Request travel or other reimbursement" expect(page).to have_field("case_contact_volunteer_address", disabled: true) expect(page).to have_text("There are two or more volunteers assigned to this case and you are trying to set the address for both of them. This is not currently possible.") end context "when user is part of a different organization" do let(:other_organization) { build(:casa_org) } let(:admin) { create(:casa_admin, casa_org: other_organization) } it "fails across organizations" do visit edit_case_contact_path(case_contact) expect(page).to have_current_path supervisors_path, ignore_query: true end end end it "can update case contact attributes", :js do expect(case_contact.active?).to be true expect(case_contact.contact_types).to contain_exactly(contact_types.first) expect(case_contact.contact_made).to be false expect(case_contact.medium_type).to eq "in-person" expect(case_contact.duration_minutes).to eq 105 expect(case_contact.volunteer_address).to be_blank expect(case_contact.notes).to be_blank expect(case_contact.contact_topic_answers).to be_empty expect(case_contact.miles_driven).to be_zero expect(case_contact.want_driving_reimbursement).to be false expect(case_contact.additional_expenses).to be_empty contact_topic = create(:contact_topic, casa_org: organization) visit edit_case_contact_path(case_contact) complete_details_page( case_numbers: [], contact_made: true, medium: "Letter", occurred_on: Time.zone.today - 5.days, hours: 1, minutes: 5 ) uncheck contact_types.first.name check contact_types.second.name click_on "Add Another Discussion Topic" answer_topic contact_topic.question, "Topic 1 Answer." fill_in_expenses_page(miles: 50, want_reimbursement: true, address: "123 Form St") click_on "Submit" expect(page).to have_text "Case contact created at #{case_contact.created_at.strftime("%-I:%-M %p on %m-%e-%Y")}, was successfully updated." case_contact.reload case_contact.contact_topic_answers&.reload expect(case_contact.duration_minutes).to eq 65 expect(case_contact.medium_type).to eq "letter" expect(case_contact.contact_made).to be true expect(case_contact.contact_types).to contain_exactly(contact_types.second) # notes expect(case_contact.contact_topic_answers).to be_present expect(case_contact.contact_topic_answers.first.contact_topic).to eq contact_topic expect(case_contact.contact_topic_answers.first.value).to eq "Topic 1 Answer." # reimbursement expect(case_contact.miles_driven).to eq 50 expect(case_contact.want_driving_reimbursement).to be true expect(case_contact.volunteer_address).to eq "123 Form St" end it "is successful with mileage reimbursement on", :js do visit edit_case_contact_path(case_contact) complete_details_page(contact_made: true, medium: "In Person", hours: 1, minutes: 45, occurred_on: Date.new(2020, 4, 4)) complete_notes_page fill_in_expenses_page(miles: 10, want_reimbursement: true, address: "123 str") click_on "Submit" expect(page).to have_text "Case contact created at #{case_contact.created_at.strftime("%-I:%-M %p on %m-%e-%Y")}, was successfully updated." case_contact.reload volunteer.reload expect(volunteer.address.content).to eq "123 str" expect(case_contact.casa_case_id).to eq casa_case.id expect(case_contact.duration_minutes).to eq 105 expect(case_contact.medium_type).to eq "in-person" expect(case_contact.contact_made).to be true end it "autosaves notes", :js do autosave_alert_div = "#contact-form-notes" autosave_alert_css = 'small[role="alert"]' autosave_alert_text = "Saved!" case_contact = create(:case_contact, duration_minutes: 105, casa_case: casa_case, creator: volunteer, notes: "Hello from the other side") visit edit_case_contact_path(case_contact) complete_details_page(contact_made: true) expect(case_contact.reload.notes).to eq "Hello from the other side" fill_in "Additional Notes", with: "Hello world" within autosave_alert_div do find(autosave_alert_css, text: autosave_alert_text) end expect(case_contact.reload.notes).to eq "Hello world" end context "when 'Create Another' option is checked" do it "creates a duplicate case contact for the second contact", :js do case_contact_draft_ids = case_contact.draft_case_ids visit edit_case_contact_path(case_contact) check "Create Another" click_on "Submit" expect(page).to have_text("successfully updated") expect(page).to have_text "New Case Contact" expect(page).to have_text casa_case.case_number expect(CaseContact.started.count).to eq(1) new_case_contact = CaseContact.last expect(new_case_contact.draft_case_ids).to match_array(case_contact_draft_ids) end end end ================================================ FILE: spec/system/case_contacts/followups/create_spec.rb ================================================ require "rails_helper" RSpec.describe "followups/create", :js, type: :system do let(:admin) { create(:casa_admin) } let(:case_contact) { create(:case_contact) } let(:note) { "Lorem ipsum dolor sit amet." } describe "Creating a followup" do before do sign_in admin visit casa_case_path(case_contact.casa_case) click_button "Make Reminder" end it "displays correct prompt" do expect(page).to have_content("Optional: Add a note about what followup is needed.") end context "when confirming the Swal alert" do it "creates a followup with a note when the note textarea is filled" do find(".swal2-textarea").set(note) click_button "Confirm" expect(page).to have_button("Resolve Reminder") case_contact.followups.reload expect(case_contact.followups.count).to eq(1) expect(case_contact.followups.last.note).to eq(note) end it "creates a followup without a note when the note textarea is empty" do click_button "Confirm" expect(page).to have_button("Resolve Reminder") case_contact.followups.reload expect(case_contact.followups.count).to eq(1) expect(case_contact.followups.last.note).to be_nil end end context "when cancelling the Swal alert" do it "does nothing when there is text in the note textarea" do find(".swal2-textarea").set(note) click_button "Cancel" expect(page).not_to have_text(note) expect(case_contact.followups.reload.count).to be_zero end it "does nothing when there is no text in the note textarea" do click_button "Cancel" expect(page).not_to have_text(note) expect(case_contact.followups.reload.count).to be_zero end end context "when closing the Swal alert" do it "does nothing when there is text in the note textarea" do find(".swal2-textarea").set(note) find(".swal2-close").click expect(page).not_to have_text(note) expect(case_contact.followups.reload.count).to be_zero end it "does nothing when there is no text in the note textarea" do find(".swal2-close").click expect(page).not_to have_text(note) expect(case_contact.followups.reload.count).to be_zero end end end end ================================================ FILE: spec/system/case_contacts/followups/resolve_spec.rb ================================================ require "rails_helper" RSpec.describe "followups/resolve", type: :system do let(:casa_org) { build(:casa_org) } let(:admin) { build(:casa_admin, casa_org: casa_org) } let(:supervisor) { build(:supervisor, casa_org: casa_org) } let(:volunteer) { build(:volunteer, casa_org: casa_org) } let(:casa_case) { create(:casa_case, casa_org: casa_org) } let(:cc_creator) { admin } let(:followup_creator) { volunteer } let(:case_contact) { build(:case_contact, casa_case: casa_case, creator: cc_creator) } let!(:followup) { create(:followup, case_contact: case_contact, creator: followup_creator) } before { sign_in admin } it "changes status of followup to resolved" do visit casa_case_path(case_contact.casa_case) click_button "Resolve Reminder" expect(page).to have_button("Make Reminder") expect(case_contact.followups.count).to eq(1) expect(case_contact.followups.first.resolved?).to be_truthy end context "logged in as admin, followup created by volunteer" do let(:cc_creator) { volunteer } let(:followup_creator) { volunteer } before { sign_in admin } it "changes status of followup to resolved" do visit casa_case_path(case_contact.casa_case) click_button "Resolve Reminder" expect(page).to have_button("Make Reminder") expect(case_contact.followups.count).to eq(1) expect(case_contact.followups.first.resolved?).to be_truthy end it "removes followup icon and button changes back to 'Make Reminder'" do visit casa_case_path(case_contact.casa_case) click_button "Resolve Reminder" expect(page).to have_button("Make Reminder") end end context "logged in as supervisor, followup created by volunteer" do let(:cc_creator) { supervisor } let(:followup_creator) { volunteer } before { sign_in supervisor } it "changes status of followup to resolved" do visit casa_case_path(case_contact.casa_case) click_button "Resolve Reminder" expect(page).to have_button("Make Reminder") expect(case_contact.followups.count).to eq(1) expect(case_contact.followups.first.resolved?).to be_truthy end end context "logged in as volunteer, followup created by admin" do let(:cc_creator) { volunteer } let(:followup_creator) { admin } before do case_contact.casa_case.assigned_volunteers << volunteer sign_in volunteer end it "changes status of followup to resolved" do visit case_contacts_path click_button "Resolve Reminder" expect(page).to have_button("Make Reminder") expect(case_contact.followups.count).to eq(1) expect(case_contact.followups.first.resolved?).to be_truthy end end end ================================================ FILE: spec/system/case_contacts/index_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contacts/index", type: :system do subject { visit case_contacts_path } let(:volunteer) { build(:volunteer, display_name: "Bob Loblaw", casa_org: organization) } let(:organization) { build(:casa_org) } before { sign_in volunteer } context "with case contacts" do let(:case_number) { "CINA-1" } let(:casa_case) { build(:casa_case, casa_org: organization, case_number: case_number) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } context "without filter" do it "can see case creator in card" do create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: 2.days.ago) subject within(".full-card", match: :first) do expect(page).to have_text("Bob Loblaw") end end it "can navigate to edit volunteer page" do subject expect(page).to have_no_link("Bob Loblaw") end it "allows the volunteer to delete a draft they created" do create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: 2.days.ago) create(:case_contact, :started_status, creator: volunteer, casa_case: casa_case, occurred_at: 3.days.ago, contact_types: [build(:contact_type, name: "DRAFT Case Contact")]) subject card = find(".container-fluid.mb-1", text: "DRAFT Case Contact") expect(card).not_to be_nil within_element(card) do expect(card).to have_text("Draft") click_on "Delete" end expect(page).to have_no_css(".container-fluid.mb-1", text: "DRAFT Case Contact") end it "displays the contact type groups" do create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: Time.zone.now, contact_types: [build(:contact_type, name: "Most Recent Case Contact")]) create(:case_contact, :started_status, creator: volunteer, casa_case: casa_case, occurred_at: 3.days.ago, contact_types: [build(:contact_type, name: "DRAFT Case Contact")]) subject expect(page).to have_text("Most Recent Case Contact") expect(page).to have_text("DRAFT Case Contact") end end describe "automated filtering case contacts" do describe "by date of contact" do it "only shows the contacts with the correct date", :js do yesterday = Time.zone.yesterday day_before_yesterday = yesterday - 1.day today = Time.zone.today create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: day_before_yesterday) create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: yesterday) create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: today) subject click_on "Expand / Hide" yesterday_display = I18n.l(yesterday, format: :full, default: nil) day_before_yesterday_display = I18n.l(day_before_yesterday, format: :full, default: nil) today_display = I18n.l(today, format: :full, default: nil) expect(page).to have_content day_before_yesterday_display expect(page).to have_content yesterday_display expect(page).to have_content today_display fill_in "filterrific_occurred_starting_at", with: yesterday fill_in "filterrific_occurred_ending_at", with: Time.zone.tomorrow expect(page).to have_no_content day_before_yesterday_display expect(page).to have_content yesterday_display expect(page).to have_content today_display end end describe "by casa_case_id" do subject { visit case_contacts_path(casa_case_id: casa_case.id) } let!(:other_casa_case) { build(:casa_case, casa_org: organization, case_number: "CINA-2") } it "displays the draft" do create(:case_contact, :details_status, creator: volunteer, draft_case_ids: [casa_case.id]) subject expect(page).to have_no_content "You have no case contacts for this case." expect(page).to have_content "Draft" end it "only displays the filtered case" do subject expect(page).to have_no_content other_casa_case.case_number expect(page).to have_content casa_case.case_number end end describe "by hide drafts" do it "does not show draft contacts", :js do build(:case_contact, creator: volunteer, casa_case: casa_case) build(:case_contact, :started_status, creator: volunteer, casa_case: casa_case) subject check "Hide drafts" expect(page).to have_no_content "Draft" end end describe "collapsing filter menu" do before do subject end it "displays sticky filters before clicking expand" do expect(page).to have_field "Hide drafts", type: :checkbox end it "does not expand menu when filtering only by sticky filter", :js do check "Hide drafts" expect(page).to have_field "Hide drafts", type: :checkbox expect(page).to have_no_content "Other filters" end it "displays other filters when expanded" do click_on "Expand / Hide" expect(page).to have_content "Other filters" end it "does not hide menu when filtering by placement filter" do click_on "Expand / Hide" select "In Person", from: "Contact medium" expect(page).to have_content "Other filters" end end end describe "case contacts text color" do let(:contact_group_text) { case_contact.contact_groups_with_types.keys.first } context "with active case contact" do let!(:case_contact) { create(:case_contact, creator: volunteer, casa_case: casa_case, occurred_at: Time.zone.yesterday) } it "displays correct color for contact" do subject within ".card-title" do title = find("strong.text-primary") expect(title).to have_content(contact_group_text) end end end end it "can show only case contacts for one case", :js do yesterday = Time.zone.yesterday day_before_yesterday = yesterday - 1.day today = Time.zone.today create(:case_contact, creator: volunteer, casa_case: casa_case, notes: "Case 1 Notes", occurred_at: day_before_yesterday) another_case_number = "CINA-2" another_case = build(:casa_case, casa_org: organization, case_number: another_case_number) create(:case_assignment, volunteer: volunteer, casa_case: another_case) create(:case_contact, creator: volunteer, casa_case: another_case, notes: "Case 2 Notes", occurred_at: today) # showing all cases visit root_path click_on "Case Contacts" within "#ddmenu_case-contacts" do click_on "All" end expect(page).to have_text("Case 1 Notes") expect(page).to have_text("Case 2 Notes") # showing case 1 visit root_path click_on "Case Contacts" within "#ddmenu_case-contacts" do click_on case_number end expect(page).to have_text("Case 1 Notes") expect(page).to have_no_text("Case 2 Notes") # showing case 2 visit root_path click_on "Case Contacts" within "#ddmenu_case-contacts" do click_on another_case_number end expect(page).to have_text("Case 2 Notes") expect(page).to have_no_text("Case 1 Notes") # filtering to only show case 2 click_on "Expand / Hide" fill_in "filterrific_occurred_starting_at", with: yesterday fill_in "filterrific_occurred_ending_at", with: Time.zone.tomorrow expect(page).to have_text("Case 2 Notes") expect(page).to have_no_text("Case 1 Notes") visit root_path click_on "Case Contacts" within "#ddmenu_case-contacts" do click_on case_number end click_on "Expand / Hide" fill_in "filterrific_occurred_starting_at", with: yesterday fill_in "filterrific_occurred_ending_at", with: Time.zone.tomorrow # no contacts because we're only showing case 1 and that occurred before the filter dates expect(page).to have_no_text("Case 1 Notes") expect(page).to have_no_text("Case 2 Notes") end describe "contact notes" do let(:contact_topics) { build_list(:contact_topic, 2, casa_org: organization) } let(:contact_topic) { contact_topics.first } let(:case_contact) { build(:case_contact, casa_case:, creator: volunteer) } let!(:contact_topic_answer) do create(:contact_topic_answer, case_contact:, contact_topic:) end let(:user) { volunteer } before { sign_in user } it "show topic answers and allows expanding to show full answer", :js do subject expect(page).to have_text contact_topics.first.question expect(page).to have_no_text contact_topics.first.details within(".truncation-container", match: :first) do expect(page).to have_content(contact_topic.question) # capybara has trouble with css `overflow: hidden` truncated text. just test class is applied/not expect(page).to have_css(".line-clamp-1", text: contact_topic_answer.value) click_on "read more" expect(page).to have_no_css(".line-clamp-1") expect(page).to have_content(contact_topic_answer.value) expect(page).to have_no_content contact_topics.first.details end end end end context "without case contacts" do it "shows helper text" do subject expect(page).to have_text("You have no case contacts for this case. Please click New Case Contact button above to create a case contact for your youth!") end end end ================================================ FILE: spec/system/case_contacts/new_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contacts/new", type: :system do subject { visit new_case_contact_path casa_case } let(:casa_org) { build :casa_org } let(:contact_type_group) { build :contact_type_group, casa_org: } let!(:school_contact_type) { create :contact_type, contact_type_group:, name: "School" } let(:volunteer) { create :volunteer, :with_single_case, casa_org: } let(:casa_admin) { build :casa_admin, casa_org: } let(:casa_case) { volunteer.casa_cases.first } let(:case_number) { casa_case.case_number } let(:user) { volunteer } before { sign_in user } it "page load creates a case_contact with status: 'started' & draft_case_ids: [casa_case.id]" do subject expect(page).to have_content("New Case Contact") expect(CaseContact.started.count).to eq(1) case_contact = CaseContact.started.last expect(case_contact.draft_case_ids).to contain_exactly(casa_case.id) expect(case_contact.casa_case_id).to be_nil end it "saves entered details and updates status to 'active'", :js do subject expect(page).to have_text "New Case Contact" case_contact = CaseContact.started.last complete_details_page( case_numbers: [case_number], contact_types: %w[School], contact_made: true, medium: "In Person", occurred_on: Time.zone.yesterday, hours: 1, minutes: 45 ) click_on "Submit" expect(page).to have_text "Case contact successfully created." case_contact.reload aggregate_failures do expect(case_contact.status).to eq "active" # entered details expect(case_contact.draft_case_ids).to eq [casa_case.id] expect(case_contact.occurred_at).to eq Time.zone.yesterday expect(case_contact.contact_types.map(&:name)).to contain_exactly("School") expect(case_contact.medium_type).to eq "in-person" expect(case_contact.contact_made).to be true expect(case_contact.duration_minutes).to eq 105 # skipped fields expect(case_contact.want_driving_reimbursement).to be false expect(case_contact.miles_driven).to be_zero expect(case_contact.volunteer_address).to be_empty expect(case_contact.notes).to be_nil # associations expect(case_contact.casa_case).to eq casa_case expect(case_contact.creator).to eq volunteer # other attributes expect(case_contact.reimbursement_complete).to be false expect(case_contact.status).to eq "active" expect(case_contact.metadata).to be_present end end context "with invalid inputs" do it "re-renders the form with errors, preserving all previously entered selections" do subject complete_details_page( case_numbers: [], contact_types: %w[School], contact_made: true, medium: nil, hours: 1, minutes: 45 ) click_on "Submit" expect(page).to have_text("Medium type can't be blank") expect(page).to have_field("case_contact_duration_hours", with: 1) expect(page).to have_field("case_contact_duration_minutes", with: 45) expect(page).to have_field("case_contact_contact_made", with: "1") expect(page).to have_field(class: "contact-form-type-checkbox", with: school_contact_type.id, checked: true) expect(CaseContact.count).to eq(1) end end describe "contact types" do it "requires a contact type", :js do contact_types = [school_contact_type] subject fill_in_contact_details(contact_types: nil) click_on "Submit" expect(page).to have_text("Contact Type must be selected") expect(CaseContact.active.count).to eq(0) check contact_types.first.name click_on "Submit" expect(page).to have_text("Case contact successfully created.") expect(CaseContact.active.count).to eq(1) end it "does not display empty contact groups or hidden contact types" do create(:contact_type, name: "Shown Checkbox", contact_type_group:) build(:contact_type_group, name: "Empty Group", casa_org:) grp_with_hidden = build(:contact_type_group, name: "OnlyHiddenTypes", casa_org:) build(:contact_type, name: "Hidden", active: false, contact_type_group: grp_with_hidden) subject expect(page).to have_text(contact_type_group.name) expect(page).to have_no_text("OnlyHiddenTypes") expect(page).to have_no_text("Empty Group") expect(page).to have_field(class: "contact-form-type-checkbox", count: 2) expect(page).to have_field("School", class: "contact-form-type-checkbox") expect(page).to have_field("Shown Checkbox", class: "contact-form-type-checkbox") expect(page).to have_no_text("Empty") expect(page).to have_no_text("Hidden") end context "when the case has case contact types assigned" do let!(:casa_case) { create(:casa_case, :with_casa_case_contact_types, :with_one_case_assignment, casa_org:) } let(:volunteer) { casa_case.volunteers.first } let(:casa_case_contact_types) { casa_case.contact_types } it "shows only the casa case's contact types" do therapist_contact_type = create :contact_type, contact_type_group:, name: "Therapist" subject expect(page).to have_field(class: "contact-form-type-checkbox", with: casa_case_contact_types.first.id) expect(page).to have_field(class: "contact-form-type-checkbox", with: casa_case_contact_types.last.id) expect(page).to have_field(class: "contact-form-type-checkbox", count: casa_case_contact_types.size) # (no others) expect(casa_org.contact_types).to contain_exactly(school_contact_type, therapist_contact_type, *casa_case_contact_types) expect(casa_case_contact_types).not_to include([school_contact_type, therapist_contact_type]) end end end describe "notes/contact topic answsers section" do let(:contact_topics) do [ create(:contact_topic, casa_org:, question: "Active Topic", active: true, soft_delete: false), create(:contact_topic, casa_org:, question: "Inactive Not Soft Deleted", active: false, soft_delete: false), create(:contact_topic, casa_org:, question: "Active Soft Deleted", active: true, soft_delete: true), create(:contact_topic, casa_org:, question: "Inactive Soft Deleted", active: false, soft_delete: true) ] end let(:notes_section_selector) { "#contact-form-notes" } let(:autosave_alert_div) { "#contact-form-notes" } let(:autosave_alert_css) { 'small[role="alert"]' } it "does not show topic questions that are inactive or soft deleted in select" do contact_topics subject within notes_section_selector do expect(page).to have_select(class: "contact-topic-id-select", options: ["Active Topic", "Select a discussion topic"]) expect(page).to have_no_text("Inactive Not Soft Deleted") expect(page).to have_no_text("Active Soft Deleted") expect(page).to have_no_text("Inactive Soft Deleted") end end it "autosaves notes & answers", :js do contact_topics subject complete_details_page( case_numbers: [case_number], contact_types: %w[School], contact_made: true, medium: "In Person", occurred_on: Date.new(2020, 4, 4), hours: 1, minutes: 45 ) answer_topic "Active Topic", "Hello world" within autosave_alert_div do find(autosave_alert_css, text: "Saved!") end expect(page.find(".contact-topic-answer-input")&.value).to eq("Hello world") end context "when org has no contact topics" do it "allows entering contact notes", :js do subject fill_in_contact_details contact_types: %w[School] fill_in "Additional Notes", with: "This is the note" click_on "Submit" expect(page).to have_text("Case contact successfully created.") expect(casa_org.contact_topics.size).to eq 0 expect(CaseContact.active.count).to eq 1 case_contact = CaseContact.active.last expect(case_contact.contact_topic_answers).to be_empty expect(case_contact.notes).to eq "This is the note" end it "guides volunteer to contact admin" do subject expect(page).to have_text("Your organization has not set any Court Report Topics yet. Contact your admin to learn more.") expect(CaseContact.active.count).to eq(0) end context "with admin user" do let(:casa_admin) { create(:casa_admin, casa_org: casa_org) } let(:user) { casa_admin } it "shows the admin the contact topics link" do subject expect(page).to have_link("Manage Case Contact Topics") expect(CaseContact.active.count).to eq(0) end end context "with supervisor user" do let(:supervisor) { create :supervisor, casa_org: } let(:user) { supervisor } it "guides supervisor to contact admin" do subject expect(page).to have_text("Your organization has not set any Court Report Topics yet. Contact your admin to learn more.") expect(CaseContact.active.count).to eq(0) end end end end describe "reimbursement section" do let(:casa_org) { build(:casa_org, :all_reimbursements_enabled) } let(:reimbursement_section_id) { "#contact-form-reimbursement" } let(:reimbursement_checkbox) { "case_contact_want_driving_reimbursement" } let(:miles_driven_input) { "case_contact_miles_driven" } let(:volunteer_address_input) { "case_contact_volunteer_address" } let(:add_expense_button_text) { "Add Another Expense" } before do allow(Flipper).to receive(:enabled?).with(:show_additional_expenses).and_return(true) allow(Flipper).to receive(:enabled?).with(:reimbursement_warning, casa_org).and_call_original end it "is not shown until 'Request travel or other reimbursement' is checked", :js do subject expect(page).to have_no_field(miles_driven_input) expect(page).to have_no_field(volunteer_address_input) expect(page).to have_no_button(add_expense_button_text) check reimbursement_checkbox expect(page).to have_field(miles_driven_input) expect(page).to have_field(volunteer_address_input) expect(page).to have_button(add_expense_button_text) end it "clears mileage info if reimbursement unchecked", :js do subject fill_in_contact_details contact_types: %w[School] check reimbursement_checkbox fill_in miles_driven_input, with: 50 fill_in volunteer_address_input, with: "123 Example St" uncheck reimbursement_checkbox click_on "Submit" expect(page).to have_text("Case contact successfully created.") expect(CaseContact.active.count).to eq(1) case_contact = CaseContact.active.last expect(case_contact.want_driving_reimbursement).to be false expect(case_contact.miles_driven).to be_zero end it "saves mileage and address information", :js do subject fill_in_contact_details contact_types: %w[School] check reimbursement_checkbox fill_in miles_driven_input, with: 50 fill_in volunteer_address_input, with: "123 Example St" click_on "Submit" expect(page).to have_text("Case contact successfully created.") expect(CaseContact.active.count).to eq(1) case_contact = CaseContact.active.last expect(case_contact.want_driving_reimbursement).to be true expect(case_contact.volunteer_address).to eq "123 Example St" expect(case_contact.miles_driven).to eq 50 end it "does not accept decimal mileage" do subject check reimbursement_checkbox fill_in miles_driven_input, with: 50.5 fill_in volunteer_address_input, with: "123 Example St" click_on "Submit" expect(page).to have_text("No changes have been saved") expect(CaseContact.active.count).to eq(0) end it "requires inputs if checkbox checked" do subject complete_details_page check reimbursement_checkbox click_on "Submit" expect(page).to have_text("Must enter a valid mailing address for the reimbursement") expect(CaseContact.active.count).to eq(0) end context "when volunteer case assignment reimbursement is false" do let(:volunteer) { build :volunteer, :with_disallow_reimbursement, casa_org: } it "does not show reimbursement section" do subject expect(page).to have_no_button(add_expense_button_text) expect(page).to have_no_field(miles_driven_input) expect(page).to have_no_field(volunteer_address_input) expect(page).to have_no_field(reimbursement_checkbox) expect(page).to have_no_selector(reimbursement_section_id) expect(page).to have_no_text("reimbursement") end end context "when casa org driving reimbursement false, additional expenses true" do before { casa_org.update! show_driving_reimbursement: false } it "does not render the reimbursement section" do subject expect(page).to have_no_field(reimbursement_checkbox, visible: :all) expect(page).to have_no_field(miles_driven_input, visible: :all) expect(page).to have_no_field(volunteer_address_input, visible: :all) expect(page).to have_no_button(add_expense_button_text, visible: :all) expect(page).to have_no_field(class: "expense-amount-input") expect(page).to have_no_field(class: "expense-describe-input") expect(page).to have_no_text("reimbursement") end end context "when casa org additional expenses false" do before { casa_org.update! additional_expenses_enabled: false } it "enables mileage reimbursement but does shows additional expenses", :js do subject complete_details_page(case_numbers: [case_number], contact_types: %w[School]) check reimbursement_checkbox expect(page).to have_field(miles_driven_input) expect(page).to have_field(volunteer_address_input) expect(page).to have_no_button(add_expense_button_text) expect(page).to have_no_field(class: "expense-amount-input", visible: :all) expect(page).to have_no_field(class: "expense-describe-input", visible: :all) end end context "when casa org does not allow mileage or expense reimbursement" do let(:casa_org) { build :casa_org, show_driving_reimbursement: false, additional_expenses_enabled: false } it "does not show reimbursement section" do subject expect(page).to have_no_button(add_expense_button_text) expect(page).to have_no_field(miles_driven_input) expect(page).to have_no_field(volunteer_address_input) expect(page).to have_no_field(reimbursement_checkbox) expect(page).to have_no_selector(reimbursement_section_id) expect(page).to have_no_text("reimbursement") end end end context "when 'Create Another' is checked" do it "redirects to the new CaseContact form with the same case selected", :js do subject complete_details_page( case_numbers: [case_number], contact_types: %w[School], contact_made: true, medium: "In Person", occurred_on: Date.today, hours: 1, minutes: 45 ) check "Create Another" click_on "Submit" expect(page).to have_text "Case contact successfully created." expect(page).to have_text "New Case Contact" expect(page).to have_text case_number expect(CaseContact.active.count).to eq(1) expect(CaseContact.started.count).to eq(1) submitted_case_contact = CaseContact.active.last next_case_contact = CaseContact.started.last expect(submitted_case_contact.reload.metadata["create_another"]).to be true # new contact uses draft_case_ids from the original & form selects them expect(next_case_contact.draft_case_ids).to eq [casa_case.id] # default values for other attributes (not from the last contact) expect(next_case_contact.status).to eq "started" expect(next_case_contact.miles_driven).to be_zero %i[casa_case_id duration_minutes occurred_at medium_type want_driving_reimbursement notes].each do |attribute| expect(next_case_contact.send(attribute)).to be_blank end expect(next_case_contact.contact_made).to be true end it "does not reset referring location", :js do visit casa_case_path casa_case # referrer will be set by CaseContactsController#new to casa_case_path(casa_case) click_on "New Case Contact" fill_in_contact_details contact_types: %w[School] # goes through CaseContactsController#new, but should not set a referring location check "Create Another" click_on "Submit" fill_in_contact_details contact_types: %w[School] click_on "Submit" # update should redirect to the original referrer, casa_case_path(casa_case) expect(page).to have_text "CASA Case Details" expect(page).to have_text "Case number: #{case_number}" end context "when multiple cases selected" do let(:volunteer) { create(:volunteer, :with_casa_cases, casa_org:) } let(:casa_case) { volunteer.casa_cases.first } let(:casa_case_two) { volunteer.casa_cases.second } let(:case_number_two) { casa_case_two.case_number } let!(:draft_case_ids) { [casa_case.id, casa_case_two.id] } it "redirects to the new CaseContact form with the same cases selected", :js do expect { visit new_case_contact_path(casa_case, {draft_case_ids:}) expect(page).to have_content("Record New Case Contact") }.to change(CaseContact.started, :count).by(1) this_case_contact = CaseContact.started.last expect(page).to have_select("case_contact_draft_case_ids", selected: [case_number, case_number_two]) complete_details_page(case_numbers: []) expect(page).to have_select("case_contact_draft_case_ids", selected: [case_number, case_number_two]) check "Create Another" expect { click_on "Submit" expect(page).to have_text "Case contacts successfully created." }.to change(CaseContact.active, :count).by(2) expect(page).to have_text "New Case Contact" expect(this_case_contact.reload.status).to eq "active" next_case_contact = CaseContact.not_active.last expect(next_case_contact).to be_present expect(next_case_contact.status).to eq "started" expect(page).to have_text case_number expect(page).to have_text case_number_two expect(next_case_contact.draft_case_ids).to match_array draft_case_ids end end end context "when volunteer has multiple cases" do let(:volunteer) { create(:volunteer, :with_casa_cases, casa_org:) } let(:first_case) { volunteer.casa_cases.first } let(:second_case) { volunteer.casa_cases.second } describe "case default selection" do it "selects no cases", :js do subject expect(page).to have_no_text(first_case.case_number) expect(page).to have_no_text(second_case.case_number) end context "when there are params defined" do it "select the cases defined in the params", :js do visit new_case_contact_path(case_contact: {casa_case_id: first_case.id}) expect(page).to have_text(first_case.case_number) expect(page).to have_no_text(second_case.case_number) end end end end context "when volunteer has one case" do let(:first_case) { volunteer.casa_cases.first } it "selects the only case" do subject expect(page).to have_text(first_case.case_number) expect(volunteer.casa_cases.size).to eq 1 end end context "with admin user" do it "can create CaseContact", :js do subject complete_details_page( case_numbers: [], contact_types: %w[School], contact_made: true, medium: "Video", occurred_on: Date.new(2020, 4, 5), hours: 1, minutes: 45 ) click_on "Submit" expect(page).to have_text "Case contact successfully created." expect(CaseContact.active.count).to eq(1) contact = CaseContact.active.last expect(contact.casa_case_id).to eq casa_case.id expect(contact.contact_types.map(&:name)).to include("School") expect(contact.duration_minutes).to eq 105 end end end ================================================ FILE: spec/system/case_court_reports/index_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.configure do |config| config.include CaseCourtReportHelpers, type: :system end RSpec.shared_context "when on the court reports page" do |user_role| let(:current_user) { send(user_role) } before do sign_in current_user visit case_court_reports_path end end RSpec.shared_examples "a user with organization-level case visibility in autocomplete" do before do open_court_report_modal open_case_select2_dropdown end it "shows all cases in their organization", :aggregate_failures do # Ensure the dropdown results area is ready expect(page).to have_css("ul.select2-results__options") # Check for the unassigned case created in the calling context expect(page).to have_css(".select2-results__option", text: /#{Regexp.escape(unassigned_case.case_number)}/i) # Check for each case assigned to the volunteer (created in the calling context) volunteer.casa_cases.each do |casa_case| # Use regex to flexibly match text format (e.g., "CASE-NUM - status(assigned...)") expect(page).to have_css(".select2-results__option", text: /#{Regexp.escape(casa_case.case_number)}/i) end end it "hides cases from other organizations", :aggregate_failures do # Find and interact with the search field input_field = find("input.select2-search__field", visible: :all) input_field.click # Ensure focus before typing input_field.send_keys(other_org_case.case_number) # Assert that "No results found" IS visible (Capybara waits) expect(page).to have_css(".select2-results__option", text: "No results found", visible: :visible, wait: 5) # Assert that the specific other org case number is NOT visible expect(page).not_to have_css(".select2-results__option", text: other_org_case.case_number, visible: :visible) end end RSpec.describe "case_court_reports/index", type: :system do context "when first arriving to 'Generate Court Report' page", :js do let(:volunteer) { create(:volunteer) } include_context "when on the court reports page", :volunteer it "generation modal hidden", :aggregate_failures do expect(page).to have_selector "#btnGenerateReport", text: "Generate Report", visible: :hidden expect(page).to have_selector "#case-selection", visible: :hidden expect(page).not_to have_selector ".select2" end end context "when opening 'Download Court Report' modal", :js do let(:volunteer) do create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor, display_name: "Volunteer") end let(:supervisor) { volunteer.supervisor } let(:casa_cases) { CasaCase.actively_assigned_to(volunteer) } include_context "when on the court reports page", :volunteer before do open_court_report_modal end it "shows the Generate button", :aggregate_failures do expect(page).to have_selector "#btnGenerateReport", text: "Generate Report", visible: :visible expect(page).not_to have_selector ".select2" end it "shows correct default dates", :aggregate_failures do date = Date.current formatted_date = date.strftime("%B %d, %Y") # January 01, 2021 expect(page.find("#start_date").value).to eq(formatted_date) expect(page.find("#end_date").value).to eq(formatted_date) end it "lists all assigned cases" do expected_number_of_options = casa_cases.size + 1 # +1 for "Select case" expect(page).to have_selector "#case-selection option", count: expected_number_of_options end it "shows correct transition status labels", :aggregate_failures do # rubocop:disable RSpec/ExampleLength younger_than_transition_age = volunteer.casa_cases.reject(&:in_transition_age?).first at_least_transition_age = volunteer.casa_cases.detect(&:in_transition_age?) expected_text_transition = "#{at_least_transition_age.case_number} - transition" expect(page).to have_selector "#case-selection option", text: expected_text_transition expected_text_non_transition = "#{younger_than_transition_age.case_number} - non-transition" expect(page).to have_selector "#case-selection option", text: expected_text_non_transition end it "adds data-lookup attribute for volunteer searching" do casa_cases.each do |casa_case| lookup = casa_case.assigned_volunteers.map(&:display_name).join(",") expect(page).to have_selector "#case-selection option[data-lookup='#{lookup}']" end end it "defaults to 'Select case number' prompt", :aggregate_failures do expect(page).to have_select "case-selection", selected: "Select case number" # Extra check for the first option specifically expect(page).to have_selector "#case-selection option:first-of-type", text: "Select case number" end it "shows an error when generating without a selection", :aggregate_failures do # rubocop:disable RSpec/ExampleLength # Ensure default is selected page.select "Select case number", from: "case-selection" click_button "Generate Report" expect(page).to have_selector(".select-required-error", visible: :visible) # Check button state remains unchanged (not disabled, spinner hidden) expect(page).to have_selector("#btnGenerateReport .lni-download", visible: :visible) expect(page).not_to have_selector("#btnGenerateReport[disabled]") expect(page).to have_selector("#spinner", visible: :hidden) end it "hides the error when a valid case is selected", :aggregate_failures do click_button "Generate Report" # First, make the error appear expect(page).to have_selector(".select-required-error", visible: :visible) test_case_number = casa_cases.detect(&:in_transition_age?).case_number.to_s page.select test_case_number, from: "case-selection" expect(page).not_to have_selector(".select-required-error", visible: :visible) end it "clears the error message when the modal is reopened", :aggregate_failures do click_button "Generate Report" # Make error appear expect(page).to have_selector(".select-required-error", visible: :visible) click_button "Close" open_court_report_modal # Reopen using the helper expect(page).not_to have_selector(".select-required-error", visible: :visible) # Error should be gone end # NOTE: select by option VALUE (stable), stub `window.open` to capture the download URL, # wait for the button to re-enable (page-level signal), and assert UI state + opened URL. it "generates a report and opens the download link on success", :aggregate_failures, :js do # rubocop:disable RSpec/ExampleLength transition_case = casa_cases.detect(&:in_transition_age?) # Stub window.open so we can capture the download URL in the browser page.execute_script(<<~JS) window.__last_opened_url = null; window.open = function(url) { window.__last_opened_url = url; }; JS # Ensure the option exists, then select it by VALUE (case number) expect(page).to have_selector("#case-selection option[value='#{transition_case.case_number}']", visible: :all) find("#case-selection").find("option[value='#{transition_case.case_number}']").select_option # Trigger generation click_button "Generate Report" # Button should be disabled while processing expect(page).to have_selector("#btnGenerateReport[disabled]") # Wait for the button to re-enable (report generated successfully) expect(page).not_to have_selector("#btnGenerateReport[disabled]", wait: 10) # Verify the UI reflects a successful generation expect(page).to have_selector("#btnGenerateReport .lni-download", visible: :visible) expect(page).to have_selector("#spinner", visible: :hidden) # Verify the browser attempted to open the generated .docx link opened_url = page.evaluate_script("window.__last_opened_url") expect(opened_url).to be_present expect(opened_url).to match(/#{Regexp.escape(transition_case.case_number)}.*\.docx$/i) end end context "when logged in as a supervisor" do let(:volunteer) do create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor, display_name: "Name Last") end let(:supervisor) { volunteer.supervisor } include_context "when on the court reports page", :supervisor it { expect(page).to have_selector ".select2" } it { expect(page).to have_text "Search by volunteer name or case number" } context "when searching for cases" do let(:casa_case) { volunteer.casa_cases.first } let(:search_term) { casa_case.case_number[-3..] } it "selects the correct case", :aggregate_failures, :js do # rubocop:disable RSpec/ExampleLength open_court_report_modal open_case_select2_dropdown send_keys(search_term) # Wait for the search result to appear in the dropdown expect(page).to have_css(".select2-results__option", text: casa_case.case_number, visible: :visible) # Click the result instead of sending enter find(".select2-results__option", text: casa_case.case_number).click # Wait for selection to update expect(page).to have_css(".select2-selection__rendered", text: casa_case.case_number, visible: :visible) end end end # rubocop:disable RSpec/MultipleMemoizedHelpers describe "case selection visibility by user role", :js do let!(:volunteer_assigned_to_case) do create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor, display_name: "Assigned Volunteer") end let(:casa_org) { volunteer_assigned_to_case.casa_org } # Derive org from the volunteer let!(:unassigned_case) { create(:casa_case, casa_org: casa_org, case_number: "UNASSIGNED-CASE-1", active: true) } let!(:other_org) { create(:casa_org) } let!(:other_org_case) { create(:casa_case, casa_org: other_org, case_number: "OTHER-ORG-CASE-99", active: true) } # rubocop:disable RSpec/LetSetup context "when logged in as a volunteer" do let(:volunteer) { volunteer_assigned_to_case } let!(:other_volunteer) { create(:volunteer, casa_org: volunteer.casa_org) } let!(:other_volunteer_case) do create(:casa_case, casa_org: volunteer.casa_org, case_number: "OTHER-VOL-CASE-88", volunteers: [other_volunteer], active: true) end include_context "when on the court reports page", :volunteer before do open_court_report_modal end it "shows all assigned cases in autocomplete search", :aggregate_failures do volunteer.casa_cases.select(&:active?).each do |c| expect(page).to have_selector("#case-selection option", text: /#{Regexp.escape(c.case_number)}/i) end end it "does not show unassigned cases in autocomplete search" do expect(page).not_to have_selector("#case-selection option", text: /#{Regexp.escape(unassigned_case.case_number)}/i) end it "does not show cases assigned to other volunteers in autocomplete search" do expect(page).not_to have_selector("#case-selection option", text: /#{Regexp.escape(other_volunteer_case.case_number)}/i) end end context "when logged in as a supervisor" do let(:volunteer) { volunteer_assigned_to_case } let(:supervisor) { create(:supervisor, casa_org: volunteer.casa_org) } include_context "when on the court reports page", :supervisor it_behaves_like "a user with organization-level case visibility in autocomplete" end context "when logged in as an admin" do let(:volunteer) { volunteer_assigned_to_case } let(:casa_admin) { create(:casa_admin, casa_org: volunteer.casa_org) } include_context "when on the court reports page", :casa_admin it_behaves_like "a user with organization-level case visibility in autocomplete" end end # rubocop:enable RSpec/MultipleMemoizedHelpers end ================================================ FILE: spec/system/case_groups/case_groups_spec.rb ================================================ require "rails_helper" RSpec.describe "Case Groups", :js, type: :system do let(:admin) { create(:casa_admin) } let(:organization) { admin.casa_org } it "create a case group" do casa_case1 = create(:casa_case) casa_case2 = create(:casa_case) sign_in admin visit case_groups_path click_on "New Case Group" fill_in "Name", with: "A family" find(".ts-control > input").click find("div.option", text: casa_case1.case_number).click find("div.option", text: casa_case2.case_number).click find("#case_group_name").click click_on "Submit" within "#case-groups" do expect(page).to have_text("A family") click_on "Edit", match: :first end fill_in "Name", with: "Another family" click_on "Submit" expect(page).to have_text("Another family") end it "remove from a case group" do casa_case1 = create(:casa_case) casa_case2 = create(:casa_case) sign_in admin visit case_groups_path click_on "New Case Group" fill_in "Name", with: "A family" find(".ts-control > input").click find("div.option", text: casa_case1.case_number).click find("div.option", text: casa_case2.case_number).click find("#case_group_name").click click_on "Submit" list_item_text = find_all("table li").map(&:text) expect(list_item_text.count).to be 2 expect(list_item_text[0]).to match casa_case1.case_number expect(list_item_text[1]).to match casa_case2.case_number within "#case-groups" do click_on "Edit", match: :first end case2_selector = find(".ts-control > div.item", text: casa_case2.case_number) within case2_selector do find("a").click end click_on "Submit" expect(page).to have_text(casa_case1.case_number) expect(page).not_to have_text(casa_case2.case_number) end it "does not create a case group if the name is not unique" do casa_case = create(:casa_case) sign_in admin visit case_groups_path click_on "New Case Group" fill_in "Name", with: "A family" find(".ts-control > input").click find("div.option", text: casa_case.case_number).click find("#case_group_name").click click_on "Submit" visit case_groups_path click_on "New Case Group" fill_in "Name", with: "A Family " find(".ts-control > input").click find("div.option", text: casa_case.case_number).click find("#case_group_name").click click_on "Submit" expect(page).to have_text("Name has already been taken") visit case_groups_path expect(page).to have_text("A family").once end end ================================================ FILE: spec/system/checklist_items/destroy_spec.rb ================================================ require "rails_helper" RSpec.describe "checklist_items/destroy", type: :system do let(:casa_admin) { create(:casa_admin) } let(:checklist_item) { create(:checklist_item) } let(:hearing_type) { create(:hearing_type, checklist_items: [checklist_item]) } before do sign_in casa_admin visit edit_hearing_type_path(hearing_type) end it "deletes checklist items", :aggregate_failures do click_on "Delete", match: :first expect(page).to have_text("Checklist item was successfully deleted.") expect(page).not_to have_text(checklist_item.category) expect(page).not_to have_text(checklist_item.description) click_on "Submit" current_date = Time.new.strftime("%m/%d/%Y") expect(page).to have_text("Updated #{current_date}") end end ================================================ FILE: spec/system/checklist_items/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "checklist_items/edit", type: :system do let(:casa_admin) { create(:casa_admin) } let(:hearing_type) { create(:hearing_type) } let(:checklist_item) { create(:checklist_item, hearing_type: hearing_type) } before do sign_in casa_admin visit edit_hearing_type_checklist_item_path(hearing_type, checklist_item) end it "edits with valid data", :aggregate_failures do fill_in "Category", with: "checklist item category EDIT" fill_in "Description", with: "checklist item description EDIT" check "Mandatory" click_on "Submit" expect(page).to have_text("Checklist item was successfully updated.") expect(page).to have_text("checklist item category EDIT") expect(page).to have_text("checklist item description EDIT") expect(page).to have_text("Yes") click_on "Submit" current_date = Time.new.strftime("%m/%d/%Y") expect(page).to have_text("Updated #{current_date}") end it "rejects with invalid data" do fill_in "Category", with: "" fill_in "Description", with: "" click_on "Submit" expect(page).to have_text("Edit this checklist item") end end ================================================ FILE: spec/system/checklist_items/new_spec.rb ================================================ require "rails_helper" RSpec.describe "checklist_items/new", type: :system do let(:casa_admin) { create(:casa_admin) } let(:hearing_type) { create(:hearing_type) } before do sign_in casa_admin visit new_hearing_type_checklist_item_path(hearing_type) end it "creates with valid data", :aggregate_failures do fill_in "Category", with: "checklist item category" fill_in "Description", with: "checklist item description" click_on "Submit" expect(page).to have_text("Checklist item was successfully created.") expect(page).to have_text("checklist item category") expect(page).to have_text("checklist item description") expect(page).to have_text("Optional") click_on "Submit" current_date = Time.new.strftime("%m/%d/%Y") expect(page).to have_text("Updated #{current_date}") end it "rejects with invalid data" do fill_in "Category", with: "" fill_in "Description", with: "" click_on "Submit" expect(page).to have_text("Add a new checklist item") end end ================================================ FILE: spec/system/components/truncated_text_component_spec.rb ================================================ require "rails_helper" RSpec.describe TruncatedTextComponent, type: :system do it "renders the component with the provided text", :js do visit("/rails/view_components/truncated_text_component/default") aggregate_failures do expect(page).to have_css("span", text: "Some Label") expect(page).to have_css(".truncation-container") expect(page).to have_css(".line-clamp-1") expect(page).to have_css("a", text: "[read more]") expect(page).to have_css("a", text: "[hide]", visible: false) end click_on "read more" aggregate_failures do expect(page).to have_css("span", text: "Some Label") expect(page).to have_no_css(".line-clamp-1") expect(page).to have_css("a", text: "[read more]", visible: false) expect(page).to have_css("a", text: "[hide]", visible: true) end end end ================================================ FILE: spec/system/contact_types/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "contact_types/edit", type: :system do let!(:organization) { create(:casa_org) } let!(:admin) { create(:casa_admin, casa_org_id: organization.id) } let!(:contact_type_group) { create(:contact_type_group, casa_org: organization, name: "Contact type group 1") } let!(:contact_type) { create(:contact_type, name: "Contact type 1") } before do sign_in admin visit edit_contact_type_path(contact_type) end it "errors with invalid name" do fill_in "Name", with: "" click_on "Submit" expect(page).to have_text("Name can't be blank") end it "creates with valid data" do fill_in "Name", with: "Edit Contact Type test" click_on "Submit" expect(page).to have_text("Contact Type was successfully updated.") end end ================================================ FILE: spec/system/contact_types/new_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "contact_types/new", type: :system do let!(:organization) { create(:casa_org) } let!(:admin) { create(:casa_admin, casa_org: organization) } let!(:contact_type_group) { create(:contact_type_group, casa_org: organization, name: "Contact type group 1") } before do sign_in admin visit new_contact_type_path end context "with valid data" do it "creates contact type successfully" do fill_in "Name", with: "New Contact Type test" click_on "Submit" expect(page).to have_text("Contact Type was successfully created.") end end context "with invalid data" do it "shows error when name is blank" do fill_in "Name", with: "" click_on "Submit" expect(page).to have_text("Name can't be blank") end it "shows error when name is not unique within group" do create(:contact_type, name: "Existing Name", contact_type_group:) fill_in "Name", with: "Existing Name" select "Contact type group 1", from: "contact_type_contact_type_group_id" click_on "Submit" expect(page).to have_text("Name should be unique per contact type group") end end end ================================================ FILE: spec/system/court_dates/edit_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "court_dates/edit", type: :system do let(:now) { Date.new(2021, 1, 1) } let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let(:volunteer) { create(:volunteer) } let(:supervisor) { create(:supervisor, casa_org: organization) } let!(:casa_case) { create(:casa_case, casa_org: organization) } let!(:past_court_date) { create(:court_date, :with_court_details, casa_case: casa_case, date: now - 1.week) } let!(:future_court_date) { create(:court_date, :with_court_details, casa_case: casa_case, date: now + 1.week) } before do travel_to now end context "as an admin" do before do sign_in admin visit casa_case_path(casa_case) click_on past_court_date.date.strftime("%B %-d, %Y") click_on "Edit" end it "shows court orders" do court_order = past_court_date.case_court_orders.first expect(page).to have_text(court_order.text) expect(page).to have_text(court_order.implementation_status.humanize) end it "adds a standard court order", :js do select("Family therapy", from: "Court Order Type") click_button("Add a court order") textarea = all("textarea.court-order-text-entry").last expect(textarea.value).to eq("Family therapy") end it "adds a custom court order", :js do click_button("Add a court order") textarea = all("textarea.court-order-text-entry").last expect(textarea.value).to eq("") end it "edits past court date", :js do expect(page).to have_text("Editing Court Date") expect(page).to have_text("Case Number:") expect(page).to have_text(casa_case.case_number) expect(page).to have_text("Add Court Date") expect(page).to have_field("court_date_date", with: "2020-12-25") expect(page).to have_text("Add Court Report Due Date") expect(page).to have_field("court_date_court_report_due_date") expect(page).to have_select("Judge") expect(page).to have_select("Hearing type") expect(page).to have_text("Court Orders - Please check that you didn't enter any youth names") expect(page).to have_text("Add a court order") page.find('button[data-action="court-order-form#add"]').click find("#court-orders-list-container").first("textarea").send_keys("Court Order Text One") within ".top-page-actions" do click_on "Update" end expect(page).to have_text("Court Order Text One") end it "allows deleting a future court date", :js do visit root_path click_on "Cases" click_on casa_case.case_number expect(page).to have_content past_court_date.date.strftime("%B %-d, %Y") expect(page).to have_content future_court_date.date.strftime("%B %-d, %Y") page.find("a", text: future_court_date.date.strftime("%B %-d, %Y")).click accept_alert "Are you sure?" do page.find("a", text: "Delete Future Court Date").click end expect(page).to have_content "Court date was successfully deleted" expect(page).to have_content past_court_date.date.strftime("%B %-d, %Y") expect(page).not_to have_content future_court_date.date.strftime("%B %-d, %Y") end end context "as a supervisor" do it "allows deleting a future court date", :js do sign_in supervisor visit root_path click_on "Cases" click_on casa_case.case_number expect(page).to have_content past_court_date.date.strftime("%B %-d, %Y") expect(page).to have_content future_court_date.date.strftime("%B %-d, %Y") page.find("a", text: future_court_date.date.strftime("%B %-d, %Y")).click accept_alert "Are you sure?" do page.find("a", text: "Delete Future Court Date").click end expect(page).to have_content "Court date was successfully deleted." expect(page).to have_content past_court_date.date.strftime("%B %-d, %Y") expect(page).not_to have_content future_court_date.date.strftime("%B %-d, %Y") end end context "as a volunteer" do it "does not allow deleting a future court date", :js do volunteer.casa_cases = [casa_case] sign_in volunteer visit root_path click_on "Cases" click_on casa_case.case_number expect(page).to have_content past_court_date.date.strftime("%B %-d, %Y") expect(page).to have_content future_court_date.date.strftime("%B %-d, %Y") page.find("a", text: future_court_date.date.strftime("%B %-d, %Y")).click expect(page).not_to have_content "Delete Future Court Date" end end end ================================================ FILE: spec/system/court_dates/new_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "court_dates/new", type: :system do let(:now) { Date.new(2021, 1, 2) } let(:casa_org) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let!(:casa_case) { create(:casa_case, casa_org: casa_org) } let!(:court_date) { create(:court_date, :with_court_details, casa_case: casa_case, date: now - 1.week) } let!(:judge) { create(:judge) } let!(:hearing_type) { create(:hearing_type) } let(:text) { Faker::Lorem.paragraph(sentence_count: 2) } before do travel_to now sign_in admin visit casa_case_path(casa_case) click_link("Add a court date") end context "when all fields are filled" do it "is successful", :js do expect(page).to have_content(casa_case.case_number) fill_in "court_date_date", with: now fill_in "court_date_court_report_due_date", with: now select judge.name, from: "Judge" select hearing_type.name, from: "Hearing type" click_on "Add a court order" text_area = first(:css, "textarea").native text_area.send_keys(text) page.find("select.implementation-status").find(:option, text: "Partially implemented").select_option within ".top-page-actions" do click_on "Create" end expect(page).to have_content("Court date was successfully created.") expect(page).to have_content(casa_case.case_number) expect(page).to have_content("Court Report Due Date:\nJanuary 2, 2021") expect(page).to have_content(judge.name) expect(page).to have_content(hearing_type.name) expect(page).to have_content(text) expect(page).to have_content("Partially implemented") end end context "without changing default court date" do it "does create a new court_date" do within ".top-page-actions" do click_on "Create" end expect(page).to have_content("Court date was successfully created.") end end end ================================================ FILE: spec/system/court_dates/view_spec.rb ================================================ require "rails_helper" RSpec.describe "court_dates/edit", type: :system do let(:organization) { create(:casa_org) } let(:now) { Date.new(2021, 1, 1) } let(:displayed_date_format) { "%B %-d, %Y" } let(:casa_case_number) { "CASA-CASE-NUMBER" } let!(:casa_case) { create(:casa_case, casa_org: organization, case_number: casa_case_number) } let(:court_date_as_date_object) { now + 1.week } let(:court_report_due_date) { now + 2.weeks } let!(:court_date) { create(:court_date, casa_case: casa_case, court_report_due_date: court_report_due_date, date: court_date_as_date_object) } before do travel_to now end shared_examples "a user able to view court date" do |user_type| let(:user) { create(user_type, casa_org: organization) } it "can visit the court order page" do if user_type === :volunteer user.casa_cases << casa_case end sign_in user visit casa_case_court_date_path(casa_case, court_date) expect(page).not_to have_text "Sorry, you are not authorized to perform this action." end it "displays all information associated with the court date correctly" do if user_type === :volunteer user.casa_cases << casa_case end sign_in user visit casa_case_court_date_path(casa_case, court_date) expect(page).to have_text court_date_as_date_object.strftime(displayed_date_format) expect(page).to have_text court_report_due_date.strftime(displayed_date_format) expect(page).to have_text casa_case_number court_date_court_orders = find(:xpath, "//h6[text()='Court Orders:']/following-sibling::p[1]") expect(court_date_court_orders).to have_text("There are no court orders associated with this court date.") court_date_hearing_type = find(:xpath, "//dt[h6[text()='Hearing Type:']]/following-sibling::dd[1]") expect(court_date_hearing_type).to have_text("None") court_date_judge = find(:xpath, "//dt[h6[text()='Judge:']]/following-sibling::dd[1]") expect(court_date_judge).to have_text("None") court_order = create(:case_court_order, casa_case: casa_case) hearing_type = create(:hearing_type) judge = create(:judge) court_date.case_court_orders << court_order court_date.hearing_type = hearing_type court_date.judge = judge court_date.save! visit current_path expect(page).to have_text court_order.text expect(page).to have_text hearing_type.name expect(page).to have_text judge.name end end context "as a user from an organization not containing the court date" do let(:other_organization) { create(:casa_org) } xit "does not allow the user to view the court date" do # TODO the app or browser can't gracefully handle the URL sign_in create(:casa_admin, casa_org: other_organization) visit casa_case_court_date_path(casa_case, court_date) expect(page).to have_text "Sorry, you are not authorized to perform this action." end end context "as a user under the same org as the court date" do context "as a volunteer not assigned to the case associated with the court date" do let(:volunteer_not_assigned_to_case) { create(:volunteer, casa_org: organization) } it "does not allow the user to view the court date" do sign_in volunteer_not_assigned_to_case visit casa_case_court_date_path(casa_case, court_date) expect(page).to have_text "Sorry, you are not authorized to perform this action." end end context "as a volunteer assigned to the case associated with the court date" do it_should_behave_like "a user able to view court date", :volunteer end context "as a supervisor belonging to the same org as the case associated with the court date" do it_should_behave_like "a user able to view court date", :supervisor end context "as an admin belonging to the same org as the case associated with the court date" do it_should_behave_like "a user able to view court date", :casa_admin end end end ================================================ FILE: spec/system/dashboard/show_spec.rb ================================================ require "rails_helper" RSpec.describe "dashboard/show", type: :system do let(:volunteer) { create(:volunteer, display_name: "Bob Loblaw") } let(:casa_admin) { create(:casa_admin, display_name: "John Doe") } context "volunteer user" do before do sign_in volunteer end it "sees all their casa cases" do casa_case_1 = build(:casa_case, active: true, casa_org: volunteer.casa_org, case_number: "CINA-1") casa_case_2 = build(:casa_case, active: true, casa_org: volunteer.casa_org, case_number: "CINA-2") casa_case_3 = build(:casa_case, active: true, casa_org: volunteer.casa_org, case_number: "CINA-3") create(:case_assignment, volunteer: volunteer, casa_case: casa_case_1) create(:case_assignment, volunteer: volunteer, casa_case: casa_case_2) visit casa_cases_path expect(page).to have_text("My Cases") expect(page).to have_text(casa_case_1.case_number) expect(page).to have_text(casa_case_2.case_number) expect(page).not_to have_text(casa_case_3.case_number) end it "volunteer does not see his name in Cases table" do casa_case = build(:casa_case, active: true, casa_org: volunteer.casa_org, case_number: "CINA-1") create(:case_assignment, volunteer: volunteer, casa_case: casa_case) visit casa_cases_path expect(page).not_to have_css("td", text: "Bob Loblaw") end it "displays 'No active cases' when they don't have any assignments", :js do visit casa_cases_path expect(page).to have_text("My Cases") expect(page).not_to have_css("td", text: "Bob Loblaw") expect(page).not_to have_text("Detail View") end end context "admin user" do before do sign_in casa_admin end it "sees volunteer names in Cases table as a link" do casa_case = build(:casa_case, active: true, casa_org: volunteer.casa_org, case_number: "CINA-1") create(:case_assignment, volunteer: volunteer, casa_case: casa_case) visit casa_cases_path expect(page).to have_text("Bob Loblaw") expect(page).to have_link("Bob Loblaw") expect(page).to have_css("td", text: "Bob Loblaw") end end end ================================================ FILE: spec/system/deep_link/deep_link_spec.rb ================================================ require "rails_helper" RSpec.describe "deep_link", type: :system do describe "when user recieves a deep link" do %w[volunteer supervisor casa_admin].each do |user_type| let(:user) { create(user_type.to_sym) } it "redirects #{user_type} to target url immediately after sign in" do visit "/users/edit" fill_in "Email", with: user.email fill_in "Password", with: "12345678" within ".actions" do find("#log-in").click end expect(page).to have_current_path "/users/edit", ignore_query: true expect(page).to have_text "Edit Profile" end end context "when user is a volunteer or supervisor" do %w[volunteer supervisor].each do |user_type| let(:user) { create(user_type.to_sym) } it "flashes unauthorized notice when #{user_type} tries to access a casa_admin link" do visit "/casa_admins" fill_in "Email", with: user.email fill_in "Password", with: "12345678" within ".actions" do find("#log-in").click end expect(page).to have_text "Sorry, you are not authorized to perform this action." end end let(:volunteer) { create(:volunteer) } it "flashes unauthorized notice when volunteer tries to access a supervisor link" do visit "/supervisors" fill_in "Email", with: volunteer.email fill_in "Password", with: "12345678" within ".actions" do find("#log-in").click end expect(page).to have_text "Sorry, you are not authorized to perform this action." end end end end ================================================ FILE: spec/system/devise/passwords/new_spec.rb ================================================ require "rails_helper" RSpec.describe "users/passwords/new", type: :system do before do visit new_user_session_path click_on "Forgot your password?" end describe "reset password page" do it "displays error messages for non-existent user" do user = build(:user, email: "glados@example.com", phone_number: "+16578900012") fill_in "Email", with: "tangerine@example.com" fill_in "Phone number", with: user.phone_number click_on "Send me reset password instructions" expect(page).to have_content "If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes." end it "displays phone number error messages for incorrect formatting" do user = create(:user, email: "glados@example.com", phone_number: "+16578900012") fill_in "Email", with: user.email fill_in "Phone number", with: "2134567eee" click_on "Send me reset password instructions" expect(page).to have_content "1 error prohibited this User from being saved:" expect(page).to have_text("Phone number must be 10 digits or 12 digits including country code (+1)") end it "displays error if user tries to submit an empty form" do click_on "Send me reset password instructions" expect(page).to have_text("Please enter at least one field.") end it "redirects to sign up page for email" do user = build(:user, email: "glados@example.com", phone_number: "+16578900012") fill_in "Email", with: user.email click_on "Send me reset password instructions" expect(page).to have_content "If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes." end end describe "reset password email" do let!(:user) { create(:user, type: "Volunteer", email: "glados@aperture.labs") } it "sends user email" do fill_in "Email", with: user.email click_on "Send me reset password instructions" expect(page).to have_content "If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes." expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.to).to eq([user.email]) end it "has reset password url with token" do fill_in "Email", with: user.email click_on "Send me reset password instructions" expect(page).to have_content "If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes." expect(reset_password_link(user.email)).to match(/http:\/\/localhost:3000\/users\/password\/edit\?reset_password_token=.*/) end it "url token matches user's encrypted token" do fill_in "Email", with: user.email click_on "Send me reset password instructions" expect(page).to have_content "If the account exists you will receive an email or SMS with instructions on how to reset your password in a few minutes." token = reset_password_link(user.email).gsub("http://localhost:3000/users/password/edit?reset_password_token=", "") encrypted_token = Devise.token_generator.digest(User, :reset_password_token, token) expect(User.find_by(reset_password_token: encrypted_token)).to be_present end it "user can update password" do fill_in "Email", with: user.email click_on "Send me reset password instructions" visit reset_password_link(user.email) fill_in "New password", with: "new password" fill_in "Confirm new password", with: "new password" click_on "Change my password" expect(page).to have_text("Your password has been changed successfully.") fill_in "Email", with: user.email fill_in "Password", with: "new password" click_on "Log In" expect(page).to have_text(user.display_name) expect(page).to have_text("My Cases") expect(page).not_to have_text("Sign in") end end end def reset_password_link(email_address) email = open_email(email_address) links = links_in_email(email) links[2] end ================================================ FILE: spec/system/emancipations/show_spec.rb ================================================ require "rails_helper" RSpec.describe "emancipations/show", type: :system do let(:org) { build(:casa_org) } let(:volunteer) { build(:volunteer, casa_org: org) } let(:supervisor) { create(:supervisor, casa_org: org) } let(:casa_case) { build(:casa_case, casa_org: org) } let!(:case_assignment) { create(:case_assignment, volunteer: volunteer, casa_case: casa_case) } it "has a download emancipation checklist button" do sign_in volunteer visit casa_case_emancipation_path(casa_case) expect(page).to have_link "Download Checklist", href: casa_case_emancipation_path(casa_case, format: :docx) end it "expands the emancipation checklist options", :js do emancipation_category = create(:emancipation_category) emancipation_option = create(:emancipation_option, emancipation_category: emancipation_category) sign_in supervisor visit casa_case_emancipation_path(casa_case) find(".category-collapse-icon").click expect(page).to have_content(emancipation_option.name) find(".category-collapse-icon").click expect(page).not_to have_content(emancipation_option.name) end end ================================================ FILE: spec/system/hearing_types/new_spec.rb ================================================ require "rails_helper" RSpec.describe "hearing_types/new", type: :system do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org_id: organization.id) } let(:hearing_type) { build_stubbed(:hearing_type, casa_org: organization, name: "Spec Test Hearing Type") } before do sign_in admin visit new_hearing_type_path end it "errors with invalid name" do fill_in "Name", with: "" click_on "Submit" expect(page).to have_text("Name can't be blank") end it "creates with valid data" do fill_in "Name", with: "Emergency Hearing Type" click_on "Submit" expect(page).to have_text("Hearing Type was successfully created.") end end ================================================ FILE: spec/system/imports/index_spec.rb ================================================ require "rails_helper" RSpec.describe "imports/index", type: :system do context "as a volunteer" do it "redirects the user with an error message" do volunteer = create(:volunteer) sign_in volunteer visit imports_path expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.") end end context "as an admin" do context "import volunteer csv with phone numbers", :js do it "shows sms opt in modal" do import_file_path = file_fixture "volunteers.csv" admin = create(:casa_admin) sign_in admin visit imports_path(:volunteer) expect(page).to have_content("Import Volunteers") expect(page).to have_button("volunteer-import-button", disabled: true) attach_file "volunteer-file", import_file_path click_button "volunteer-import-button" expect(page).to have_text("SMS Opt In") expect(page).to have_button("sms-opt-in-continue-button", disabled: true) check "sms-opt-in-checkbox" click_button "sms-opt-in-continue-button" expect(page).to have_text("You successfully imported") end end context "import volunteer csv without phone numbers", :js do it "shows successful import" do import_file_path = file_fixture "volunteers_without_phone_numbers.csv" admin = create(:casa_admin) sign_in admin visit imports_path(:volunteer) expect(page).to have_content("Import Volunteers") attach_file "volunteer-file", import_file_path click_button "volunteer-import-button" expect(page).to have_text("You successfully imported") end end context "import volunteer csv without display names", :js do it "shows failed import modal" do import_file_path = file_fixture "volunteers_without_display_names.csv" admin = create(:casa_admin) sign_in admin visit imports_path(:volunteer) expect(page).to have_content("Import Volunteers") attach_file "volunteer-file", import_file_path click_button "volunteer-import-button" check "sms-opt-in-checkbox" click_button "sms-opt-in-continue-button" expect(page).to have_text("CSV Import Error") end end context "import supervisors csv with phone numbers", :js do it "shows sms opt in modal" do import_file_path = file_fixture "supervisors.csv" admin = create(:casa_admin) sign_in admin visit imports_path click_on "supervisor-tab" expect(page).to have_content("Import Supervisors") expect(page).to have_button("supervisor-import-button", disabled: true) attach_file "supervisor-file", import_file_path click_button "supervisor-import-button" expect(page).to have_text("SMS Opt In") expect(page).to have_button("sms-opt-in-continue-button", disabled: true) find("#sms-opt-in-checkbox", visible: true).check click_button "sms-opt-in-continue-button" expect(page).to have_text("You successfully imported") end end context "import supervisors csv without phone numbers", :js do it "shows successful import" do import_file_path = file_fixture "supervisors_without_phone_numbers.csv" admin = create(:casa_admin) sign_in admin visit imports_path click_on "Import Supervisors" expect(page).to have_content("Import Supervisors") attach_file "supervisor-file", import_file_path click_button "supervisor-import-button" expect(page).to have_text("You successfully imported") end end context "import supervisors csv without display names", :js do it "shows failed import modal" do import_file_path = file_fixture "supervisors_without_display_names.csv" admin = create(:casa_admin) sign_in admin visit imports_path click_on "Import Supervisors" expect(page).to have_content("Import Supervisors") attach_file "supervisor-file", import_file_path click_button "supervisor-import-button" check "sms-opt-in-checkbox" click_button "sms-opt-in-continue-button" expect(page).to have_text("CSV Import Error") end end end end ================================================ FILE: spec/system/judges/new_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "judges/new", type: :system do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org_id: organization.id) } let(:active_name) { Faker::Name.unique.name } let(:inactive_name) { Faker::Name.unique.name } before do sign_in admin visit new_judge_path end # rubocop:disable RSpec/ExampleLength it "creates an active judge with valid name", :aggregate_failures do submit_judge_form(name: active_name, active: true) expect(page).to have_text("Judge was successfully created.") expect(page).to have_text(active_name) judge = Judge.find_by(name: active_name) expect(judge).not_to be_nil expect(judge.active).to be true end # rubocop:enable RSpec/ExampleLength # rubocop:disable RSpec/ExampleLength it "creates an inactive judge with valid name", :aggregate_failures do submit_judge_form(name: inactive_name, active: false) expect(page).to have_text("Judge was successfully created.") expect(page).to have_text(inactive_name) judge = Judge.find_by(name: inactive_name) expect(judge).not_to be_nil expect(judge.active).to be false end # rubocop:enable RSpec/ExampleLength # rubocop:disable RSpec/ExampleLength it "creates a judge with a very long name", :aggregate_failures do long_name = Faker::Lorem.characters(number: 255) submit_judge_form(name: long_name) expect(page).to have_text("Judge was successfully created.") expect(page).to have_text(long_name) judge = Judge.find_by(name: long_name) expect(judge).not_to be_nil end # rubocop:enable RSpec/ExampleLength # rubocop:disable RSpec/ExampleLength it "creates a judge with special characters in the name", :aggregate_failures do special_name = "#{Faker::Lorem.characters(number: 30, min_alpha: 10, min_numeric: 5)}!@#$%^&*()" submit_judge_form(name: special_name) expect(page).to have_text("Judge was successfully created.") expect(page).to have_text(special_name) judge = Judge.find_by(name: special_name) expect(judge).not_to be_nil end # rubocop:enable RSpec/ExampleLength context "when validations fail" do it "shows validation error when name is blank" do submit_judge_form(name: "") expect(page).to have_text("Name can't be blank") end it "does not allow duplicate judge names in the same organization", :aggregate_failures do duplicate_name = Faker::Name.unique.name create(:judge, name: duplicate_name, casa_org: organization) submit_judge_form(name: duplicate_name) expect(page).to have_text("Name has already been taken") expect(Judge.where(name: duplicate_name, casa_org: organization).count).to eq 1 end end private def submit_judge_form(name:, active: true) fill_in "Name", with: name active ? check("Active?") : uncheck("Active?") click_on "Submit" end end ================================================ FILE: spec/system/languages/languages_spec.rb ================================================ require "rails_helper" RSpec.describe "languages/new", type: :system do let(:admin) { create(:casa_admin) } let(:organization) { admin.casa_org } before do sign_in admin visit new_language_path end it "requires name text field" do expect(page).to have_selector("input[required=required]", id: "language_name") end end ================================================ FILE: spec/system/learning_hours/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "learning_hours/edit", type: :system do let(:organization) { create(:casa_org) } let(:volunteer) { create(:volunteer, casa_org_id: organization.id) } let(:learning_hours) { create(:learning_hour, user: volunteer) } before do sign_in volunteer visit edit_learning_hour_path(learning_hours) end it "shows error message when future date entered" do datepicker_input = find("#learning_hour_occurred_at") datepicker_input.set((Date.today + 1.month).strftime("%Y-%m-%d")) click_on "Update Learning Hours Entry" expect(page).to have_text("Date cannot be in the future") end it "can update learning hours entry with proper data" do title = "Updated Title" expect(page).to have_field("Learning Hours Title", with: learning_hours.name) fill_in "Learning Hours Title", with: title click_on "Update Learning Hours Entry" expect(page).to have_text("Entry was successfully updated.") expect(page).to have_text(title) end end ================================================ FILE: spec/system/learning_hours/index_spec.rb ================================================ require "rails_helper" RSpec.describe "Learning Hours Index", type: :system do let!(:supervisor) { create(:supervisor, :with_volunteers) } let!(:volunteer) { supervisor.volunteers.first } let!(:learning_hours) { create_list(:learning_hour, 2, user: volunteer) } before do sign_in user end context "when the user is a volunteer" do let(:user) { volunteer } it "displays the volunteer learning hours" do visit learning_hours_path expect(page).to have_content("Learning Hours") expect(page).to have_content("Title") expect(page).to have_content("Time Spent") expect(page).to have_link("Record Learning Hours", href: new_learning_hour_path) end end context "when the user is a supervisor or admin" do let(:user) { supervisor } before do visit learning_hours_path end it "displays a list of volunteers and the learning hours they completed", :js do expect(page).to have_content("Learning Hours") expect(page).to have_content("Volunteer") expect(page).to have_content(volunteer.display_name) expect(page).to have_content("Time Completed") expect(page).to have_content("#{volunteer.learning_hours.sum(:duration_hours)} hours") end it "when clicking on a volunteer's name it redirects to the `learning_hours_volunteer_path` for the volunteer" do click_on volunteer.display_name expect(page).to have_current_path(learning_hours_volunteer_path(volunteer.id)) end RSpec.shared_examples_for "functioning sort buttons" do it "sorts table columns" do expect(page).to have_selector("tr:nth-child(1)", text: expected_first_ordered_value) find("th", text: column_to_sort).click expect(page).to have_selector("th.sorting_asc", text: column_to_sort) expect(page).to have_selector("tr:nth-child(1)", text: expected_last_ordered_value) end end it "shows pagination", :js do expect(page).to have_content("Previous") expect(page).to have_content("Next") end end end ================================================ FILE: spec/system/learning_hours/new_spec.rb ================================================ require "rails_helper" RSpec.describe "learning_hours/new", :js, type: :system do let(:organization) { create(:casa_org) } let(:volunteer) { create(:volunteer, casa_org_id: organization.id) } before do create(:learning_hour_type, casa_org: organization, name: "Book") sign_in volunteer visit new_learning_hour_path end it "errors without selected type of learning" do fill_in "Learning Hours Title", with: "Test title" fill_in "Hour(s)", with: "0" fill_in "Minute(s)", with: "30" click_on "Create New Learning Hours Entry" expect(page).to have_text("Learning hour type must exist") end it "creates learning hours entry with valid data" do fill_in "Learning Hours Title", with: "Test title" select "Book", from: "Type of Learning" fill_in "Hour(s)", with: "0" fill_in "Minute(s)", with: "30" click_on "Create New Learning Hours Entry" expect(page).to have_text("New entry was successfully created.") end it "creates learning hours entry without minutes duration" do fill_in "Learning Hours Title", with: "Test title" select "Book", from: "Type of Learning" fill_in "Hour(s)", with: "3" click_on "Create New Learning Hours Entry" expect(page).to have_text("New entry was successfully created.") end it "creates learning hours entry without hours duration" do fill_in "Learning Hours Title", with: "Test title" select "Book", from: "Type of Learning" fill_in "Minute(s)", with: "30" click_on "Create New Learning Hours Entry" expect(page).to have_text("New entry was successfully created.") end it "errors without hours and minutes duration" do fill_in "Learning Hours Title", with: "Test title" select "Book", from: "Type of Learning" click_on "Create New Learning Hours Entry" expect(page).to have_text("Duration minutes and hours (total duration) must be greater than 0") end it "errors if occured on date set in the future" do fill_in "Learning Hours Title", with: "Test title" select "Book", from: "Type of Learning" fill_in "Hour(s)", with: "2" fill_in "Minute(s)", with: "30" fill_in "Occurred On", with: Date.tomorrow click_on "Create New Learning Hours Entry" expect(page).to have_text("Date cannot be in the future") end end ================================================ FILE: spec/system/learning_hours/volunteers/show_spec.rb ================================================ require "rails_helper" RSpec.describe "LearningHours::Volunteers #show", type: :system do let!(:volunteer) { create(:volunteer) } let!(:supervisor) { create(:supervisor) } let!(:learning_hours) { create_list(:learning_hour, 5, user: volunteer) } before do sign_in user end context "when the user is a volunteer" do let(:user) { volunteer } it "cannot access this page" do visit learning_hours_volunteer_path(volunteer.id) expect(page).to have_content("Sorry, you are not authorized to perform this action.") end end context "when the user is a supervisor or admin" do let(:user) { supervisor } before do visit learning_hours_volunteer_path(volunteer.id) end it "displays the volunteer's name" do expect(page).to have_content("#{volunteer.display_name}'s Learning Hours") end it "displays the volunteer's first learning hours", :js do expect(page).to have_content(learning_hours.first.name) expect(page).to have_content(learning_hours.first.occurred_at.strftime("%B %-d, %Y")) end it "displays the volunteer's last learning hours", :js do expect(page).to have_content(learning_hours.last.name) expect(page).to have_content(learning_hours.last.occurred_at.strftime("%B %-d, %Y")) end end end ================================================ FILE: spec/system/mileage_rates/mileage_rates_spec.rb ================================================ require "rails_helper" RSpec.describe "mileage_rates/new", :js, type: :system do let(:admin) { create(:casa_admin) } let(:organization) { admin.casa_org } before do sign_in admin visit mileage_rates_path end it "add new mileage rate" do click_on "New Mileage Rate" expect(page).to have_text("New Mileage Rate") fill_in "Effective date", with: Date.new(2020, 1, 2) fill_in "Amount", with: 1.35 uncheck "Currently active?" click_on "Save Mileage Rate" expect(page).to have_text("Mileage Rates") expect(page).to have_text("Effective date") expect(page).to have_text("January 2, 2020") expect(page).to have_text("Amount") expect(page).to have_text("$1.35") expect(page).to have_text("Active?") expect(page).to have_text("No") expect(page).to have_text("Actions") expect(page).to have_text("Edit") end end ================================================ FILE: spec/system/notifications/index_spec.rb ================================================ require "rails_helper" RSpec.describe "notifications/index", :js, type: :system do let(:admin) { create(:casa_admin) } let(:volunteer) { build(:volunteer) } let(:case_contact) { create(:case_contact, creator: volunteer) } let(:casa_case) { case_contact.casa_case } before { casa_case.assigned_volunteers << volunteer } context "FollowupResolvedNotifier" do let(:notification_message) { "#{volunteer.display_name} resolved a follow up. Click to see more." } let!(:followup) { create(:followup, creator: admin, case_contact: case_contact) } before do sign_in volunteer visit case_contacts_path click_button "Resolve Reminder" has_button?("Make Reminder") end it "lists my notifications" do sign_in admin visit notifications_path expect(page).to have_text(notification_message) expect(page).to have_text("Followup resolved") expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") end context "when volunteer changes its name" do let(:created_by_name) { "Foo bar" } let(:new_notification_message) { "#{created_by_name} resolved a follow up. Click to see more." } it "lists notifications showing it's current name" do visit edit_users_path fill_in "Display name", with: created_by_name click_on "Update Profile" expect(page).to have_content "Profile was successfully updated" sign_in admin visit notifications_path expect(page).to have_text(new_notification_message) expect(page).not_to have_text(notification_message) expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") end end end context "FollowupNotifier", :js do let(:note) { "Lorem ipsum dolor sit amet." } let(:notification_message_heading) { "#{admin.display_name} has flagged a Case Contact that needs follow up." } let(:notification_message_more_info) { "Click to see more." } let(:inline_notification_message) { "#{notification_message_heading} #{notification_message_more_info}" } before do sign_in admin visit casa_case_path(casa_case) end context "when followup has a note" do before do click_button "Make Reminder" find(".swal2-textarea").set(note) click_button "Confirm" end it "lists followup notifications, showing their note" do within("#resolve", wait: 5) do expect(page).to have_content "Resolve Reminder" end sign_in volunteer visit notifications_path expect(page).to have_text(notification_message_heading) expect(page).to have_text(note) expect(page).to have_text(notification_message_more_info) expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") expect(page).to have_text("New followup") end end context "when followup doesn't have a note" do before do click_button "Make Reminder" click_button "Confirm" end it "lists followup notifications, showing the information in a single line when there are no notes" do within("#resolve", wait: 5) do expect(page).to have_content "Resolve Reminder" end sign_in volunteer visit notifications_path expect(page).to have_text(inline_notification_message) expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") expect(page).to have_text("New followup") end end context "when admin changes its name" do let(:created_by_name) { "Foo bar" } let(:new_notification_message) { "#{created_by_name} has flagged a Case Contact that needs follow up." } before do click_button "Make Reminder" end it "lists followup notifications showing admin current name" do click_button "Confirm" within("#resolve", wait: 5) do expect(page).to have_content "Resolve Reminder" end visit edit_users_path fill_in "Display name", with: created_by_name click_on "Update Profile" expect(page).to have_content "Profile was successfully updated" sign_in volunteer visit notifications_path expect(page).to have_text(new_notification_message) expect(page).not_to have_text(inline_notification_message) expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") expect(page).to have_text("New followup") end end end context "EmancipationChecklistReminder" do let(:notifier) { create(:emancipation_checklist_reminder_notifier, params: {casa_case: casa_case}) } let(:notification) { create(:notification, :emancipation_checklist_reminder, event: notifier) } before do volunteer.notifications << notification sign_in volunteer visit notifications_path end it "displays a notification reminder that links to the emancipation checklist" do notification_message = "Your case #{casa_case.case_number} is a transition aged youth. We want to make sure that along the way, we’re preparing our youth for emancipation. Make sure to check the emancipation checklist." expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") expect(page).to have_content("Emancipation Checklist Reminder") expect(page).to have_link(notification_message, href: mark_as_read_notification_path(notification)) end end context "YouthBirthdayNotifier" do let(:notifier) { create(:youth_birthday_notifier, params: {casa_case: casa_case}) } let(:notification) { create(:notification, :youth_birthday, event: notifier) } before do volunteer.notifications << notification sign_in volunteer visit notifications_path end it "displays a notification on the notifications page" do notification_message = "Your youth, case number: #{casa_case.case_number} has a birthday next month." expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") expect(page).to have_content("Youth Birthday Notification") expect(page).to have_link(notification_message, href: mark_as_read_notification_path(notification)) end end context "ReimbursementCompleteNotifier" do it "displays a notification on the notifications page" do case_contact = create(:case_contact, :wants_reimbursement, casa_case: volunteer.casa_cases.first) notifier = create(:reimbursement_complete_notifier, params: {case_contact: case_contact}) notification = create(:notification, :reimbursement_complete, event: notifier) volunteer.notifications << notification sign_in volunteer visit notifications_path notification_message = "Volunteer #{case_contact.creator.display_name}'s request for reimbursement for " \ "#{case_contact.miles_driven}mi on #{case_contact.occurred_at_display} has been processed and is " \ "en route." expect(page).not_to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") expect(page).to have_content("Reimbursement Approved") expect(page).to have_content(notification_message) expect(page).to have_link(href: mark_as_read_notification_path(notification)) end end context "when there are no notifications" do it "displays a message to the user" do sign_in volunteer visit notifications_path expect(page).to have_text("You currently don't have any notifications. Notifications are generated when someone requests follow-up on a case contact.") end end end ================================================ FILE: spec/system/other_duties/new_spec.rb ================================================ require "rails_helper" RSpec.describe "other_duties/new", type: :system do let(:casa_org) { build(:casa_org) } let(:admin) { create(:casa_admin, casa_org: casa_org) } let(:case_number) { "12345" } let!(:next_year) { (Date.today.year + 1).to_s } let(:court_date) { 21.days.from_now } let(:organization) { build(:casa_org) } let(:volunteer) { create(:volunteer, :with_casa_cases, casa_org: organization) } before do sign_in volunteer visit root_path end context "as a volunteer", :js do it "sees a New Duty link" do visit other_duties_path expect(page).to have_link("New Duty", href: new_other_duty_path) end it "sees all their other duties", :js do volunteer_2 = create(:volunteer, display_name: "Other Volunteer") other_duty_1 = create(:other_duty, notes: "Test 1", creator_id: volunteer.id) other_duty_2 = create(:other_duty, notes: "Test 2", creator_id: volunteer.id) other_duty_3 = create(:other_duty, notes: "Test 3", creator_id: volunteer_2.id) visit other_duties_path expect(page).to have_text("Other Duties") expect(page).to have_text(other_duty_1.notes) expect(page).to have_text(other_duty_2.notes) expect(page).not_to have_text(other_duty_3.notes) end it "has an error if a new duty is attempted to be created without any notes" do click_on "Other Duties" click_on "New Duty" click_on "Submit" message = page.find("#other_duty_notes").native.attribute("validationMessage") expect(message).to match(/Please fill (in|out) this field./) end end end ================================================ FILE: spec/system/placements/destroy_spec.rb ================================================ require "rails_helper" RSpec.describe "placements/destroy", type: :system do let(:now) { Date.new(2025, 1, 2) } let(:casa_org) { create(:casa_org, :with_placement_types) } let(:admin) { create(:casa_admin, casa_org:) } let(:casa_case) { create(:casa_case, casa_org:, case_number: "123") } let(:placement_type) { create(:placement_type, name: "Reunification", casa_org:) } let(:placement) { create(:placement, placement_started_at: "2024-08-15 20:40:44 UTC", casa_case:, placement_type:) } before do travel_to now sign_in admin visit casa_case_placements_path(casa_case, placement) click_on "Delete" end it "does not delete on modal close" do expect(page).to have_text("Delete Placement?") click_on "Close" expect(page).to have_text("Reunification") expect(page).to have_text("August 15, 2024 - Present") end it "deletes placement" do expect(page).to have_text("Delete Placement?") click_on "Confirm" expect(page).to have_text("Placement was successfully deleted.") expect(page).not_to have_text("Reunification") expect(page).not_to have_text("August 15, 2024 - Present") end end ================================================ FILE: spec/system/placements/edit_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "placements/edit", type: :system do let(:now) { Date.new(2025, 1, 2) } let(:casa_org) { create(:casa_org, :with_placement_types) } let(:admin) { create(:casa_admin, casa_org:) } let(:casa_case) { create(:casa_case, casa_org:, case_number: "123") } let(:placement_type) { create(:placement_type, name: "Reunification", casa_org:) } let(:placement) { create(:placement, placement_started_at: "2024-08-15 20:40:44 UTC", casa_case:, placement_type:) } before do travel_to now sign_in admin visit casa_case_placement_path(casa_case, placement) click_link("Edit") end it "updates placement with valid form data", :js do expect(page).to have_content("123") fill_in "Placement Started At", with: now - 5.years select "Kinship", from: "Placement Type" click_on "Update" expect(page).to have_content("Placement was successfully updated.") expect(page).to have_content("123") expect(page).to have_content("January 2, 2020") expect(page).to have_content("Kinship") end it "rejects placement update with invalid form data" do fill_in "Placement Started At", with: 1000.years.ago click_on "Update" expect(page).to have_content("1 error prohibited this Placement from being saved:\nPlacement started at cannot be prior to 1/1/1989.") end end ================================================ FILE: spec/system/placements/index_spec.rb ================================================ require "rails_helper" RSpec.describe "placements", type: :system do let(:now) { Date.new(2025, 1, 2) } let(:casa_org) { create(:casa_org, :with_placement_types) } let(:admin) { create(:casa_admin, casa_org:) } let(:casa_case) { create(:casa_case, casa_org:, case_number: "123") } let(:placement_current) { create(:placement_type, name: "Reunification", casa_org:) } let(:placement_prev) { create(:placement_type, name: "Kinship", casa_org:) } let(:placement_first) { create(:placement_type, name: "Adoption", casa_org:) } let(:placements) do [ create(:placement, placement_started_at: "2024-08-15 20:40:44 UTC", casa_case:, placement_type: placement_current), create(:placement, placement_started_at: "2023-06-02 00:00:00 UTC", casa_case:, placement_type: placement_prev), create(:placement, placement_started_at: "2021-12-25 10:10:10 UTC", casa_case:, placement_type: placement_first) ] end before do travel_to now sign_in admin visit casa_case_placements_path(casa_case, placements) end it "displays all placements for org" do expect(page).to have_text("Reunification") expect(page).to have_text("August 15, 2024 - Present") expect(page).to have_text("Kinship") expect(page).to have_text("June 2, 2023 - August 15, 2024") expect(page).to have_text("Adoption") expect(page).to have_text("December 25, 2021 - June 2, 2023") end end ================================================ FILE: spec/system/placements/new_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "placements/new", type: :system do let(:now) { Date.current } let(:formatted_date) { now.strftime("%B %-d, %Y") } let(:casa_org) { create(:casa_org, :with_placement_types) } let(:admin) { create(:casa_admin, casa_org:) } let(:casa_case) { create(:casa_case, casa_org:, case_number: "123") } let(:placement_type) { create(:placement_type, name: "Reunification", casa_org:) } let(:placement) { create(:placement, placement_started_at: "2024-08-15 20:40:44 UTC", casa_case:, placement_type:) } before do sign_in admin visit casa_case_placements_path(casa_case) click_link("New Placement") end it "creates placement with valid form data", :js do expect(page).to have_content("123") fill_in "Placement Started At", with: now select placement_type.name, from: "Placement Type" click_on "Create" expect(page).to have_content("Placement was successfully created.") expect(page).to have_content("123") expect(page).to have_content(formatted_date) expect(page).to have_content("Reunification") end it "rejects placement with invalid form data" do fill_in "Placement Started At", with: 1000.years.ago click_on "Create" expect(page).to have_content("2 errors prohibited this Placement from being saved:\nPlacement type must exist Placement started at cannot be prior to 1/1/1989.") end end ================================================ FILE: spec/system/reimbursements/reimbursements_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "reimbursements", type: :system do let(:admin) { create(:casa_admin) } let!(:contact1) { create(:case_contact, :wants_reimbursement) } let!(:contact2) { create(:case_contact, :wants_reimbursement) } before do sign_in admin visit reimbursements_path end it "shows reimbursements", :js do expect(page).to have_content("Needs Review") expect(page).to have_content("Reimbursement Complete") expect(page).to have_content("Occurred At") expect(page).to have_content(contact1.casa_case.case_number) expect(page).to have_content(contact2.miles_driven) end it "shows pagination", :js do expect(page).to have_content("Previous") expect(page).to have_content("Next") end it "filters by volunteers", :js do expect(page).to have_selector("#reimbursements-datatable tbody tr", count: 2) page.find(".select2-search__field").click send_keys(contact1.creator.display_name) send_keys(:enter) expect(page).to have_selector("#reimbursements-datatable tbody tr", count: 1) expect(page).to have_content contact1.creator.display_name page.find(".select2-selection__choice__remove").click expect(page).to have_selector("#reimbursements-datatable tbody tr", count: 2) end end ================================================ FILE: spec/system/reports/export_data_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contact_reports/index", type: :system do let(:admin) { create(:casa_admin) } it "filters report by date and selected contact type", :js do sign_in admin contact_type_group = create(:contact_type_group) court = create(:contact_type, name: "Court", contact_type_group: contact_type_group) school = create(:contact_type, name: "School", contact_type_group: contact_type_group) contact1 = create(:case_contact, occurred_at: 20.days.ago, contact_types: [court], notes: "Case Contact 1") contact2 = create(:case_contact, occurred_at: 20.days.ago, contact_types: [court], notes: "Case Contact 2") contact3 = create(:case_contact, occurred_at: 20.days.ago, contact_types: [court, school], notes: "Case Contact 3") excluded_by_date = create(:case_contact, occurred_at: 40.days.ago, contact_types: [court], notes: "Excluded by date") excluded_by_contact_type = create(:case_contact, occurred_at: 20.days.ago, contact_types: [school], notes: "Excluded by Contact Type") visit reports_path start_date = 30.days.ago end_date = 10.days.ago fill_in "report_start_date", with: start_date fill_in "report_end_date", with: end_date select court.name, from: "multiple-select-field3" click_button "Download Report" wait_for_download expect(download_content).to include(contact1.notes) expect(download_content).to include(contact2.notes) expect(download_content).to include(contact3.notes) expect(download_content).not_to include(excluded_by_date.notes) expect(download_content).not_to include(excluded_by_contact_type.notes) end it "filters report by contact type group", :js do sign_in admin contact_type_group = create(:contact_type_group) court = create(:contact_type, name: "Court", contact_type_group: contact_type_group) contact1 = create(:case_contact, occurred_at: Date.yesterday, contact_types: [court], notes: "Case Contact 1") excluded_contact_type_group = create(:contact_type_group) school = create(:contact_type, name: "School", contact_type_group: excluded_contact_type_group) excluded_by_contact_type_group = create(:case_contact, occurred_at: Date.yesterday, contact_types: [school], notes: "Excluded by Contact Type") visit reports_path select contact_type_group.name, from: "multiple-select-field4" click_button "Download Report" wait_for_download expect(download_content).to include(contact1.notes) expect(download_content).not_to include(excluded_by_contact_type_group.notes) end it "downloads mileage report", :js do sign_in admin supervisor = create(:supervisor) volunteer = create(:volunteer, supervisor: supervisor) case_contact_with_mileage = create(:case_contact, want_driving_reimbursement: true, miles_driven: 10, creator: volunteer) case_contact_without_mileage = create(:case_contact) visit reports_path click_button "Mileage Report" wait_for_download expect(download_file_name).to match(/mileage-report-\d{4}-\d{2}-\d{2}.csv/) expect(download_content).to include(case_contact_with_mileage.creator.display_name) expect(download_content).to include(case_contact_with_mileage.creator.supervisor.display_name) expect(download_content).not_to include(case_contact_without_mileage.creator.display_name) end it "downloads missing data report", :js do sign_in admin visit reports_path click_button "Missing Data Report" wait_for_download expect(download_file_name).to match(/missing-data-report-\d{4}-\d{2}-\d{2}.csv/) end it "downloads learning hours report", :js do sign_in admin visit reports_path click_button "Learning Hours Report" wait_for_download expect(download_file_name).to match(/learning-hours-report-\d{4}-\d{2}-\d{2}.csv/) end it "downloads followup report", :js do sign_in admin visit reports_path click_button "Followups Report" wait_for_download expect(download_file_name).to match(/followup-report-\d{4}-\d{2}-\d{2}.csv/) end context "as volunteer" do let(:volunteer) { create(:volunteer) } it "cannot accesses reports page" do sign_in volunteer visit reports_path expect(page).to have_current_path(casa_cases_path, ignore_query: true) expect(page).to have_text "Sorry, you are not authorized to perform this action." end it "cannot download followup report" do sign_in volunteer visit followup_reports_path expect(page).to have_current_path(casa_cases_path, ignore_query: true) expect(page).to have_text "Sorry, you are not authorized to perform this action." end end end ================================================ FILE: spec/system/reports/index_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" VOLUNTEER_SELECT_ID = "multiple-select-field2" SUPERVISOR_SELECT_ID = "multiple-select-field1" CONTACT_TYPE_SELECT_ID = "multiple-select-field3" CONTACT_TYPE_GROUP_SELECT_ID = "multiple-select-field4" RSpec.describe "reports", :js, type: :system do shared_examples "downloads report button" do |button_name, feedback| it "downloads #{button_name.downcase}", :aggregate_failures do expect(page).to have_button(button_name) click_on button_name expect(page).to have_text(feedback) end end shared_examples "downloads case contacts report with filter" do |filter_name, setup_action, filter_action| it "downloads case contacts report with #{filter_name}" do instance_exec(&setup_action) if setup_action visit reports_path instance_exec(&filter_action) click_on "Download Report" expect(page).to have_text("Downloading Report") end end shared_examples "empty select downloads report" do |select_id, description| it "renders the #{description} select with no options and downloads the report" do expect(page).to have_select(select_id, options: []) click_on "Download Report" expect(page).to have_text("Downloading Report") end end context "with a volunteer user" do before do user = create(:volunteer) sign_in user visit reports_path end it "redirects to root", :aggregate_failures do expect(page).not_to have_text "Case Contacts Report" expect(page).to have_text "Sorry, you are not authorized to perform this action." end end %i[supervisor casa_admin].each do |role| # rubocop:disable RSpec/MultipleMemoizedHelpers context "with a #{role} user" do let(:user) { create(role) } let(:volunteer_name) { Faker::Name.unique.name } let(:supervisor_name) { Faker::Name.unique.name } let(:contact_type_name) { Faker::Lorem.unique.word } let(:contact_type_group_name) { Faker::Lorem.unique.word } let(:filter_start_date) { "2025-01-01" } let(:filter_end_date) { "2025-10-08" } before do sign_in user visit reports_path end it "renders form elements", :aggregate_failures do expect(page).to have_text "Case Contacts Report" expect(page).to have_field("report_start_date", with: 6.months.ago.strftime("%Y-%m-%d")) expect(page).to have_field("report_end_date", with: Date.today) expect(page).to have_text "Assigned To" expect(page).to have_text "Volunteers" expect(page).to have_text "Contact Type" expect(page).to have_text "Contact Type Group" expect(page).to have_text "Want Driving Reimbursement" expect(page).to have_text "Contact Made" expect(page).to have_text "Transition Aged Youth" expect(page).to have_field("Both", count: 3) expect(page).to have_field("Yes", count: 3) expect(page).to have_field("No", count: 3) end it "downloads case contacts report with default filters" do click_on "Download Report" expect(page).to have_text("Downloading Report") end include_examples "downloads report button", "Mileage Report", "Downloading Mileage Report" include_examples "downloads report button", "Missing Data Report", "Downloading Missing Data Report" include_examples "downloads report button", "Learning Hours Report", "Downloading Learning Hours Report" include_examples "downloads report button", "Export Volunteers Emails", "Downloading Export Volunteers Emails" include_examples "downloads report button", "Followups Report", "Downloading Followups Report" include_examples "downloads report button", "Placements Report", "Downloading Placements Report" shared_examples "case contacts report with filter" do |filter_type| it "downloads case contacts report with #{filter_type}" do click_on "Download Report" expect(page).to have_text("Downloading Report") end end context "with an assigned supervisor filter" do before do create(:supervisor, casa_org: user.casa_org, display_name: supervisor_name) visit reports_path select_report_filter_option(SUPERVISOR_SELECT_ID, supervisor_name) end include_examples "case contacts report with filter", "assigned supervisor" end context "with a volunteer filter" do before do create(:volunteer, casa_org: user.casa_org, display_name: volunteer_name) visit reports_path select_report_filter_option(VOLUNTEER_SELECT_ID, volunteer_name) end include_examples "case contacts report with filter", "volunteer" end context "with a contact type filter" do before do create(:contact_type, casa_org: user.casa_org, name: contact_type_name) visit reports_path select_report_filter_option(CONTACT_TYPE_SELECT_ID, contact_type_name) end include_examples "case contacts report with filter", "contact type" end context "with a contact type group filter" do before do create(:contact_type_group, casa_org: user.casa_org, name: contact_type_group_name) visit reports_path select_report_filter_option(CONTACT_TYPE_GROUP_SELECT_ID, contact_type_group_name) end include_examples "case contacts report with filter", "contact type group" end context "with a driving reimbursement filter" do before do visit reports_path choose_report_radio_option("want_driving_reimbursement", "true") end include_examples "case contacts report with filter", "driving reimbursement" end context "with a contact made filters" do before do visit reports_path choose_report_radio_option("contact_made", "true") end include_examples "case contacts report with filter", "contact made" end context "with a transition aged youth filter" do before do visit reports_path choose_report_radio_option("has_transitioned", "true") end include_examples "case contacts report with filter", "transition aged youth" end context "with a date range filter" do before do visit reports_path set_report_date_range(start_date: filter_start_date, end_date: filter_end_date) end include_examples "case contacts report with filter", "date range" end context "with multiple filters" do before do create(:volunteer, casa_org: user.casa_org, display_name: volunteer_name) create(:contact_type, casa_org: user.casa_org, name: contact_type_name) visit reports_path set_report_date_range(start_date: filter_start_date, end_date: filter_end_date) select_report_filter_option(VOLUNTEER_SELECT_ID, volunteer_name) select_report_filter_option(CONTACT_TYPE_SELECT_ID, contact_type_name) choose_report_radio_option("want_driving_reimbursement", "false") end include_examples "case contacts report with filter", "multiple filters" end context "with no volunteers in the org" do include_examples "empty select downloads report", VOLUNTEER_SELECT_ID, "volunteers" end context "with no contact type groups in the org" do include_examples "empty select downloads report", CONTACT_TYPE_GROUP_SELECT_ID, "contact type groups" end context "with no contact types in the org" do include_examples "empty select downloads report", CONTACT_TYPE_SELECT_ID, "contact types" end end # rubocop:enable RSpec/MultipleMemoizedHelpers end private def select_report_filter_option(select_id, option) expect(page).to have_select(select_id, with_options: [option]) find("##{select_id}").select(option) end def set_report_date_range(start_date:, end_date:) fill_in "report_start_date", with: start_date fill_in "report_end_date", with: end_date end def choose_report_radio_option(field_name, value) find("input[name=\"report[#{field_name}]\"][value=\"#{value}\"]", visible: :all).click end end ================================================ FILE: spec/system/sessions/destroy_spec.rb ================================================ require "rails_helper" RSpec.describe "sessions/destroy", type: :system do context "when a user is timed out" do let(:user) { build(:casa_admin) } before { sign_in user } it "ends the current session and redirects to sign in page after timeout" do allow(user).to receive(:timedout?).and_return(true) visit "/case_contacts/new" expect(page).to have_current_path "/users/sign_in", ignore_query: true expect(page).to have_text "Your session expired. Please sign in again to continue." end end end ================================================ FILE: spec/system/sessions/login_spec.rb ================================================ require "rails_helper" RSpec.describe "User Login", type: :system do %w[volunteer supervisor casa_admin].each do |user_type| let!(:user) { create(user_type.to_sym) } it "shows the user's email after successful login" do visit new_user_session_path fill_in "Email", with: user.email fill_in "Password", with: "12345678" within ".actions" do find("#log-in").click end expect(page).to have_text user.email end it "shows an error message after failed login" do visit new_user_session_path fill_in "Email", with: user.email fill_in "Password", with: "wrong_password" within ".actions" do find("#log-in").click end expect(page).to have_content(/invalid email or password/i) end end end ================================================ FILE: spec/system/sessions/new_spec.rb ================================================ require "rails_helper" RSpec.describe "sessions/new", type: :system do context "when guest" do it "renders sign in page with no flash messages" do visit "/" expect(page).to have_text "Login" expect(page).not_to have_text "sign in before continuing" end %w[volunteer supervisor casa_admin].each do |user_type| before do visit "/" end it "allows #{user_type} to click email link" do expect(page).to have_text "Want to use the CASA Volunteer Tracking App?" expect(page).to have_link("casa@rubyforgood.org", href: "mailto:casa@rubyforgood.org?Subject=CASA%20Interest") end it "renders sign in page with no flash messages" do expect(page).to have_text "Login" expect(page).not_to have_text "sign in before continuing" end context "when a #{user_type} fills in their email and password" do let!(:user) { create(user_type.to_sym) } before do visit "/users/sign_in" fill_in "Email", with: user.email fill_in "Password", with: "12345678" within ".actions" do find("#log-in").click end end it "allows them to sign in" do expect(page).to have_text user.email end context "but they are inactive" do let!(:user) { create(user_type.to_sym, active: false) } it "does not allow them to sign in" do expect(page).to have_text I18n.t("devise.failure.inactive") end end end end it "does not allow AllCasaAdmin to sign in" do user = build_stubbed(:all_casa_admin) visit "/users/sign_in" expect(page).to have_text "Log In" expect(page).not_to have_text "sign in before continuing" fill_in "Email", with: user.email fill_in "Password", with: "12345678" within ".actions" do find("#log-in").click end expect(page).to have_text(/invalid email or password/i) end end context "when authenticated admin" do let(:user) { create(:casa_admin) } before { sign_in user } it "renders dashboard page and shows correct flash message upon sign out" do visit "/" expect(page).to have_text "Volunteers" # click_link "Log out" # expect(page).to_not have_text "sign in before continuing" # expect(page).to have_text "Signed out successfully" end end end ================================================ FILE: spec/system/static/index_spec.rb ================================================ require "rails_helper" RSpec.describe "static/index", type: :system do context "when visiting the CASA volunteer landing page", :js do describe "when all organizations have logos" do before do create_list(:casa_org, 3, :with_logo, display_name: "CASA of Awesome") visit root_path end it "has CASA organizations section" do expect(page).to have_text "CASA Organizations Powered by Our App" expect(page).to have_text "CASA of Awesome" end it "displays all organizations that have attached logos" do within("#organizations") do expect(page).to have_css(".org_logo", count: 3) end end end describe "when some orgs are missing logos" do before do create(:casa_org, :with_logo) create(:casa_org) visit root_path end it "does not display organizations that don't have attached logos" do within("#organizations") do expect(page).to have_css(".org_logo", count: 1) end end end end end ================================================ FILE: spec/system/supervisors/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "supervisors/edit", type: :system do let(:organization) { create(:casa_org) } context "logged in as an admin" do let(:user) { create(:casa_admin, casa_org: organization) } it "can edit supervisor by clicking on the edit link from the supervisors list page" do supervisor_name = "Leslie Knope" create(:supervisor, display_name: supervisor_name, casa_org: organization) sign_in user visit supervisors_path expect(page).to have_text(supervisor_name) within "#supervisors" do click_on "Edit", match: :first end expect(page).to have_text("Editing Supervisor") end it "can edit supervisor by clicking on the supervisor's name from the supervisors list page" do supervisor_name = "Leslie Knope" create(:supervisor, display_name: supervisor_name, casa_org: organization) sign_in user visit supervisors_path within "#supervisors" do click_on supervisor_name end expect(page).to have_text("Editing Supervisor") end context "with invalid data" do let(:role) { "supervisor" } let(:supervisor) { create(:supervisor, display_name: "Leslie Knope", casa_org: organization) } before do sign_in user visit edit_supervisor_path(supervisor) end it_behaves_like "shows error for invalid phone numbers" it "shows error for invalid date of birth" do fill_in "Date of birth", with: 5.days.from_now.strftime("%Y/%m/%d") end end it "can go to the supervisor edit page and see red message when there are no active volunteers" do supervisor = create :supervisor, casa_org: organization sign_in user visit edit_supervisor_path(supervisor) expect(page).to have_text("There are no active, unassigned volunteers available") end it "can go to the supervisor edit page and see invite and login info" do supervisor = create :supervisor, casa_org: organization sign_in user visit edit_supervisor_path(supervisor) expect(page).to have_text "CASA organization " expect(page).to have_text "Added to system " expect(page).to have_text "Invitation email sent never" expect(page).to have_text "Last logged in" expect(page).to have_text "Invitation accepted never" expect(page).to have_text "Password reset last sent never" end it "can deactivate a supervisor", :js do supervisor = create :supervisor, casa_org: organization sign_in user visit edit_supervisor_path(supervisor) dismiss_confirm do click_link "Deactivate Supervisor" end accept_confirm do click_link "Deactivate Supervisor" end expect(page).to have_text("Supervisor was deactivated on") expect(supervisor.reload).not_to be_active end it "can activate a supervisor" do inactive_supervisor = create(:supervisor, casa_org_id: organization.id) inactive_supervisor.deactivate sign_in user visit edit_supervisor_path(inactive_supervisor) click_on "Activate supervisor" expect(page).not_to have_text("Supervisor was deactivated on") expect(inactive_supervisor.reload).to be_active end it "can resend invitation to a supervisor" do supervisor = create :supervisor, casa_org: organization sign_in user visit edit_supervisor_path(supervisor) click_on "Resend Invitation" expect(page).to have_content("Invitation sent") deliveries = ActionMailer::Base.deliveries expect(deliveries.count).to eq(1) expect(deliveries.last.subject).to have_text "CASA Console invitation instructions" end it "can convert the supervisor to an admin" do supervisor = create(:supervisor, casa_org_id: organization.id) sign_in user visit supervisors_path visit edit_supervisor_path(supervisor) click_on "Change to Admin" expect(page).to have_text("Supervisor was changed to Admin.") expect(User.find(supervisor.id)).to be_casa_admin end context "logged in as a supervisor" do let(:supervisor) { create(:supervisor) } it "can't deactivate a supervisor" do supervisor2 = create :supervisor, casa_org: organization sign_in supervisor visit edit_supervisor_path(supervisor2) expect(page).not_to have_text("Deactivate supervisor") end it "can't activate a supervisor" do inactive_supervisor = create(:supervisor, casa_org_id: organization.id) inactive_supervisor.deactivate sign_in supervisor visit edit_supervisor_path(inactive_supervisor) expect(page).not_to have_text("Activate supervisor") end end context "when entering valid information" do before do sign_in user @supervisor = create(:supervisor) @old_email = @supervisor.email visit edit_supervisor_path(@supervisor) fill_in "supervisor_email", with: "new_supervisor_email@example.com" fill_in "supervisor_phone_number", with: "+14155556876" fill_in "supervisor_date_of_birth", with: "2003/05/06" click_on "Submit" @supervisor.reload end it "sends a confirmation email to the supervisor and displays current email" do expect(page).to have_text "Supervisor was successfully updated. Confirmation Email Sent." expect(page).to have_field("Email", with: @old_email) expect(@supervisor.unconfirmed_email).to eq("new_supervisor_email@example.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "correctly updates the supervisor email once confirmed" do @supervisor.confirm @supervisor.reload visit edit_supervisor_path(@supervisor) expect(page).to have_field("Email", with: "new_supervisor_email@example.com") expect(@supervisor.old_emails).to match([@old_email]) end end context "when entering invalid information" do before do sign_in user @supervisor = create(:supervisor) visit edit_supervisor_path(@supervisor) end it "shows error message for invalid phone number" do fill_in "supervisor_phone_number", with: "+24155556760" click_on "Submit" expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for invalid date of birth" do fill_in "supervisor_date_of_birth", with: 5.days.from_now.strftime("%Y/%m/%d") click_on "Submit" expect(page).to have_text "Date of birth must be in the past." end end context "when the email exists already" do let!(:existing_supervisor) { create(:supervisor, casa_org_id: organization.id) } it "responds with a notice" do sign_in user supervisor = create(:supervisor) visit edit_supervisor_path(supervisor) fill_in "supervisor_email", with: "" fill_in "supervisor_email", with: existing_supervisor.email click_on "Submit" within "#error_explanation" do expect(page).to have_content(/already been taken/i) end end end end context "logged in as a supervisor" do before do sign_in user visit edit_supervisor_path(supervisor) end context "when editing other supervisor" do let(:user) { build(:supervisor, casa_org: organization) } let(:supervisor) { create(:supervisor, casa_org: organization) } it "sees red message when there are no active volunteers" do expect(page).to have_text("There are no active, unassigned volunteers available") end it "does not have a submit button" do expect(page).not_to have_selector(:link_or_button, "Submit") end end context "when editing own page" do let(:supervisor) { create(:supervisor, casa_org: organization) } let(:user) { supervisor } it "displays a submit button" do visit edit_supervisor_path(supervisor) expect(page).to have_selector(:link_or_button, "Submit") end it "sees last invite and login info" do expect(page).to have_text "Added to system " expect(page).to have_text "Invitation email sent never" expect(page).to have_text "Last logged in" expect(page).to have_text "Invitation accepted never" expect(page).to have_text "Password reset last sent never" end context "when no volunteers exist" do let!(:volunteer_1) { create(:volunteer, display_name: "AAA", casa_org: organization) } it "does not error out when adding non-existent volunteer" do visit edit_supervisor_path(supervisor) select volunteer_1.display_name, from: "Select a Volunteer" click_on "Assign Volunteer" expect(page.find_button("Assign Volunteer", disabled: true)).to be_present expect(page).to have_text("There are no active, unassigned volunteers available.") end end context "when there are assigned volunteers" do let(:supervisor) { create(:supervisor, :with_volunteers, casa_org: organization) } it "shows assigned volunteers" do visit edit_supervisor_path(supervisor) expect(page).to have_text "Assigned Volunteers" expect(page).not_to have_button("Include unassigned") expect(page).not_to have_text("Currently Assigned To") supervisor.volunteers.each do |volunteer| expect(page).to have_text volunteer.email end end context "when there are previously unassigned volunteers" do let!(:unassigned_volunteer) { create(:supervisor_volunteer, :inactive, supervisor: supervisor).volunteer } it "does not show them by default" do visit edit_supervisor_path(supervisor) expect(page).not_to have_text unassigned_volunteer.email expect(page).to have_button("Include unassigned") click_on "Include unassigned" expect(page).to have_button("Hide unassigned") expect(page).to have_text("All Volunteers") expect(page).to have_text unassigned_volunteer.email expect(page).to have_text "Currently Assigned To" end end end context "when there are no currently assigned volunteers" do let(:supervisor) { create(:supervisor, casa_org: organization) } context "and there are previously unassigned volunteers" do let!(:unassigned_volunteer) { create(:supervisor_volunteer, :inactive, supervisor: supervisor).volunteer } it "does not show them by default" do visit edit_supervisor_path(supervisor) expect(page).to have_text "Assigned Volunteers" expect(page).not_to have_text unassigned_volunteer.email expect(page).to have_button("Include unassigned") click_on "Include unassigned" expect(page).to have_button("Hide unassigned") expect(page).to have_text unassigned_volunteer.email expect(page).to have_text "No One" expect(page).to have_text "Currently Assigned To" click_on "Hide unassigned" expect(page).not_to have_text "Currently Assigned To" expect(page).not_to have_text "No One" end end end end end end ================================================ FILE: spec/system/supervisors/index_spec.rb ================================================ require "rails_helper" RSpec.describe "supervisors/index", type: :system do shared_examples_for "functioning sort buttons" do it "sorts table columns" do expect(page).to have_css("tr:nth-child(1)", text: expected_first_ordered_value) find("th", text: column_to_sort).click expect(page).to have_css("th.sorting_asc", text: column_to_sort) expect(page).to have_css("tr:nth-child(1)", text: expected_last_ordered_value) end end let(:organization) { build(:casa_org) } let(:supervisor_user) { create(:supervisor, casa_org: organization, display_name: "Logged Supervisor") } let(:organization_two) { build(:casa_org) } let(:supervisor_other_org) { create(:supervisor, casa_org: organization_two, display_name: "No Volunteers Org") } let(:other_supervisor) { create(:supervisor, casa_org: organization, display_name: "Other Supervisor") } let(:only_contacts_supervisor) { create(:supervisor, casa_org: organization, display_name: "Only Contacts Supervisor") } let(:no_contacts_supervisor) { create(:supervisor, casa_org: organization, display_name: "No Contacts Supervisor") } let(:no_active_volunteers_supervisor) { create(:supervisor, casa_org: organization, display_name: "No Active Volunteers Supervisor") } let(:admin) { create(:casa_admin, casa_org: organization) } context "when signed in as a supervisor" do before { sign_in supervisor_user } context "when editing supervisor", :js do let(:supervisor_name) { "Leslie Knope" } let!(:supervisor) { create(:supervisor, display_name: supervisor_name, casa_org: organization) } before { visit supervisors_path } it "can edit supervisor by clicking on the edit link from the supervisors list page" do expect(page).to have_text(supervisor_name) within "#supervisors" do click_on "Edit", match: :first end expect(page).to have_text("Editing Supervisor") end it "can edit supervisor by clicking on the supervisor's name from the supervisors list page" do expect(page).to have_text(supervisor_name) within "#supervisors" do click_on supervisor_name end expect(page).to have_text("Editing Supervisor") end end describe "supervisor table" do let!(:first_supervisor) { create(:supervisor, display_name: "First Supervisor", casa_org: organization) } let!(:last_supervisor) { create(:supervisor, display_name: "Last Supervisor", casa_org: organization) } let!(:active_volunteers_for_first_supervisor) { create_list(:volunteer, 2, supervisor: first_supervisor, casa_org: organization) } let!(:active_volunteers_for_last_supervisor) { create_list(:volunteer, 5, supervisor: last_supervisor, casa_org: organization) } let!(:deacticated_supervisor) { create(:supervisor, :inactive, display_name: "Deactivated supervisor", casa_org: organization) } before do # Stub our `@supervisors` collection so we've got control over column values for sorting. allow_any_instance_of(SupervisorPolicy::Scope).to receive(:resolve).and_return( Supervisor.where.not(display_name: supervisor_user.display_name).order(display_name: :asc) ) active_volunteers_for_first_supervisor.map { |av| casa_case = create(:casa_case, casa_org: av.casa_org) create(:case_contact, contact_made: false, occurred_at: 1.week.ago, casa_case_id: casa_case.id) create(:case_assignment, casa_case: casa_case, volunteer: av) } active_volunteers_for_last_supervisor.map { |av| casa_case = create(:casa_case, casa_org: av.casa_org) create(:case_contact, contact_made: false, occurred_at: 1.week.ago, casa_case_id: casa_case.id) create(:case_assignment, casa_case: casa_case, volunteer: av) } sign_in supervisor_user visit supervisors_path end context "with active and deactivated supervisors" do it "shows deactivated supervisor on show button click", :js do expect(page).to have_text("Showing 1 to 2 of 2 entries (filtered from 3 total entries)") expect(page).to have_no_text("Deactivated supervisor") find(".supervisor-filters").click_on("Filter Status") check("status_option_inactive") expect(page).to have_text("Showing 1 to 3 of 3 entries") expect(page).to have_text("Deactivated supervisor") uncheck("status_option_inactive") expect(page).to have_text("Showing 1 to 2 of 2 entries (filtered from 3 total entries)") expect(page).to have_no_text("Deactivated supervisor") end end context "with unassigned volunteers" do let(:unassigned_volunteer_name) { "Tony Ruiz" } let!(:unassigned_volunteer) { create(:volunteer, casa_org: organization, display_name: unassigned_volunteer_name) } before do sign_in supervisor_user visit supervisors_path end it "shows a list of unassigned volunteers" do expect(page).to have_text("Active volunteers not assigned to supervisors") expect(page).to have_text("Assigned to Case(s)") expect(page).to have_text(unassigned_volunteer_name) expect(page).to have_no_text("There are no unassigned volunteers") end it "links to edit page of volunteer" do click_on unassigned_volunteer_name expect(page).to have_current_path("/volunteers/#{unassigned_volunteer.id}/edit") end end context "without unassigned volunteers" do before do sign_in supervisor_other_org visit supervisors_path end it "does not show a list of volunteers not assigned to supervisors", :js do expect(page).to have_text("There are no active volunteers without supervisors to display here") expect(page).to have_no_text("Active volunteers not assigned to supervisors") expect(page).to have_no_text("Assigned to Case(s)") end end end describe "supervisor table filters" do let(:supervisor_user) { create(:supervisor, casa_org: organization) } before do sign_in supervisor_user visit supervisors_path end describe "status", :js do let!(:active_supervisor) do create(:supervisor, display_name: "Active Supervisor", casa_org: organization, active: true) end let!(:inactive_supervisor) do create(:supervisor, display_name: "Inactive Supervisor", casa_org: organization, active: false) end context "when only active checked" do it "filters the supervisors correctly", :aggregate_failures do within(:css, ".supervisor-filters") do click_on "Status" find(:css, ".active").set(false) find(:css, ".active").set(true) find(:css, ".inactive").set(false) end within("table#supervisors") do expect(page).to have_text("Active Supervisor") expect(page).to have_no_text("Inactive Supervisor") end end end context "when only inactive checked" do it "filters the supervisors correctly", :aggregate_failures do within(:css, ".supervisor-filters") do click_on "Status" find(:css, ".active").set(false) find(:css, ".inactive").set(true) click_on "Status" end within("table#supervisors") do expect(page).to have_no_content("Active Supervisor") expect(page).to have_content("Inactive Supervisor") end end end context "when both checked" do it "filters the supervisors correctly", :aggregate_failures do # TODO fix test within(:css, ".supervisor-filters") do click_on "Status" find(:css, ".active").set(true) find(:css, ".inactive").set(true) click_on "Status" end within("table#supervisors") do expect(page).to have_content("Active Supervisor") expect(page).to have_content("Inactive Supervisor") end end end end end end context "when signed in as an admin" do let!(:no_active_volunteers_supervisor) { create(:supervisor, casa_org: organization, display_name: "No Active Volunteers Supervisor") } let!(:no_contact_volunteer) do create( :volunteer, :with_casa_cases, :with_assigned_supervisor, supervisor: supervisor_user, casa_org: organization ) end let!(:no_contact_pre_transition_volunteer) do create( :volunteer, :with_pretransition_age_case, :with_assigned_supervisor, supervisor: supervisor_user, casa_org: organization ) end let!(:with_contact_volunteer) do create( :volunteer, :with_cases_and_contacts, :with_assigned_supervisor, supervisor: supervisor_user, casa_org: organization ) end let!(:active_unassigned) do create( :volunteer, :with_casa_cases, casa_org: organization ) end let!(:other_supervisor_active_volunteer1) do create( :volunteer, :with_cases_and_contacts, :with_assigned_supervisor, supervisor: other_supervisor, casa_org: organization ) end let!(:other_supervisor_active_volunteer2) do create( :volunteer, :with_cases_and_contacts, :with_assigned_supervisor, supervisor: other_supervisor, casa_org: organization ) end let!(:other_supervisor_no_contact_volunteer1) do create( :volunteer, :with_casa_cases, :with_assigned_supervisor, supervisor: other_supervisor, casa_org: organization ) end let!(:other_supervisor_no_contact_volunteer2) do create( :volunteer, :with_casa_cases, :with_assigned_supervisor, supervisor: other_supervisor, casa_org: organization ) end let!(:only_contact_volunteer1) do create( :volunteer, :with_cases_and_contacts, :with_assigned_supervisor, supervisor: only_contacts_supervisor, casa_org: organization ) end let!(:only_contact_volunteer2) do create( :volunteer, :with_cases_and_contacts, :with_assigned_supervisor, supervisor: only_contacts_supervisor, casa_org: organization ) end let!(:only_contact_volunteer3) do create( :volunteer, :with_cases_and_contacts, :with_assigned_supervisor, supervisor: only_contacts_supervisor, casa_org: organization ) end let!(:no_contact_volunteer1) do create( :volunteer, :with_casa_cases, :with_assigned_supervisor, supervisor: no_contacts_supervisor, casa_org: organization ) end let!(:no_contact_volunteer2) do create( :volunteer, :with_casa_cases, :with_assigned_supervisor, supervisor: no_contacts_supervisor, casa_org: organization ) end before do sign_in admin visit supervisors_path end it "shows all active supervisors", :js do supervisor_table = page.find("table#supervisors") expect(supervisor_table.all("div.supervisor_case_contact_stats").length).to eq(5) end it "shows the correct volunteers for the first supervisor with both volunteer types", :js do supervisor_table = page.find("table#supervisors") expect(supervisor_table).to have_text(supervisor_user.display_name.html_safe) supervisor_stats = page.find("tr#supervisor-#{supervisor_user.id}-information") active_contacts_expected = 1 no_active_contacts_expected = 2 transition_aged_youth_expected = 2 active_contact_element = supervisor_stats.find("span.attempted-contact") no_active_contact_element = supervisor_stats.find("span.no-attempted-contact") expect(active_contact_element).to have_text(active_contacts_expected) expect(active_contact_element).to match_css(".pr-#{active_contacts_expected * 15}") expect(no_active_contact_element).to have_text(no_active_contacts_expected) expect(no_active_contact_element).to match_css(".pl-#{no_active_contacts_expected * 15}") expect(supervisor_stats.find(".supervisor-stat.deactive-bg")).to have_text(transition_aged_youth_expected) end it "shows the correct volunteers for the second supervisor with both volunteer types", :js do supervisor_table = page.find("table#supervisors") expect(supervisor_table).to have_text(other_supervisor.display_name.html_safe) supervisor_stats = page.find("tr#supervisor-#{other_supervisor.id}-information") active_contacts_expected = 2 no_active_contacts_expected = 2 transition_aged_youth_expected = 4 active_contact_element = supervisor_stats.find("span.attempted-contact") no_active_contact_element = supervisor_stats.find("span.no-attempted-contact") expect(active_contact_element).to have_text(active_contacts_expected) expect(active_contact_element).to match_css(".pr-#{active_contacts_expected * 15}") expect(no_active_contact_element).to have_text(no_active_contacts_expected) expect(no_active_contact_element).to match_css(".pl-#{no_active_contacts_expected * 15}") expect(supervisor_stats.find(".supervisor-stat.deactive-bg")).to have_text(transition_aged_youth_expected) end it "shows the correct element for a supervisor with only contact volunteers", :js do supervisor_table = page.find("table#supervisors") expect(supervisor_table).to have_text(only_contacts_supervisor.display_name.html_safe) supervisor_stats = page.find("tr#supervisor-#{only_contacts_supervisor.id}-information") active_contacts_expected = 3 transition_aged_youth_expected = 3 active_contact_element = supervisor_stats.find("span.attempted-contact") expect(active_contact_element).to have_text(active_contacts_expected) expect(active_contact_element).to match_css(".pl-#{active_contacts_expected * 15}") expect(supervisor_stats.find(".supervisor-stat.deactive-bg")).to have_text(transition_aged_youth_expected) expect(supervisor_stats).not_to have_css("span.no-attempted-contact") end it "shows the correct element for a supervisor with only no contact volunteers", :js do supervisor_table = page.find("table#supervisors") expect(supervisor_table).to have_text(no_contacts_supervisor.display_name.html_safe) supervisor_stats = page.find("tr#supervisor-#{no_contacts_supervisor.id}-information") no_contacts_expected = 2 transition_aged_youth_expected = 2 no_contact_element = supervisor_stats.find("span.no-attempted-contact") expect(no_contact_element).to have_text(no_contacts_expected) expect(no_contact_element).to match_css(".pl-#{no_contacts_expected * 15}") expect(supervisor_stats.find(".supervisor-stat.deactive-bg")).to have_text(transition_aged_youth_expected) expect(supervisor_stats).not_to have_css("span.attempted-contact") expect(supervisor_stats).not_to have_css("span.attempted-contact-end") end it "shows the correct text with a supervisor with no assigned volunteers", :js do supervisor_table = page.find("table#supervisors") expect(supervisor_table).to have_text(no_active_volunteers_supervisor.display_name.html_safe) supervisor_no_volunteer_stats = page.find("tr#supervisor-#{no_active_volunteers_supervisor.id}-information") expect(supervisor_no_volunteer_stats).to have_text("No assigned volunteers") expect(supervisor_no_volunteer_stats.find("span.no-volunteers")).to be_truthy expect(supervisor_no_volunteer_stats.find("span.no-volunteers").style("flex-grow")).to eq({"flex-grow" => "1"}) end end end ================================================ FILE: spec/system/supervisors/new_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "supervisors/new", type: :system do context "when logged in as an admin" do let(:admin) { create(:casa_admin) } let(:new_supervisor_name) { Faker::Name.name } let(:new_supervisor_email) { Faker::Internet.email } let(:new_supervisor_phone_number) { "1234567890" } before do # Stub the request to the URL shortener service (needed if phone is provided) stub_request(:post, "https://api.short.io/links") .to_return( status: 200, body: {shortURL: "https://short.url/example"}.to_json, headers: {"Content-Type" => "application/json"} ) sign_in admin visit new_supervisor_path end context "with valid form submission" do let(:new_supervisor) { User.find_by(email: new_supervisor_email) } before do fill_in "Email", with: new_supervisor_email fill_in "Display name", with: new_supervisor_name fill_in "Phone number", with: new_supervisor_phone_number click_on "Create Supervisor" end it "shows a success message" do expect(page).to have_text("New supervisor created successfully.") end it "redirects to the edit supervisor page", :aggregate_failures do expect(page).to have_text("New supervisor created successfully.") # Guard to ensure redirection happened expect(page).to have_current_path(edit_supervisor_path(new_supervisor)) end it "persists the new supervisor with correct attributes", :aggregate_failures do expect(new_supervisor).to be_present expect(new_supervisor.display_name).to eq(new_supervisor_name) expect(new_supervisor.phone_number).to end_with(new_supervisor_phone_number) expect(new_supervisor.supervisor?).to be(true) expect(new_supervisor.active?).to be(true) end it "sends an invitation email to the new supervisor", :aggregate_failures do last_email = ActionMailer::Base.deliveries.last expect(last_email.to).to eq [new_supervisor_email] expect(last_email.subject).to have_text "CASA Console invitation instructions" expect(last_email.html_part.body.encoded).to have_text "your new Supervisor account." end end context "with invalid form submission" do before do # Don't fill in any fields click_on "Create Supervisor" end it "does not create a new user" do expect(User.count).to eq(1) # Only the admin user exists end it "shows validation error messages" do expect(page).to have_text "errors prohibited this Supervisor from being saved:" end it "stays on the new supervisor page", :aggregate_failures do expect(page).to have_text "errors prohibited this Supervisor from being saved:" # Guard to ensure no redirection happened expect(page).to have_current_path(supervisors_path) end end end context "when logged in as a supervisor" do let(:supervisor) { create(:supervisor) } before { sign_in supervisor } it "redirects the user with an error message" do visit new_supervisor_path expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.") end end context "when logged in as a volunteer" do let(:volunteer) { create(:volunteer) } before { sign_in volunteer } it "redirects the user with an error message" do visit new_supervisor_path expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.") end end end ================================================ FILE: spec/system/users/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "users/edit", type: :system do context "volunteer user" do it "displays password errors messages when user is unable to set a password with incorrect current password" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345" fill_in "New Password", with: "123456789" fill_in "New Password Confirmation", with: "123456789" click_on "Update Password" expect(page).to have_content "1 error prohibited this password change from being saved:" expect(page).to have_text("Current password is incorrect") end it "displays password errors messages when user is unable to set a password" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123" fill_in "New Password Confirmation", with: "1234" click_on "Update Password" expect(page).to have_content "2 errors prohibited this password change from being saved:" expect(page).to have_text("Password confirmation doesn't match Password") expect(page).to have_text("Password is too short (minimum is #{User.password_length.min} characters)") end it "displays sms notification events for the volunteer user" do organization = create(:casa_org, twilio_enabled: true) volunteer = create(:volunteer, casa_org: organization) SmsNotificationEvent.delete_all SmsNotificationEvent.new(name: "sms_event_test_volunteer", user_type: Volunteer).save SmsNotificationEvent.new(name: "sms_event_test_supervisor", user_type: Supervisor).save SmsNotificationEvent.new(name: "sms_event_test_casa_admin", user_type: CasaAdmin).save sign_in volunteer visit edit_users_path expect(page).to have_content "sms_event_test_volunteer" expect(page).not_to have_content "sms_event_test_supervisor" expect(page).not_to have_content "sms_event_test_casa_admin" end it "notifies a user when they update their password" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123456789" fill_in "New Password Confirmation", with: "123456789" click_on "Update Password" expect(page).to have_text("Password was successfully updated.") end it "notifies password changed by email", :aggregate_failures do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123456789" fill_in "Password Confirmation", with: "123456789" click_on "Update Password" page.has_content?("Password was successfully updated.") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Your CASA password has been changed.") end it "is able to send a confirmation email when Volunteer updates their email" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path click_on "Change Email" expect(page).to have_field("New Email", disabled: false) fill_in "current_password_email", with: "12345678" fill_in "New Email", with: "new_volunteer@example.com" click_on "Update Email" expect(page).to have_content "Click the link in your new email to finalize the email transfer" expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to have_text("Click here to confirm your email") end it "displays email errors messages when user is unable to set a email with incorrect current password" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path click_on "Change Email" fill_in "current_password_email", with: "12345" fill_in "New Email", with: "new_volunteer@example.com" click_on "Update Email" expect(page).to have_content "1 error prohibited this Volunteer from being saved:" expect(page).to have_text("Current password is incorrect") end it "displays current sign in date" do organization = create(:casa_org) volunteer = create( :volunteer, casa_org: organization, last_sign_in_at: "2020-01-01 00:00:00", current_sign_in_at: "2020-01-02 00:00:00" ) sign_in volunteer visit edit_users_path formatted_current_sign_in_at = I18n.l(volunteer.current_sign_in_at, format: :edit_profile, default: nil) formatted_last_sign_in_at = I18n.l(volunteer.last_sign_in_at, format: :edit_profile, default: nil) expect(page).to have_text("Last logged in #{formatted_current_sign_in_at}") expect(page).not_to have_text("Last logged in #{formatted_last_sign_in_at}") end it "displays Volunteer error message if no communication preference is selected" do organization = create(:casa_org, twilio_enabled: true) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path uncheck "user_receive_email_notifications" click_on "Save Preferences" expect(page).to have_content "1 error prohibited this Volunteer from being saved:" expect(page).to have_text("At least one communication preference must be selected.") end it "displays Volunteer error message if SMS communication preference is selected without adding a valid phone number" do organization = create(:casa_org, twilio_enabled: true) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path uncheck "user_receive_email_notifications" check "user_receive_sms_notifications" click_on "Save Preferences" expect(page).to have_content "1 error prohibited this Volunteer from being saved:" expect(page).to have_text("Must add a valid phone number to receive SMS notifications.") end it "displays notification events selection as enabled if sms notification preference is selected" do organization = create(:casa_org, twilio_enabled: true) volunteer = create(:volunteer, casa_org: organization) SmsNotificationEvent.delete_all SmsNotificationEvent.new(name: "sms_event_test_volunteer", user_type: Volunteer).save SmsNotificationEvent.new(name: "sms_event_test_supervisor", user_type: Supervisor).save SmsNotificationEvent.new(name: "sms_event_test_casa_admin", user_type: CasaAdmin).save sign_in volunteer visit edit_users_path check "user_receive_sms_notifications" expect(page).to have_field("toggle-sms-notification-event", type: "checkbox", disabled: false) end it "displays notification events selection as disabled if sms notification preference is not selected", :js do organization = create(:casa_org, twilio_enabled: true) volunteer = create(:volunteer, casa_org: organization) SmsNotificationEvent.delete_all SmsNotificationEvent.new(name: "sms_event_test_volunteer", user_type: Volunteer).save SmsNotificationEvent.new(name: "sms_event_test_supervisor", user_type: Supervisor).save SmsNotificationEvent.new(name: "sms_event_test_casa_admin", user_type: CasaAdmin).save sign_in volunteer visit edit_users_path uncheck "user_receive_sms_notifications" expect(page).to have_field("toggle-sms-notification-event", type: "checkbox", disabled: true) end end context "when a user's casa organization does not have twilio enabled" do it "disables a users SMS communication checkbox" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit edit_users_path expect(page).to have_field("Enable Twilio For Text Messaging", type: "checkbox", disabled: true) end end context "supervisor user" do it "notifies password changed by email", :aggregate_failures do org = create(:casa_org) supervisor = create(:supervisor, casa_org: org) sign_in supervisor visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123456789" fill_in "Password Confirmation", with: "123456789" click_on "Update Password" page.has_content?("Password was successfully updated.") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Your CASA password has been changed.") end it "is able to send a confrimation email when supervisor is updating email" do org = create(:casa_org) supervisor = create(:supervisor, casa_org: org) sign_in supervisor visit edit_users_path click_on "Change Email" expect(page).to have_field("New Email", disabled: false) fill_in "current_password_email", with: "12345678" fill_in "New Email", with: "new_supervisor@example.com" click_on "Update Email" expect(page).to have_content "Click the link in your new email to finalize the email transfer" expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "displays email errors messages when user is unable to set a email with incorrect current password" do org = create(:casa_org) supervisor = create(:supervisor, casa_org: org) sign_in supervisor visit edit_users_path click_on "Change Email" fill_in "current_password_email", with: "12345" fill_in "New Email", with: "new_supervisor@example" click_on "Update Email" expect(page).to have_content "1 error prohibited this Supervisor from being saved:" expect(page).to have_text("Current password is incorrect") end it "displays sms notification events for the supervisor user" do org = create(:casa_org, twilio_enabled: true) supervisor = create(:supervisor, casa_org: org) SmsNotificationEvent.delete_all SmsNotificationEvent.new(name: "sms_event_test_volunteer", user_type: Volunteer).save SmsNotificationEvent.new(name: "sms_event_test_supervisor", user_type: Supervisor).save SmsNotificationEvent.new(name: "sms_event_test_casa_admin", user_type: CasaAdmin).save sign_in supervisor visit edit_users_path expect(page).not_to have_content "sms_event_test_volunteer" expect(page).to have_content "sms_event_test_supervisor" expect(page).not_to have_content "sms_event_test_casa_admin" end it "displays Supervisor error message if no communication preference is selected" do org = create(:casa_org) supervisor = create(:supervisor, casa_org: org) sign_in supervisor visit edit_users_path uncheck "user_receive_email_notifications" click_on "Save Preferences" expect(page).to have_content "1 error prohibited this Supervisor from being saved:" expect(page).to have_text("At least one communication preference must be selected.") end it "displays Supervisor error message if SMS communication preference is selected without adding a valid phone number" do org = create(:casa_org, twilio_enabled: true) supervisor = create(:supervisor, casa_org: org) sign_in supervisor visit edit_users_path uncheck "user_receive_email_notifications" check "user_receive_sms_notifications" click_on "Save Preferences" expect(page).to have_content "1 error prohibited this Supervisor from being saved:" expect(page).to have_text("Must add a valid phone number to receive SMS notifications.") end it "displays Supervisor error message if invalid date of birth" do org = create(:casa_org) supervisor = create(:supervisor, casa_org: org) sign_in supervisor visit edit_users_path fill_in "Date of birth", with: 8.days.from_now.strftime("%Y/%m/%d") click_on "Update Profile" expect(page).to have_content "1 error prohibited this Supervisor from being saved:" expect(page).to have_text("Date of birth must be in the past.") end end context "when admin" do it "is not able to update the profile without display name as an admin" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path fill_in "Display name", with: "" click_on "Update Profile" expect(page).to have_text("Display name can't be blank") end context "shows error for invalid phone number" do it "shows error message for phone number < 12 digits" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path fill_in "Phone number", with: "+141632489" click_on("Update Profile") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for phone number > 12 digits" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path fill_in "Phone number", with: "+141632180923" click_on("Update Profile") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for bad phone number" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path fill_in("Phone number", with: "+141632u809o") click_on("Update Profile") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for phone number without country code" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path fill_in("Phone number", with: "+24163218092") click_on("Update Profile") expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end end it "is able to send a confirmation email when Casa Admin updates their email" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path click_on "Change Email" expect(page).to have_field("New Email", disabled: false) fill_in "current_password_email", with: "12345678" fill_in "New Email", with: "new_admin@example.com" click_on "Update Email" expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "displays email errors messages when user is unable to set a email with incorrect current password" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path click_on "Change Email" fill_in "current_password_email", with: "12345" fill_in "New Email", with: "new_admin@example.com" click_on "Update Email" expect(page).to have_content "1 error prohibited this Casa admin from being saved:" expect(page).to have_text("Current password is incorrect") end it "displays password errors messages when admin is unable to set a password" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123" fill_in "Password Confirmation", with: "1234" click_on "Update Password" expect(page).to have_content "2 errors prohibited this password change from being saved:" expect(page).to have_text("Password confirmation doesn't match Password") expect(page).to have_text("Password is too short (minimum is #{User.password_length.min} characters)") end it "display success message when admin update password" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123456789" fill_in "Password Confirmation", with: "123456789" click_on "Update Password" expect(page).to have_text("Password was successfully updated.") end it "displays sms notification events for the casa admin user" do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) SmsNotificationEvent.delete_all SmsNotificationEvent.new(name: "sms_event_test_volunteer", user_type: Volunteer).save SmsNotificationEvent.new(name: "sms_event_test_supervisor", user_type: Supervisor).save SmsNotificationEvent.new(name: "sms_event_test_casa_admin", user_type: CasaAdmin).save sign_in admin visit edit_users_path expect(page).not_to have_content "sms_event_test_volunteer" expect(page).not_to have_content "sms_event_test_supervisor" expect(page).to have_content "sms_event_test_casa_admin" end it "notifies password changed by email", :aggregate_failures do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path click_on "Change Password" fill_in "Current Password", with: "12345678" fill_in "New Password", with: "123456789" fill_in "Password Confirmation", with: "123456789" click_on "Update Password" page.has_content?("Password was successfully updated.") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Your CASA password has been changed.") end it "displays admin error message if no communication preference is selected" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path uncheck "user_receive_email_notifications" click_on "Save Preferences" expect(page).to have_content "1 error prohibited this Casa admin from being saved:" expect(page).to have_text("At least one communication preference must be selected.") end it "displays admin error message if SMS communication preference is selected without adding a valid phone number" do org = create(:casa_org, twilio_enabled: true) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path uncheck "user_receive_email_notifications" check "user_receive_sms_notifications" click_on "Save Preferences" expect(page).to have_content "1 error prohibited this Casa admin from being saved:" expect(page).to have_text("Must add a valid phone number to receive SMS notifications.") end it "displays admin error message if invalid date of birth" do org = create(:casa_org) admin = create(:casa_admin, casa_org: org) sign_in admin visit edit_users_path fill_in "Date of birth", with: 8.days.from_now.strftime("%Y/%m/%d") click_on "Update Profile" expect(page).to have_text("Date of birth must be in the past.") end end end ================================================ FILE: spec/system/volunteers/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "volunteers/edit", type: :system do describe "updating volunteer personal data" do context "with valid data" do it "updates successfully" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_display_name", with: "Kamisato Ayato" fill_in "volunteer_phone_number", with: "+14163248967" fill_in "volunteer_date_of_birth", with: Date.new(1998, 7, 1) click_on "Submit" expect(page).to have_text "Volunteer was successfully updated." end end context "with invalid data" do context "shows error for invalid phone number" do it "shows error message for phone number < 12 digits" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_phone_number", with: "+141632489" click_on "Submit" expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for phone number > 12 digits" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_phone_number", with: "+141632180923" click_on "Submit" expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for bad phone number" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_phone_number", with: "+141632u809o" click_on "Submit" expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for phone number without country code" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_phone_number", with: "+24163218092" click_on "Submit" expect(page).to have_text "Phone number must be 10 digits or 12 digits including country code (+1)" end it "shows error message for invalid date of birth" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_date_of_birth", with: 5.days.from_now click_on "Submit" expect(page).to have_text "Date of birth must be in the past." end end it "shows error message for duplicate email" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) volunteer.supervisor = build(:supervisor) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_display_name", with: "Kamisato Ayato" fill_in "volunteer_email", with: admin.email fill_in "volunteer_display_name", with: "Mickey Mouse" click_on "Submit" expect(page).to have_text "already been taken" end it "shows error message for empty fields" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) volunteer.supervisor = create(:supervisor) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_email", with: "" fill_in "volunteer_display_name", with: "" click_on "Submit" expect(page).to have_text "can't be blank" end end end describe "updating a volunteer's email" do context "with a valid email" do it "sends volunteer a confirmation email and does not change the displayed email" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) old_email = volunteer.email sign_in admin visit edit_volunteer_path(volunteer) fill_in "Email", with: "newemail@example.com" click_on "Submit" expect(page).to have_text "Volunteer was successfully updated. Confirmation Email Sent." expect(page).to have_field("Email", with: old_email) expect(volunteer.reload.unconfirmed_email).to eq("newemail@example.com") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first).to be_a(Mail::Message) expect(ActionMailer::Base.deliveries.first.body.encoded) .to match("Click here to confirm your email") end it "succesfully displays the new email once the user confirms" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) old_email = volunteer.email sign_in admin visit edit_volunteer_path(volunteer) fill_in "Email", with: "newemail@example.com" click_on "Submit" volunteer.reload volunteer.confirm visit edit_volunteer_path(volunteer) expect(page).to have_field("Email", with: "newemail@example.com") expect(page).not_to have_field("Email", with: old_email) expect(volunteer.old_emails).to eq([old_email]) end end end it "saves the user as inactive, but only if the admin confirms", :js do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) sign_in admin visit edit_volunteer_path(volunteer) dismiss_confirm do scroll_to(".actions") click_on "Deactivate volunteer" end expect(page).not_to have_text("Volunteer was deactivated on") accept_confirm do click_on "Deactivate volunteer" end expect(page).to have_text("Volunteer was deactivated on") expect(volunteer.reload).not_to be_active end it "allows an admin to reactivate a volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) inactive_volunteer = build(:volunteer, casa_org: organization) inactive_volunteer.deactivate sign_in admin visit edit_volunteer_path(inactive_volunteer) click_on "Activate volunteer" expect(page).not_to have_text("Volunteer was deactivated on") expect(inactive_volunteer.reload).to be_active end it "allows the admin to unassign a volunteer from a supervisor" do organization = create(:casa_org) supervisor = build(:supervisor, display_name: "Haka Haka", casa_org: organization) volunteer = create(:volunteer, display_name: "Bolu Bolu", supervisor: supervisor, casa_org: organization) admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_volunteer_path(volunteer) expect(page).to have_content("Current Supervisor: Haka Haka") click_on "Unassign from Supervisor" expect(page).to have_content("Bolu Bolu was unassigned from Haka Haka") end it "shows the admin the option to assign an unassigned volunteer to a different active supervisor" do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) deactivated_supervisor = build(:supervisor, active: false, casa_org: organization, display_name: "Inactive Supervisor") active_supervisor = create(:supervisor, active: true, casa_org: organization, display_name: "Active Supervisor") admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_volunteer_path(volunteer) expect(page).not_to have_select("supervisor_volunteer[supervisor_id]", with_options: [deactivated_supervisor.display_name]) expect(page).to have_select("supervisor_volunteer[supervisor_id]", options: [active_supervisor.display_name]) expect(page).to have_content("Select a Supervisor") expect(page).to have_content("Assign a Supervisor") end context "when the volunteer is unassigned from all of their cases" do it "does not show any active assignment status in the Manage Cases section" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) casa_case_1 = create(:casa_case, casa_org: organization, case_number: "CINA1") casa_case_2 = create(:casa_case, casa_org: organization, case_number: "CINA2") create(:case_assignment, volunteer: volunteer, casa_case: casa_case_1) create(:case_assignment, volunteer: volunteer, casa_case: casa_case_2) casa_case_1.update!(active: false) casa_case_2.update!(active: false) sign_in admin visit edit_volunteer_path(volunteer) within "#manage_cases" do expect(page).not_to have_content("Volunteer is Active") end end it "shows the unassigned cases in the Manage Cases section" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) casa_case_1 = create(:casa_case, casa_org: organization, case_number: "CINA1") casa_case_2 = create(:casa_case, casa_org: organization, case_number: "CINA2") case_assignment_1 = create(:case_assignment, volunteer: volunteer, casa_case: casa_case_1) case_assignment_2 = create(:case_assignment, volunteer: volunteer, casa_case: casa_case_2) casa_case_1.update!(active: false) casa_case_2.update!(active: false) sign_in admin visit edit_volunteer_path(volunteer) within "#case_assignment_#{case_assignment_1.id}" do expect(page).to have_link("CINA1", href: "/casa_cases/#{casa_case_1.case_number.parameterize}") end within "#case_assignment_#{case_assignment_2.id}" do expect(page).to have_link("CINA2", href: "/casa_cases/#{casa_case_2.case_number.parameterize}") end end it "shows assignment status as 'Volunteer is Unassigned' for each unassigned case" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) casa_case_1 = create(:casa_case, casa_org: organization, case_number: "CINA1") casa_case_2 = create(:casa_case, casa_org: organization, case_number: "CINA2") case_assignment_1 = build(:case_assignment, volunteer: volunteer, casa_case: casa_case_1) case_assignment_2 = build(:case_assignment, volunteer: volunteer, casa_case: casa_case_2) case_assignment_1.active = false case_assignment_2.active = false case_assignment_1.save case_assignment_2.save sign_in admin visit edit_volunteer_path(volunteer) within "#case_assignment_#{case_assignment_1.id}" do expect(page).to have_content("Volunteer is Unassigned") end within "#case_assignment_#{case_assignment_2.id}" do expect(page).to have_content("Volunteer is Unassigned") end end end context "with a deactivated case" do it "displays inactive message" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) deactivated_casa_case = create(:casa_case, active: false, casa_org: volunteer.casa_org, volunteers: [volunteer]) sign_in admin visit edit_volunteer_path(volunteer) expect(page).to have_text "Case was deactivated on: #{I18n.l(deactivated_casa_case.updated_at, format: :standard, default: nil)}" end end context "when volunteer is assigned to multiple cases" do it "supervisor assigns multiple cases to the same volunteer" do casa_org = build(:casa_org) supervisor = create(:casa_admin, casa_org: casa_org) volunteer = create(:volunteer, casa_org: casa_org, display_name: "AAA") casa_case_1 = create(:casa_case, casa_org: casa_org, case_number: "CINA1") casa_case_2 = create(:casa_case, casa_org: casa_org, case_number: "CINA2") sign_in supervisor visit edit_volunteer_path(volunteer) select casa_case_1.case_number, from: "Select a Case" click_on "Assign Case" expect(page).to have_text("Volunteer assigned to case") expect(page).to have_text(casa_case_1.case_number) select casa_case_2.case_number, from: "Select a Case" click_on "Assign Case" expect(page).to have_text("Volunteer assigned to case") expect(page).to have_text(casa_case_2.case_number) end end context "with previously assigned cases" do it "shows the unassign button for assigned cases and not for unassigned cases" do casa_org = build(:casa_org) supervisor = create(:casa_admin, casa_org: casa_org) volunteer = create(:volunteer, casa_org: casa_org, display_name: "AAA") casa_case_1 = build(:casa_case, casa_org: casa_org, case_number: "CINA1") casa_case_2 = build(:casa_case, casa_org: casa_org, case_number: "CINA2") assignment1 = volunteer.case_assignments.create(casa_case: casa_case_1, active: true) assignment2 = volunteer.case_assignments.create(casa_case: casa_case_2, active: false) sign_in supervisor visit edit_volunteer_path(volunteer) within("#case_assignment_#{assignment1.id}") do expect(page).to have_text(casa_case_1.case_number) expect(page).to have_button("Unassign Case") end within("#case_assignment_#{assignment2.id}") do expect(page).to have_text(casa_case_2.case_number) expect(page).not_to have_button("Unassign Case") end select casa_case_2.case_number, from: "Select a Case" click_on "Assign Case" within("#case_assignment_#{assignment2.id}") do expect(page).to have_text(casa_case_2.case_number) expect(page).to have_button("Unassign Case") end end end describe "inactive case visibility" do it "supervisor does not have inactive cases as an option to assign to a volunteer" do organization = build(:casa_org) active_casa_case = create(:casa_case, casa_org: organization, case_number: "ACTIVE") inactive_casa_case = create(:casa_case, casa_org: organization, active: false, case_number: "INACTIVE") volunteer = create(:volunteer, display_name: "Awesome Volunteer", casa_org: organization) supervisor = build(:casa_admin, casa_org: organization) sign_in supervisor visit edit_volunteer_path(volunteer) expect(page).to have_content(active_casa_case.case_number) expect(page).not_to have_content(inactive_casa_case.case_number) end end describe "resend invite" do it "allows a supervisor resend invitation to a volunteer" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) supervisor = create(:supervisor, casa_org: organization) sign_in supervisor visit edit_volunteer_path(volunteer) click_on "Resend Invitation" expect(page).to have_content("Invitation sent") deliveries = ActionMailer::Base.deliveries expect(deliveries.count).to eq(1) expect(deliveries.last.subject).to have_text "CASA Console invitation instructions" end end it "allows an administrator resend invitation to a volunteer" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_volunteer_path(volunteer) click_on "Resend Invitation" expect(page).to have_content("Invitation sent") deliveries = ActionMailer::Base.deliveries expect(deliveries.count).to eq(1) expect(deliveries.last.subject).to have_text "CASA Console invitation instructions" end describe "Send Reactivation (SMS)" do it "allows admin to send a reactivation SMS to a volunteer if their org has twilio enabled" do organization = create(:casa_org, twilio_enabled: true) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) admin = create(:casa_admin, casa_org: organization) sign_in admin visit edit_volunteer_path(volunteer) expect(page).to have_content("Send Reactivation Alert (SMS)") expect(page).not_to have_content("Enable Twilio") expect(page).to have_selector("#twilio_enabled") end context "admin's organization does not have twilio enabled" do it "displays a disabed (SMS) button with appropriate message" do org_twilio = create(:casa_org, twilio_enabled: false) admin_twilio = create(:casa_admin, casa_org: org_twilio) volunteer_twilio = create(:volunteer, casa_org: org_twilio) sign_in admin_twilio visit edit_volunteer_path(volunteer_twilio) expect(page).to have_content("Enable Twilio To Send Reactivation Alert (SMS)") expect(page).to have_selector("#twilio_disabled") end end end describe "send reminder as a supervisor" do it "emails the volunteer" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) supervisor = create(:supervisor, casa_org: organization) sign_in supervisor visit edit_volunteer_path(volunteer) expect(page).to have_button("Send Reminder") expect(page).to have_text("Send CC to Supervisor") uncheck "with_cc" click_on "Send Reminder" expect(page).to have_content("Reminder sent to volunteer") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first.cc).to be_empty end it "emails volunteer and cc's the supervisor" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) supervisor = create(:supervisor, casa_org: organization) sign_in supervisor visit edit_volunteer_path(volunteer) check "with_cc" click_on "Send Reminder" expect(page).to have_content("Reminder sent to volunteer") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first.cc).to include(volunteer.supervisor.email) end it "emails the volunteer without a supervisor" do organization = create(:casa_org) volunteer_without_supervisor = create(:volunteer) supervisor = create(:supervisor, casa_org: organization) sign_in supervisor visit edit_volunteer_path(volunteer_without_supervisor) check "with_cc" click_on "Send Reminder" expect(page).to have_content("Reminder sent to volunteer") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first.cc).to be_empty end end describe "send reminder as admin" do it "emails the volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) expect(page).to have_button("Send Reminder") expect(page).to have_text("Send CC to Supervisor and Admin") click_on "Send Reminder" expect(page).to have_content("Reminder sent to volunteer") expect(ActionMailer::Base.deliveries.count).to eq(1) end it "emails the volunteer and cc's their supervisor and admin" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) check "with_cc" click_on "Send Reminder" expect(page).to have_content("Reminder sent to volunteer") expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.first.cc).to include(volunteer.supervisor.email) expect(ActionMailer::Base.deliveries.first.cc).to include(admin.email) end end describe "impersonate button" do context "when user is an admin" do it "impersonates the volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org: organization) volunteer = create(:volunteer, casa_org: organization, display_name: "John Doe") sign_in admin visit edit_volunteer_path(volunteer) click_on "Impersonate" within(".header") do expect(page).to have_text( "You (#{admin.display_name}) are signed in as John Doe. " \ "Click here to stop impersonating." ) end end end context "when user is a supervisor" do it "impersonates the volunteer" do organization = create(:casa_org) supervisor = create(:supervisor, casa_org: organization) volunteer = create(:volunteer, casa_org: organization, display_name: "John Doe") sign_in supervisor visit edit_volunteer_path(volunteer) click_on "Impersonate" within(".header") do expect(page).to have_text( "You (#{supervisor.display_name}) are signed in as John Doe. " \ "Click here to stop impersonating." ) end end end context "when user is a volunteer" do it "does not show the impersonate button", :aggregate_failures do organization = create(:casa_org) volunteer = create(:volunteer, casa_org: organization) user = create(:volunteer, casa_org: organization) sign_in user visit edit_volunteer_path(volunteer) expect(page).not_to have_link("Impersonate") expect(page).to have_no_current_path(edit_volunteer_path(volunteer), ignore_query: true) end end end context "logged in as an admin" do it "can save notes about a volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) freeze_time do current_date = Date.today fill_in("note[content]", with: "Great job today.") within(".notes") do click_on("Save Note") end expect(page).to have_current_path(edit_volunteer_path(volunteer), ignore_query: true) within(".notes") do expect(page).to have_text("Great job today.") expect(page).to have_text(admin.display_name) expect(page).to have_text(I18n.l(current_date.to_date, format: :standard, default: "")) end end end it "can delete notes about a volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) volunteer.notes.create(creator: admin, content: "Note_1") volunteer.notes.create(creator: admin, content: "Note_2") volunteer.notes.create(creator: admin, content: "Note_3") sign_in admin visit edit_volunteer_path(volunteer) expect(page).to have_css ".notes .table tbody tr", count: 3 click_on("Delete", match: :first) expect(page).to have_css ".notes .table tbody tr", count: 2 end end context "logged in as a supervisor" do it "can save notes about a volunteer" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) supervisor = volunteer.supervisor sign_in supervisor visit edit_volunteer_path(volunteer) freeze_time do current_date = Date.today fill_in("note[content]", with: "Great job today.") within(".notes") do click_on("Save Note") end expect(page).to have_current_path(edit_volunteer_path(volunteer), ignore_query: true) within(".notes") do expect(page).to have_text("Great job today.") expect(page).to have_text(volunteer.supervisor.display_name) expect(page).to have_text(I18n.l(current_date.to_date, format: :standard, default: "")) end end end it "can delete notes about a volunteer" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) supervisor = volunteer.supervisor volunteer.notes.create(creator: admin, content: "Note_1") volunteer.notes.create(creator: admin, content: "Note_2") volunteer.notes.create(creator: admin, content: "Note_3") sign_in supervisor visit edit_volunteer_path(volunteer) expect(page).to have_css ".notes .table tbody tr", count: 3 click_on("Delete", match: :first) expect(page).to have_css ".notes .table tbody tr", count: 2 end end context "logged in as volunteer" do it "can't see the notes section" do organization = create(:casa_org) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) sign_in volunteer visit edit_volunteer_path(volunteer) expect(page).not_to have_selector(".notes") expect(page).to have_content("Sorry, you are not authorized to perform this action.") end end describe "updating volunteer address" do context "with mileage reimbursement turned on" do it "shows 'Mailing address' label" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) expect(page).to have_text "Mailing address" expect(page).to have_selector "input[type=text][id=volunteer_address_attributes_content]" end it "updates successfully" do organization = create(:casa_org) admin = create(:casa_admin, casa_org_id: organization.id) volunteer = create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) sign_in admin visit edit_volunteer_path(volunteer) fill_in "volunteer_address_attributes_content", with: "123 Main St" click_on "Submit" expect(page).to have_text "Volunteer was successfully updated." expect(page).to have_selector("input[value='123 Main St']") end end end end ================================================ FILE: spec/system/volunteers/index_spec.rb ================================================ require "rails_helper" RSpec.describe "view all volunteers", :js, type: :system do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } let!(:supervisor) { create(:supervisor, casa_org: organization) } context "admin user" do context "when no logo_url" do it "can see volunteers and navigate to their cases", :js do volunteer = create(:volunteer, display_name: "User 1", email: "casa@example.com", casa_org: organization, supervisor: supervisor) volunteer.casa_cases << build(:casa_case, casa_org: organization, birth_month_year_youth: CasaCase::TRANSITION_AGE.years.ago) volunteer.casa_cases << build(:casa_case, casa_org: organization, birth_month_year_youth: CasaCase::TRANSITION_AGE.years.ago) casa_case = volunteer.casa_cases[0] sign_in admin visit volunteers_path expect(page).to have_text("User 1") expect(page).to have_text(casa_case.case_number) within "#volunteers" do click_on volunteer.casa_cases.first.case_number end expect(page).to have_text("CASA Case Details") expect(page).to have_text("Case number: #{casa_case.case_number}") expect(page).to have_text("Transition Aged Youth: Yes") expect(page).to have_text("Next Court Date:") expect(page).to have_text("Court Report Status: Not submitted") expect(page).to have_text("Assigned Volunteers:") end it "displays default logo" do sign_in admin visit volunteers_path expect(page).to have_css("#casa-logo[src*='default-logo']") expect(page).to have_css("#casa-logo[alt='CASA Logo']") end end it "displays last attempted contact by default", :js do create(:volunteer, display_name: "User 1", email: "casa@example.com", casa_org: organization, supervisor: supervisor) sign_in admin visit volunteers_path expect(page).to have_content(:visible, "Last Attempted Contact") end it "can show/hide columns on volunteers table", :js do sign_in admin visit volunteers_path expect(page).to have_text("Pick displayed columns") click_on "Pick displayed columns" expect(page).to have_text("Name") expect(page).to have_text("Status") expect(page).to have_text("Contact Made In Past 60 Days") expect(page).to have_text("Last Attempted Contact") check "Name" check "Status" uncheck "Contact Made In Past 60 Days" uncheck "Last Attempted Contact" within(".modal-dialog") do click_on "Close" end expect(page).to have_text("Name") expect(page).to have_text("Status") within("#volunteers") do expect(page).to have_no_text("Contact Made In Past 60 Days") expect(page).to have_no_text("Last Attempted Contact") end end it "can filter volunteers", :js do assigned_volunteers = create_list(:volunteer, 2, casa_org: organization, supervisor: supervisor) inactive_volunteers = create_list(:volunteer, 2, :inactive, casa_org: organization) unassigned_volunteer = create(:volunteer, casa_org: organization) sign_in admin visit volunteers_path expect(page).to have_css(".volunteer-filters") assigned_volunteers.each do |assigned_volunteer| expect(page).to have_text assigned_volunteer.display_name end expect(page).to have_text unassigned_volunteer.display_name click_on "Status" find(:css, 'input[data-value="true"]').set(false) expect(page).to have_text("No matching records found") find(:css, 'input[data-value="false"]').set(true) inactive_volunteers.each do |inactive_volunteer| expect(page).to have_text inactive_volunteer.display_name end expect(page).to have_css("table#volunteers tbody tr", count: inactive_volunteers.count) visit volunteers_path click_on "Supervisor" find_by_id("unassigned-vol-filter").set(false) assigned_volunteers.each do |assigned_volunteer| expect(page).to have_text assigned_volunteer.display_name end expect(page).to have_css("table#volunteers tbody tr", count: assigned_volunteers.count) end it "can go to the volunteer edit page from the volunteer list", :js do create(:volunteer, casa_org: organization, supervisor: supervisor) sign_in admin visit volunteers_path within "#volunteers" do click_on "Edit" end expect(page).to have_text("Editing Volunteer") end it "can go to the new volunteer page" do sign_in admin visit volunteers_path click_on "New Volunteer" expect(page).to have_text("New Volunteer") expect(page).to have_css("form#new_volunteer") end describe "supervisor column of volunteers table" do it "is blank when volunteer has no supervisor", :js do create(:volunteer, casa_org: organization) sign_in admin visit volunteers_path click_on "Supervisor" find_by_id("unassigned-vol-filter").set(true) expect(page).to have_css("tbody .supervisor-column", text: "") end it "displays supervisor's name when volunteer has supervisor", :js do name = "Superduper Visor" supervisor = create(:supervisor, display_name: name, casa_org: organization) create(:volunteer, supervisor: supervisor, casa_org: organization) sign_in admin visit volunteers_path expect(page).to have_css("tbody .supervisor-column", text: name) end it "is blank when volunteer's supervisor is inactive", :js do create(:volunteer, :with_inactive_supervisor, casa_org: organization) sign_in admin visit volunteers_path click_on "Supervisor" find_by_id("unassigned-vol-filter").set(true) expect(page).to have_css("tbody .supervisor-column", text: "") end end describe "Manage Volunteers button" do let!(:volunteers) { create_list(:volunteer, 2, casa_org: organization) } before do sign_in admin visit volunteers_path end it "does not display by default" do expect(page).to have_no_text "Manage Volunteer" end context "when one or more volunteers selected" do it "is displayed" do find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}").click expect(page).to have_text "Manage Volunteer" end it "displays number of volunteers selected" do volunteers.each_with_index do |volunteer, index| find("#supervisor_volunteer_volunteer_ids_#{volunteer.id}").click expect(page).to have_css("[data-select-all-target='buttonLabel']", text: "#{index + 1})") end end it "text matches pluralization of volunteers selected" do find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}").click expect(page).to have_no_text "Manage Volunteers" find("#supervisor_volunteer_volunteer_ids_#{volunteers[1].id}").click expect(page).to have_text "Manage Volunteers" end it "is hidden when all volunteers unchecked" do find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}").click expect(page).to have_text "Manage Volunteer" find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}").click expect(page).to have_no_text "Manage Volunteer" end end end describe "Select All Checkbox" do let!(:volunteers) { create_list(:volunteer, 2, casa_org: organization) } before do sign_in admin visit volunteers_path end it "selects all volunteers" do find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}") # Wait for data table to be loaded find_by_id("checkbox-toggle-all").click volunteers.each do |volunteer| expect(page).to have_field("supervisor_volunteer_volunteer_ids_#{volunteer.id}", checked: true) end end context "when all are checked" do it "deselects all volunteers" do volunteers.each do |volunteer| find("#supervisor_volunteer_volunteer_ids_#{volunteer.id}").click end find_by_id("checkbox-toggle-all").click expect(page).to have_field("checkbox-toggle-all", checked: false) volunteers.each do |volunteer| expect(page).to have_field("supervisor_volunteer_volunteer_ids_#{volunteer.id}", checked: false) end end end context "when some are checked" do it "is semi-checked (indeterminate)" do find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}").click expect(page).to have_field("checkbox-toggle-all", checked: false) expect(find_by_id("checkbox-toggle-all")[:indeterminate]).to eq("true") end it "selects all volunteers" do find("#supervisor_volunteer_volunteer_ids_#{volunteers[0].id}").click find_by_id("checkbox-toggle-all").click volunteers.each do |volunteer| expect(page).to have_field("supervisor_volunteer_volunteer_ids_#{volunteer.id}", checked: true) end end end end describe "Select Supervisor Modal Submit button" do let!(:volunteer) { create(:volunteer, casa_org: organization) } before do sign_in admin visit volunteers_path end it "is disabled by default" do find("#supervisor_volunteer_volunteer_ids_#{volunteer.id}").click find("[data-select-all-target='button']").click expect(page).to have_button("Confirm", disabled: true, class: %w[deactive-btn main-btn]) end context "when none is selected" do it "is enabled" do find("#supervisor_volunteer_volunteer_ids_#{volunteer.id}").click find("[data-select-all-target='button']").click select "None", from: "supervisor_volunteer_supervisor_id" expect(page).to have_button("Confirm", disabled: false, class: %w[!deactive-btn dark-btn btn-hover]) end end context "when a supervisor is selected" do it "is enabled" do find("#supervisor_volunteer_volunteer_ids_#{volunteer.id}").click find("[data-select-all-target='button']").click select supervisor.display_name, from: "supervisor_volunteer_supervisor_id" expect(page).to have_button("Confirm", disabled: false, class: %w[!deactive-btn dark-btn btn-hover]) end end context "when Choose a supervisor is selected" do it "is disabled" do visit volunteers_path find("#supervisor_volunteer_volunteer_ids_#{volunteer.id}").click find("[data-select-all-target='button']").click select supervisor.display_name, from: "supervisor_volunteer_supervisor_id" select "Choose a supervisor", from: "supervisor_volunteer_supervisor_id" expect(page).to have_button("Confirm", disabled: true, class: %w[deactive-btn !dark-btn !btn-hover]) end end end end context "supervisor user" do it "can filter volunteers", :js do active_volunteer = create(:volunteer, :with_assigned_supervisor, casa_org: organization) active_volunteer.supervisor = supervisor inactive_volunteers = create_list(:volunteer, 2, :inactive, supervisor: supervisor, casa_org: organization) sign_in supervisor visit volunteers_path expect(page).to have_css(".volunteer-filters") expect(page).to have_css("table#volunteers tbody tr", count: 1) click_on "Status" find(:css, 'input[data-value="true"]').set(false) expect(page).to have_text("No matching records found") find(:css, 'input[data-value="false"]').set(true) inactive_volunteers.each do |inactive_volunteer| expect(page).to have_text inactive_volunteer.display_name end expect(page).to have_css("table#volunteers tbody tr", count: inactive_volunteers.count) end it "can show/hide columns on volunteers table", :js do sign_in supervisor visit volunteers_path expect(page).to have_text("Pick displayed columns") click_on "Pick displayed columns" expect(page).to have_text("Name") expect(page).to have_text("Status") expect(page).to have_text("Contact Made In Past 60 Days") expect(page).to have_text("Last Attempted Contact") check "Name" check "Status" uncheck "Contact Made In Past 60 Days" uncheck "Last Attempted Contact" within(".modal-dialog") do click_on "Close" end expect(page).to have_text("Name") expect(page).to have_text("Status") within("#volunteers") do expect(page).to have_no_text("Contact Made In Past 60 Days") expect(page).to have_no_text("Last Attempted Contact") end end it "can persist 'show/hide' column preference settings", :js do sign_in supervisor visit volunteers_path expect(page).to have_text("Pick displayed columns") within("#volunteers") do expect(page).to have_text("Name") expect(page).to have_text("Email") expect(page).to have_text("Status") expect(page).to have_text("Assigned To Transition Aged Youth") expect(page).to have_text("Case Number(s)") expect(page).to have_text("Last Attempted Contact") expect(page).to have_text("Contacts Made in Past 60 Day") end click_on "Pick displayed columns" uncheck "Name" uncheck "Status" uncheck "Contact Made In Past 60 Days" uncheck "Last Attempted Contact" within(".modal-dialog") do click_on "Close" end within("#volunteers") do expect(page).to have_no_text("Name") expect(page).to have_no_text("Status") expect(page).to have_no_text("Contact Made In Past 60 Days") expect(page).to have_no_text("Last Attempted Contact") expect(page).to have_text("Email") expect(page).to have_text("Assigned To Transition Aged Youth") expect(page).to have_text("Case Number(s)") end refresh # Expectations after page reload within("#volunteers") do expect(page).to have_no_text("Name") expect(page).to have_no_text("Status") expect(page).to have_no_text("Contact Made In Past 60 Days") expect(page).to have_no_text("Last Attempted Contact") expect(page).to have_text("Email") expect(page).to have_text("Assigned To Transition Aged Youth") expect(page).to have_text("Case Number(s)") end end context "with volunteers" do it "Search history is clean after navigating away from volunteers view", :js do sign_in supervisor visit volunteers_path page.fill_in("Search:", with: "Test") visit supervisors_path visit volunteers_path expect(page).to have_css("#volunteers_filter input", text: "") end end end end ================================================ FILE: spec/system/volunteers/invite_spec.rb ================================================ require "rails_helper" RSpec.describe "Inviting volunteers", type: :system do let(:organization) { create(:casa_org) } let(:admin) { create(:casa_admin, casa_org: organization) } before do sign_in admin end describe "creating and sending invitation" do it "creates a new volunteer and sends invitation email" do visit new_volunteer_path fill_in "Email", with: "new_volunteer@example.com" fill_in "Display name", with: "Jane Doe" fill_in "Date of birth", with: Date.new(1995, 5, 15) click_on "Create Volunteer" expect(page).to have_selector(".notice", text: "New volunteer created successfully") volunteer = Volunteer.find_by(email: "new_volunteer@example.com") expect(volunteer).to be_present expect(volunteer.invitation_created_at).not_to be_nil expect(volunteer.invitation_accepted_at).to be_nil # Verify invitation email was sent last_email = ActionMailer::Base.deliveries.last expect(last_email.to).to eq ["new_volunteer@example.com"] expect(last_email.subject).to have_text "CASA Console invitation instructions" expect(last_email.html_part.body.encoded).to have_text "your new Volunteer account" end it "sets invitation_created_at timestamp" do visit new_volunteer_path fill_in "Email", with: "volunteer_with_token@example.com" fill_in "Display name", with: "John Smith" fill_in "Date of birth", with: Date.new(1990, 1, 1) click_on "Create Volunteer" expect(page).to have_selector(".notice", text: "New volunteer created successfully") volunteer = Volunteer.find_by(email: "volunteer_with_token@example.com") expect(volunteer.invitation_created_at).to be_present expect(volunteer.invitation_accepted_at).to be_nil end end describe "accepting invitation" do let(:volunteer) { create(:volunteer, casa_org: organization, phone_number: nil) } let!(:invitation_token) do volunteer.invite!(admin) volunteer.raw_invitation_token end before do sign_out admin end it "shows the invitation acceptance form" do visit accept_user_invitation_path(invitation_token: invitation_token) expect(page).to have_text "Set my password" expect(page).to have_field("Password") expect(page).to have_field("Password confirmation") expect(page).to have_button("Set my password") end it "allows volunteer to set password and accept invitation" do visit accept_user_invitation_path(invitation_token: invitation_token) expect(page).to have_text "Set my password" fill_in "Password", with: "SecurePassword123!" fill_in "Password confirmation", with: "SecurePassword123!" click_on "Set my password" expect(page).to have_selector(".notice", text: "Your password was set successfully. You are now signed in") expect(page).to have_text("My Cases") volunteer.reload expect(volunteer.invitation_accepted_at).not_to be_nil end it "shows error when passwords don't match" do visit accept_user_invitation_path(invitation_token: invitation_token) fill_in "Password", with: "SecurePassword123!" fill_in "Password confirmation", with: "DifferentPassword456!" click_on "Set my password" expect(page).to have_text "Password confirmation doesn't match" volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end it "shows error when password is too short" do visit accept_user_invitation_path(invitation_token: invitation_token) fill_in "Password", with: "short" fill_in "Password confirmation", with: "short" click_on "Set my password" expect(page).to have_text "Password is too short" volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end it "shows error when password is blank" do visit accept_user_invitation_path(invitation_token: invitation_token) fill_in "Password", with: "" fill_in "Password confirmation", with: "" click_on "Set my password" expect(page).to have_text "can't be blank" volunteer.reload expect(volunteer.invitation_accepted_at).to be_nil end end describe "resending invitation" do let(:volunteer) { create(:volunteer, casa_org: organization, phone_number: nil) } before do volunteer.invite!(admin) end it "allows admin to resend invitation to volunteer who hasn't accepted" do visit edit_volunteer_path(volunteer) click_on "Resend Invitation" expect(page).to have_text "Invitation sent" last_email = ActionMailer::Base.deliveries.last expect(last_email.to).to eq [volunteer.email] expect(last_email.subject).to have_text "CASA Console invitation instructions" end it "hides resend button after invitation is accepted" do volunteer.update!(invitation_accepted_at: Time.current) visit edit_volunteer_path(volunteer) expect(page).not_to have_link("Resend Invitation") end end describe "supervisor creating volunteer" do let(:supervisor) { create(:supervisor, casa_org: organization) } before do sign_out admin sign_in supervisor end it "allows supervisor to create and invite a volunteer" do visit new_volunteer_path fill_in "Email", with: "supervisor_volunteer@example.com" fill_in "Display name", with: "Supervisor's Volunteer" fill_in "Date of birth", with: Date.new(1992, 3, 20) click_on "Create Volunteer" expect(page).to have_selector(".notice", text: "New volunteer created successfully") volunteer = Volunteer.find_by(email: "supervisor_volunteer@example.com") expect(volunteer).to be_present expect(volunteer.invitation_created_at).not_to be_nil # Verify invitation email was sent last_email = ActionMailer::Base.deliveries.last expect(last_email.to).to eq ["supervisor_volunteer@example.com"] end end describe "volunteer user trying to create another volunteer" do let(:volunteer) { create(:volunteer, casa_org: organization) } before do sign_out admin sign_in volunteer end it "denies access with error message" do visit new_volunteer_path expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.") end end end ================================================ FILE: spec/system/volunteers/new_spec.rb ================================================ require "rails_helper" RSpec.describe "volunteers/new", type: :system do context "when supervisor" do let(:supervisor) { create(:supervisor) } it "creates a new volunteer", :js do sign_in supervisor visit new_volunteer_path fill_in "Email", with: "new_volunteer2@example.com" fill_in "Display name", with: "New Volunteer Display Name 2" fill_in "Date of birth", with: Date.new(2000, 1, 2) click_on "Create Volunteer" visit volunteers_path expect(page).to have_text("New Volunteer Display Name 2") expect(page).to have_text("new_volunteer2@example.com") expect(page).to have_text("Active") end end context "volunteer user" do it "redirects the user with an error message" do volunteer = create(:volunteer) sign_in volunteer visit new_volunteer_path expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.") end it "displays learning hour topic when enabled", :js do organization = build(:casa_org, learning_topic_active: true) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit new_learning_hour_path expect(page).to have_text("Learning Topic") end it "does not display learning hour topic when disabled", :js do organization = build(:casa_org) volunteer = create(:volunteer, casa_org: organization) sign_in volunteer visit new_learning_hour_path expect(page).not_to have_text("Learning Topic") end end end ================================================ FILE: spec/system/volunteers/notes/edit_spec.rb ================================================ require "rails_helper" RSpec.describe "volunteers/notes/edit", type: :system do let(:organization) { create(:casa_org) } let(:admin) { build(:casa_admin, casa_org_id: organization.id) } let(:volunteer) { create(:volunteer, :with_assigned_supervisor, casa_org_id: organization.id) } let(:note) { volunteer.notes.create(creator: admin, content: "Good job.") } context "when logged in as an admin" do before do sign_in admin visit edit_volunteer_note_path(volunteer, note) end scenario "editing an existing note" do expect(page).to have_text("Good job.") fill_in("note[content]", with: "Great job!") click_on("Update Note") expect(page).to have_current_path edit_volunteer_path(volunteer), ignore_query: true expect(page).to have_text("Great job!") expect(note.reload.content).to eq "Great job!" end end end ================================================ FILE: spec/validators/casa_org_validator_spec.rb ================================================ require "rails_helper" RSpec.describe CasaOrgValidator, type: :validator do # TODO: Add tests for CasaOrgValidator pending "add some tests for CasaOrgValidator" end ================================================ FILE: spec/validators/court_report_validator_spec.rb ================================================ require "rails_helper" RSpec.describe CourtReportValidator, type: :validator do # TODO: Add tests for CourtReportValidator pending "add some tests for CourtReportValidator" end ================================================ FILE: spec/validators/url_validator_spec.rb ================================================ require "rails_helper" RSpec.describe UrlValidator, type: :validator do # TODO: Add tests for UrlValidator pending "add some tests for UrlValidator" end ================================================ FILE: spec/validators/user_validator_spec.rb ================================================ require "rails_helper" RSpec.describe UserValidator, type: :validator do # TODO: Add tests for UserValidator pending "add some tests for UserValidator" end ================================================ FILE: spec/values/all_casa_admin_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe AllCasaAdminParameters do # TODO: Add tests for AllCasaAdminParameters pending "add some tests for AllCasaAdminParameters" end ================================================ FILE: spec/values/banner_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe BannerParameters do subject { described_class.new(params, user, timezone) } let(:params) { ActionController::Parameters.new( banner: ActionController::Parameters.new( active: "1", content: "content", name: "name" ) ) } let(:user) { create(:user) } let(:timezone) { nil } it "returns data" do expect(subject["active"]).to eq("1") expect(subject["content"]).to eq("content") expect(subject["name"]).to eq("name") expect(subject["expires_at"]).to be_blank expect(subject["user"]).to eq(user) end context "when expires_at is set" do let(:params) { ActionController::Parameters.new( banner: ActionController::Parameters.new( expires_at: "2024-06-10T12:12" ) ) } let(:timezone) { "America/Los_Angeles" } it "attaches timezone information to expires_at" do expect(subject["expires_at"]).to eq("2024-06-10 12:12:00 -0700") end end end ================================================ FILE: spec/values/casa_admin_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe CasaAdminParameters do # TODO: Add tests for CasaAdminParameters pending "add some tests for CasaAdminParameters" end ================================================ FILE: spec/values/case_contact_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe CaseContactParameters do subject { described_class.new(params) } let(:params) { ActionController::Parameters.new( case_contact: ActionController::Parameters.new( duration_hours: "1", duration_minutes: "2", occurred_at: "occurred_at", contact_made: "contact_made", medium_type: "medium_type", miles_driven: "123", want_driving_reimbursement: "want_driving_reimbursement", notes: "notes", contact_type_ids: [], contact_topic_answers_attributes:, metadata: {"create_another" => "1", "bad_key" => "bad_value"} ) ) } let(:contact_topic_answers_attributes) do {"0" => {"id" => 1, "value" => "test", "question" => "question", "selected" => true}} end it "returns data" do aggregate_failures do expect(subject["duration_minutes"]).to eq(62) expect(subject["occurred_at"]).to eq("occurred_at") expect(subject["contact_made"]).to eq("contact_made") expect(subject["medium_type"]).to eq("medium_type") expect(subject["miles_driven"]).to eq(123) expect(subject["want_driving_reimbursement"]).to eq("want_driving_reimbursement") expect(subject["notes"]).to eq("notes") expect(subject["contact_type_ids"]).to eq([]) expected_attrs = contact_topic_answers_attributes["0"].except("question") expect(subject["contact_topic_answers_attributes"]["0"].to_h).to eq(expected_attrs) expect(subject["metadata"]["create_another"]).to eq(true) expect(subject["metadata"]["bad_key"]).not_to be_present end end end ================================================ FILE: spec/values/supervisor_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe SupervisorParameters do # TODO: Add tests for SupervisorParameters pending "add some tests for SupervisorParameters" end ================================================ FILE: spec/values/user_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe UserParameters do # TODO: Add tests for UserParameters pending "add some tests for UserParameters" end ================================================ FILE: spec/values/volunteer_parameters_spec.rb ================================================ require "rails_helper" RSpec.describe VolunteerParameters do subject { described_class.new(params) } let(:params) { ActionController::Parameters.new( volunteer: ActionController::Parameters.new( email: "volunteer@example.com", display_name: "Volunteer", phone_number: "1(401) 827-9485", date_of_birth: "", receive_reimbursement_email: "0" ) ) } it "returns data" do expect(subject["email"]).to eq("volunteer@example.com") expect(subject["display_name"]).to eq("Volunteer") expect(subject["phone_number"]).to eq("14018279485") end end ================================================ FILE: spec/views/all_casa_admins/casa_orgs/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "all_casa_admins/casa_orgs/new", type: :view do let(:organization) { create :casa_org } let(:admin) { build_stubbed(:all_casa_admin) } before do allow(view).to receive(:selected_organization).and_return(organization) sign_in admin render template: "all_casa_admins/casa_orgs/new" end it "shows new CASA Organization page title" do expect(rendered).to have_text("Create a new CASA Organization") end it "shows new CASA Organization form" do expect(rendered).to have_selector("input", id: "casa_org_name") expect(rendered).to have_selector("input", id: "casa_org_display_name") expect(rendered).to have_selector("input", id: "casa_org_address") expect(rendered).to have_selector("button", id: "submit") end it "requires name text field" do expect(rendered).to have_selector("input[required=required]", id: "casa_org_name") end end ================================================ FILE: spec/views/all_casa_admins/casa_orgs/show.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "all_casa_admins/casa_orgs/show", type: :view do context "All casa admin organization dashboard" do let(:organization) { create :casa_org } let(:user) { build_stubbed(:all_casa_admin) } let(:metrics) { { "metric name 1" => 1, "metric name 2" => 2 } } before do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:selected_organization).and_return(organization) assign :casa_org_metrics, metrics render end it "shows new admin button" do expect(rendered).to have_text("New CASA Admin") end it "shows metrics" do expect(rendered).to have_text("metric name 1: 1") expect(rendered).to have_text("metric name 2: 2") end end end ================================================ FILE: spec/views/all_casa_admins/edit.html.erb_spec.rb ================================================ # frozen_string_literal: true require "rails_helper" RSpec.describe "all_casa_admins/edit", type: :view do let(:user) { build_stubbed(:all_casa_admin) } before do assign(:user, user) render end it "renders the edit profile form", :aggregate_failures do expect(rendered).to have_selector("form[action='#{all_casa_admins_path}'][method='post']") expect(rendered).to have_field("all_casa_admin[email]") expect(rendered).to have_button("Update Profile") end it "renders the change password collapse section, hidden by default", :aggregate_failures do expect(rendered).to have_selector("#collapseOne.collapse") expect(rendered).not_to include('class="collapse show"') expect(rendered).to have_field("all_casa_admin[password]") expect(rendered).to have_field("all_casa_admin[password_confirmation]") expect(rendered).to have_button("Update Password") end context "when there are error and flash messages" do before do user.errors.add(:email, "can't be blank") flash[:notice] = "Profile updated" render end it "renders error and flash messages partials", :aggregate_failures do expect(rendered).to have_selector("#error_explanation.alert") expect(rendered).to have_text("can't be blank") expect(rendered).to have_selector(".header-flash") expect(rendered).to have_text("Profile updated") end end context "when submitting the password change form" do before do sign_in user assign(:user, user) render end it "shows error when password fields are blank", :aggregate_failures do user.errors.add(:password, "can't be blank") render expect(rendered).to have_selector("#error_explanation.alert") expect(rendered).to have_text("can't be blank") end it "shows error when password confirmation does not match", :aggregate_failures do user.errors.add(:password_confirmation, "doesn't match Password") render expect(rendered).to have_selector("#error_explanation.alert") expect(rendered).to have_text("doesn't match Password") end end end ================================================ FILE: spec/views/all_casa_admins/patch_notes/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "patch_notes/index", type: :view do let(:all_casa_admin) { build(:all_casa_admin) } let!(:patch_notes) { create_list(:patch_note, 2) } before do assign(:patch_notes, patch_notes) assign(:patch_note_groups, PatchNoteGroup.all) assign(:patch_note_types, PatchNoteType.all) sign_in all_casa_admin end describe "the new patch note form" do it "is present on the page" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#new-patch-note").length).to eq(1) end it "contains a button to submit the form" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#new-patch-note button").length).to eq(1) end it "contains a dropdown for the patch note group" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#new-patch-note #new-patch-note-group").length).to eq(1) end it "contains a dropdown for the patch note type" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#new-patch-note #new-patch-note-type").length).to eq(1) end it "contains a textarea to enter the patch note" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#new-patch-note textarea").length).to eq(1) end describe "the patch note group dropdown" do let!(:patch_note_group_1) { create(:patch_note_group, value: "8Sm02WT!zZnJ") } it "contains all the patch note group values as options" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) option_text = parsed_html.css("#new-patch-note #new-patch-note-group option").text expect(option_text).to include(patch_note_group_1.value) end end describe "the patch note type dropdown" do let!(:patch_note_type_1) { create(:patch_note_type, name: "3dI!9a9@s$KX") } it "contains all the patch note type values as options" do render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) option_text = parsed_html.css("#new-patch-note #new-patch-note-type option").text expect(option_text).to include(patch_note_type_1.name) end end end describe "the patch note list" do it "displays the patch_notes" do patch_notes[0].update(note: "?UvV*Z~v\"`P]4ol") patch_notes[1].update(note: "#tjJ/+o\"3s@osjV") render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css(".patch-note-list-item textarea").text).to include(patch_notes[0].note) expect(parsed_html.css(".patch-note-list-item textarea").text).to include(patch_notes[1].note) end it "displays the latest patch notes first" do patch_notes[0].update(note: "#'hQ+`dGC(qc=}wu") patch_notes[1].update(note: "k2cz&c'xYLr|&)B)") render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css(".patch-note-list-item textarea")[1].text).to include(patch_notes[0].note) expect(parsed_html.css(".patch-note-list-item textarea")[2].text).to include(patch_notes[1].note) expect(patch_notes[0].created_at < patch_notes[1].created_at).to eq(true) end it "displays the correct patch note group and patch note type with the patch note" do patch_notes[0].update(note: "#'hQ+`dGC(qc=}wu") patch_notes[1].update(note: "k2cz&c'xYLr|&)B)") render template: "all_casa_admins/patch_notes/index" parsed_html = Nokogiri.HTML5(rendered) patch_note_element = parsed_html.css(".patch-note-list-item")[1] expect(patch_note_element.css("textarea").text).to include(patch_notes[0].note) expect(patch_note_element .css("#patch-note-#{patch_notes[0].id}-group option[@selected=\"selected\"]") .attr("value").value).to eq(patch_notes[0].patch_note_group_id.to_s) expect(patch_note_element .css("#patch-note-#{patch_notes[0].id}-type option[@selected=\"selected\"]") .attr("value").value).to eq(patch_notes[0].patch_note_type_id.to_s) end end end ================================================ FILE: spec/views/banners/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "banners/new", type: :view do context "when new banner is marked as inactive" do it "does not warn that current active banner will be deactivated" do user = build_stubbed(:casa_admin) current_organization = user.casa_org current_organization_banner = build(:banner, active: true, casa_org: current_organization) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(current_organization) without_partial_double_verification do allow(view).to receive(:browser_time_zone).and_return("America/New_York") end allow(current_organization).to receive(:has_alternate_active_banner?).and_return(true) assign :banners, [current_organization_banner] assign :banner, Banner.new(active: false) render template: "banners/new" expect(rendered).not_to have_checked_field("banner_active") expect(rendered).to have_css("span.d-none", text: "Warning: This will replace your current active banner") end end context "when organization has an active banner" do context "when new banner is marked as active" do it "warns that current active banner will be deactivated" do user = build_stubbed(:casa_admin) current_organization = user.casa_org current_organization_banner = build(:banner, active: true, casa_org: current_organization) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(current_organization) without_partial_double_verification do allow(view).to receive(:browser_time_zone).and_return("America/New_York") end allow(current_organization).to receive(:has_alternate_active_banner?).and_return(true) assign :banners, [current_organization_banner] assign :banner, Banner.new(active: true) render template: "banners/new" expect(rendered).to have_checked_field("banner_active") expect(rendered).not_to have_css("span.d-none", text: "Warning: This will replace your current active banner") end end end context "when organization has no active banner" do context "when new banner is marked as active" do it "does not warn that current active banner will be deactivated" do user = build_stubbed(:casa_admin) current_organization = user.casa_org allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(current_organization) without_partial_double_verification do allow(view).to receive(:browser_time_zone).and_return("America/New_York") end allow(current_organization).to receive(:has_alternate_active_banner?).and_return(false) assign :banners, [] assign :banner, Banner.new(active: true) render template: "banners/new" expect(rendered).to have_checked_field("banner_active") expect(rendered).to have_css("span.d-none", text: "Warning: This will replace your current active banner") end end end end ================================================ FILE: spec/views/bulk_court_date/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "bulk_court_date/new.html.erb", type: :view do pending "add some examples to (or delete) #{__FILE__}" end ================================================ FILE: spec/views/casa_admins/admins_table.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "admins_table", type: :view do it "allows editing admin users" do admin = build_stubbed :casa_admin enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) assign :admins, [admin.decorate] sign_in admin render template: "casa_admins/index" expect(rendered).to have_link("Edit", href: "/casa_admins/#{admin.id}/edit") end end ================================================ FILE: spec/views/casa_admins/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_admins/edit", type: :view do let(:admin) { build_stubbed :casa_admin } it "shows invite and login info" do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) allow(view).to receive(:current_organization).and_return(admin.casa_org) assign :casa_admin, admin render template: "casa_admins/edit" expect(rendered).to have_text("Added to system ") expect(rendered).to have_text("Invitation email sent \n never") expect(rendered).to have_text("Last logged in") expect(rendered).to have_text("Invitation accepted \n never") expect(rendered).to have_text("Password reset last sent \n never") end describe "'Change to Supervisor' button" do let(:supervisor) { build_stubbed :supervisor } before do assign :casa_admin, admin assign :available_volunteers, [] end it "shows for an admin editing an admin" do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) allow(view).to receive(:current_organization).and_return(admin.casa_org) render template: "casa_admins/edit" expect(rendered).to have_text("Change to Supervisor") expect(rendered).to include(change_to_supervisor_casa_admin_path(admin)) end it "does not show for a supervisor editing an admin" do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) render template: "casa_admins/edit" expect(rendered).not_to have_text("Change to Supervisor") expect(rendered).not_to include(change_to_supervisor_casa_admin_path(admin)) end end end ================================================ FILE: spec/views/casa_cases/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/edit", type: :view do let(:organization) { create(:casa_org) } let(:contact_type_group) { create(:contact_type_group, casa_org: organization) } let(:contact_type) { create(:contact_type, contact_type_group: contact_type_group) } before do enable_pundit(view, user) assign :contact_types, [] allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(user.casa_org) end context "when accessed by a volunteer" do let(:user) { build_stubbed(:volunteer, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } it "does not allow editing the case number" do assign :casa_case, casa_case render template: "casa_cases/edit" expect(rendered).to have_link(casa_case.case_number, href: "/casa_cases/#{casa_case.case_number.parameterize}") expect(rendered).not_to have_selector("input[value='#{casa_case.case_number}']") end it "does not include volunteer assignment" do assign :casa_case, casa_case render template: "casa_cases/edit" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#volunteer-assignment").length).to eq(0) end end context "when accessed by an admin" do let(:user) { build_stubbed(:casa_admin, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } it "includes an editable case number" do assign :casa_case, casa_case assign :contact_types, organization.contact_types render template: "casa_cases/edit" expect(rendered).to have_link(casa_case.case_number, href: "/casa_cases/#{casa_case.case_number.parameterize}") expect(rendered).to have_selector("input[value='#{casa_case.case_number}']") end it "includes volunteer assignment" do assign :casa_case, casa_case assign :contact_types, organization.contact_types render template: "casa_cases/edit" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#volunteer-assignment").length).to eq(1) end end context "when assigning a new volunteer" do let(:user) { build_stubbed(:casa_admin, casa_org: organization) } it "does not have an option to select a volunteer that is already assigned to the casa case" do casa_case = create(:casa_case, casa_org: organization) assign :casa_case, casa_case assign :contact_types, organization.contact_types assigned_volunteer = build_stubbed(:volunteer) build_stubbed(:case_assignment, volunteer: assigned_volunteer, casa_case: casa_case) unassigned_volunteer = create(:volunteer) render template: "casa_cases/edit" expect(rendered).to have_select("case_assignment_casa_case_id", options: ["Please Select Volunteer", unassigned_volunteer.display_name]) end end end ================================================ FILE: spec/views/casa_cases/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/index", type: :view do context "when accessed by a volunteer" do it "can not see the Assigned To column" do user = create(:volunteer, display_name: "Bob Loblaw") enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) casa_case = build(:casa_case, active: true, casa_org: user.casa_org, case_number: "CINA-1") create(:case_assignment, volunteer: user, casa_case: casa_case) assign :casa_cases, [casa_case] assign :duties, OtherDuty.none render template: "casa_cases/index" expect(rendered).not_to have_text "Assigned To" expect(rendered).not_to have_text("Bob Loblaw") end end context "when accessed by an admin" do it "can see the New Case button" do organization = create(:casa_org) user = build_stubbed(:casa_admin, casa_org: organization) enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) assign :casa_cases, [] assign :duties, OtherDuty.none render template: "casa_cases/index" expect(rendered).to have_link "New Case" end end context "when accessed by a supervisor" do it "does not see the New Case button" do organization = create(:casa_org) user = build_stubbed(:supervisor, casa_org: organization) enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) assign :casa_cases, [] assign :duties, OtherDuty.none render template: "casa_cases/index" expect(rendered).not_to have_link "New Case" end end end ================================================ FILE: spec/views/casa_cases/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/new", type: :view do let(:casa_org) { create(:casa_org) } let(:user) { create(:casa_admin, casa_org: casa_org) } let(:contact_type_group) { create(:contact_type_group, casa_org: casa_org) } let(:contact_type) { create(:contact_type, contact_type_group: contact_type_group) } context "while signed in as admin" do it "has youth birth month and year" do enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(casa_org) assign :casa_case, build(:casa_case, casa_org: casa_org) assign :contact_types, casa_org.contact_types render template: "casa_cases/new" expect(rendered).to include("Youth's Birth Month & Year") end end context "when trying to assign a volunteer to a case" do it "is able to assign volunteers" do enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(user.casa_org) assign :casa_case, build(:casa_case, casa_org: user.casa_org) assign :contact_types, casa_org.contact_types render template: "casa_cases/new" expect(rendered).to have_content("Assign a Volunteer") expect(rendered).to have_css("#casa_case_case_assignments_attributes_0_volunteer_id") end end end ================================================ FILE: spec/views/casa_cases/show.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_cases/show", type: :view do let(:user) { create(:casa_admin) } before do enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(user.casa_org) end context "when there is court date" do it "renders casa case with court dates" do casa_case = create(:casa_case, case_number: "111") create(:court_date, casa_case: casa_case, date: Date.new(2023, 5, 6)) assign(:casa_case, casa_case) render expect(rendered).to match("111") expect(rendered).to match("May 6, 2023") end it "render button to add court date" do casa_case = create(:casa_case) assign(:casa_case, casa_case) render expect(rendered).to have_content("Add a court date") end end context "where there is no court date" do it "render casa case without court dates" do casa_case = create(:casa_case) assign(:casa_case, casa_case) render expect(rendered).to match(casa_case.case_number) expect(rendered).to have_content("No Court Dates") end it "render button to add court date" do casa_case = create(:casa_case) assign(:casa_case, casa_case) render expect(rendered).to have_content("Add a court date") end end context "when there is a placement" do it "renders casa case with placements" do casa_case = create(:casa_case, case_number: "111") create(:placement, casa_case: casa_case, placement_started_at: Date.new(2023, 5, 6)) assign(:casa_case, casa_case) render expect(rendered).to match("111") expect(rendered).to match("Current Placement:") expect(rendered).to match(/Placement Type/) expect(rendered).to match("Placed since: May 6, 2023") expect(rendered).to match("See All Placements") end end context "when there is no placement" do it "renders casa case without placements" do casa_org = create(:casa_org, :with_placement_types) casa_case = create(:casa_case, casa_org:) assign(:casa_case, casa_case) render expect(rendered).to match(casa_case.case_number) expect(rendered).to have_content("Current Placement:") expect(rendered).to have_content("Unknown") expect(rendered).to have_content("See All Placements") end it "renders nothing about placements when org has no placement types" do casa_case = create(:casa_case) assign(:casa_case, casa_case) render expect(rendered).to match(casa_case.case_number) expect(rendered).not_to have_content("Current Placement:") expect(rendered).not_to have_content("Unknown") expect(rendered).not_to have_content("See All Placements") end end end ================================================ FILE: spec/views/casa_orgs/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "casa_org/edit", type: :view do let(:organization) { build_stubbed(:casa_org) } before do assign(:contact_type_groups, []) assign(:contact_types, []) assign(:hearing_types, []) assign(:judges, []) assign(:learning_hour_types, []) assign(:learning_hour_topics, []) assign(:sent_emails, []) assign(:contact_topics, []) assign(:custom_org_links, []) assign(:placement_types, []) allow(view).to receive(:current_organization).and_return(organization) sign_in build_stubbed(:casa_admin) end it "has casa org edit page text" do render template: "casa_org/edit" expect(rendered).to have_text "Editing CASA Organization" expect(rendered).not_to have_text "sign in before continuing" expect(rendered).to have_selector("input[required=required]", id: "casa_org_name") end it "has contact topic content" do contact_topic = build_stubbed(:contact_topic, question: "Test Question", details: "Test details") assign(:contact_topics, [contact_topic]) render template: "casa_org/edit" expect(rendered).to have_text("Test Question") expect(rendered).to have_text("Test details") expect(rendered).to have_table("contact-topics", with_rows: [ ["Test Question", "Test details", "Edit"] ]) end it "has contact types content" do contact_type = build_stubbed(:contact_type, name: "Contact type 1") assign(:contact_types, [contact_type]) render template: "casa_org/edit" expect(rendered).to have_text("Contact type 1") expect(rendered).to have_text(contact_type.name) expect(rendered).to have_table("contact-types", with_rows: [ ["Contact type 1", "Yes", "Edit"] ]) end it "has contact type groups content" do contact_type_group = build_stubbed(:contact_type_group, casa_org: organization, name: "Contact type group 1") assign(:contact_type_groups, [contact_type_group]) render template: "casa_org/edit" expect(rendered).to have_text("Contact type group 1") expect(rendered).to have_text(contact_type_group.name) expect(rendered).to have_table("contact-type-groups", with_rows: [ ["Contact type group 1", "Yes", "Edit"] ]) end it "has hearing types content" do hearing_type = build_stubbed(:hearing_type, casa_org: organization, name: "Hearing type 1") assign(:hearing_types, [hearing_type]) render template: "casa_org/edit" expect(rendered).to have_text("Hearing type 1") expect(rendered).to have_table("hearing-types", with_rows: [ ["Hearing type 1", "Yes", "Edit"] ]) end it "has judge content" do judge = build_stubbed(:judge, casa_org: organization, name: "Joey Tom") assign(:judges, [judge]) render template: "casa_org/edit" expect(rendered).to have_text(judge.name) end it "has placement types content" do placement_type = build_stubbed(:placement_type, name: "Placement type 1") placement_type_2 = build_stubbed(:placement_type, name: "Placement type 2") assign(:placement_types, [placement_type, placement_type_2]) render template: "casa_org/edit" expect(rendered).to have_text("Manage Case Placement Types") expect(rendered).to have_table("placement-types-table", with_rows: [ ["Placement type 1", "Edit"], ["Placement type 2", "Edit"] ]) end it "does not show download prompt with no custom template" do render template: "casa_org/edit" expect(rendered).not_to have_text("Download Current Template") end it "has sent emails content" do admin = build_stubbed(:casa_admin, casa_org: organization) without_partial_double_verification do allow(view).to receive(:to_user_timezone).and_return(Time.zone.local(2021, 1, 2, 12, 30, 0)) end sent_email = build_stubbed(:sent_email, user: admin, created_at: Time.zone.local(2021, 1, 2, 12, 30, 0)) assign(:sent_emails, [sent_email]) render template: "casa_org/edit" expect(rendered).to have_text(sent_email.sent_address) expect(rendered).to have_table("sent-emails", with_rows: [ ["Mailer Type", "Mail Action Category", admin.email, "12:30pm 02 Jan 2021"] ]) end context "with a template uploaded" do # NOTE(@abachman): Use create instead of build_stubbed because ActiveStorage # needs to save to the DB let(:organization) { create(:casa_org) } it "renders a prompt to download current template" do organization.court_report_template.attach(io: File.open("#{Rails.root.join("app/documents/templates/default_report_template.docx")}"), filename: 'default_report_template .docx', content_type: "application/docx") render template: "casa_org/edit" expect(rendered).to have_text("Download Current Template") end end describe "additional expense feature flag" do context "enabled" do it "has option to enable additional expenses" do allow(Flipper).to receive(:enabled?).with(:show_additional_expenses).and_return(true) render template: "casa_org/edit" expect(rendered).to have_text("Volunteers can add Other Expenses") end end context "disabled" do it "has option to enable additional expenses" do allow(Flipper).to receive(:enabled?).with(:show_additional_expenses).and_return(false) render template: "casa_org/edit" expect(rendered).not_to have_text("Volunteers can add Other Expenses") end end end describe "custom org links" do let(:casa_org) { build_stubbed :casa_org } before { allow(view).to receive(:current_organization).and_return(casa_org) } it "has custom org link content" do render template: "casa_org/edit" expect(rendered).to have_text("Custom Links") end context "when the org has no custom links" do before { assign(:custom_org_links, []) } it "includes a helpful message" do render template: "casa_org/edit" expect(rendered).to have_text("No custom links have been added for this organization.") end end context "when the org has custom links" do let(:link_text) { "Example Link" } let(:link_url) { "https://www.example.com" } let(:custom_org_link) { build_stubbed :custom_org_link, text: link_text, url: link_url } before { assign(:custom_org_links, [custom_org_link]) } it "has custom link details" do render template: "casa_org/edit" expect(rendered).to have_text link_text expect(rendered).to have_text link_url end end end end ================================================ FILE: spec/views/case_contacts/case_contact.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contacts/case_contact", type: :view do let(:casa_org) { create(:casa_org) } let(:admin) { build_stubbed(:casa_admin, casa_org:) } let(:volunteer) { create(:volunteer, casa_org:) } let(:supervisor) { build_stubbed(:supervisor, casa_org:) } describe "case contact notes" do before do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) end context "when case contact has contact topic responses" do let(:case_contact) do build_stubbed(:case_contact, contact_topic_answers: [contact_topic_answer1, contact_topic_answer2], creator: volunteer) end let(:contact_topic1) { build_stubbed(:contact_topic, question: "Some question") } let(:contact_topic2) { build_stubbed(:contact_topic, question: "Hidden question") } let(:contact_topic_answer1) do build_stubbed(:contact_topic_answer, contact_topic: contact_topic1, value: "Some answer") end let(:contact_topic_answer2) do build_stubbed(:contact_topic_answer, contact_topic: contact_topic2, value: "") end it "shows the contact topic responses" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).to have_text("Some question:") expect(rendered).to have_text("Some answer") expect(rendered).not_to have_text("Hidden question") end end context "when case contact has no notes" do let(:case_contact) { build_stubbed(:case_contact, notes: nil) } it "does not show the notes" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).not_to have_text("Additional Notes:") end end context "when case contact has notes" do let(:case_contact) { build_stubbed(:case_contact, notes: "This is a note") } it "shows the notes" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).to have_text("Additional Notes:") expect(rendered).to have_text("This is a note") end end end describe "edit and make reminder buttons" do before do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) end context "occurred_at is before the last day of the month in the quarter that the case contact was created" do let(:case_contact) { build_stubbed(:case_contact, creator: volunteer) } let(:case_contact2) { build_stubbed(:case_contact, deleted_at: Time.current, creator: volunteer) } it "shows edit button" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).to have_link(nil, href: "/case_contacts/#{case_contact.id}/edit") end it "shows make reminder button" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).to have_button("Make Reminder") end end end describe "delete and undelete buttons" do let(:case_contact) { build_stubbed(:case_contact, creator: volunteer) } let(:case_contact2) { build_stubbed(:case_contact, deleted_at: Time.current, creator: volunteer) } context "when logged in as admin" do before do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) end it "shows delete button" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).to have_link("Delete", href: "/case_contacts/#{case_contact.id}") end it "shows undelete button" do assign :case_contact, case_contact2 assign :casa_cases, [case_contact2.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact2}) expect(rendered).to have_link("undelete", href: "/case_contacts/#{case_contact2.id}/restore") end end context "when logged in as volunteer" do before do enable_pundit(view, volunteer) allow(view).to receive(:current_user).and_return(volunteer) end it "does not show delete button" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).not_to have_link("Delete", href: "/case_contacts/#{case_contact.id}") end it "does not show undelete button" do assign :case_contact, case_contact2 assign :casa_cases, [case_contact2.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact2}) expect(rendered).not_to have_link("undelete", href: "/case_contacts/#{case_contact2.id}/restore") end end context "when logged in as supervisor" do before do enable_pundit(view, supervisor) allow(view).to receive(:current_user).and_return(supervisor) end it "shows delete button" do assign :case_contact, case_contact assign :casa_cases, [case_contact.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact}) expect(rendered).to have_link("Delete", href: "/case_contacts/#{case_contact.id}") end it "does not show undelete button" do assign :case_contact, case_contact2 assign :casa_cases, [case_contact2.casa_case] render(partial: "case_contacts/case_contact", locals: {contact: case_contact2}) expect(rendered).not_to have_link("undelete", href: "/case_contacts/#{case_contact2.id}/restore") end end end end ================================================ FILE: spec/views/case_contacts/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "case_contacts/index", type: :view do let(:user) { build_stubbed(:volunteer) } let(:case_contacts) { CaseContact.all } let(:pagy) { Pagy.new(count: 0) } let(:filterrific_param_set) do param_set = Filterrific::ParamSet.new(case_contacts, {}) param_set.select_options = {sorted_by: CaseContact.options_for_sorted_by} param_set end let(:groups) do user.casa_org.contact_type_groups .joins(:contact_types) .where(contact_types: {active: true}) .uniq end before do enable_pundit(view, user) # Allow filterrific to fetch the correct controller name allow_any_instance_of(ActionView::TestCase::TestController).to receive(:controller_name).and_return("case_contacts") allow(RequestStore).to receive(:read).with(:current_user).and_return(user) allow(RequestStore).to receive(:read).with(:current_organization).and_return(user.casa_org) assign(:current_organization_groups, groups) assign(:filterrific, filterrific_param_set) assign(:presenter, CaseContactPresenter.new(case_contacts)) assign(:pagy, pagy) render template: "case_contacts/index" end it "Displays the Case Contacts title" do expect(rendered).to have_text("Case Contacts") end it "Has a New Case Contact button" do expect(rendered).to have_link("New Case Contact", href: new_case_contact_path) end end ================================================ FILE: spec/views/case_court_reports/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "case_court_reports/index", type: :view do context "Volunteer views 'Generate Court Report' form" do let(:user) { create(:volunteer, :with_casa_cases) } let(:active_assigned_cases) { CasaCase.actively_assigned_to(user) } before do allow(view).to receive(:current_user).and_return(user) assign :assigned_cases, active_assigned_cases render end it "renders the index page" do expect(controller.request.fullpath).to eq case_court_reports_path end it "has a card with card title 'Generate Court Report'", :aggregate_failures do expect(rendered).to have_selector("h6", text: "Court Reports", count: 1) expect(rendered).to have_selector("div", class: "card-style", count: 1) end it "page has title 'Gererate Reports'" do expect(rendered).to have_selector("h1", text: "Generate Reports", count: 1) end it "has button with 'Download Court Report as .docx' text" do expect(rendered).to have_selector("button", text: /Download Court Report as \.docx/i, count: 1) end end end ================================================ FILE: spec/views/checklist_items/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "checklist_items/edit", type: :view do let(:admin) { build_stubbed(:casa_admin) } before do assign :hearing_type, HearingType.new(id: 1) assign :checklist_item, ChecklistItem.new sign_in admin render template: "checklist_items/edit" end it "shows edit checklist item page title" do expect(rendered).to have_text("Edit this checklist item") end it "shows edit checklist item form" do expect(rendered).to have_selector("input", id: "checklist_item_category") expect(rendered).to have_selector("input", id: "checklist_item_description") expect(rendered).to have_selector("input", id: "checklist_item_mandatory") expect(rendered).to have_selector(:link_or_button, "Submit") end it "requires category text field" do expect(rendered).to have_selector("input[required=required]", id: "checklist_item_category") end it "requires description text field" do expect(rendered).to have_selector("input[required=required]", id: "checklist_item_description") end end ================================================ FILE: spec/views/checklist_items/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "checklist_items/new", type: :view do let(:admin) { build_stubbed(:casa_admin) } before do assign :hearing_type, HearingType.new(id: 1) assign :checklist_item, ChecklistItem.new sign_in admin render template: "checklist_items/new" end it "shows new checklist item page title" do expect(rendered).to have_text("Add a new checklist item") end it "shows new checklist item form" do expect(rendered).to have_selector("input", id: "checklist_item_category") expect(rendered).to have_selector("input", id: "checklist_item_description") expect(rendered).to have_selector("input", id: "checklist_item_mandatory") expect(rendered).to have_selector("button[type=submit]") end it "requires category text field" do expect(rendered).to have_selector("input[required=required]", id: "checklist_item_category") end it "requires description text field" do expect(rendered).to have_selector("input[required=required]", id: "checklist_item_description") end end ================================================ FILE: spec/views/court_dates/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "court_dates/edit", type: :view do subject { render template: "court_dates/edit" } let(:organization) { create(:casa_org) } let(:user) { build_stubbed(:casa_admin, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:court_date) { create(:court_date, :with_court_details, casa_case: casa_case) } let(:court_order) { court_date.case_court_orders.first } let(:implementation_status_name) do "court_date_case_court_orders_attributes_0_implementation_status" end let(:implementation_status) do court_order.implementation_status.humanize end before do assign :casa_case, casa_case assign :court_date, court_date enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(user.casa_org) end it { is_expected.to have_select("court_date_judge_id", selected: court_date.judge.name) } it { is_expected.to have_select("court_date_hearing_type_id", selected: court_date.hearing_type.name) } it { is_expected.to have_selector("textarea", text: court_order.text) } it { is_expected.to have_select(implementation_status_name, selected: implementation_status) } it { is_expected.to have_selector(".primary-btn") } end ================================================ FILE: spec/views/court_dates/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "court_dates/new", type: :view do subject { render template: "court_dates/new" } before do assign :casa_case, casa_case assign :court_date, CourtDate.new allow(view).to receive(:current_organization).and_return(user.casa_org) end let(:user) { build_stubbed(:casa_admin) } let(:casa_case) { create(:casa_case) } it { is_expected.to have_selector("h1", text: "New Court Date") } it { is_expected.to have_selector("h6", text: casa_case.case_number) } it { is_expected.to have_link(casa_case.case_number, href: "/casa_cases/#{casa_case.case_number.parameterize}") } it { is_expected.to have_selector(".primary-btn") } end ================================================ FILE: spec/views/court_dates/show.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "court_dates/show", type: :view do RSpec.shared_examples_for "a past court date with all court details" do let(:court_date) { create(:court_date, :with_court_details) } let(:case_court_order) { court_date.case_court_orders.first } before { render template: "court_dates/show" } it "displays all court details" do expect(rendered).to include("/casa_cases/#{court_date.casa_case.case_number.parameterize}") expect(rendered).to include(ERB::Util.html_escape(court_date.judge.name)) expect(rendered).to include(court_date.hearing_type.name) expect(rendered).to include(case_court_order.text) expect(rendered).to include(case_court_order.implementation_status.humanize) end context "when judge's name has escaped characters" do let(:court_date) { create(:court_date, :with_court_details, judge: create(:judge, name: "/-'<>#&")) } it "correctly displays judge's name" do expect(rendered).to include(ERB::Util.html_escape(court_date.judge.name)) end end it "displays the download button for .docx" do expect(rendered).to include "Download Report (.docx)" expect(rendered).to include "/casa_cases/#{court_date.casa_case.case_number.parameterize}/court_dates/#{court_date.id}.docx" end end RSpec.shared_examples_for "a past court date with no court details" do let(:court_date) { create(:court_date) } it "displays all court details" do render template: "court_dates/show" expect(rendered).to include("Judge:") expect(rendered).to include("Hearing Type") expect(rendered).to include("None") expect(rendered).to include("There are no court orders associated with this court date.") end end let(:organization) { create(:casa_org) } let(:casa_case) { create(:casa_case, casa_org: organization) } before do enable_pundit(view, user) assign :casa_case, court_date.casa_case assign :court_date, court_date allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(user.casa_org) end context "with court details" do context "when accessed by a casa admin" do let(:user) { build_stubbed(:casa_admin, casa_org: organization) } it_behaves_like "a past court date with all court details" end context "when accessed by a supervisor" do let(:user) { build_stubbed(:supervisor, casa_org: organization) } it_behaves_like "a past court date with all court details" end context "when accessed by a volunteer" do let(:user) { build_stubbed(:volunteer, casa_org: organization) } it_behaves_like "a past court date with all court details" end end context "without court details" do context "when accessed by an admin" do let(:user) { build_stubbed(:casa_admin, casa_org: organization) } it_behaves_like "a past court date with no court details" end context "when accessed by a supervisor" do let(:user) { build_stubbed(:supervisor, casa_org: organization) } it_behaves_like "a past court date with no court details" end context "when accessed by a volunteer" do let(:user) { build_stubbed(:volunteer, casa_org: organization) } it_behaves_like "a past court date with no court details" end end end ================================================ FILE: spec/views/devise/invitations/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "users/invitations/edit", type: :view do it "displays title" do render template: "devise/invitations/edit" expect(rendered).to have_text("Set password") end it "displays fields for user to set password" do render template: "devise/invitations/edit" expect(rendered).to have_field("user_invitation_token", type: :hidden) expect(rendered).to have_text("Password") expect(rendered).to have_field("user_password") expect(rendered).to have_text("Password confirmation") expect(rendered).to have_field("user_password_confirmation") expect(rendered).to have_button("Set my password") end end ================================================ FILE: spec/views/devise/invitations/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "users/invitations/new", type: :view do it "displays title" do render template: "devise/invitations/new" expect(rendered).to have_text("Send invitation") end it "displays fields for inviting a user" do render template: "devise/invitations/new" expect(rendered).to have_text("Email") expect(rendered).to have_field("user_email") expect(rendered).to have_button("Send an invitation") end end ================================================ FILE: spec/views/devise/passwords/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "users/password/new", type: :view do it "displays title" do render template: "devise/passwords/new" expect(rendered).to have_text("Forgot your password?") end it "displays text above form fields" do render template: "devise/passwords/new" expect(rendered).to have_text("Please enter email or phone number to receive reset instructions.") end it "displays contact fields for user to reset password" do render template: "devise/passwords/new" expect(rendered).to have_text("Email") expect(rendered).to have_field("user_email") expect(rendered).to have_text("Phone number") expect(rendered).to have_field("user_phone_number") end end ================================================ FILE: spec/views/emancipations/show.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "emancipation/show", type: :view do subject { render template: "emancipation/show" } let(:organization) { build_stubbed(:casa_org) } let(:admin) { build_stubbed(:casa_admin, casa_org: organization) } let(:casa_case) { build_stubbed(:casa_case) } let(:emancipation_form_data) { [build_stubbed(:emancipation_category)] } before do assign :current_case, casa_case assign :emancipation_form_data, emancipation_form_data end it "has a link to return to case from emancipation" do sign_in admin render template: "emancipations/show" expect(rendered).to have_link(casa_case.case_number, href: "/casa_cases/#{casa_case.id}") end end ================================================ FILE: spec/views/hearing_types/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "hearing_types/edit", type: :view do let(:admin) { build_stubbed(:casa_admin) } before do assign :hearing_type, HearingType.new sign_in admin render template: "hearing_types/edit" end it "shows edit hearing type form" do expect(rendered).to have_text("Edit") expect(rendered).to have_selector("input", id: "hearing_type_name") expect(rendered).to have_selector("input", id: "hearing_type_active") expect(rendered).to have_selector("button[type=submit]") end it "requires name text_field" do expect(rendered).to have_selector("input[required=required]", id: "hearing_type_name") end end ================================================ FILE: spec/views/hearing_types/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "hearing_types/new", type: :view do let(:admin) { build_stubbed(:casa_admin) } before do assign :hearing_type, HearingType.new sign_in admin render template: "hearing_types/new" end it "shows new hearing type form" do expect(rendered).to have_text("New Hearing Type") expect(rendered).to have_selector("input", id: "hearing_type_name") expect(rendered).to have_selector("input", id: "hearing_type_active") expect(rendered).to have_selector(:link_or_button, "Submit") end it "requires name text_field" do expect(rendered).to have_selector("input[required=required]", id: "hearing_type_name") end end ================================================ FILE: spec/views/judges/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "judges/new", type: :view do let(:admin) { build_stubbed(:casa_admin) } before do assign :judge, Judge.new sign_in admin render template: "judges/new" end it "shows new judge form" do expect(rendered).to have_text("New Judge") expect(rendered).to have_selector("input", id: "judge_name") expect(rendered).to have_selector("input", id: "judge_active") expect(rendered).to have_selector("button[type=submit]") end it "requires name text_field" do expect(rendered).to have_selector("input[required=required]", id: "judge_name") end end ================================================ FILE: spec/views/layouts/application.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "layouts/application", type: :view do subject { rendered } let(:title) { "CASA Volunteer Tracking" } let(:description) { "Volunteer activity tracking for CASA volunteers, supervisors, and administrators." } it "renders correct title" do render expect(subject).to match "#{title}" end it "renders correct description" do render expect(subject).to match "" end it "renders correct og meta tags" do render expect(rendered.scan(/Role: Casa Admin" expect(rendered).to match CGI.escapeHTML user.display_name expect(rendered).to match CGI.escapeHTML user.email end it "renders help issue link on the header" do render partial: "layouts/header" expect(rendered).to have_link("Help", href: "https://thunder-flower-8c2.notion.site/Casa-Volunteer-Tracking-App-HelpSite-3b95705e80c742ffa729ccce7beeabfa") end end context "when logged in as a supervisor" do let(:user) { build_stubbed :supervisor } it "renders user information", :aggregate_failures do sign_in user render partial: "layouts/header" expect(rendered).to match "Role: Supervisor" expect(rendered).to match CGI.escapeHTML user.display_name expect(rendered).to match CGI.escapeHTML user.email end it "renders help issue link on the header" do render partial: "layouts/header" expect(rendered).to have_link("Help", href: "https://thunder-flower-8c2.notion.site/Casa-Volunteer-Tracking-App-HelpSite-3b95705e80c742ffa729ccce7beeabfa") end end context "when logged in as a volunteer" do let(:user) { build_stubbed :volunteer } it "renders user information", :aggregate_failures do sign_in user render partial: "layouts/header" expect(rendered).to match "Role: Volunteer" expect(rendered).to match CGI.escapeHTML user.display_name expect(rendered).to match CGI.escapeHTML user.email end it "does not render unauthorized links" do sign_in user render partial: "layouts/header" expect(rendered).not_to have_link("Edit Organization") end it "renders help issue link on the header" do render partial: "layouts/header" expect(rendered).to have_link("Help", href: "https://thunder-flower-8c2.notion.site/Casa-Volunteer-Tracking-App-HelpSite-Volunteers-c24d9d2ef8b249bbbda8192191365039?pvs=4") end end context "notifications" do let(:user) { build_stubbed :casa_admin } it "displays unread notification count if the user has unread notifications" do sign_in user build(:notification) allow(user).to receive_message_chain(:notifications, :unread).and_return([:notification]) render partial: "layouts/header" expect(rendered).to match "1" end it "does not display unread notification count if the user has no unread notifications" do sign_in user allow(user).to receive_message_chain(:notifications, :unread).and_return([]) render partial: "layouts/header" expect(rendered).not_to match "0" end end context "impersonation" do let(:user) { build_stubbed :volunteer } let(:true_user) { build_stubbed :casa_admin } it "renders correct role name when impersonating a volunteer" do allow(view).to receive(:true_user).and_return(true_user) render partial: "layouts/header" expect(rendered).to match "Role: Volunteer" end it "renders a stop impersonating link when impersonating" do allow(view).to receive(:true_user).and_return(true_user) render partial: "layouts/header" expect(rendered).to have_link(href: "/volunteers/stop_impersonating") end end end ================================================ FILE: spec/views/layouts/sidebar.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "layout/sidebar", type: :view do before do view.class.include PretenderContext enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:true_user).and_return(user) allow(view).to receive(:user_signed_in?).and_return(true) allow(view).to receive(:current_role).and_return(user.role) allow(view).to receive(:current_organization).and_return(user.casa_org) assign :casa_org, user.casa_org end shared_examples_for "properly rendering custom org links" do let(:active_link_text) { "Example Link" } let(:active_link_url) { "https://www.example.com" } let(:inactive_link_text) { "Hidden Link" } let(:inactive_link_url) { "https://www.nothing.com" } let(:other_org_link_text) { "That Other Link" } let(:other_org_link_url) { "https://www.elsewhere.com" } before do create :custom_org_link, casa_org: user.casa_org, text: active_link_text, url: active_link_url, active: true create :custom_org_link, casa_org: user.casa_org, text: inactive_link_text, url: inactive_link_url, active: false create :custom_org_link, text: other_org_link_text, url: other_org_link_url, active: true end it "renders active custom links for the user's org" do render partial: "layouts/sidebar" expect(rendered).to have_link(active_link_text, href: active_link_url) end it "does not render inactive custom links for the user's org" do render partial: "layouts/sidebar" expect(rendered).not_to have_link(inactive_link_text, href: inactive_link_url) end it "does not render custom links for other orgs" do render partial: "layouts/sidebar" expect(rendered).not_to have_link(other_org_link_text, href: other_org_link_url) end end context "when no organization logo is set" do let(:user) { build_stubbed :volunteer } it "displays default logo" do sign_in user render partial: "layouts/sidebar" expect(rendered).to have_xpath("//img[contains(@src,'default-logo') and @alt='CASA Logo']") end end context "when logged in as a supervisor" do let(:user) do build_stubbed :supervisor, display_name: "Supervisor's name", email: "supervisor&email@test.com" end it "renders only menu items visible by supervisors" do sign_in user render partial: "layouts/sidebar" expect(rendered).to have_link("Supervisors", href: "/supervisors") expect(rendered).to have_link("Volunteers", href: "/volunteers") expect(rendered).to have_link("Cases", href: "/casa_cases") expect(rendered).not_to have_link("Case Contacts", href: "/case_contacts") expect(rendered).not_to have_link("Admins", href: "/casa_admins") expect(rendered).to have_link("Generate Court Reports", href: "/case_court_reports") expect(rendered).to have_link("Export Data", href: "/reports") expect(rendered).not_to have_link("Emancipation Checklist", href: "/emancipation_checklists/0") expect(rendered).not_to have_link("System Settings", href: "/settings") expect(rendered).to have_link("Other Duties", href: "/other_duties") expect(rendered).not_to have_link("Organization Details", href: "/casa_org/#{user.casa_org.id}/edit#organization-details") expect(rendered).not_to have_link("Contact Types", href: "/casa_org/#{user.casa_org.id}/edit#contact-types") expect(rendered).not_to have_link("Court Details", href: "/casa_org/#{user.casa_org.id}/edit#court-details") expect(rendered).not_to have_link("Learning Hours", href: "/casa_org/#{user.casa_org.id}/edit#learning-hours") expect(rendered).not_to have_link("Case Contact Topics", href: "/casa_org/#{user.casa_org.id}/edit#case-contact-topics") end it_behaves_like "properly rendering custom org links" context "when casa_org other_duties_enabled is true" do before do user.casa_org.other_duties_enabled = true sign_in user render partial: "layouts/sidebar" end it "renders Other Duties" do expect(rendered).to have_link("Other Duties", href: "/other_duties") end end context "when casa_org other_duties_enabled is false" do before do user.casa_org.other_duties_enabled = false sign_in user render partial: "layouts/sidebar" end it "does not renders Other Duties" do expect(rendered).not_to have_link("Other Duties", href: "/other_duties") end end end context "when logged in as a volunteer" do let(:organization) { build(:casa_org) } let(:user) do create( :volunteer, casa_org: organization, display_name: "Volunteer's name%" ) end it "renders only menu items visible by volunteers" do sign_in user render partial: "layouts/sidebar" expect(rendered).to have_link("All", href: "/casa_cases") expect(rendered).to have_link("All", href: "/case_contacts") expect(rendered).to have_link("Generate Court Report", href: "/case_court_reports") expect(rendered).not_to have_link("Export Data", href: "/reports") expect(rendered).not_to have_link("Volunteers", href: "/volunteers") expect(rendered).not_to have_link("Supervisors", href: "/supervisors") expect(rendered).not_to have_link("Admins", href: "/casa_admins") expect(rendered).not_to have_link("System Settings", href: "/settings") expect(rendered).to have_link("Other Duties", href: "/other_duties") expect(rendered).not_to have_link("Organization Details", href: "/casa_org/#{user.casa_org.id}/edit#organization-details") expect(rendered).not_to have_link("Contact Types", href: "/casa_org/#{user.casa_org.id}/edit#contact-types") expect(rendered).not_to have_link("Court Details", href: "/casa_org/#{user.casa_org.id}/edit#court-details") expect(rendered).not_to have_link("Learning Hours", href: "/casa_org/#{user.casa_org.id}/edit#learning-hours") expect(rendered).not_to have_link("Case Contact Topics", href: "/casa_org/#{user.casa_org.id}/edit#case-contact-topics") end it_behaves_like "properly rendering custom org links" context "when casa_org other_duties_enabled is true" do before do user.casa_org.other_duties_enabled = true sign_in user render partial: "layouts/sidebar" end it "renders Other Duties" do expect(rendered).to have_link("Other Duties", href: "/other_duties") end end context "when casa_org other_duties_enabled is false" do before do user.casa_org.other_duties_enabled = false sign_in user render partial: "layouts/sidebar" end it "does not renders Other Duties" do expect(rendered).not_to have_link("Other Duties", href: "/other_duties") end end context "when the volunteer does not have a transitioning case" do it "does not render emancipation checklist(s)" do sign_in user # 0 Cases render partial: "layouts/sidebar" expect(rendered).not_to have_link("Emancipation Checklist", href: "/emancipation_checklists") # 1 Non transitioning case casa_case = build_stubbed(:casa_case, :pre_transition, casa_org: organization) build_stubbed(:case_assignment, volunteer: user, casa_case: casa_case) render partial: "layouts/sidebar" expect(rendered).not_to have_link("Emancipation Checklist", href: "/emancipation_checklists") end end context "when the user has only inactive or unassigned transiting cases" do it "does not render emancipation checklist(s)" do sign_in user inactive_case = build_stubbed(:casa_case, casa_org: organization, active: false) build_stubbed(:case_assignment, volunteer: user, casa_case: inactive_case) unassigned_case = build_stubbed(:casa_case, casa_org: organization) build_stubbed(:case_assignment, volunteer: user, casa_case: unassigned_case, active: false) render partial: "layouts/sidebar" expect(rendered).not_to have_link("Emancipation Checklist", href: "/emancipation_checklists") end end context "when the volunteer has a transitioning case" do let(:casa_case) { create(:casa_case, casa_org: organization) } let!(:case_assignment) { create(:case_assignment, volunteer: user, casa_case: casa_case) } it "renders emancipation checklist(s)" do sign_in user render partial: "layouts/sidebar" expect(rendered).to have_link("Emancipation Checklist", href: "/emancipation_checklists") end end end context "when logged in as a casa admin" do let(:user) { build_stubbed :casa_admin, display_name: "Superviso's another n&ame" } it "renders only menu items visible by admins" do sign_in user render partial: "layouts/sidebar" expect(rendered).to have_link("Volunteers", href: "/volunteers") expect(rendered).to have_link("Cases", href: "/casa_cases") expect(rendered).not_to have_link("Case Contacts", href: "/case_contacts") expect(rendered).to have_link("Supervisors", href: "/supervisors") expect(rendered).to have_link("Admins", href: "/casa_admins") expect(rendered).to have_link("System Imports", href: "/imports") expect(rendered).to have_link("Generate Court Reports", href: "/case_court_reports") expect(rendered).to have_link("Export Data", href: "/reports") expect(rendered).not_to have_link("Emancipation Checklist", href: "/emancipation_checklists") expect(rendered).to have_link("Other Duties", href: "/other_duties") expect(rendered).to have_link("Organization Details", href: "/casa_org/#{user.casa_org.id}/edit#organization-details") expect(rendered).to have_link("Contact Types", href: "/casa_org/#{user.casa_org.id}/edit#contact-types") expect(rendered).to have_link("Court Details", href: "/casa_org/#{user.casa_org.id}/edit#court-details") expect(rendered).to have_link("Learning Hours", href: "/casa_org/#{user.casa_org.id}/edit#learning-hours") expect(rendered).to have_link("Case Contact Topics", href: "/casa_org/#{user.casa_org.id}/edit#case-contact-topics") end it_behaves_like "properly rendering custom org links" context "when casa_org other_duties_enabled is true" do before do user.casa_org.other_duties_enabled = true sign_in user render partial: "layouts/sidebar" end it "renders Other Duties" do expect(rendered).to have_link("Other Duties", href: "/other_duties") end end context "when casa_org other_duties_enabled is false" do before do user.casa_org.other_duties_enabled = false sign_in user render partial: "layouts/sidebar" end it "does not renders Other Duties" do expect(rendered).not_to have_link("Other Duties", href: "/other_duties") end end end end ================================================ FILE: spec/views/mileage_rates/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "Index Mileage Rates", type: :view do let(:admin) { build_stubbed :casa_admin } let(:mileage_rate) { build_stubbed :mileage_rate } before do enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) sign_in admin end it "allows editing the mileage rate" do assign :mileage_rates, [mileage_rate] render template: "mileage_rates/index" expect(rendered).to have_link("Edit", href: "/mileage_rates/#{mileage_rate.id}/edit") end end ================================================ FILE: spec/views/notifications/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "notifications/index", type: :view do let(:notification_1_hour_ago) { create(:notification, :followup_with_note) } let(:notification_1_day_ago) { create(:notification, :emancipation_checklist_reminder) } let(:notification_2_days_ago) { create(:notification, :youth_birthday) } let(:notification_3_days_ago) { create(:notification, :reimbursement_complete) } let(:patch_note_group_all_users) { create(:patch_note_group, :all_users) } let(:patch_note_group_no_volunteers) { create(:patch_note_group, :only_supervisors_and_admins) } let(:patch_note_type_a) { create(:patch_note_type, name: "patch_note_type_a") } let(:patch_note_type_b) { create(:patch_note_type, name: "patch_note_type_b") } let(:patch_note_1) { create(:patch_note, note: "Patch Note 1", patch_note_type: patch_note_type_a) } let(:patch_note_2) { create(:patch_note, note: "Patch Note B", patch_note_type: patch_note_type_b) } before do assign(:notifications, Noticed::Notification.all.newest_first) assign(:patch_notes, PatchNote.all) assign(:deploy_time, deploy_time) end context "when there is a deploy date" do let(:deploy_time) { 2.days.ago } before do Health.instance.update_attribute(:latest_deploy_time, deploy_time) end context "when there are notifications" do before do notification_1_hour_ago.update!(created_at: 1.hour.ago) notification_1_day_ago.update!(created_at: 1.day.ago) notification_2_days_ago.update!(created_at: 2.days.ago) notification_3_days_ago.update!(created_at: 3.days.ago) patch_note_1.update!(patch_note_group: patch_note_group_all_users) end it "has all notifications created after and including the deploy date above the patch note" do render template: "notifications/index" notifications_html = Nokogiri::HTML5(rendered).css(".list-group-item") patch_note_index = notifications_html.index { |node| node.text.include?("Patch Notes") } aggregate_failures do expect(notifications_html[0].text).to match(/User \d+ has flagged a Case Contact that needs follow up/) expect(notifications_html[1].text).to match(/Your case CINA-\d+ is a transition aged youth/) expect(notifications_html[2].text).to match(/Your youth, case number: CINA-\d+ has a birthday next month/) expect(patch_note_index).to eq(3) end end it "has all notifications created before the deploy date below the patch note" do render template: "notifications/index" notifications_html = Nokogiri::HTML5(rendered).css(".list-group-item") patch_note_index = notifications_html.index { |node| node.text.include?("Patch Notes") } expect(patch_note_index).to eq(3) expect(notifications_html[patch_note_index + 1].text).to match(/Volunteer User \d+'s request for reimbursement for 0mi on .* has been processed and is en route./) end end context "when there are patch notes" do before do patch_note_1.update_attribute(:patch_note_group, patch_note_group_all_users) patch_note_2.update_attribute(:patch_note_group, patch_note_group_no_volunteers) end it "shows all the patch notes available" do render template: "notifications/index" expect(rendered).to have_text("Patch Note 1") expect(rendered).to have_text("Patch Note B") end it "shows the patch notes under the correct type" do render template: "notifications/index" queryable_html = Nokogiri.HTML5(rendered) patch_note_type_a_header = queryable_html.xpath("//*[text()[contains(.,'#{patch_note_type_a.name}')]]").first patch_note_type_b_header = queryable_html.xpath("//*[text()[contains(.,'#{patch_note_type_b.name}')]]").first patch_note_a_data = patch_note_type_a_header.parent.css("ul").first expect(patch_note_a_data.text).to include("Patch Note 1") patch_note_b_data = patch_note_type_b_header.parent.css("ul").first expect(patch_note_b_data.text).to include("Patch Note B") end end end context "without a deploy date" do let(:deploy_time) { nil } before do notification_1_hour_ago.update_attribute(:created_at, 1.hour.ago) notification_1_day_ago.update_attribute(:created_at, 1.day.ago) notification_2_days_ago.update_attribute(:created_at, 2.days.ago) notification_3_days_ago.update_attribute(:created_at, 3.days.ago) patch_note_1.update_attribute(:patch_note_group, patch_note_group_all_users) patch_note_2.update_attribute(:patch_note_group, patch_note_group_no_volunteers) end it "shows the correct number of notifications" do render template: "notifications/index" expect(rendered).to have_css(".list-group-item", count: 4) end it "does not display patch notes" do render template: "notifications/index" notifications_html = Nokogiri::HTML5(rendered).css(".list-group-item") view_patch_notes = notifications_html.select { |node| node.text.include?("Patch Notes") } expect(PatchNote.all.size).to eql(2) expect(view_patch_notes).to be_empty end end end ================================================ FILE: spec/views/other_duties/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "other_duties/edit", type: :view do let(:other_duty) { create(:other_duty) } before do assign :other_duty, other_duty end it "display all form fields" do render template: "other_duties/edit" expect(rendered).to have_text("Editing Duty") expect(rendered).to have_text("Occurred On") expect(rendered).to have_text("Duty Duration") expect(rendered).to have_text("Enter Notes") end it "displays occurred time in the occurred at form field" do render template: "other_duties/edit" expect(rendered).to include(other_duty.occurred_at.strftime("%Y-%m-%d")) end it "displays notes in the notes form field" do render template: "other_duties/edit" expect(rendered).to include(CGI.escapeHTML(other_duty.notes)) end end ================================================ FILE: spec/views/other_duties/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "other_duties/new", type: :view do let(:current_time) { Time.zone.now.strftime("%Y-%m-%d") } before do assign :other_duty, OtherDuty.new render template: "other_duties/new" end it "display all form fields" do expect(rendered).to have_text("New Duty") expect(rendered).to have_field("Occurred On", with: current_time) expect(rendered).to have_text("Duty Duration") expect(rendered).to have_text("Enter Notes") end end ================================================ FILE: spec/views/placement_types/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "placement_types/edit.html.erb", type: :view do let(:organization) { build_stubbed :casa_org } let(:admin) { build_stubbed :casa_admin, casa_org: organization } let(:placement_type) { build_stubbed :placement_type, casa_org: organization } before do enable_pundit(view, admin) sign_in admin end it "allows editing the placement type" do assign :placement_type, placement_type render template: "placement_types/edit" expect(rendered).to have_text("Edit Placement Type") expect(rendered).to have_field("placement_type[name]", with: placement_type.name) expect(rendered).to have_button("Submit") end end ================================================ FILE: spec/views/placement_types/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "placement_types/new.html.erb", type: :view do let(:organization) { build_stubbed :casa_org } let(:admin) { build_stubbed :casa_admin, casa_org: organization } let(:placement_type) { organization.placement_types.new } before do enable_pundit(view, admin) sign_in admin end it "allows creating a placement type" do assign :placement_type, placement_type render template: "placement_types/new" expect(rendered).to have_text("New Placement Type") expect(rendered).to have_field("placement_type[name]") expect(rendered).to have_button("Submit") end end ================================================ FILE: spec/views/placements/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "placements/edit", type: :view do subject { render template: "placements/edit" } let(:organization) { create(:casa_org) } let(:user) { build_stubbed(:casa_admin, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization, case_number: "123") } let(:placement_type) { create(:placement_type, name: "Reunification") } let(:placement) { create(:placement, placement_started_at: "2024-08-15 12:39:00 UTC", placement_type:, casa_case:) } before do assign :casa_case, casa_case assign :placement, placement enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return(user.casa_org) render end it { is_expected.to have_selector("h1", text: "Editing Placement") } it "has a date input for placement started at with the correct value" do expect(rendered).to have_field("placement[placement_started_at]", with: "2024-08-15") end it "has a select input for placement type with the correct placeholder" do expect(rendered).to have_select("placement[placement_type_id]", with_options: ["-Select Placement Type-"]) end end ================================================ FILE: spec/views/placements/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "placements/index", type: :view do let(:casa_org) { create(:casa_org, :with_placement_types) } let(:casa_case) { create(:casa_case, casa_org:, case_number: "CINA-12345") } let(:placement_current) { create(:placement_type, name: "Reunification", casa_org:) } let(:placement_prev) { create(:placement_type, name: "Kinship", casa_org:) } let(:placement_first) { create(:placement_type, name: "Adoption", casa_org:) } let(:placements) do [ create(:placement, placement_started_at: "2024-08-15 20:40:44 UTC", casa_case:, placement_type: placement_current), create(:placement, placement_started_at: "2023-06-02 00:00:00 UTC", casa_case:, placement_type: placement_prev), create(:placement, placement_started_at: "2021-12-25 10:10:10 UTC", casa_case:, placement_type: placement_first) ] end before do assign(:casa_case, casa_case) assign(:placements, placements.sort_by(&:placement_started_at).reverse) render end it "displays the case number in the header" do expect(rendered).to have_selector("h1", text: "CINA-12345") end it "has a link to create a new placement" do expect(rendered).to have_link("New Placement", href: new_casa_case_placement_path(casa_case)) end it "displays placement information for each placement" do expect(rendered).to have_content("Reunification") expect(rendered).to have_content(/August 15, 2024/) expect(rendered).to have_content(/Present/) expect(rendered).to have_content("Kinship") expect(rendered).to have_content(/June 2, 2023/) expect(rendered).to have_content("Adoption") expect(rendered).to have_content(/December 25, 2021/) end it "has edit links for each placement" do placements.each do |placement| expect(rendered).to have_link("Edit", href: edit_casa_case_placement_path(casa_case, placement)) end end it "has delete buttons for each placement" do placements.each do |placement| expect(rendered).to have_selector("a[data-bs-target='##{placement.id}']", text: "Delete") end end it "renders delete confirmation modals for each placement" do placements.each do |placement| expect(rendered).to have_selector("##{placement.id}.modal") within "##{placement.id}" do expect(rendered).to have_content("Delete Placement?") expect(rendered).to have_link("Delete Placement", href: casa_case_placement_path(casa_case, placement)) end end end end ================================================ FILE: spec/views/placements/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "placements/new", type: :view do subject { render template: "placements/new" } before do assign :casa_case, casa_case assign :placement, Placement.new allow(view).to receive(:current_organization).and_return(user.casa_org) end let(:user) { build_stubbed(:casa_admin) } let(:casa_case) { create(:casa_case) } it { is_expected.to have_selector("h1", text: "New Placement") } it { is_expected.to have_selector("h6", text: casa_case.case_number) } it { is_expected.to have_link(casa_case.case_number, href: "/casa_cases/#{casa_case.case_number.parameterize}") } it { is_expected.to have_selector(".primary-btn") } end ================================================ FILE: spec/views/reimbursements/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "reimbursements/index", type: :view do before do admin = build_stubbed :casa_admin enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) end it "does not have any translation missing classes" do supervisor = create :supervisor volunteer = create :volunteer, supervisor: supervisor case_contact = create :case_contact, :wants_reimbursement, creator: volunteer, contact_made: true, occurred_at: 6.days.ago assign :reimbursements, [case_contact] assign :grouped_reimbursements, [] assign :volunteers_for_filter, [] render template: "reimbursements/index" expect(rendered).not_to have_css("span.translation_missing") end end ================================================ FILE: spec/views/supervisor_mailer/weekly_digest.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "supervisor_mailer/weekly_digest", type: :view do let(:organization) { create(:casa_org) } let(:supervisor) { create(:supervisor, casa_org: organization) } let(:volunteer) { create(:volunteer, casa_org: organization) } let(:casa_case) { create(:casa_case, casa_org: organization) } let(:inactive_volunteer) { create(:volunteer, :with_casa_cases, casa_org: organization) } let(:supervisor_mailer) { SupervisorMailer.new } let(:contact_topic_1) { create(:contact_topic, question: "Contact Topic 1") } let(:contact_topic_2) { create(:contact_topic, question: "Contact Topic 2") } let(:contact_topic_answer_1) { create(:contact_topic_answer, contact_topic: contact_topic_1, value: "Contact Topic 1 Answer") } let(:contact_topic_answer_2) { create(:contact_topic_answer, contact_topic: contact_topic_2, value: "") } context "when there are successful and unsuccessful contacts" do before do supervisor.volunteers << volunteer inactive_volunteer.update active: false supervisor.volunteers_ever_assigned << inactive_volunteer volunteer.casa_cases << casa_case create_list :case_contact, 2, creator: volunteer, casa_case: casa_case, contact_made: false, occurred_at: 6.days.ago @case_contact = create :case_contact, creator: volunteer, casa_case: casa_case, contact_made: true, occurred_at: 6.days.ago, contact_topic_answers: [contact_topic_answer_1, contact_topic_answer_2] assign :supervisor, supervisor assign :inactive_volunteers, [] sign_in supervisor @inactive_messages = InactiveMessagesService.new(supervisor).inactive_messages render template: "supervisor_mailer/weekly_digest" end it { expect(rendered).to have_text("Here's a summary of what happened with your volunteers this last week.") } it { expect(rendered).to have_link(volunteer.display_name) } it { expect(rendered).to have_link(casa_case.case_number) } it { expect(rendered).not_to have_text(inactive_volunteer.display_name) } it { expect(rendered).to have_text("Number of unsuccessful case contacts made this week: 2") } it { expect(rendered).to have_text("Number of successful case contacts made this week: 1") } it { expect(rendered).to have_text("- Date: #{I18n.l(@case_contact.occurred_at, format: :full, default: nil)}") } it { expect(rendered).to have_text("- Type: #{@case_contact.decorate.contact_types}") } it { expect(rendered).to have_text("- Duration: #{@case_contact.duration_minutes}") } it { expect(rendered).to have_text("- Contact Made: #{@case_contact.contact_made}") } it { expect(rendered).to have_text("- Medium Type: #{@case_contact.medium_type}") } it { expect(rendered).to have_text("- Notes: #{@case_contact.notes}") } it { expect(rendered).to have_text("Contact Topic 1") } it { expect(rendered).to have_text("Contact Topic 1 Answer") } it { expect(rendered).not_to have_text("Contact Topic 2") } end context "when there are no volunteers" do before do sign_in supervisor assign :supervisor, supervisor assign :inactive_volunteers, [] @inactive_messages = InactiveMessagesService.new(supervisor).inactive_messages render template: "supervisor_mailer/weekly_digest" end it { expect(rendered).to have_text("You have no volunteers with assigned cases at the moment. When you do, you will see their status here.") } end context "when there are volunteers but no contacts" do before do supervisor.volunteers << volunteer inactive_volunteer.update active: false supervisor.volunteers_ever_assigned << inactive_volunteer volunteer.casa_cases << casa_case sign_in supervisor assign :supervisor, supervisor assign :inactive_volunteers, [] @inactive_messages = InactiveMessagesService.new(supervisor).inactive_messages render template: "supervisor_mailer/weekly_digest" end it { expect(rendered).to have_text("No contact attempts were logged for this week.") } end context "when a volunteer has been reassigned to a new supervisor" do before do supervisor.volunteers << volunteer volunteer.casa_cases << casa_case # reassign volunteer volunteer.supervisor_volunteer.update(is_active: false) other_supervisor.volunteers << volunteer volunteer.supervisor_volunteer.update(is_active: true) sign_in supervisor assign :supervisor, supervisor assign :inactive_volunteers, [] render template: "supervisor_mailer/weekly_digest" end let(:other_supervisor) { create(:supervisor) } it { expect(rendered).to include("The following volunteers have been unassigned from you", volunteer.display_name) } end context "when a volunteer has been unassigned" do before do supervisor.volunteers << volunteer sign_in supervisor volunteer.supervisor_volunteer.update(is_active: false) new_supervisor.volunteers << volunteer volunteer.supervisor_volunteer.update(is_active: true) assign :supervisor, supervisor assign :inactive_volunteers, [] render template: "supervisor_mailer/weekly_digest" end let(:new_supervisor) { create(:supervisor) } it { expect(rendered).to have_text("The following volunteers have been unassigned from you") } it { expect(rendered).to have_text("- #{volunteer.display_name}") } end context "when a volunteer unassigned and has not been assigned to a new supervisor" do before do supervisor.volunteers << volunteer sign_in supervisor assign :supervisor, supervisor assign :inactive_volunteers, [] volunteer.supervisor_volunteer.update(is_active: false) @inactive_messages = [] render template: "supervisor_mailer/weekly_digest" end it { expect(rendered).to have_text("The following volunteers have been unassigned from you") } it { expect(rendered).to have_text("- #{volunteer.display_name}") } it { expect(rendered).to have_text("(not assigned to a new supervisor)") } end context "when a volunteer has not recently signed in, within 30 days" do let(:volunteer) { create(:volunteer, casa_org: organization, last_sign_in_at: 39.days.ago) } before do supervisor.volunteers << volunteer sign_in supervisor assign :supervisor, supervisor assign :inactive_volunteers, supervisor.inactive_volunteers render template: "supervisor_mailer/weekly_digest" end it { expect(rendered).to have_text("The following volunteers have not signed in or created case contacts in the last 30 days") } it { expect(rendered).to have_text("- #{volunteer.display_name}") } end end ================================================ FILE: spec/views/supervisors/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "supervisors/edit", type: :view do before do admin = build_stubbed :casa_admin enable_pundit(view, admin) allow(view).to receive(:current_user).and_return(admin) allow(view).to receive(:current_organization).and_return(admin.casa_org) end it "displays the 'Unassign' button next to volunteer being currently supervised by the supervisor" do supervisor = create :supervisor volunteer = create :volunteer, supervisor: supervisor assign :supervisor, supervisor assign :all_volunteers_ever_assigned, [volunteer] assign :available_volunteers, [] render template: "supervisors/edit" expect(rendered).to include(unassign_supervisor_volunteer_path(volunteer)) expect(rendered).not_to include("Unassigned") end it "does not display the 'Unassign' button next to volunteer no longer supervised by the supervisor" do casa_org = create :casa_org supervisor = create :supervisor, casa_org: casa_org volunteer = create :volunteer, casa_org: casa_org create :supervisor_volunteer, :inactive, supervisor: supervisor, volunteer: volunteer assign :supervisor, supervisor assign :supervisor_has_unassigned_volunteers, true assign :all_volunteers_ever_assigned, [volunteer] assign :available_volunteers, [] render template: "supervisors/edit" expect(rendered).not_to include(unassign_supervisor_volunteer_path(volunteer)) expect(rendered).to include("Unassigned") end describe "invite and login info" do let(:volunteer) { create :volunteer } let(:supervisor) { build_stubbed :supervisor } let(:admin) { build_stubbed :casa_admin } it "shows for a supervisor editig a supervisor" do enable_pundit(view, supervisor) allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :supervisor, supervisor assign :all_volunteers_ever_assigned, [volunteer] assign :available_volunteers, [] render template: "supervisors/edit" expect(rendered).to have_text("Added to system ") expect(rendered).to have_text("Invitation email sent \n never") expect(rendered).to have_text("Last logged in") expect(rendered).to have_text("Invitation accepted \n never") expect(rendered).to have_text("Password reset last sent \n never") end it "shows profile info form fields as editable for a supervisor editing their own profile" do enable_pundit(view, supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :supervisor, supervisor assign :all_volunteers_ever_assigned, [volunteer] assign :available_volunteers, [] render template: "supervisors/edit" expect(rendered).to have_field("supervisor_email") expect(rendered).to have_field("supervisor_display_name") expect(rendered).to have_field("supervisor_phone_number") end it "shows profile info form fields as disabled for a supervisor editing another supervisor" do enable_pundit(view, supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :supervisor, build_stubbed(:supervisor, casa_org: build_stubbed(:casa_org)) assign :all_volunteers_ever_assigned, [volunteer] assign :available_volunteers, [] render template: "supervisors/edit" expect(rendered).not_to have_field("supervisor_email") expect(rendered).not_to have_field("supervisor_display_name") expect(rendered).not_to have_field("supervisor_phone_number") end it "shows for an admin editing a supervisor" do enable_pundit(view, supervisor) allow(view).to receive(:current_user).and_return(admin) assign :supervisor, supervisor assign :all_volunteers_ever_assigned, [volunteer] assign :available_volunteers, [] render template: "supervisors/edit" expect(rendered).to have_text("Added to system ") expect(rendered).to have_text("Invitation email sent \n never") expect(rendered).to have_text("Last logged in") expect(rendered).to have_text("Invitation accepted \n never") expect(rendered).to have_text("Password reset last sent \n never") end end describe "'Change to Admin' button" do let(:supervisor) { build_stubbed :supervisor } before do assign :supervisor, supervisor assign :available_volunteers, [] end it "shows for an admin editing a supervisor" do render template: "supervisors/edit" expect(rendered).to have_text("Change to Admin") expect(rendered).to include(change_to_admin_supervisor_path(supervisor)) end it "does not show for a supervisor editing a supervisor" do enable_pundit(view, supervisor) allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) render template: "supervisors/edit" expect(rendered).not_to have_text("Change to Admin") expect(rendered).not_to include(change_to_admin_supervisor_path(supervisor)) end end end ================================================ FILE: spec/views/supervisors/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "supervisors/index", type: :view do context "when logged in as an admin" do it "can access the 'New Supervisor' button" do user = create(:casa_admin) enable_pundit(view, user) casa_cases = create_list(:casa_case, 2, court_dates: []) assign :casa_cases, casa_cases assign :supervisors, [] assign :available_volunteers, [] sign_in user render template: "supervisors/index" expect(rendered).to have_link("New Supervisor", href: new_supervisor_path) end it "show casa_cases list" do user = create(:casa_admin) enable_pundit(view, user) casa_case1 = create(:casa_case, case_number: "123", active: true, birth_month_year_youth: "1999-01-01".to_date) casa_case2 = create(:casa_case, case_number: "456", active: false, birth_month_year_youth: "2024-01-01".to_date) assign :casa_cases, [casa_case1, casa_case2] assign :supervisors, [] assign :available_volunteers, [] sign_in user render template: "supervisors/index" expect(rendered).to have_text "123" expect(rendered).to have_text "Active" expect(rendered).to have_text "Yes #{CasaCase::TRANSITION_AGE_YOUTH_ICON}" expect(rendered).to have_text "456" expect(rendered).to have_text "Inactive" expect(rendered).to have_text "No #{CasaCase::NON_TRANSITION_AGE_YOUTH_ICON}" end context "when a supervisor has volunteers who have and have not submitted a case contact in 14 days" do it "shows positive and negative numbers" do supervisor = create(:supervisor) enable_pundit(view, supervisor) create(:volunteer, :with_cases_and_contacts, supervisor: supervisor) create(:volunteer, :with_casa_cases, supervisor: supervisor) assign :supervisors, [supervisor] assign :casa_cases, [] assign :available_volunteers, [] sign_in supervisor render template: "supervisors/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#supervisors .success-bg").length).to eq(1) expect(parsed_html.css("#supervisors .danger-bg").length).to eq(1) end it "accurately displays the number of active and inactive volunteers per supervisor" do user = create(:casa_admin) enable_pundit(view, user) supervisor = create(:supervisor) create_list(:volunteer, 2, :with_cases_and_contacts, supervisor: supervisor) create(:volunteer, :with_casa_cases, supervisor: supervisor) casa_cases = create_list(:casa_case, 2, court_dates: []) assign :supervisors, [supervisor] assign :casa_cases, casa_cases assign :available_volunteers, [] sign_in user render template: "supervisors/index" parsed_html = Nokogiri.HTML5(rendered) active_bar = parsed_html.css("#supervisors .success-bg") inactive_bar = parsed_html.css("#supervisors .danger-bg") active_flex = active_bar.inner_html inactive_flex = inactive_bar.inner_html active_content = active_bar.children[0].text.strip inactive_content = inactive_bar.children[0].text.strip expect(active_flex).to eq(active_content) expect(inactive_flex).to eq(inactive_content) expect(active_flex.to_i).to eq(2) expect(inactive_flex.to_i).to eq(1) end end context "when a supervisor only has volunteers who have not submitted a case contact in 14 days" do it "omits the attempted contact stat bar" do user = create(:casa_admin) enable_pundit(view, user) supervisor = create(:supervisor) create(:volunteer, :with_casa_cases, supervisor: supervisor) casa_cases = create_list(:casa_case, 2, court_dates: []) assign :supervisors, [supervisor] assign :casa_cases, casa_cases assign :available_volunteers, [] sign_in user render template: "supervisors/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#supervisors .success-bg").length).to eq(0) expect(parsed_html.css("#supervisors .danger-bg").length).to eq(1) end end context "when a supervisor only has volunteers who have submitted a case contact in 14 days" do it "shows the end of the attempted contact bar instead of the no attempted contact bar" do user = create(:casa_admin) enable_pundit(view, user) supervisor = create(:supervisor) create(:volunteer, :with_cases_and_contacts, supervisor: supervisor) casa_cases = create_list(:casa_case, 2, court_dates: []) assign :supervisors, [supervisor] assign :casa_cases, casa_cases assign :available_volunteers, [] sign_in user render template: "supervisors/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#supervisors .success-bg").length).to eq(1) expect(parsed_html.css("#supervisors .danger-bg").length).to eq(0) end end context "when a supervisor does not have volunteers" do it "shows a no assigned volunteers message instead of attempted and no attempted contact bars" do user = create(:casa_admin) enable_pundit(view, user) supervisor = create(:supervisor) casa_cases = create_list(:casa_case, 2, court_dates: []) assign :supervisors, [supervisor] assign :casa_cases, casa_cases assign :available_volunteers, [] sign_in user render template: "supervisors/index" parsed_html = Nokogiri.HTML5(rendered) expect(parsed_html.css("#supervisors .success-bg").length).to eq(0) expect(parsed_html.css("#supervisors .danger-bg").length).to eq(0) expect(parsed_html.css("#supervisors .bg-secondary").length).to eq(1) end end end context "when logged in as a supervisor" do it "cannot access the 'New Supervisor' button" do user = create(:supervisor) enable_pundit(view, user) casa_cases = create_list(:casa_case, 2, court_dates: []) assign :casa_cases, casa_cases assign :supervisors, [] assign :available_volunteers, [] sign_in user render template: "supervisors/index" expect(rendered).not_to have_link("New Supervisor", href: new_supervisor_path) end end end ================================================ FILE: spec/views/supervisors/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "supervisors/new", type: :view do subject { render template: "supervisors/new" } before do assign :supervisor, Supervisor.new end context "while signed in as admin" do before do sign_in_as_admin end end end ================================================ FILE: spec/views/templates/email_templates_spec.rb ================================================ require "rails_helper" RSpec.shared_examples "compares opening and closing tags" do include TemplateHelper it "validates html tags" do file_content = File.read(Rails.root.join(file_path)) tags_are_equal = validate_closing_tags_exist(file_content) expect(tags_are_equal).to be true end end RSpec.describe "casa_admin_mailer", type: :view do describe "should validate that account_setup email template is valid" do let(:file_path) { "app/views/casa_admin_mailer/account_setup.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that deactivation email template is valid" do let(:file_path) { "app/views/casa_admin_mailer/deactivation.html.erb" } it_behaves_like "compares opening and closing tags" end end RSpec.describe "devise", type: :view do describe "should validate that confirmation_instructions email template is valid" do let(:file_path) { "app/views/devise/mailer/confirmation_instructions.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that email_changed email template is valid" do let(:file_path) { "app/views/devise/mailer/email_changed.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that invitation_instruction email template is valid" do let(:file_path) { "app/views/devise/mailer/invitation_instructions.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that password_change email template is valid" do let(:file_path) { "app/views/devise/mailer/password_change.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that reset_password_instructions email template is valid" do let(:file_path) { "app/views/devise/mailer/reset_password_instructions.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that unlock_instructions email template is valid" do let(:file_path) { "app/views/devise/mailer/unlock_instructions.html.erb" } it_behaves_like "compares opening and closing tags" end end RSpec.describe "supervisor_mailer", type: :view do describe "should validate that account_setup email template is valid" do let(:file_path) { "app/views/supervisor_mailer/account_setup.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that weekly_digest email template is valid" do let(:file_path) { "app/views/supervisor_mailer/weekly_digest.html.erb" } it_behaves_like "compares opening and closing tags" end end RSpec.describe "user_mailer", type: :view do describe "should validate that password_changed_reminder email template is valid" do let(:file_path) { "app/views/user_mailer/password_changed_reminder.html.erb" } it_behaves_like "compares opening and closing tags" end end RSpec.describe "volunteer_mailer", type: :view do describe "should validate that account_setup email template is valid" do let(:file_path) { "app/views/volunteer_mailer/account_setup.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that case_contacts_reminder email template is valid" do let(:file_path) { "app/views/volunteer_mailer/case_contacts_reminder.html.erb" } it_behaves_like "compares opening and closing tags" end describe "should validate that court_report_reminder email template is valid" do let(:file_path) { "app/views/volunteer_mailer/court_report_reminder.html.erb" } it_behaves_like "compares opening and closing tags" end end ================================================ FILE: spec/views/volunteers/edit.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "volunteers/edit", type: :view do it "allows an administrator to edit a volunteers email address" do administrator = build_stubbed :casa_admin enable_pundit(view, administrator) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(administrator) allow(view).to receive(:current_organization).and_return(administrator.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_email", readonly: true) end it "allows an administrator to edit a volunteers phone number" do administrator = build_stubbed :casa_admin enable_pundit(view, administrator) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(administrator) allow(view).to receive(:current_organization).and_return(administrator.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_email", readonly: true) end it "allows an administrator to edit a volunteers date of birth" do administrator = build_stubbed :casa_admin enable_pundit(view, administrator) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(administrator) allow(view).to receive(:current_organization).and_return(administrator.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_date_of_birth", readonly: true) expect(rendered).to have_field("volunteer_date_of_birth", readonly: false) end it "allows a supervisor to edit a volunteers email address" do supervisor = build_stubbed :supervisor enable_pundit(view, supervisor) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_email", readonly: true) end it "allows a supervisor in the same org to edit a volunteers phone number" do org = create :casa_org supervisor = build_stubbed :supervisor, casa_org: org enable_pundit(view, supervisor) volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).to have_field("volunteer_phone_number") end it "allows a supervisor in the same org to edit a volunteers date of birth" do org = create :casa_org supervisor = build_stubbed :supervisor, casa_org: org enable_pundit(view, supervisor) volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_date_of_birth", readonly: true) expect(rendered).to have_field("volunteer_date_of_birth", readonly: false) end it "does not allow a supervisor from a different org to edit a volunteers phone number" do different_supervisor = build_stubbed :supervisor enable_pundit(view, different_supervisor) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(different_supervisor) allow(view).to receive(:current_organization).and_return(different_supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_phone_number") end it "does not allow a supervisor from a different org to edit a volunteers date of birth" do different_supervisor = build_stubbed :supervisor enable_pundit(view, different_supervisor) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(different_supervisor) allow(view).to receive(:current_organization).and_return(different_supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).not_to have_field("volunteer_date_of_birth", readonly: false) end it "shows invite and login info" do supervisor = build_stubbed :supervisor enable_pundit(view, supervisor) org = create :casa_org volunteer = create :volunteer, casa_org: org allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).to have_text("Added to system ") expect(rendered).to have_text("Invitation email sent \n never") expect(rendered).to have_text("Last logged in") expect(rendered).to have_text("Invitation accepted \n never") expect(rendered).to have_text("Password reset last sent \n never") expect(rendered).to have_text("Learning Hours This Year\n 0h 0min") end context "the user has requested to reset their password" do describe "shows resend invitation" do it "allows an administrator resend invitation to a volunteer" do volunteer = create :volunteer supervisor = build_stubbed :supervisor admin = build_stubbed :casa_admin enable_pundit(view, supervisor) allow(view).to receive(:current_user).and_return(admin) allow(view).to receive(:current_organization).and_return(admin.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).to have_content("Resend Invitation") end it "allows a supervisor to resend invitation to a volunteer" do volunteer = create :volunteer supervisor = build_stubbed :supervisor enable_pundit(view, supervisor) allow(view).to receive(:current_user).and_return(supervisor) allow(view).to receive(:current_organization).and_return(supervisor.casa_org) assign :volunteer, volunteer assign :supervisors, [] render template: "volunteers/edit" expect(rendered).to have_content("Resend Invitation") end end end end ================================================ FILE: spec/views/volunteers/index.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "volunteers", type: :view do subject { render template: "volunteers/index" } let(:user) { build_stubbed :volunteer } let(:volunteer) { create :volunteer } before do enable_pundit(view, user) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_organization).and_return user.casa_org assign :volunteers, [volunteer] sign_in user end context "when NOT signed in as an admin" do it { is_expected.not_to have_selector("a", text: "New Volunteer") } end context "when signed in as an admin" do let(:user) { build_stubbed :casa_admin } it { is_expected.to have_selector("a", text: "New Volunteer") } end describe "supervisor's dropdown" do let!(:supervisor_volunteer) { create(:supervisor_volunteer, volunteer: volunteer, supervisor: supervisor) } context "when the supervisor is active" do let(:supervisor) { build(:supervisor) } it "shows up in the supervisor dropdown" do expect(subject).to include(CGI.escapeHTML(supervisor.display_name)) end end context "when the supervisor is not active" do let(:supervisor) { build(:supervisor, active: false) } it "doesn't show up in the dropdown" do expect(subject).not_to include(supervisor.display_name) end end end end ================================================ FILE: spec/views/volunteers/new.html.erb_spec.rb ================================================ require "rails_helper" RSpec.describe "volunteers/new", type: :view do subject { render template: "volunteers/new" } before do assign :volunteer, Volunteer.new end context "while signed in as admin" do before do sign_in_as_admin end end end ================================================ FILE: storage/.keep ================================================ ================================================ FILE: swagger/v1/swagger.yaml ================================================ --- openapi: 3.0.1 info: title: API V1 version: v1 components: schemas: login_success: type: object properties: api_token: type: string refresh_token: type: string user: id: type: integer display_name: type: string email: type: string token_expires_at: type: datetime refresh_token_expires_at: type: datetime login_failure: type: object properties: message: type: string sign_out: type: object properties: message: type: string paths: "/api/v1/users/sign_in": post: summary: Signs in a user tags: - Sessions parameters: [] responses: '201': description: user signed in content: application/json: schema: "$ref": "#/components/schemas/login_success" '401': description: invalid credentials content: application/json: schema: "$ref": "#/components/schemas/login_failure" requestBody: content: application/json: schema: type: object properties: email: type: string password: type: string required: - email - password "/api/v1/users/sign_out": delete: summary: Signs out a user tags: - Sessions parameters: - name: Authorization in: header required: true schema: type: string responses: '200': description: user signed out content: application/json: schema: "$ref": "#/components/schemas/sign_out" '401': description: unauthorized content: application/json: schema: "$ref": "#/components/schemas/sign_out" servers: - url: https://{defaultHost} variables: defaultHost: default: www.example.com ================================================ FILE: vendor/.keep ================================================ ================================================ FILE: vendor/pdftk/lib/libgcj.so.12 ================================================ [File too large to display: 47.1 MB]