diff --git a/.allow_skipping_tests b/.allow_skipping_tests index 53c4343744..70f79741f3 100644 --- a/.allow_skipping_tests +++ b/.allow_skipping_tests @@ -9,11 +9,13 @@ controllers/learning_hour_types_controller.rb controllers/mileage_reports_controller.rb controllers/placements_controller.rb controllers/users/sessions_controller.rb +controllers/learning_hour_topics_controller.rb datatables/application_datatable.rb decorators/application_decorator.rb decorators/case_assignment_decorator.rb decorators/court_date_decorator.rb decorators/learning_hour_decorator.rb +decorators/learning_hour_topic_decorator.rb helpers/all_casa_admins/casa_orgs_helper.rb helpers/api_base_helper.rb helpers/date_helper.rb @@ -35,6 +37,7 @@ notifications/youth_birthday_notification.rb policies/fund_request_policy.rb policies/learning_hour_policy.rb policies/learning_hour_type_policy.rb +policies/learning_hour_topic_policy.rb policies/note_policy.rb presenters/base_presenter.rb presenters/case_contact_presenter.rb diff --git a/app/controllers/casa_org_controller.rb b/app/controllers/casa_org_controller.rb index 6dda47ad19..115645b78e 100644 --- a/app/controllers/casa_org_controller.rb +++ b/app/controllers/casa_org_controller.rb @@ -4,6 +4,7 @@ class CasaOrgController < ApplicationController before_action :set_hearing_types, only: %i[edit update] before_action :set_judges, only: %i[edit update] before_action :set_learning_hour_types, only: %i[edit update] + before_action :set_learning_hour_topics, only: %i[edit update] before_action :set_sent_emails, only: %i[edit update] before_action :require_organization! after_action :verify_authorized @@ -52,7 +53,8 @@ def casa_org_update_params :twilio_phone_number, :twilio_api_key_sid, :twilio_api_key_secret, - :twilio_enabled + :twilio_enabled, + :learning_topic_active ) end @@ -76,4 +78,8 @@ def set_learning_hour_types def set_sent_emails @sent_emails = SentEmail.for_organization(@casa_org).order("created_at DESC").limit(10) end + + def set_learning_hour_topics + @learning_hour_topics = LearningHourTopic.for_organization(@casa_org) + end end diff --git a/app/controllers/learning_hour_topics_controller.rb b/app/controllers/learning_hour_topics_controller.rb new file mode 100644 index 0000000000..7063e9ff65 --- /dev/null +++ b/app/controllers/learning_hour_topics_controller.rb @@ -0,0 +1,46 @@ +class LearningHourTopicsController < ApplicationController + before_action :set_learning_hour_topic, only: %i[edit update] + after_action :verify_authorized + + def new + authorize LearningHourTopic + @learning_hour_topic = LearningHourTopic.new + end + + def edit + authorize @learning_hour_topic + end + + def create + authorize LearningHourTopic + @learning_hour_topic = LearningHourTopic.new(learning_hour_topic_params) + + if @learning_hour_topic.save + redirect_to edit_casa_org_path(current_organization), notice: "Learning Topic was successfully created." + else + render :new + end + end + + def update + authorize @learning_hour_topic + + if @learning_hour_topic.update(learning_hour_topic_params) + redirect_to edit_casa_org_path(current_organization), notice: "Learning Topic was successfully updated." + else + render :edit + end + end + + private + + def set_learning_hour_topic + @learning_hour_topic = LearningHourTopic.find(params[:id]) + end + + def learning_hour_topic_params + params.require(:learning_hour_topic).permit(:name, :active).merge( + casa_org: current_organization + ) + end +end diff --git a/app/controllers/learning_hours_controller.rb b/app/controllers/learning_hours_controller.rb index 93644b210a..1728c319ae 100644 --- a/app/controllers/learning_hours_controller.rb +++ b/app/controllers/learning_hours_controller.rb @@ -58,11 +58,11 @@ def set_learning_hour def learning_hours_params params.require(:learning_hour).permit(:occurred_at, :duration_minutes, :duration_hours, :name, :user_id, - :learning_hour_type_id) + :learning_hour_type_id, :learning_hour_topic_id) end def update_learning_hours_params params.require(:learning_hour).permit(:occurred_at, :duration_minutes, :duration_hours, :name, - :learning_hour_type_id) + :learning_hour_type_id, :learning_hour_topic_id) end end diff --git a/app/decorators/learning_hour_topic_decorator.rb b/app/decorators/learning_hour_topic_decorator.rb new file mode 100644 index 0000000000..13b9a0051a --- /dev/null +++ b/app/decorators/learning_hour_topic_decorator.rb @@ -0,0 +1,12 @@ +class LearningHourTopicDecorator < ApplicationDecorator + delegate_all + + # Define presentation-specific methods here. Helpers are accessed through + # `helpers` (aka `h`). You can override attributes, for example: + # + # def created_at + # helpers.content_tag :span, class: 'time' do + # object.created_at.strftime("%a %m/%d/%y") + # end + # end +end diff --git a/app/models/casa_org.rb b/app/models/casa_org.rb index 6ba3158377..e92af6e465 100644 --- a/app/models/casa_org.rb +++ b/app/models/casa_org.rb @@ -22,6 +22,7 @@ class CasaOrg < ApplicationRecord has_many :placements, through: :casa_cases has_many :banners, dependent: :destroy has_many :learning_hour_types, dependent: :destroy + has_many :learning_hour_topics, dependent: :destroy has_many :case_groups, dependent: :destroy has_one_attached :logo has_one_attached :court_report_template @@ -131,6 +132,7 @@ def normalize_phone_number # address :string # display_name :string # footer_links :string default([]), is an Array +# learning_topic_active :boolean default(FALSE) # name :string not null # show_driving_reimbursement :boolean default(TRUE) # show_fund_request :boolean default(FALSE) diff --git a/app/models/learning_hour.rb b/app/models/learning_hour.rb index 8967657267..b9fb3565d9 100644 --- a/app/models/learning_hour.rb +++ b/app/models/learning_hour.rb @@ -1,12 +1,14 @@ class LearningHour < ApplicationRecord belongs_to :user belongs_to :learning_hour_type + belongs_to :learning_hour_topic, optional: true validates :duration_minutes, presence: true validates :duration_minutes, numericality: {greater_than: 0}, if: :zero_duration_hours? validates :name, presence: {message: "/ Title cannot be blank"} validates :occurred_at, presence: true validate :occurred_at_not_in_future + validates :learning_hour_topic, presence: true, if: :user_org_learning_topic_enable? private @@ -21,26 +23,32 @@ def occurred_at_not_in_future errors.add(:date, "cannot be in the future") end end + + def user_org_learning_topic_enable? + user.casa_org.learning_topic_active + end end # == Schema Information # # Table name: learning_hours # -# id :bigint not null, primary key -# duration_hours :integer not null -# duration_minutes :integer not null -# name :string not null -# occurred_at :datetime not null -# created_at :datetime not null -# updated_at :datetime not null -# learning_hour_type_id :bigint -# user_id :bigint not null +# id :bigint not null, primary key +# duration_hours :integer not null +# duration_minutes :integer not null +# name :string not null +# occurred_at :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# learning_hour_topic_id :bigint +# learning_hour_type_id :bigint +# user_id :bigint not null # # Indexes # -# index_learning_hours_on_learning_hour_type_id (learning_hour_type_id) -# index_learning_hours_on_user_id (user_id) +# index_learning_hours_on_learning_hour_topic_id (learning_hour_topic_id) +# index_learning_hours_on_learning_hour_type_id (learning_hour_type_id) +# index_learning_hours_on_user_id (user_id) # # Foreign Keys # diff --git a/app/models/learning_hour_topic.rb b/app/models/learning_hour_topic.rb new file mode 100644 index 0000000000..5e7da7f6e0 --- /dev/null +++ b/app/models/learning_hour_topic.rb @@ -0,0 +1,32 @@ +class LearningHourTopic < ApplicationRecord + belongs_to :casa_org + validates :name, presence: true, uniqueness: {scope: %i[casa_org], case_sensitive: false} + before_validation :strip_name + scope :for_organization, ->(org) { where(casa_org: org).order(:name) } + + private + + def strip_name + self.name = name.strip if name + end +end + +# == Schema Information +# +# Table name: learning_hour_topics +# +# id :bigint not null, primary key +# name :string not null +# position :integer default(1) +# created_at :datetime not null +# updated_at :datetime not null +# casa_org_id :bigint not null +# +# Indexes +# +# index_learning_hour_topics_on_casa_org_id (casa_org_id) +# +# Foreign Keys +# +# fk_rails_... (casa_org_id => casa_orgs.id) +# diff --git a/app/policies/learning_hour_topic_policy.rb b/app/policies/learning_hour_topic_policy.rb new file mode 100644 index 0000000000..1e4f0d92f6 --- /dev/null +++ b/app/policies/learning_hour_topic_policy.rb @@ -0,0 +1,2 @@ +class LearningHourTopicPolicy < ApplicationPolicy +end diff --git a/app/views/casa_org/_learning_hour_topics.html.erb b/app/views/casa_org/_learning_hour_topics.html.erb new file mode 100644 index 0000000000..c53925f5bc --- /dev/null +++ b/app/views/casa_org/_learning_hour_topics.html.erb @@ -0,0 +1,50 @@ +
+
+
+
+
+

