Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app/controllers/organisation_brands_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class OrganisationBrandsController < WebController
after_action :verify_authorized

def new
authorize organisation, :can_manage_organisation_brands?

@brand_input = Organisations::BrandInput.new(organisation:)
end

def create
authorize organisation, :can_manage_organisation_brands?

@brand_input = Organisations::BrandInput.new(brand_input_params)

if @brand_input.submit
redirect_to organisation_path(organisation), success: t(".success", brand_name: @brand_input.brand.name)
else
render :new, status: :unprocessable_content
end
end

def destroy
authorize organisation, :can_manage_organisation_brands?

organisation_brand = organisation.organisation_brands.find_by!(brand_id: params[:id])
organisation_brand.destroy!

redirect_to organisation_path(organisation), success: t(".success", brand_name: organisation_brand.brand.name)
end

private

def organisation
@organisation ||= Organisation.find(params[:organisation_id])
end

def brand_input_params
params.require(:organisations_brand_input).permit(:brand_id).merge(organisation:).tap do |p|
clear_param_if_autocomplete_empty(p, :brand_id, params.dig(:organisations_brand_input, :brand_id_raw))
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/organisations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def index
def show
authorize Organisation, :can_view_organisations?

@organisation = Organisation.includes(:organisation_domains, mou_signatures: :user).find(params[:id])
@organisation = Organisation.includes(:organisation_domains, :brands, mou_signatures: :user).find(params[:id])
end

private
Expand Down
25 changes: 4 additions & 21 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,17 @@ def filtered_users

def user_params
params.require(:user).permit(:has_access, :name, :role, :organisation_id).tap do |p|
# We have to take steps to detect when the autocomplete component is
# empty. We use the value of rawAttribute, which is the text input when JS
# is enabled. When it's empty, the user has cleared it. This isn't needed
# when the no JS select is used but we have to allow organisation_id to be
# nil still.
if p.key?(:organisation_id) && organisation_id_raw && organisation_id_raw.empty?
p[:organisation_id] = nil
end
clear_param_if_autocomplete_empty(p, :organisation_id, params.dig(:user, :organisation_id_raw))
end
end

def user
@user ||= User.find(params[:id])
end

def organisation_id_raw
params.dig(:user, :organisation_id_raw)
end

def filter_params
filters = params[:filter]&.permit(:name, :email, :organisation_id, :role, :has_access) || {}

# if the text in the organisation input has been cleared, don't use the last selected organisation_id
organisation_id_raw = params.dig(:filter, :organisation_id_raw)
if filters.key?(:organisation_id) && organisation_id_raw && organisation_id_raw.empty?
filters[:organisation_id] = nil
end

filters
params[:filter]&.permit(:name, :email, :organisation_id, :role, :has_access)&.tap do |filters|
clear_param_if_autocomplete_empty(filters, :organisation_id, params.dig(:filter, :organisation_id_raw))
end || {}
end
end
10 changes: 10 additions & 0 deletions app/controllers/web_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ def authenticate_user!

private

# We have to take steps to detect when the autocomplete component is empty.
# We use the value of rawAttribute, which is the text input when JS is
# enabled. When it's empty, the user has cleared it, so any previously
# selected value is stale and should be discarded. This isn't needed when
# the no JS select is used but we have to allow the attribute to be nil
# still.
def clear_param_if_autocomplete_empty(permitted_params, attribute, raw_value)
permitted_params[attribute] = nil if permitted_params.key?(attribute) && raw_value && raw_value.empty?
end

def authenticate_and_check_access
authenticate_user!

Expand Down
17 changes: 9 additions & 8 deletions app/input_objects/forms/brand_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ class Forms::BrandInput < BaseInput

attr_accessor :form, :brand_id

validates :brand_id, inclusion: { in: ->(_input) { Forms::BrandInput.allowed_brand_ids } }, allow_blank: true
validates :brand_id, inclusion: { in: ->(input) { input.allowed_brand_ids } }, allow_blank: true

