-
Notifications
You must be signed in to change notification settings - Fork 197
Expand file tree
/
Copy pathedition.rb
More file actions
505 lines (395 loc) · 14.9 KB
/
edition.rb
File metadata and controls
505 lines (395 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
class Edition < ApplicationRecord
include Edition::Traits
include Edition::NullImages
include Edition::NullWorldLocations
include Edition::NullAttachables
include Edition::BasePermissionMethods
include Edition::Identifiable
include Edition::LimitedAccess
# Adds a statemachine for the publishing workflow. States and methods like
# `publish` and `withdraw` are defined here.
include Edition::Workflow
# Adds support for `unpublishing`, change notes and version numbers.
include Edition::Publishing
include AuditTrail
include Edition::ActiveEditors
include Edition::Translatable
include Edition::Scopes::Orderable
include Edition::Scopes::SearchableByTitle
include Edition::Scopes::FilterableByAuthor
include Edition::Scopes::FilterableByInvalid
include Edition::Scopes::FilterableByBrokenLinks
include Edition::Scopes::FilterableByDate
include Edition::Scopes::FilterableByTopicalEvent # legacy
include Edition::Scopes::FilterableByType
include Edition::Scopes::FilterableByWorldLocation
include Edition::Scopes::FindableByOrganisation
include Edition::Scopes::FilterableByDocumentLink
include Edition::Scopes::FilterableByFeature
include Dependable
include DateValidation
date_attributes :scheduled_publication, :first_published_at, :delivered_on, :opening_at, :closing_at
has_many :editorial_remarks, dependent: :destroy
has_many :edition_authors, dependent: :destroy
has_many :authors, through: :edition_authors, source: :user
has_many :topical_event_featurings, inverse_of: :edition # legacy
has_one :link_check_report, class_name: "LinkCheckerApiReport", dependent: :destroy
has_many :edition_dependencies, dependent: :destroy
has_many :depended_upon_contacts, through: :edition_dependencies, source: :dependable, source_type: "Contact"
has_many :depended_upon_editions, through: :edition_dependencies, source: :dependable, source_type: "Edition"
# Add validation rules on legacy Edition content types, but opt out of them on StandardEdition
# types whose validation rules are configured via JSON and applied via StandardEdition::BlockContent.
validates_with SafeHtmlValidator, unless: ->(record) { record.is_a?(StandardEdition) }
validates_with NoFootnotesInGovspeakValidator, attribute: :body, unless: ->(record) { record.is_a?(StandardEdition) }
validates_with LinkCheckReportValidator, on: :publish # TODO: make this a configurable StandardEdition property
validates_with InternalPathLinksValidator, attribute: :body, on: :publish # TODO: make this a configurable StandardEdition property
validates_with GovspeakContactEmbedValidator, attribute: :body, on: :publish, unless: ->(record) { record.is_a?(StandardEdition) }
validates_with TaxonValidator, on: :publish, if: :requires_taxon?
validates :creator, presence: true
validates :title, presence: true, if: :title_required?, length: { maximum: 255 }
validates :body, presence: true, if: :body_required?, length: { maximum: 16_777_215 }, unless: ->(record) { record.is_a?(StandardEdition) }
validates :summary, presence: true, if: :summary_required?, length: { maximum: 65_535 }
validates :previously_published, inclusion: { in: [true, false], message: "You must specify whether the document has been published before" }
validates :first_published_at, presence: true, if: -> { previously_published || published_major_version }
validates :first_published_at, inclusion: { in: proc { Date.parse("1900-01-01")..Time.zone.now } }, if: :draft?, allow_blank: true
validates :scheduled_publication, inclusion: { in: proc { Time.zone.now.. }, message: "must be in the future" }, if: :draft?, allow_blank: true
validates :political, inclusion: { in: [true, false] }
validates :image_display_option, inclusion: { in: ["no_image", "organisation_image", "custom_image", nil] }
UNMODIFIABLE_STATES = %w[scheduled published superseded deleted unpublished].freeze
FROZEN_STATES = %w[superseded deleted].freeze
PRE_PUBLICATION_STATES = %w[draft submitted rejected scheduled].freeze
POST_PUBLICATION_STATES = %w[published superseded withdrawn unpublished].freeze
PUBLICLY_VISIBLE_STATES = %w[published withdrawn].freeze
before_create :set_auth_bypass_id
before_save :set_public_timestamp
after_validation :update_revalidated_at, if: -> { validation_context == :publish }
after_create :update_document_edition_references
after_update :update_document_edition_references, if: :saved_change_to_state?
after_update :republish_topical_event_to_publishing_api # legacy
after_update :republish_featurable_to_publishing_api, if: :saved_change_to_state?
accepts_nested_attributes_for :document
validates_with UnmodifiableValidator, if: :unmodifiable?
def self.format_name
@format_name ||= model_name.human.downcase
end
# TODO: Either retain this method or refactor other featurables to send 'title' to offsite link views
# See app/views/admin/offsite_links/new.html.erb#L2 for example
def name
title
end
def self.concrete_descendants
descendants.reject { |model| model.descendants.any? }.sort_by(&:name)
end
def self.enforcer(user)
Whitehall::Authority::Enforcer.new(user, self)
end
def skip_main_validation?
FROZEN_STATES.include?(state)
end
def update_revalidated_at
new_value = errors.empty? ? Time.current : nil
if persisted?
update_column(:revalidated_at, new_value)
else
self.revalidated_at = new_value
end
end
def unmodifiable?
persisted? && UNMODIFIABLE_STATES.include?(state_was)
end
def self.publicly_visible_and_available_in_english
with_translations(:en).publicly_visible
end
def creator
edition_authors.first&.user
end
def creator=(user)
if new_record?
edition_author = edition_authors.first || edition_authors.build
edition_author.user = user
else
raise "author can only be set on new records"
end
end
def publicly_visible?
PUBLICLY_VISIBLE_STATES.include?(state)
end
def versioning_completed?
return true unless change_note_required?
change_note.present? || minor_change
end
def can_be_marked_political?
true
end
def path_name
to_model.class.name.underscore
end
def has_been_tagged?
api_response = Services.publishing_api.get_expanded_links(content_id, with_drafts: false)
return false if api_response["expanded_links"].nil? || api_response["expanded_links"]["taxons"].nil?
api_response["expanded_links"]["taxons"].any?
rescue GdsApi::HTTPNotFound
false
end
def create_draft(user, allow_creating_draft_from_deleted_edition: false)
ActiveRecord::Base.transaction do
lock!
if allow_creating_draft_from_deleted_edition
raise "Edition not in the deleted state" unless deleted?
elsif !can_supersede?
raise "Cannot create new edition based on edition in the #{state} state"
end
ignorable_attribute_keys = %w[id
type
state
created_at
updated_at
change_note
minor_change
force_published
scheduled_publication]
draft_attributes = attributes.except(*ignorable_attribute_keys)
.merge("state" => "draft", "creator" => user, "previously_published" => previously_published)
self.class.new(draft_attributes).tap do |draft|
traits.each { |t| t.process_associations_before_save(draft) }
if (draft.valid? || !draft.errors.key?(:base)) && draft.save(validate: false)
traits.each { |t| t.process_associations_after_save(draft) }
end
end
end
end
def author_names
edition_authors.map(&:user).map(&:name).uniq
end
def image_disallowed_in_body_text?(_index)
false
end
def rejected_by
author_of_latest_state_change_to("rejected")
end
def published_by
versions_desc.where(state: "published").first.try(:user)
end
def scheduled_by
versions_desc.where(state: "scheduled").first.try(:user)
end
def submitted_by
author_of_latest_state_change_to("submitted")
end
def title_with_state
"#{title} (#{state})"
end
def body_without_markup
Govspeak::Document.new(body).to_text
end
def other_editions
if persisted?
document.editions.where(self.class.arel_table[:id].not_eq(id))
else
document.editions
end
end
def previous_edition
document.ever_published_editions.where.not(id:).last
end
def is_latest_edition?
document.latest_edition == self
end
def all_nation_applicability_selected?
true
end
def most_recent_change_note
if minor_change?
previous_major_version = Edition.unscoped.where("document_id=? and published_major_version=? and published_minor_version=0", document_id, published_major_version)
previous_major_version.first.change_note if previous_major_version.any?
else
change_note unless first_published_version?
end
end
delegate :format_name, to: :class
def new_content_warning; end
def display_type
I18n.t("document.type.#{display_type_key}", count: 1)
end
def display_type_key
format_name.tr(" ", "_")
end
def first_public_at
first_published_at
end
def make_public_at(date)
self.first_published_at ||= date
end
def alternative_format_contact_email
nil
end
def editable?
draft? || submitted? || rejected?
end
def can_have_some_invalid_data?
deleted? || superseded?
end
def set_public_timestamp
self.public_timestamp = if first_published_version?
first_public_at
else
major_change_published_at
end
end
def set_auth_bypass_id
self.auth_bypass_id = SecureRandom.uuid
end
def title_required?
true
end
def summary_required?
true
end
def body_required?
true
end
def requires_taxon?
true
end
# 'previously_published' is a transient attribute populated
# by request parameters, and because it's not persisted it's
# not converted to a boolean, hence this manual attr writer method.
# NOTE: This method isn't called when the user fails to select an
# option for this field and so the value remains nil.
def previously_published=(value)
@previously_published = value.to_s == "true"
end
def previously_published
return first_published_at.present? unless new_record?
@previously_published
end
def can_set_previously_published?
true
end
def superseded_at
versions.find { |v| v.state == "superseded" }&.created_at
end
def published_at
versions.find { |v| v.state == "published" }&.created_at
end
def government
if government_id.present?
Government.find(government_id)
elsif date_for_government.present?
Government.on_date(date_for_government)
end
end
def historic?
return false unless government
political? && !government.current?
end
def withdrawn?
state == "withdrawn"
end
def content_store_document_type
PublishingApiPresenters.presenter_for(self).document_type
end
def has_legacy_tags?
has_primary_sector? || has_secondary_sectors?
end
# For more info, see https://docs.publishing.service.gov.uk/manual/content-preview.html#authentication
def auth_bypass_token
JWT.encode(
{
"sub" => auth_bypass_id,
"content_id" => content_id,
"iat" => Time.zone.now.to_i,
"exp" => 1.month.from_now.to_i,
},
Rails.application.credentials.jwt_auth_secret,
"HS256",
)
end
def has_enabled_shareable_preview?
PRE_PUBLICATION_STATES.include?(state)
end
# TODO: this can be removed once rails/rails#44770 is released.
def attribute_names
@attributes.keys
end
def base_path
url_slug = slug || document_id.to_param
"/government/generic-editions/#{url_slug}"
end
def public_path(options = {})
return if base_path.nil?
options[:locale] ||= primary_locale
append_url_options(base_path, options)
end
def public_url(options = {})
return if base_path.nil?
website_root = if options[:draft]
Plek.external_url_for("draft-origin")
else
Plek.website_root
end
website_root + public_path(options)
end
def force_scheduled?
force_published? && state == "scheduled"
end
def images_have_unique_filenames?
names = images.map(&:filename)
names.uniq.length == names.length
end
def associated_documents
[]
end
def deleted_associated_documents
[]
end
def error_labels
{}
end
def access_limited_named_users=(users)
return unless Flipflop.enabled?(:access_limited_named_users)
user_emails = users.split(",").map(&:strip).reject(&:empty?)
user_emails.each do |email|
edition_user_accesses.create!(email: email)
end
end
private
def date_for_government
published_edition_date = first_public_at.try(:to_date)
draft_edition_date = updated_at.try(:to_date)
published_edition_date || draft_edition_date
end
# legacy
def republish_topical_event_to_publishing_api
topical_event_featurings.each do |topical_event_featuring|
Whitehall::PublishingApi.republish_async(topical_event_featuring.topical_event)
end
end
def republish_featurable_to_publishing_api
active_features_referencing_this_document = document.features.where(ended_at: nil)
active_features_referencing_this_document.each(&:republish_to_publishing_api_async)
end
def update_document_edition_references
document.update_edition_references
end
def author_of_latest_state_change_to(state)
# Find this edition's most recent state change to the state passed in.
# This will tell us when it was most recent transition to this state,
# even if there were subsequent changes from other users while the
# document remained in a the same state. Then return it's user.
latest_version_with_state = versions_desc.select("created_at, id")
.where(state:)
.limit(1)
previous_version_with_different_state = versions_desc.select("created_at, id")
.where.not(state:)
.where("(created_at, id) < (:latest_version_with_state)", latest_version_with_state:)
.limit(1)
if latest_version_with_state.present? && previous_version_with_different_state.blank?
return versions_asc.where(state:).limit(1).first.try(:user)
end
first_version_with_state = versions_asc.where(state:)
.where("(created_at, id) > (:previous_version_with_different_state)", previous_version_with_different_state:)
.first
first_version_with_state.try(:user)
end
end