diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2783fd451f..dd0403a5af 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -10,6 +10,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
@@ -42,27 +43,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] = response.code == 201 || response.code == 200 ? 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
@@ -77,32 +82,32 @@ 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
- "error"
- rescue # unverfied error isnt picked up by Twilio::Rest::RestError
+ twilio_res.error_code.nil? ? 'sent' : 'error'
+ rescue Twilio::REST::RestError => e
+ @error = e
+ 'error'
+ rescue StandardError # unverfied error isnt picked up by Twilio::Rest::RestError
# https://www.twilio.com/docs/errors/21608
- @error = "Phone number is unverifiied"
- "error"
+ @error = 'Phone number is unverifiied'
+ 'error'
end
end
def sms_acct_creation_notice(resource_name, sms_status)
case sms_status
- when "blank"
+ when 'blank'
"New #{resource_name} created successfully."
- when "error"
+ when 'error'
"New #{resource_name} created successfully. SMS not sent. Error: #{@error}."
- when "sent"
+ when 'sent'
"New #{resource_name} created successfully. SMS has been sent!"
end
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:)
@@ -136,22 +141,18 @@ def set_current_organization
def not_authorized
session[:user_return_to] = nil
- flash[:notice] = "Sorry, you are not authorized to perform this action."
+ flash[:notice] = 'Sorry, you are not authorized to perform this action.'
redirect_to(root_url)
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..741da7297f 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.where(soft_delete: false)
+ 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..1df8e54091
--- /dev/null
+++ b/app/controllers/custom_links_controller.rb
@@ -0,0 +1,61 @@
+class CustomLinksController < ApplicationController
+ before_action :set_custom_link, only: %i[edit update destroy]
+
+ # GET /custom_links/new
+ def new
+ authorize CustomLink
+ custom_link = CustomLink.new(casa_org_id: current_user.casa_org_id)
+ @custom_link = custom_link
+ end
+
+ # GET /custom_links/1/edit
+ def edit
+ authorize @custom_link
+ end
+
+ # POST /custom_links
+ def create
+ authorize CustomLink
+
+ @custom_link = CustomLink.new(custom_link_params)
+
+ 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 b5b9c7461c..4c7b0158c0 100644
--- a/app/models/casa_org.rb
+++ b/app/models/casa_org.rb
@@ -26,6 +26,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
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..2358f21137
--- /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..47239921a2 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 4352a14943..800842012e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -139,6 +139,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 e4ba1f6685..7025b05919 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -301,6 +301,17 @@
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 "soft_delete", default: false, 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
@@ -718,6 +729,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/requests/cutom_links_spec.rb b/spec/requests/cutom_links_spec.rb
new file mode 100644
index 0000000000..78a258181f
--- /dev/null
+++ b/spec/requests/cutom_links_spec.rb
@@ -0,0 +1,150 @@
+require 'rails_helper'
+
+RSpec.describe CustomLinksController, 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
+ allow_any_instance_of(CustomLinkPolicy).to receive(:new?).and_return(true)
+ allow_any_instance_of(CustomLinkPolicy).to receive(:edit?).and_return(true)
+ allow_any_instance_of(CustomLinkPolicy).to receive(:create?).and_return(true)
+ allow_any_instance_of(CustomLinkPolicy).to receive(:update?).and_return(true)
+ allow_any_instance_of(CustomLinkPolicy).to receive(:destroy?).and_return(true)
+ end
+
+ describe 'GET #new' do
+ it 'authorizes the action' do
+ expect_any_instance_of(CustomLinkPolicy).to receive(:new?).and_return(true)
+ get new_custom_link_path
+ 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)).to be_a_new(CustomLink)
+ expect(assigns(:custom_link).casa_org_id).to eq(user.casa_org_id)
+ end
+
+ it 'renders the new template' do
+ get new_custom_link_path
+ expect(response).to render_template(:new)
+ 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
+ expect_any_instance_of(CustomLinkPolicy).to receive(:edit?).and_return(true)
+ get edit_custom_link_path(custom_link)
+ 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
+ expect_any_instance_of(CustomLinkPolicy).to receive(:create?).and_return(true)
+ post custom_links_path, params: { custom_link: valid_attributes }
+ 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.to change(CustomLink, :count).by(0)
+ 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
+ let(: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
+ expect_any_instance_of(CustomLinkPolicy).to receive(:update?).and_return(true)
+ patch custom_link_path(custom_link), params: { custom_link: new_attributes }
+ 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
+ expect_any_instance_of(CustomLinkPolicy).to receive(:destroy?).and_return(true)
+ delete custom_link_path(custom_link)
+ end
+ end
+end