class << self
def brands
Settings.branding.available_brands
def brands
@brands ||= begin
organisation = form.group&.organisation
organisation.nil? ? Brand.none : organisation.brands
end
end

def allowed_brand_ids
brands.map(&:id)
end
def allowed_brand_ids
brands.pluck(:slug)
end

def brand_options
default_option = BrandOption.new("", I18n.t("helpers.label.forms_brand_input.brand_id.options.default"))
[default_option] + self.class.brands.map { |brand| BrandOption.new(brand.id, brand.name) }
[default_option] + brands.map { |brand| BrandOption.new(brand.slug, brand.name) }
end

def submit
Expand Down
29 changes: 29 additions & 0 deletions app/input_objects/organisations/brand_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Organisations
class BrandInput < BaseInput
attr_accessor :organisation, :brand_id

validates :brand_id, presence: true
validate :brand_is_available, if: -> { brand_id.present? }

def submit
return false if invalid?

organisation.organisation_brands.create!(brand:)
true
end

def brand
@brand ||= Brand.find_by(id: brand_id)
end

def available_brands
Brand.where.not(id: organisation.brands.select(:id)).order(:name)
end

private

def brand_is_available
errors.add(:brand_id, :inclusion) unless available_brands.exists?(id: brand_id)
end
end
end
2 changes: 2 additions & 0 deletions app/models/brand.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Brand < ApplicationRecord
has_many :organisation_brands, dependent: :destroy

validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/, allow_blank: true }
validates :name, presence: true
end
3 changes: 3 additions & 0 deletions app/models/organisation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class Organisation < ApplicationRecord
has_many :mou_signatures
has_many :organisation_domains, dependent: :destroy

has_many :organisation_brands, dependent: :destroy
has_many :brands, -> { order(:name) }, through: :organisation_brands

scope :not_closed, -> { where(closed: false) }
scope :with_users, -> { joins(:users).distinct.order(:name) }

Expand Down
6 changes: 6 additions & 0 deletions app/models/organisation_brand.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class OrganisationBrand < ApplicationRecord
belongs_to :organisation
belongs_to :brand

validates :brand_id, uniqueness: { scope: :organisation_id }
end
4 changes: 4 additions & 0 deletions app/policies/organisation_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ class OrganisationPolicy < ApplicationPolicy
def can_view_organisations?
user.super_admin?
end

def can_manage_organisation_brands?
user.super_admin?
end
end
31 changes: 31 additions & 0 deletions app/views/organisation_brands/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<% set_page_title(title_with_error_prefix(t(".title"), @brand_input.errors.any?)) %>
<% content_for :back_link, govuk_back_link_to(organisation_path(@brand_input.organisation)) %>

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= form_with(model: @brand_input, url: organisation_brands_path(@brand_input.organisation)) do |f| %>
<% if @brand_input.errors.any? %>
<%= f.govuk_error_summary %>
<% end %>

<h1 id="heading" class="govuk-heading-l">
<span class="govuk-caption-l"><%= @brand_input.organisation.name_with_abbreviation %></span>
<span class="govuk-visually-hidden"> - </span>
<%= t(".title") %>
</h1>

<%= render DfE::Autocomplete::View.new(
f,
attribute_name: :brand_id,
form_field: f.govuk_collection_select(:brand_id, @brand_input.available_brands, :id, :name,
class: ['govuk-!-width-three-quarters'],
options: { prompt: t('.prompt') },
label: { text: t('.label'), size: 'm' })
) %>

<%= f.govuk_submit t(".submit") %>
<% end %>
</div>
</div>

<%= init_autocomplete_script(show_all_values: false, raw_attribute: true, source: false) %>
30 changes: 30 additions & 0 deletions app/views/organisations/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,35 @@
<% else %>
<p class="govuk-body"><%= t("organisations.show.domains.none") %></p>
<% end %>

