Skip to content
Draft
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
1 change: 1 addition & 0 deletions app/controllers/admin/edition_access_limited_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def edition_params
.fetch(:edition, {})
.permit(
:access_limited,
:access_limited_named_users,
:editorial_remark,
{
lead_organisation_ids: [],
Expand Down
1 change: 1 addition & 0 deletions app/controllers/admin/editions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def permitted_edition_attributes
:scheduled_publication,
:lock_version,
:access_limited,
:access_limited_named_users,
:alternative_format_provider_id,
:opening_at,
:closing_at,
Expand Down
85 changes: 81 additions & 4 deletions app/models/concerns/edition/limited_access.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
module Edition::LimitedAccess
extend ActiveSupport::Concern

class Trait < Edition::Traits::Trait
def process_associations_after_save(draft)
@edition.named_accesses.each do |na|
draft.named_accesses.create!(email: na.email)
end
end
end

included do
enum :access_limited, { disabled: 0, organisations: 1, named_users: 2 }
has_many :named_accesses, dependent: :destroy, inverse_of: :edition, autosave: true
after_initialize :set_access_limited
before_save :destroy_named_accesses_unless_named_users
before_save :ensure_creator_in_named_accesses, if: :named_users?
validate :validate_named_users_emails, if: -> { named_users? }
add_trait Trait
end

module ClassMethods
Expand All @@ -16,18 +30,81 @@ def access_limited_object
end

def access_limited?
self[:access_limited]
organisations? || named_users?
end

delegate :access_limited_by_default?, to: :class

def access_limited=(value)
@_access_limited_explicitly_set = true
super
end

def set_access_limited
if new_record? && access_limited.nil?
self.access_limited = access_limited_by_default?
end
return unless new_record?
return if @_access_limited_explicitly_set

self.access_limited = access_limited_by_default? ? :organisations : :disabled
@_access_limited_explicitly_set = false
end

def accessible_to?(user)
user.present? && Whitehall::Authority::Enforcer.new(user, self).can?(:see)
end

def access_limited_named_users=(value)
@access_limited_named_users_input = value
new_emails = parse_named_user_emails(value).map(&:downcase).uniq

existing = active_named_accesses.index_by { |na| na.email.downcase }

existing.each do |email, na|
na.mark_for_destruction unless new_emails.include?(email)
end

new_emails.each do |email|
named_accesses.build(email:) unless existing.key?(email)
end
end

def access_limited_named_users
@access_limited_named_users_input || active_named_accesses.map(&:email).join(", ")
end

private

def active_named_accesses
named_accesses.reject(&:marked_for_destruction?)
end

def parse_named_user_emails(value)
(value || "").split(/[\n,]/).map(&:strip).reject(&:blank?)
end

def validate_named_users_emails
if active_named_accesses.empty?
errors.add(:access_limited_named_users, "must include at least one email address")
end

active_named_accesses.select(&:new_record?).each do |na|
next if URI::MailTo::EMAIL_REGEXP.match?(na.email)

errors.add(:access_limited_named_users, "#{na.email} is not a valid email address")
end
end

def destroy_named_accesses_unless_named_users
return if named_users?

named_accesses.each(&:mark_for_destruction)
end

def ensure_creator_in_named_accesses
return if creator&.email.blank?

email = creator.email.downcase
return if active_named_accesses.any? { |na| na.email.casecmp?(email) }

named_accesses.build(email:)
end
end
8 changes: 8 additions & 0 deletions app/models/named_access.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class NamedAccess < ApplicationRecord
belongs_to :edition, inverse_of: :named_accesses

validates :email,
presence: true,
uniqueness: { scope: :edition_id, case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
end
2 changes: 1 addition & 1 deletion app/services/draft_edition_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def verb
private

def should_check_current_user_will_retain_access?
@options[:current_user].present? && edition.access_limited?
@options[:current_user].present? && edition.organisations?
end

def access_limit_excludes_current_user?
Expand Down
2 changes: 1 addition & 1 deletion app/services/edition_publisher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def verb

def prepare_edition
flag_if_political_content!
edition.access_limited = false
edition.access_limited = :disabled
edition.major_change_published_at = Time.zone.now unless edition.minor_change?
edition.make_public_at(edition.major_change_published_at)
edition.increment_version_number
Expand Down
45 changes: 32 additions & 13 deletions app/views/admin/editions/_access_limiting_fields.html.erb
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
<div class="govuk-!-margin-bottom-6">
<%= render "govuk_publishing_components/components/fieldset", {
legend_text: "Limit access",
heading_level: 3,
heading_size: "m",
} do %>
<%= form.hidden_field :access_limited, value: "0" %>

<%= render "govuk_publishing_components/components/checkboxes", {
<% named_users_value = edition.named_users? ? edition.access_limited_named_users.presence : nil %>
<% named_users_value ||= current_user.email %>
<% named_users_error = edition.errors[:access_limited_named_users].first %>
<%= render "govuk_publishing_components/components/radio", {
heading: "Limit access",
name: "edition[access_limited]",
id: "edition_access_limited",
error_items: errors_for(edition.errors, :access_limited),
items: [
{
label: "Limit access to publishers from organisations associated with this document before you publish",
value: 1,
checked: edition.access_limited,
value: :disabled,
text: "No – This document should be available to all publishers",
bold: true,
checked: edition.disabled?,
},
{
value: :organisations,
text: "Limit access to publishers from organisations associated with this document",
bold: true,
checked: edition.organisations?,
},
{
value: :named_users,
text: "Limit access to named publishers",
bold: true,
checked: edition.named_users?,
conditional: (render "govuk_publishing_components/components/textarea", {
label: {
text: "Add publishers who will have access",
bold: true,
},
name: "edition[access_limited_named_users]",
textarea_id: "edition_access_limited_named_users",
error_message: named_users_error,
value: named_users_value,
hint: "Add the emails of the publishers who will have access to this document before publishing. After publishing the document will be available to all publishers in the organisation associated with this document.",
}),
},
],
} %>
<% end %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class MigrateAccessLimitedToIntegerEnum < ActiveRecord::Migration[8.0]
def up
safety_assured do
change_column :editions, :access_limited, :integer, null: false, default: 0
end
end

def down
safety_assured do
change_column :editions, :access_limited, :boolean, null: false
end
end
end
11 changes: 11 additions & 0 deletions db/migrate/20260507120000_create_named_accesses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateNamedAccesses < ActiveRecord::Migration[8.0]
def change
create_table :named_accesses do |t|
t.references :edition, type: :integer, null: false, foreign_key: true
t.string :email, null: false
t.timestamps
end

add_index :named_accesses, %i[edition_id email], unique: true
end
end
14 changes: 12 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_05_03_161913) do
ActiveRecord::Schema[8.1].define(version: 2026_05_07_120000) do
create_table "assets", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "asset_manager_id", null: false
t.bigint "assetable_id"
Expand Down Expand Up @@ -327,6 +327,15 @@
t.index ["locale"], name: "index_edition_translations_on_locale"
end

create_table "named_accesses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.integer "edition_id", null: false
t.string "email", null: false
t.datetime "updated_at", null: false
t.index ["edition_id", "email"], name: "index_named_accesses_on_edition_id_and_email", unique: true
t.index ["edition_id"], name: "index_named_accesses_on_edition_id"
end

create_table "edition_world_locations", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.datetime "created_at", precision: nil
t.integer "edition_id"
Expand All @@ -347,7 +356,7 @@
end

create_table "editions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.boolean "access_limited", null: false
t.integer "access_limited", default: 0, null: false
t.string "additional_related_mainstream_content_title"
t.string "additional_related_mainstream_content_url"
t.boolean "all_nation_applicability", default: true
Expand Down Expand Up @@ -1228,6 +1237,7 @@

add_foreign_key "documents", "editions", column: "latest_edition_id", on_update: :cascade, on_delete: :nullify
add_foreign_key "documents", "editions", column: "live_edition_id", on_update: :cascade, on_delete: :nullify
add_foreign_key "named_accesses", "editions"
add_foreign_key "editions", "governments", on_delete: :nullify
add_foreign_key "link_checker_api_report_links", "link_checker_api_reports"
add_foreign_key "link_checker_api_reports", "editions"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@statistics_publication = create(
:publication,
:draft,
access_limited: false,
access_limited: :disabled,
publication_type_id: PublicationType::OfficialStatistics.id,
title:,
)
Expand Down
2 changes: 1 addition & 1 deletion features/step_definitions/most_recent_editions_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
current = Edition.find_by(title:).document.latest_edition
new_draft = current.create_draft(random_editor)
new_draft.organisations << org
new_draft.access_limited = true
new_draft.access_limited = :organisations
new_draft.change_note = "Limited to #{org.name}"
new_draft.save!
end
Expand Down
4 changes: 3 additions & 1 deletion lib/whitehall/authority/rules/edition_rules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ def can_with_a_historic_instance?(action)
end

def access_limit_enforced?
if subject.access_limited?
if subject.organisations?
organisations = subject.organisations
organisations += subject.edition_organisations.map(&:organisation) if subject.respond_to?(:edition_organisations)
organisations.exclude?(actor.organisation)
elsif subject.named_users?
subject.named_accesses.none? { |na| na.email.casecmp?(actor.email) }
else
false
end
Expand Down
2 changes: 1 addition & 1 deletion test/components/admin/editions/tags_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class Admin::Editions::TagsComponentTest < ViewComponent::TestCase
end

test "adds an access limited tag if edition has limited access" do
edition = build(:edition, access_limited: true)
edition = build(:edition, access_limited: 1)

expected_output = "<span class=\"govuk-tag govuk-tag--s govuk-tag--blue\">Draft</span> " \
"<span class=\"govuk-tag govuk-tag--s govuk-tag--red\">Limited access</span>"
Expand Down
2 changes: 1 addition & 1 deletion test/factories/editions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
scheduled_publication { 7.days.from_now }
end

trait(:access_limited) { access_limited { true } }
trait(:access_limited) { access_limited { :organisations } }

trait(:with_alternative_format_provider) do
association :alternative_format_provider, factory: :organisation_with_alternative_format_contact_email
Expand Down
6 changes: 6 additions & 0 deletions test/factories/named_accesses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :named_access do
association :edition
email { generate(:email) }
end
end
Loading