diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 024dd07985..378e1b66b8 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
before_action :set_timeout_duration
before_action :set_current_organization
before_action :set_active_banner
+ before_action :set_custom_links
after_action :verify_authorized, except: :index, unless: :devise_controller?
# after_action :verify_policy_scoped, only: :index
@@ -45,27 +46,31 @@ def set_active_banner
@active_banner = nil if @active_banner&.expired?
end
+ def set_custom_links
+ return unless current_organization
+
+ @custom_links = current_organization.custom_links.active
+ end
+
protected
def handle_short_url(url_list)
hash_of_short_urls = {}
- url_list.each_with_index { |val, index|
+ url_list.each_with_index do |val, index|
# call short io service to shorten url
# create an entry in hash if api is success
short_io_service = ShortUrlService.new
response = short_io_service.create_short_url(val)
short_url = short_io_service.short_url
- hash_of_short_urls[index] = (response.code == 201 || response.code == 200) ? short_url : nil
- }
+ hash_of_short_urls[index] = [201, 200].include?(response.code) ? short_url : nil
+ end
hash_of_short_urls
end
# volunteer/supervisor/casa_admin controller uses to send SMS
# returns appropriate flash notice for SMS
def deliver_sms_to(resource, body_msg)
- if resource.phone_number.blank? || !resource.casa_org.twilio_enabled?
- return "blank"
- end
+ return "blank" if resource.phone_number.blank? || !resource.casa_org.twilio_enabled?
body = body_msg
to = resource.phone_number
@@ -81,8 +86,8 @@ def deliver_sms_to(resource, body_msg)
begin
twilio_res = @twilio.send_sms(req_params)
twilio_res.error_code.nil? ? "sent" : "error"
- rescue Twilio::REST::RestError => error
- @error = error
+ rescue Twilio::REST::RestError => e
+ @error = e
"error"
rescue # unverfied error isnt picked up by Twilio::Rest::RestError
# https://www.twilio.com/docs/errors/21608
@@ -103,9 +108,9 @@ def sms_acct_creation_notice(resource_name, sms_status)
end
def store_referring_location
- if request.referer && !request.referer.end_with?("users/sign_in") && params[:ignore_referer].blank?
- session[:return_to] = request.referer
- end
+ return unless request.referer && !request.referer.end_with?("users/sign_in") && params[:ignore_referer].blank?
+
+ session[:return_to] = request.referer
end
def redirect_back_to_referer(fallback_location:)
@@ -170,17 +175,13 @@ def unsupported_media_type
end
def log_and_reraise(error)
- unless KNOWN_ERRORS.include?(error.class)
- Bugsnag.notify(error)
- end
+ Bugsnag.notify(error) unless KNOWN_ERRORS.include?(error.class)
raise
end
def check_unconfirmed_email_notice(user)
notice = "#{user.role} was successfully updated."
- if user.saved_changes.include?("unconfirmed_email")
- notice += " Confirmation Email Sent."
- end
+ notice += " Confirmation Email Sent." if user.saved_changes.include?("unconfirmed_email")
notice
end
end
diff --git a/app/controllers/casa_org_controller.rb b/app/controllers/casa_org_controller.rb
index 16bc3d7701..1b980aa5c5 100644
--- a/app/controllers/casa_org_controller.rb
+++ b/app/controllers/casa_org_controller.rb
@@ -7,6 +7,7 @@ class CasaOrgController < ApplicationController
before_action :set_learning_hour_topics, only: %i[edit update]
before_action :set_sent_emails, only: %i[edit update]
before_action :set_contact_topics, only: %i[edit update]
+ before_action :set_custom_links, only: %i[edit update]
before_action :require_organization!
after_action :verify_authorized
before_action :set_active_storage_url_options, only: %i[edit update]
@@ -90,6 +91,10 @@ def set_contact_topics
@contact_topics = @casa_org.contact_topics.where(soft_delete: false)
end
+ def set_custom_links
+ @custom_links = @casa_org.custom_links
+ end
+
def set_active_storage_url_options
ActiveStorage::Current.url_options = {host: request.base_url}
end
diff --git a/app/controllers/custom_links_controller.rb b/app/controllers/custom_links_controller.rb
new file mode 100644
index 0000000000..ce8baa6961
--- /dev/null
+++ b/app/controllers/custom_links_controller.rb
@@ -0,0 +1,59 @@
+class CustomLinksController < ApplicationController
+ before_action :set_custom_link, only: %i[edit update destroy]
+
+ # GET /custom_links/new
+ def new
+ @custom_link = CustomLink.new(casa_org_id: current_user.casa_org_id)
+ authorize @customLink
+ end
+
+ # GET /custom_links/1/edit
+ def edit
+ authorize @custom_link
+ end
+
+ # POST /custom_links
+ def create
+ @custom_link = CustomLink.new(custom_link_params)
+ authorize @custom_link
+
+ if @custom_link.save
+ redirect_to edit_casa_org_path(current_organization), notice: "Custom link was successfully created."
+ else
+ render :new
+ end
+ end
+
+ # PATCH/PUT /custom_links/1
+ def update
+ authorize @custom_link
+ if @custom_link.update(custom_link_params)
+ redirect_to edit_casa_org_path(current_organization), notice: "Custom link was successfully updated."
+ else
+ render :edit
+ end
+ end
+
+ # DELETE /custom_links/1/delete
+ def destroy
+ authorize @custom_link
+
+ if @custom_link.destroy
+ redirect_to edit_casa_org_path(current_organization), notice: "Custom link was successfully removed."
+ else
+ render :show, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ # Use callbacks to share common setup or constraints between actions.
+ def set_custom_link
+ @custom_link = CustomLink.find(params[:id])
+ end
+
+ # Only allow a list of trusted parameters through.
+ def custom_link_params
+ params.require(:custom_link).permit(:text, :url, :active, :casa_org_id)
+ end
+end
diff --git a/app/models/casa_org.rb b/app/models/casa_org.rb
index 5c98594cda..7b3220ad72 100644
--- a/app/models/casa_org.rb
+++ b/app/models/casa_org.rb
@@ -27,6 +27,7 @@ class CasaOrg < ApplicationRecord
has_many :learning_hour_topics, dependent: :destroy
has_many :case_groups, dependent: :destroy
has_many :contact_topics
+ has_many :custom_links
has_one_attached :logo
has_one_attached :court_report_template
has_many :placement_types, dependent: :destroy
diff --git a/app/models/custom_link.rb b/app/models/custom_link.rb
new file mode 100644
index 0000000000..8f4959e6c4
--- /dev/null
+++ b/app/models/custom_link.rb
@@ -0,0 +1,31 @@
+class CustomLink < ApplicationRecord
+ belongs_to :casa_org
+ # Validate that the URL is present, has a valid format, and is unique
+ validates :url, presence: true,
+ format: {with: /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/, message: "must be a valid URL"}
+ # Validate that the title is present and has a maximum length of 255 characters
+ validates :text, presence: true, length: {maximum: 255}
+
+ scope :active, -> { where(active: true) }
+end
+
+# == Schema Information
+#
+# Table name: custom_links
+#
+# id :bigint not null, primary key
+# active :boolean default(TRUE), not null
+# text :string
+# url :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# casa_org_id :bigint not null
+#
+# Indexes
+#
+# index_custom_links_on_casa_org_id (casa_org_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (casa_org_id => casa_orgs.id)
+#
diff --git a/app/policies/custom_link_policy.rb b/app/policies/custom_link_policy.rb
new file mode 100644
index 0000000000..4de5cb29d8
--- /dev/null
+++ b/app/policies/custom_link_policy.rb
@@ -0,0 +1,8 @@
+class CustomLinkPolicy < ApplicationPolicy
+ alias_method :create?, :is_admin_same_org?
+ alias_method :edit?, :is_admin_same_org?
+ alias_method :new?, :is_admin_same_org?
+ alias_method :show?, :is_admin_same_org?
+ alias_method :update?, :is_admin_same_org?
+ alias_method :destroy?, :is_admin_same_org?
+end
diff --git a/app/views/casa_org/_custom_links.html.erb b/app/views/casa_org/_custom_links.html.erb
new file mode 100644
index 0000000000..53d18ec0cb
--- /dev/null
+++ b/app/views/casa_org/_custom_links.html.erb
@@ -0,0 +1,64 @@
+
+
+
+
+
+
Custom Link
+
+
+
+
+ <%= link_to new_custom_link_path, class: "btn-sm main-btn primary-btn btn-hover" do %>
+
+ New Custom Link
+ <% end %>
+
+
+
+
+
+
+
+
diff --git a/app/views/casa_org/edit.html.erb b/app/views/casa_org/edit.html.erb
index d5327109bb..74b607030e 100644
--- a/app/views/casa_org/edit.html.erb
+++ b/app/views/casa_org/edit.html.erb
@@ -163,3 +163,17 @@
<%= render "contact_topics" %>
+
+
+
+
+
+ Manage Custom Links
+
+
+
+
+
+
+ <%= render "custom_links" %>
+
diff --git a/app/views/custom_links/_form.html.erb b/app/views/custom_links/_form.html.erb
new file mode 100644
index 0000000000..9011c5d159
--- /dev/null
+++ b/app/views/custom_links/_form.html.erb
@@ -0,0 +1,39 @@
+
+
+
+
+ <%= form_with(model: custom_link, local: true) do |form| %>
+ <%= form.hidden_field :casa_org_id %>
+
+ <%= render "/shared/error_messages", resource: custom_link %>
+
+
+ <%= form.label :text, "Display Text" %>
+ <%= form.text_field :text, class: "form-control", required: true %>
+
+
+ <%= form.label :url, "URL" %>
+ <%= form.text_field :url, rows: 5, 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 %>
+
+
diff --git a/app/views/custom_links/edit.html.erb b/app/views/custom_links/edit.html.erb
new file mode 100644
index 0000000000..dd75347084
--- /dev/null
+++ b/app/views/custom_links/edit.html.erb
@@ -0,0 +1 @@
+<%= render partial: "form", locals: {title: "Custom Link", custom_link: @custom_link} %>
diff --git a/app/views/custom_links/new.html.erb b/app/views/custom_links/new.html.erb
new file mode 100644
index 0000000000..d225077815
--- /dev/null
+++ b/app/views/custom_links/new.html.erb
@@ -0,0 +1 @@
+<%= render partial: "form", locals: {title: "New Custom Link", custom_link: @custom_link} %>
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
index 75ccef2185..d0c7a863d9 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -72,6 +72,13 @@
<%= current_user.email %>
+ <% @custom_links&.each do |custom_link| %>
+
+ <%= link_to custom_link.url, target: "_blank" do %>
+ <%= custom_link.text %>
+ <% end %>
+
+ <% end %>
<%= link_to edit_users_path do %>
diff --git a/config/routes.rb b/config/routes.rb
index 7344e21169..af2013cbc6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -147,6 +147,9 @@
delete "soft_delete", on: :member
end
+ resources :custom_links, except: %i[index show] do
+ end
+
resources :followup_reports, only: :index
resources :placement_reports, only: :index
resources :banners, except: %i[show] do
diff --git a/db/migrate/20240513155246_create_custom_links.rb b/db/migrate/20240513155246_create_custom_links.rb
new file mode 100644
index 0000000000..86a10fd772
--- /dev/null
+++ b/db/migrate/20240513155246_create_custom_links.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Migration file to create custom_links table
+class CreateCustomLinks < ActiveRecord::Migration[7.1]
+ def change
+ create_table :custom_links do |t|
+ t.string :text
+ t.text :url
+ t.references :casa_org, null: false, foreign_key: true
+ t.boolean :active, null: false, default: true
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7e2fe6d13c..bd8ac1e876 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -315,6 +315,16 @@
t.index ["judge_id"], name: "index_court_dates_on_judge_id"
end
+ create_table "custom_links", force: :cascade do |t|
+ t.string "text"
+ t.text "url"
+ t.bigint "casa_org_id", 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_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
@@ -732,6 +742,7 @@
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_links", "casa_orgs"
add_foreign_key "emancipation_options", "emancipation_categories"
add_foreign_key "followups", "users", column: "creator_id"
add_foreign_key "judges", "casa_orgs"
diff --git a/spec/factories/custom_links.rb b/spec/factories/custom_links.rb
new file mode 100644
index 0000000000..de892f0a6a
--- /dev/null
+++ b/spec/factories/custom_links.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :custom_link do
+ text { "Example Link" }
+ url { "http://example.com" }
+ association :casa_org
+ end
+end
diff --git a/spec/models/custom_link_spec.rb b/spec/models/custom_link_spec.rb
new file mode 100644
index 0000000000..73f9974535
--- /dev/null
+++ b/spec/models/custom_link_spec.rb
@@ -0,0 +1,5 @@
+require "rails_helper"
+
+RSpec.describe CustomLink, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/policies/custom_link_policy_spec.rb b/spec/policies/custom_link_policy_spec.rb
new file mode 100644
index 0000000000..dd6509c6b9
--- /dev/null
+++ b/spec/policies/custom_link_policy_spec.rb
@@ -0,0 +1,29 @@
+require "rails_helper"
+
+RSpec.describe CustomLinkPolicy do
+ subject(:organization) { create(:casa_org) }
+
+ let(:volunteer) { create(:volunteer, :with_single_case, casa_org: organization) }
+ let(:supervisor) { create(:supervisor, casa_org: organization) }
+ let(:casa_admin) { create(:casa_admin, casa_org: organization) }
+ let(:custom_link) { create(:custom_link, casa_org: organization) }
+ let(:other_org_admin) { create(:casa_admin) }
+
+ permissions :create?, :edit?, :new?, :show?, :update? do
+ it "allows same org casa_admins" do
+ expect(described_class).to permit(casa_admin, custom_link)
+ end
+
+ it "does not allow different org casa_admins" do
+ expect(described_class).not_to permit(other_org_admin, custom_link)
+ end
+
+ it "does not permit supervisor" do
+ expect(described_class).not_to permit(supervisor, custom_link)
+ end
+
+ it "does not permit volunteer" do
+ expect(described_class).not_to permit(volunteer, custom_link)
+ end
+ end
+end
diff --git a/spec/requests/custom_links_spec.rb b/spec/requests/custom_links_spec.rb
new file mode 100644
index 0000000000..e2aa2b0997
--- /dev/null
+++ b/spec/requests/custom_links_spec.rb
@@ -0,0 +1,168 @@
+require "rails_helper"
+
+RSpec.describe "/casa_cases/:casa_case_id/custom_links/", type: :request do
+ let(:user) { create(:user) }
+ let(:admin) { create(:casa_admin, casa_org: user.casa_org) }
+ let(:custom_link) { create(:custom_link, casa_org_id: user.casa_org_id) }
+ let(:valid_attributes) { {text: "Link Text", url: "http://example.com", active: true, casa_org_id: user.casa_org_id} }
+ let(:invalid_attributes) { {text: "", url: "invalid", active: nil} }
+
+ before do
+ sign_in user
+ custom_link_policy = instance_double(CustomLinkPolicy)
+ allow(custom_link_policy).to receive_messages(
+ new?: true,
+ edit?: true,
+ create?: true,
+ update?: true,
+ destroy?: true
+ )
+
+ allow(CustomLinkPolicy).to receive(:new).and_return(custom_link_policy)
+ end
+
+ describe "GET #new" do
+ it "authorizes the action" do
+ custom_link_policy = instance_double(CustomLinkPolicy)
+
+ allow(CustomLinkPolicy).to receive(:new).and_return(custom_link_policy)
+ allow(custom_link_policy).to receive(:new?).and_return(true)
+
+ get new_custom_link_path
+ expect(response).to have_http_status(:found)
+ end
+
+ it "assigns a new CustomLink with the current user's casa_org_id to @custom_link" do
+ get new_custom_link_path
+ expect(assigns(:custom_link).casa_org_id).to eq(user.casa_org_id)
+ end
+ end
+
+ describe "GET #edit" do
+ it "assigns the requested custom_link as @custom_link" do
+ get edit_custom_link_path(custom_link)
+ expect(assigns(:custom_link)).to eq(custom_link)
+ end
+
+ it "authorizes the action" do
+ custom_link_policy = instance_double(CustomLinkPolicy)
+ allow(CustomLinkPolicy).to receive(:new).and_return(custom_link_policy)
+ allow(custom_link_policy).to receive(:edit?).and_return(true)
+ get edit_custom_link_path(custom_link)
+ expect(custom_link_policy).to have_received(:edit?)
+ end
+ end
+
+ describe "POST #create" do
+ context "with valid parameters" do
+ it "creates a new CustomLink" do
+ expect do
+ post custom_links_path, params: {custom_link: valid_attributes}
+ end.to change(CustomLink, :count).by(1)
+ end
+
+ it "redirects to the edit_casa_org_path" do
+ post custom_links_path, params: {custom_link: valid_attributes}
+ expect(response).to redirect_to(edit_casa_org_path(user.casa_org))
+ end
+
+ it "sets a success notice" do
+ post custom_links_path, params: {custom_link: valid_attributes}
+ expect(flash[:notice]).to eq("Custom link was successfully created.")
+ end
+
+ it "authorizes the action" do
+ custom_link_policy = instance_double(CustomLinkPolicy)
+
+ allow(CustomLinkPolicy).to receive(:new).and_return(custom_link_policy)
+ allow(custom_link_policy).to receive(:create?).and_return(true)
+
+ post custom_links_path, params: {custom_link: valid_attributes}
+ expect(response).to have_http_status(:found)
+ end
+ end
+
+ context "with invalid parameters" do
+ it "does not create a new CustomLink" do
+ expect do
+ post custom_links_path, params: {custom_link: invalid_attributes}
+ end.not_to change(CustomLink, :count)
+ end
+
+ it "renders the new template" do
+ post custom_links_path, params: {custom_link: invalid_attributes}
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+
+ describe "PATCH/PUT #update" do
+ context "with valid parameters" do
+ subject(:new_attributes) { {text: "Updated Text", url: "http://updated.com"} }
+
+ it "updates the requested custom_link" do
+ patch custom_link_path(custom_link), params: {custom_link: new_attributes}
+ custom_link.reload
+ expect(custom_link.text).to eq("Updated Text")
+ end
+
+ it "redirects to the edit_casa_org_path" do
+ patch custom_link_path(custom_link), params: {custom_link: new_attributes}
+ expect(response).to redirect_to(edit_casa_org_path(user.casa_org))
+ end
+
+ it "sets a success notice" do
+ patch custom_link_path(custom_link), params: {custom_link: new_attributes}
+ expect(flash[:notice]).to eq("Custom link was successfully updated.")
+ end
+
+ it "authorizes the action" do
+ custom_link_policy = instance_double(CustomLinkPolicy)
+ allow(CustomLinkPolicy).to receive(:new).and_return(custom_link_policy)
+ allow(custom_link_policy).to receive(:update?).and_return(true)
+ patch custom_link_path(custom_link), params: {custom_link: new_attributes}
+ expect(custom_link_policy).to have_received(:update?)
+ end
+ end
+
+ context "with invalid parameters" do
+ it "does not update the requested custom_link" do
+ patch custom_link_path(custom_link), params: {custom_link: invalid_attributes}
+ custom_link.reload
+ expect(custom_link.text).not_to be_empty
+ end
+
+ it "renders the edit template" do
+ patch custom_link_path(custom_link), params: {custom_link: invalid_attributes}
+ expect(response).to render_template(:edit)
+ end
+ end
+ end
+
+ describe "DELETE #destroy" do
+ it "destroys the requested custom_link" do
+ custom_link
+ expect do
+ delete custom_link_path(custom_link)
+ end.to change(CustomLink, :count).by(-1)
+ end
+
+ it "redirects to the edit_casa_org_path" do
+ delete custom_link_path(custom_link)
+ expect(response).to redirect_to(edit_casa_org_path(user.casa_org))
+ end
+
+ it "sets a success notice" do
+ delete custom_link_path(custom_link)
+ expect(flash[:notice]).to eq("Custom link was successfully removed.")
+ end
+
+ it "authorizes the action" do
+ custom_link_policy = instance_double(CustomLinkPolicy)
+ allow(CustomLinkPolicy).to receive(:new).and_return(custom_link_policy)
+ allow(custom_link_policy).to receive(:destroy?).and_return(true)
+ delete custom_link_path(custom_link)
+ expect(custom_link_policy).to have_received(:destroy?)
+ end
+ end
+end