<h2 class="govuk-heading-m"><%= t("organisations.show.brands.heading") %></h2>
<p class="govuk-body"><%= t("organisations.show.brands.guidance") %></p>
<% if @organisation.brands.any? %>
<%= render ScrollingWrapperComponent::View.new(aria_label: t("organisations.show.brands.heading")) do %>
<%= govuk_table do |table| %>
<%= table.with_head do |head|
head.with_row do |row|
row.with_cell(header: true, text: t("organisations.show.brands.table_headings.name"))
row.with_cell(header: true, text: t("organisations.show.brands.table_headings.actions"))
end
end %>

<%= table.with_body do |body| %>
<% @organisation.brands.each do |brand| %>
<%= body.with_row do |row| %>
<%= row.with_cell { govuk_link_to(brand.name, brand_path(brand)) } %>
<%= row.with_cell do %>
<%= govuk_button_to(t("organisations.show.brands.remove"), organisation_brand_path(@organisation, brand), method: :delete, secondary: true) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% else %>
<p class="govuk-body"><%= t("organisations.show.brands.none") %></p>
<% end %>

<%= govuk_button_link_to t("organisations.show.brands.add"), new_organisation_brand_path(@organisation), secondary: true %>
</div>
</div>
19 changes: 19 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,16 @@ en:

If you pasted the web address, check you copied the entire address.
title: Page not found
organisation_brands:
create:
success: "%{brand_name} has been added to this organisation’s brands"
destroy:
success: "%{brand_name} has been removed from this organisation’s brands"
new:
label: Brand
prompt: Select a brand
submit: Add brand
title: Add a brand
organisations:
boolean:
'false': 'No'
Expand Down Expand Up @@ -1531,6 +1541,15 @@ en:
admin_users:
heading: Organisation admins
none: This organisation does not have any organisation admins.
brands:
add: Add a brand
guidance: Add brands to make them available for forms in this organisation to use. Forms that already use a brand will continue to use it, even if you remove the brand from this organisation.
heading: Available brands
none: This organisation does not have any available brands. Forms use the default GOV.UK branding.
remove: Remove
table_headings:
actions: Actions
name: Name
domains:
heading: Email domains
none: This organisation does not have any email domains.
Expand Down
10 changes: 10 additions & 0 deletions config/locales/input_objects/organisation_brand.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
en:
activemodel:
errors:
models:
organisations/brand_input:
attributes:
brand_id:
blank: Select a brand
inclusion: Select a brand from the list of available brands
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@

resources :mou_signatures, only: %i[index], path: "mous"

resources :organisations, only: %i[index show]
resources :organisations, only: %i[index show] do
resources :brands, controller: :organisation_brands, only: %i[new create destroy]
end

resources :brands, only: %i[index show new create edit update]

Expand Down
9 changes: 0 additions & 9 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,6 @@ features:
send_filler_answers:
enabled_by_group: true

# Brands that a form can be styled with in forms-runner. A form with no
# brand_id uses the default GOV.UK branding.
branding:
available_brands:
- id: cheshire-east
name: Cheshire East Council
- id: south-gloucestershire
name: South Gloucestershire Council

forms_api:
# Authentication key to authenticate with forms-api
auth_key: development_key
Expand Down
11 changes: 11 additions & 0 deletions db/migrate/20260714090000_create_organisation_brands.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateOrganisationBrands < ActiveRecord::Migration[8.1]
def change
create_table :organisation_brands do |t|
t.references :organisation, null: false, foreign_key: true
t.references :brand, null: false, foreign_key: true
t.timestamps
end

add_index :organisation_brands, %i[organisation_id brand_id], unique: true, name: "index_organisation_brands_unique"
end
end
14 changes: 13 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
gds.organisation_domains.create! domain: "digital.cabinet-office.gov.uk"
gds.organisation_domains.create! domain: "dsit.gov.uk"
gds.brands = Brand.all

# Create default super-admin
default_user = User.create!({ email: "example@example.com",
Expand Down
Loading