Learning Topic

+
+
+ +
+
+
+ + + + + + + + + <% @learning_hour_topics.each do |learning_hour_topic| %> + + + + + + <% end %> + +
NameActions
+ <%= learning_hour_topic.name %> + + <%= link_to edit_learning_hour_topic_path(learning_hour_topic) do %> +
+ +
+ <% end %> +
+
+
+
+
diff --git a/app/views/casa_org/edit.html.erb b/app/views/casa_org/edit.html.erb index 26a7426157..35da5b82a9 100644 --- a/app/views/casa_org/edit.html.erb +++ b/app/views/casa_org/edit.html.erb @@ -45,6 +45,10 @@ <%= 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 :twilio_enabled, class: 'form-check-input accordionTwilio' %> <%= form.label :twilio_enabled, "Enable Twilio", class: 'form-check-label mb-2' %> @@ -137,3 +141,6 @@
<%= render "learning_hour_types" %>
+
+ <%= render "learning_hour_topics" %> +
diff --git a/app/views/learning_hour_topics/_form.html.erb b/app/views/learning_hour_topics/_form.html.erb new file mode 100644 index 0000000000..71e5e46ff1 --- /dev/null +++ b/app/views/learning_hour_topics/_form.html.erb @@ -0,0 +1,31 @@ +
+
+
+
+

+ <%= 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 %> +
+ diff --git a/app/views/learning_hour_topics/edit.html.erb b/app/views/learning_hour_topics/edit.html.erb new file mode 100644 index 0000000000..4c6ab7fc4f --- /dev/null +++ b/app/views/learning_hour_topics/edit.html.erb @@ -0,0 +1 @@ +<%= render partial: "form", locals: {title: "Learning Topic", learning_hour_topic: @learning_hour_topic} %> diff --git a/app/views/learning_hour_topics/new.html.erb b/app/views/learning_hour_topics/new.html.erb new file mode 100644 index 0000000000..c8749bb6e3 --- /dev/null +++ b/app/views/learning_hour_topics/new.html.erb @@ -0,0 +1 @@ +<%= render partial: "form", locals: {title: "New Learning Topic", learning_hour_topic: @learning_hour_topic} %> diff --git a/app/views/learning_hours/_form.html.erb b/app/views/learning_hours/_form.html.erb index f6fa58f344..74d97ec6b8 100644 --- a/app/views/learning_hours/_form.html.erb +++ b/app/views/learning_hours/_form.html.erb @@ -27,7 +27,20 @@ id: "learning-hours-form" do |form| %> 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
diff --git a/app/views/learning_hours/_update_form.html.erb b/app/views/learning_hours/_update_form.html.erb index b24d1ca79e..ea7e585a66 100644 --- a/app/views/learning_hours/_update_form.html.erb +++ b/app/views/learning_hours/_update_form.html.erb @@ -26,6 +26,19 @@ 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
diff --git a/config/routes.rb b/config/routes.rb index c648995436..9b8ac8faa3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -110,6 +110,7 @@ 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 :followup_reports, only: :index resources :placement_reports, only: :index resources :banners, only: %i[index new edit create update destroy] diff --git a/db/migrate/20230817144910_create_learning_hour_topics.rb b/db/migrate/20230817144910_create_learning_hour_topics.rb new file mode 100644 index 0000000000..74d1e2cace --- /dev/null +++ b/db/migrate/20230817144910_create_learning_hour_topics.rb @@ -0,0 +1,11 @@ +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 diff --git a/db/migrate/20230819124840_add_learning_hour_topic_id_to_learning_hour.rb b/db/migrate/20230819124840_add_learning_hour_topic_id_to_learning_hour.rb new file mode 100644 index 0000000000..fd1fbc56ad --- /dev/null +++ b/db/migrate/20230819124840_add_learning_hour_topic_id_to_learning_hour.rb @@ -0,0 +1,7 @@ +class AddLearningHourTopicIdToLearningHour < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + add_reference :learning_hours, :learning_hour_topic, index: {algorithm: :concurrently} + end +end diff --git a/db/migrate/20230819132316_add_learning_topic_active_to_casa_org.rb b/db/migrate/20230819132316_add_learning_topic_active_to_casa_org.rb new file mode 100644 index 0000000000..536728d54a --- /dev/null +++ b/db/migrate/20230819132316_add_learning_topic_active_to_casa_org.rb @@ -0,0 +1,5 @@ +class AddLearningTopicActiveToCasaOrg < ActiveRecord::Migration[7.0] + def change + add_column :casa_orgs, :learning_topic_active, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 85c450ac5f..b6a7999e3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -159,6 +159,7 @@ 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.index ["slug"], name: "index_casa_orgs_on_slug", unique: true end @@ -370,6 +371,15 @@ t.index ["casa_org_id"], name: "index_languages_on_casa_org_id" 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 + t.index ["casa_org_id"], name: "index_learning_hour_topics_on_casa_org_id" + end + create_table "learning_hour_types", force: :cascade do |t| t.bigint "casa_org_id", null: false t.string "name" @@ -389,6 +399,8 @@ 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 ["learning_hour_topic_id"], name: "index_learning_hours_on_learning_hour_topic_id" t.index ["learning_hour_type_id"], name: "index_learning_hours_on_learning_hour_type_id" t.index ["user_id"], name: "index_learning_hours_on_user_id" end @@ -618,6 +630,7 @@ 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" diff --git a/db/seeds.rb b/db/seeds.rb index 1026c42493..1300fdd00c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -63,6 +63,7 @@ def active_record_classes Judge, Language, LearningHourType, + LearningHourTopic, MileageRate, Supervisor, SupervisorVolunteer, diff --git a/db/seeds/db_populator.rb b/db/seeds/db_populator.rb index 2dac6e18ea..f14db750cf 100644 --- a/db/seeds/db_populator.rb +++ b/db/seeds/db_populator.rb @@ -51,6 +51,7 @@ def create_org(options_hash) create_languages(casa_org) create_mileage_rates(casa_org) create_learning_hour_types(casa_org) + create_learning_hour_topics(casa_org) casa_org end @@ -377,6 +378,14 @@ def create_learning_hour_types(casa_org) 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 most_recent_past_court_date(casa_case_id) CourtDate.where( "date < ? AND casa_case_id = ?", diff --git a/spec/factories/learning_hour_topics.rb b/spec/factories/learning_hour_topics.rb new file mode 100644 index 0000000000..975eb565a1 --- /dev/null +++ b/spec/factories/learning_hour_topics.rb @@ -0,0 +1,7 @@ +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 diff --git a/spec/models/learning_hour_spec.rb b/spec/models/learning_hour_spec.rb index cf6d370248..19afbc75c5 100644 --- a/spec/models/learning_hour_spec.rb +++ b/spec/models/learning_hour_spec.rb @@ -39,4 +39,17 @@ learning_hour = build_stubbed(:learning_hour, occurred_at: 1.day.from_now.strftime("%d %b %Y")) expect(learning_hour).to_not be_valid 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).to_not be_valid + expect(learning_hour.errors[:learning_hour_topic]).to eq(["can't be blank"]) + end end diff --git a/spec/models/learning_hour_topic_spec.rb b/spec/models/learning_hour_topic_spec.rb new file mode 100644 index 0000000000..9600fd861c --- /dev/null +++ b/spec/models/learning_hour_topic_spec.rb @@ -0,0 +1,38 @@ +# 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") } + .to_not 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 diff --git a/spec/system/volunteers/new_spec.rb b/spec/system/volunteers/new_spec.rb index 511ac0c559..1fbb6f80be 100644 --- a/spec/system/volunteers/new_spec.rb +++ b/spec/system/volunteers/new_spec.rb @@ -31,9 +31,9 @@ fill_in "Email", with: "new_volunteer2@example.com" fill_in "Display name", with: "New Volunteer Display Name 2" - expect { + expect do click_on "Create Volunteer" - }.to change(User, :count).by(1) + end.to change(User, :count).by(1) end end @@ -48,4 +48,22 @@ expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.") end end + + it "displays learning hour topic for volunteers when enabled", js: true do + organization = create(:casa_org, learning_topic_active: true) + volunteer = create(:volunteer, casa_org: organization) + + sign_in volunteer + visit new_volunteer_learning_hour_path(volunteer) + expect(page).to have_text("Learning Topic") + end + + it "learning hour topic hidden when disabled", js: true do + organization = create(:casa_org) + volunteer = create(:volunteer, casa_org: organization) + + sign_in volunteer + visit new_volunteer_learning_hour_path(volunteer) + expect(page).to_not have_text("Learning Topic") + end end diff --git a/spec/views/casa_orgs/edit.html.erb_spec.rb b/spec/views/casa_orgs/edit.html.erb_spec.rb index e08833db09..ec89074bfe 100644 --- a/spec/views/casa_orgs/edit.html.erb_spec.rb +++ b/spec/views/casa_orgs/edit.html.erb_spec.rb @@ -7,6 +7,7 @@ assign(:hearing_types, []) assign(:judges, []) assign(:learning_hour_types, []) + assign(:learning_hour_topics, []) assign(:sent_emails, []) sign_in build_stubbed(:casa_admin)