diff --git a/README.md b/README.md index f36dc98e1..e6df52a9b 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,9 @@ for more information. Events older then a month are archived to S3, you can import these events back into your local DB by running the rake tasks in lib/tasks/events.rake, after you set up the relavent ENV variables. For example if you want to find all the -events that are relavant for a particular content item you can run: +events that are relavant for a particular content_id you can run: ```sh -rake 'events:import_content_item_events[a796ca43-021b-4960-9c99-f41bb8ef2266]' +rake 'events:import_content_id_events[a796ca43-021b-4960-9c99-f41bb8ef2266]' ``` see the rake task for more details. diff --git a/app/commands/base_command.rb b/app/commands/base_command.rb index ced0d7fcc..6be914060 100644 --- a/app/commands/base_command.rb +++ b/app/commands/base_command.rb @@ -56,15 +56,16 @@ def self.raise_validation_command_error(e) end private_class_method :execute_callbacks, :raise_validation_command_error - def check_version_and_raise_if_conflicting(current_versioned_item, previous_version_number) - current_version = LockVersion.find_by(target: current_versioned_item) + def check_version_and_raise_if_conflicting(current_versioned_item, previous_version) + return unless current_versioned_item && previous_version.present? - return unless current_versioned_item && current_version + current_version = current_versioned_item.stale_lock_version - if current_version.conflicts_with?(previous_version_number) - friendly_message = "A lock-version conflict occurred. The `previous_version` you've sent " + - "(#{previous_version_number}) is not the same as the current " + - "lock version of the content item (#{current_version.number})." + if current_version != previous_version.to_i + friendly_message = "A lock-version conflict occurred. The " + + "`previous_version` you've sent (#{previous_version}) is not the " + + "same as the current lock version of the edition " + + "(#{current_version})." fields = { fields: { @@ -74,6 +75,7 @@ def check_version_and_raise_if_conflicting(current_versioned_item, previous_vers raise_command_error(409, "Conflict", fields, friendly_message: friendly_message) end + current_version end diff --git a/app/commands/v2/discard_draft.rb b/app/commands/v2/discard_draft.rb index a32cfe8cd..183862c6b 100644 --- a/app/commands/v2/discard_draft.rb +++ b/app/commands/v2/discard_draft.rb @@ -4,33 +4,37 @@ class DiscardDraft < BaseCommand def call raise_error_if_missing_draft! - check_version_and_raise_if_conflicting(draft, payload[:previous_version]) + check_version_and_raise_if_conflicting(document, payload[:previous_version]) - delete_supporting_objects + delete_supporting_objects(document.draft) delete_draft_from_database - increment_live_lock_version if live + increment_lock_version after_transaction_commit do - downstream_discard_draft(draft.base_path, draft.content_id, locale) + downstream_discard_draft( + document.draft.base_path, + document.content_id, + document.locale + ) end - Action.create_discard_draft_action(draft, locale, event) - Success.new(content_id: content_id) + Action.create_discard_draft_action(document.draft, document.locale, event) + Success.new(content_id: document.content_id) end private def raise_error_if_missing_draft! - return if draft.present? + return if document.draft.present? - code = live.present? ? 422 : 404 - message = "There is no draft content item to discard" + code = document.published_or_unpublished.present? ? 422 : 404 + message = "There is not a draft edition of this document to discard" raise CommandError.new(code: code, message: message) end def delete_draft_from_database - draft.destroy + document.draft.destroy end def downstream_discard_draft(path_used, content_id, locale) @@ -46,43 +50,26 @@ def downstream_discard_draft(path_used, content_id, locale) ) end - def delete_supporting_objects - Location.find_by(content_item: draft).try(:destroy) - State.find_by(content_item: draft).try(:destroy) - Translation.find_by(content_item: draft).try(:destroy) - UserFacingVersion.find_by(content_item: draft).try(:destroy) - LockVersion.find_by(target: draft).try(:destroy) - AccessLimit.find_by(content_item: draft).try(:destroy) - ChangeNote.where(content_item: draft).destroy_all + def delete_supporting_objects(edition) + Location.find_by(edition: edition).try(:destroy) + State.find_by(edition: edition).try(:destroy) + Translation.find_by(edition: edition).try(:destroy) + UserFacingVersion.find_by(edition: edition).try(:destroy) + LockVersion.find_by(target: edition).try(:destroy) + AccessLimit.find_by(edition: edition).try(:destroy) + ChangeNote.where(edition: edition).destroy_all end - def increment_live_lock_version - LockVersion.find_by!(target: live).increment! + def increment_lock_version + document.increment!(:stale_lock_version) end - def draft - @draft ||= ContentItem.find_by( - content_id: content_id, - locale: locale, - state: "draft", + def document + @document ||= Document.find_or_create_locked( + content_id: payload[:content_id], + locale: payload.fetch(:locale, Edition::DEFAULT_LOCALE), ) end - - def live - @live ||= ContentItem.find_by( - content_id: content_id, - locale: locale, - state: %w(published unpublished), - ) - end - - def content_id - payload[:content_id] - end - - def locale - payload[:locale] || ContentItem::DEFAULT_LOCALE - end end end end diff --git a/app/commands/v2/patch_link_set.rb b/app/commands/v2/patch_link_set.rb index caef93aea..4399f000f 100644 --- a/app/commands/v2/patch_link_set.rb +++ b/app/commands/v2/patch_link_set.rb @@ -4,10 +4,11 @@ class PatchLinkSet < BaseCommand def call raise_unless_links_hash_is_provided validate_schema - link_set = find_or_create_link_set(content_id) + link_set = LinkSet.find_or_create_locked(content_id: content_id) check_version_and_raise_if_conflicting(link_set, previous_version_number) - LockVersion.find_or_create_by!(target: link_set).increment! + + link_set.increment!(:stale_lock_version) grouped_links.each do |group, payload_content_ids| # For each set of links in a LinkSet scoped by link_type, this iterator @@ -32,23 +33,6 @@ def call private - def find_or_create_link_set(content_id) - begin - retries ||= 0 - LinkSet.transaction(requires_new: true) do - LinkSet.find_or_create_by!(content_id: content_id).lock! - end - rescue ActiveRecord::RecordNotUnique - # This should never need more than 1 retry as the scenario this error - # would occur is: inbetween rails find_or_create SELECT & INSERT - # queries a concurrent request ran an INSERT. Thus on retry the - # SELECT would succeed. - # So if this actually throws an exception here we probably have a - # weird underlying problem. - retry if (retries += 1) == 1 - end - end - def content_id payload.fetch(:content_id) end @@ -82,10 +66,10 @@ def raise_unless_links_hash_is_provided def send_downstream return unless downstream - draft_locales = Queries::LocalesForContentItems.call([content_id]) + draft_locales = Queries::LocalesForEditions.call([content_id], %w[draft live]) draft_locales.each { |(content_id, locale)| downstream_draft(content_id, locale) } - live_locales = Queries::LocalesForContentItems.call([content_id], %w[published unpublished]) + live_locales = Queries::LocalesForEditions.call([content_id], %w[live]) live_locales.each { |(content_id, locale)| downstream_live(content_id, locale) } end @@ -133,7 +117,7 @@ def schema_validator def schema_name @schema_name ||= Queries::GetLatest.( - ContentItem.where(content_id: payload[:content_id]) + Edition.with_document.where("documents.content_id": content_id) ).pluck(:schema_name).first end end diff --git a/app/commands/v2/post_action.rb b/app/commands/v2/post_action.rb index 41fa9f8df..53302a543 100644 --- a/app/commands/v2/post_action.rb +++ b/app/commands/v2/post_action.rb @@ -2,59 +2,51 @@ module Commands module V2 class PostAction < BaseCommand def call - check_version_and_raise_if_conflicting(content_item, previous_version_number) + check_version_and_raise_if_conflicting(document, previous_version_number) Action.create!( - content_id: content_id, - locale: locale, + content_id: document.content_id, + locale: document.locale, action: action_type, - content_item: content_item, + edition: edition, user_uid: event.user_uid, event: event, ) Success.new( - { content_id: content_id, locale: locale, action: action_type }, + { content_id: document.content_id, locale: document.locale, action: action_type }, code: 201, ) end private - def content_id - payload.fetch(:content_id) - end - - def locale - payload.fetch(:locale, ContentItem::DEFAULT_LOCALE) + def document + @document ||= Document.find_or_create_locked( + content_id: payload.fetch(:content_id), + locale: payload.fetch(:locale, Edition::DEFAULT_LOCALE), + ) end def draft? payload[:draft].nil? ? true : payload[:draft] end - def content_item - @content_item ||= find_content_item + def edition + edition = draft? ? document.draft : document.published_or_unpublished + + unless edition + message = "Could not find an edition to associate this action with" + raise_command_error(404, message, fields: {}) + end + + edition end def action_type payload[:action] end - def find_content_item - content_item = ContentItem.find_by( - content_id: content_id, - locale: locale, - state: draft? ? %w(draft) : %w(published unpublished), - ) - - unless content_item - message = "Could not find a content item to associate this action with" - raise_command_error(404, message, fields: {}) - end - content_item - end - def previous_version_number payload[:previous_version].to_i if payload[:previous_version] end diff --git a/app/commands/v2/publish.rb b/app/commands/v2/publish.rb index 5bf20df70..052efa97f 100644 --- a/app/commands/v2/publish.rb +++ b/app/commands/v2/publish.rb @@ -4,10 +4,10 @@ class Publish < BaseCommand def call validate - publish_content_item + publish_edition after_transaction_commit do - send_downstream(content_item.content_id, content_item.locale, update_type) + send_downstream(document.content_id, document.locale, update_type) end Success.new(content_id: content_id) @@ -15,56 +15,60 @@ def call private - def publish_content_item + def publish_edition delete_change_notes unless update_type == "major" previous_item.supersede if previous_item - unless content_item.pathless? + unless edition.pathless? redirect_old_base_path clear_published_items_of_same_locale_and_base_path end set_public_updated_at set_first_published_at - content_item.publish + edition.publish remove_access_limit create_publish_action end def create_publish_action - Action.create_publish_action(content_item, locale, event) + Action.create_publish_action(edition, document.locale, event) end def remove_access_limit - AccessLimit.find_by(content_item: content_item).try(:destroy) + AccessLimit.find_by(edition: edition).try(:destroy) end def validate - no_draft_item_exists unless content_item + no_draft_item_exists unless edition validate_update_type - check_version_and_raise_if_conflicting(content_item, previous_version_number) + check_version_and_raise_if_conflicting(document, previous_version_number) end def update_type - @update_type ||= payload[:update_type] || content_item.update_type + @update_type ||= payload[:update_type] || edition.update_type + end + + def edition + document.draft end def previous_item - @previous_item ||= lookup_previous_item + document.published_or_unpublished end def redirect_old_base_path return unless previous_item previous_base_path = previous_item.base_path - if previous_base_path != content_item.base_path - publish_redirect(previous_base_path, content_item.locale) + if previous_base_path != edition.base_path + publish_redirect(previous_base_path, document.locale) end end def no_draft_item_exists if already_published? - message = "Cannot publish an already published content item" + message = "Cannot publish an already published edition" raise_command_error(400, message, fields: {}) else message = "Item with content_id #{content_id} and locale #{locale} does not exist" @@ -85,15 +89,22 @@ def validate_update_type end def delete_change_notes - ChangeNote.where(content_item: content_item).delete_all + ChangeNote.where(edition: edition).delete_all + end + + def document + @document ||= Document.find_or_create_locked( + content_id: payload[:content_id], + locale: payload.fetch(:locale, Edition::DEFAULT_LOCALE), + ) end def content_id - payload[:content_id] + document.content_id end def locale - payload.fetch(:locale, ContentItem::DEFAULT_LOCALE) + document.locale end def previous_version_number @@ -104,28 +115,17 @@ def valid_update_types %w(major minor republish links) end - def content_item - @content_item ||= ContentItem.find_by( - id: pessimistic_content_item_scope.pluck(:id), - state: "draft", - ) - end - def already_published? - ContentItem.exists?(content_id: content_id, locale: locale, state: "published") - end - - def pessimistic_content_item_scope - ContentItem.where(content_id: content_id, locale: locale).lock + document.editions.exists?(state: "published") end def clear_published_items_of_same_locale_and_base_path SubstitutionHelper.clear!( - new_item_document_type: content_item.document_type, - new_item_content_id: content_item.content_id, + new_item_document_type: edition.document_type, + new_item_content_id: document.content_id, state: "published", - locale: content_item.locale, - base_path: content_item.base_path, + locale: document.locale, + base_path: edition.base_path, downstream: downstream, callbacks: callbacks, nested: true, @@ -133,32 +133,32 @@ def clear_published_items_of_same_locale_and_base_path end def set_public_updated_at - return if content_item.public_updated_at.present? + return if edition.public_updated_at.present? if update_type == "major" - content_item.update_attributes!(public_updated_at: Time.zone.now) + edition.update_attributes!(public_updated_at: Time.zone.now) elsif update_type == "minor" - content_item.update_attributes!(public_updated_at: previous_item.public_updated_at) + edition.update_attributes!(public_updated_at: previous_item.public_updated_at) end end def set_first_published_at - return if content_item.first_published_at.present? - content_item.update_attributes!(first_published_at: Time.zone.now) + return if edition.first_published_at.present? + edition.update_attributes!(first_published_at: Time.zone.now) end def publish_redirect(previous_base_path, locale) - draft_redirect = ContentItem.find_by( + draft_redirect = Edition.with_document.find_by( state: "draft", - locale: locale, + "documents.locale": locale, base_path: previous_base_path, schema_name: "redirect", ) self.class.call( { - content_id: draft_redirect.content_id, - locale: locale, + content_id: draft_redirect.document.content_id, + locale: draft_redirect.document.locale, update_type: "major", }, downstream: downstream, @@ -167,20 +167,6 @@ def publish_redirect(previous_base_path, locale) ) if draft_redirect end - def lookup_previous_item - previous_items = ContentItem.where( - content_id: content_id, - locale: locale, - state: %w(published unpublished), - ) - - if previous_items.size > 1 - raise "There should only be one previous published or unpublished item" - end - - previous_items.order("content_id").first - end - def send_downstream(content_id, locale, update_type) return unless downstream diff --git a/app/commands/v2/put_content.rb b/app/commands/v2/put_content.rb index 49ed1730f..0f2c6ff3e 100644 --- a/app/commands/v2/put_content.rb +++ b/app/commands/v2/put_content.rb @@ -5,14 +5,21 @@ def call PutContentValidator.new(payload, self).validate prepare_content_with_base_path - content_item = create_or_update_content_item - update_content_dependencies(content_item) + edition = create_or_update_edition + update_content_dependencies(edition) after_transaction_commit do - send_downstream(content_item.content_id, locale) + send_downstream(document.content_id, document.locale) end - Success.new(present_response(content_item)) + Success.new(present_response(edition)) + end + + def document + @document ||= Document.find_or_create_locked( + content_id: payload.fetch(:content_id), + locale: payload.fetch(:locale, Edition::DEFAULT_LOCALE), + ) end private @@ -27,12 +34,12 @@ def prepare_content_with_base_path clear_draft_items_of_same_locale_and_base_path end - def update_content_dependencies(content_item) + def update_content_dependencies(edition) create_redirect - access_limit(content_item) - update_last_edited_at(content_item, payload[:last_edited_at]) - ChangeNote.create_from_content_item(payload, content_item) - Action.create_put_content_action(content_item, locale, event) + access_limit(edition) + update_last_edited_at(edition, payload[:last_edited_at]) + ChangeNote.create_from_edition(payload, edition) + Action.create_put_content_action(edition, document.locale, event) end def create_redirect @@ -42,56 +49,52 @@ def create_redirect payload, callbacks).create end - - def present_response(content_item) + def present_response(edition) Presenters::Queries::ContentItemPresenter.present( - content_item, + edition, include_warnings: true, ) end - def access_limit(content_item) + def access_limit(edition) if payload[:access_limited] && (users = payload[:access_limited][:users]) - AccessLimit.find_or_create_by(content_item: content_item).tap do |access_limit| + AccessLimit.find_or_create_by(edition: edition).tap do |access_limit| access_limit.update_attributes!(users: users) end else - AccessLimit.find_by(content_item: content_item).try(:destroy) + AccessLimit.find_by(edition: edition).try(:destroy) end end - def create_or_update_content_item + def create_or_update_edition if previously_drafted_item - updated_item, @previous_item = UpdateExistingDraftContentItem.new(previously_drafted_item, self, payload).call + updated_item, @previous_item = UpdateExistingDraftEdition.new(previously_drafted_item, self, payload).call else - new_draft_content_item = CreateDraftContentItem.new(self, payload, previously_published_item).call + new_draft_edition = CreateDraftEdition.new(self, payload, previously_published_item).call end - updated_item || new_draft_content_item + updated_item || new_draft_edition end def previously_published_item @previously_published_item ||= PreviouslyPublishedItem.new( - content_id, payload[:base_path], locale, self + document, payload[:base_path], self ).call end def base_path_required? - !ContentItem::EMPTY_BASE_PATH_FORMATS.include?(payload[:schema_name]) + !Edition::EMPTY_BASE_PATH_FORMATS.include?(payload[:schema_name]) end def previously_drafted_item - @previously_drafted_item ||= ContentItem.find_by( - id: pessimistic_content_item_scope.pluck(:id), - state: "draft", - ) + document.draft end def clear_draft_items_of_same_locale_and_base_path SubstitutionHelper.clear!( new_item_document_type: payload[:document_type], - new_item_content_id: content_id, + new_item_content_id: document.content_id, state: "draft", - locale: locale, + locale: document.locale, base_path: payload[:base_path], downstream: downstream, callbacks: callbacks, @@ -99,24 +102,12 @@ def clear_draft_items_of_same_locale_and_base_path ) end - def locale - payload.fetch(:locale, ContentItem::DEFAULT_LOCALE) - end - - def pessimistic_content_item_scope - ContentItem.where(content_id: content_id, locale: locale).lock - end - - def content_id - payload.fetch(:content_id) - end - - def update_last_edited_at(content_item, last_edited_at = nil) + def update_last_edited_at(edition, last_edited_at = nil) if last_edited_at.nil? && %w(major minor).include?(payload[:update_type]) last_edited_at = Time.zone.now end - content_item.update_attributes(last_edited_at: last_edited_at) if last_edited_at + edition.update_attributes(last_edited_at: last_edited_at) if last_edited_at end def bulk_publishing? diff --git a/app/commands/v2/represent_downstream.rb b/app/commands/v2/represent_downstream.rb index 986e988e0..0963149b4 100644 --- a/app/commands/v2/represent_downstream.rb +++ b/app/commands/v2/represent_downstream.rb @@ -7,11 +7,11 @@ def self.name def call(content_ids, draft = false) if draft - with_locales = Queries::LocalesForContentItems.call(content_ids, draft_states) + with_locales = Queries::LocalesForEditions.call(content_ids, %w[draft live]) with_locales.each { |(content_id, locale)| downstream_draft(content_id, locale) } end - with_locales = Queries::LocalesForContentItems.call(content_ids, live_states) + with_locales = Queries::LocalesForEditions.call(content_ids, %w[live]) with_locales.each_with_index do |(content_id, locale), index| sleep 60 if (index + 1) % 10_000 == 0 downstream_live(content_id, locale) @@ -20,14 +20,6 @@ def call(content_ids, draft = false) private - def draft_states - %w{draft published unpublished} - end - - def live_states - %w{published unpublished} - end - def downstream_draft(content_id, locale) event_payload = { content_id: content_id, diff --git a/app/commands/v2/unpublish.rb b/app/commands/v2/unpublish.rb index f5d6972d6..0faf97b68 100644 --- a/app/commands/v2/unpublish.rb +++ b/app/commands/v2/unpublish.rb @@ -3,17 +3,19 @@ module V2 class Unpublish < BaseCommand def call validate - previous_item.supersede if previous_item_should_be_superseded? + + previous.supersede if previous_edition_should_be_superseded? transition_state - AccessLimit.find_by(content_item: content_item).try(:destroy) + AccessLimit.find_by(edition: edition).try(:destroy) after_transaction_commit do send_downstream end - Action.create_unpublish_action(content_item, unpublishing_type, locale, event) + Action.create_unpublish_action(edition, unpublishing_type, + document.locale, event) - Success.new(content_id: content_id) + Success.new(content_id: document.content_id) end private @@ -36,12 +38,8 @@ def raise_invalid_unpublishing_type raise_command_error(422, message, fields: {}) end - def content_item - @content_item ||= find_unpublishable_content_item - end - - def content_id - @content_id ||= payload.fetch(:content_id) + def edition + @edition ||= find_unpublishable_edition end def validate_allow_discard_draft @@ -51,20 +49,20 @@ def validate_allow_discard_draft end end - def validate_content_item_presence - unless content_item.present? - message = "Could not find a content item to unpublish" + def validate_edition_presence + unless edition.present? + message = "Could not find an edition to unpublish" raise_command_error(404, message, fields: {}) end end def validate_draft_presence - if draft_exists? && !payload[:allow_draft] + if document.draft.present? && !payload[:allow_draft] if payload[:discard_drafts] == true DiscardDraft.call( { - content_id: content_id, - locale: locale, + content_id: document.content_id, + locale: document.locale, }, downstream: downstream, callbacks: callbacks, @@ -79,13 +77,13 @@ def validate_draft_presence def validate validate_allow_discard_draft - validate_content_item_presence - check_version_and_raise_if_conflicting(content_item, previous_version_number) + validate_edition_presence + check_version_and_raise_if_conflicting(document, previous_version_number) validate_draft_presence end def unpublish - content_item.unpublish(payload.slice(:type, :explanation, :alternative_path, :unpublished_at)) + edition.unpublish(payload.slice(:type, :explanation, :alternative_path, :unpublished_at)) rescue ActiveRecord::RecordInvalid => e raise_command_error(422, e.message, fields: {}) end @@ -95,68 +93,46 @@ def send_downstream DownstreamDraftWorker.perform_async_in_queue( DownstreamDraftWorker::HIGH_QUEUE, - content_id: content_item.content_id, - locale: locale, + content_id: document.content_id, + locale: document.locale, payload_version: event.id, update_dependencies: true, ) DownstreamLiveWorker.perform_async_in_queue( DownstreamLiveWorker::HIGH_QUEUE, - content_id: content_item.content_id, - locale: locale, + content_id: document.content_id, + locale: document.locale, payload_version: event.id, update_dependencies: true, ) end - def locale - payload.fetch(:locale, ContentItem::DEFAULT_LOCALE) - end - def previous_version_number payload[:previous_version].to_i if payload[:previous_version] end - def find_unpublishable_content_item - allowed_states = %w(published unpublished) - + def find_unpublishable_edition if payload[:allow_draft] - allowed_states = %w(draft) + document.draft + elsif previous && !Unpublishing.is_substitute?(previous) + previous end - - content_item = ContentItem.where( - content_id: content_id, - locale: locale, - state: allowed_states - ).order(nil).lock.first - - content_item if content_item && (payload[:allow_draft] || !Unpublishing.is_substitute?(content_item)) end - def previous_item_should_be_superseded? - previous_item && find_unpublishable_content_item != previous_item + def previous + document.published_or_unpublished end - def previous_item - raise "There should only be one previous published or unpublished item" if previous_items.size > 1 - previous_items.first + def previous_edition_should_be_superseded? + previous && (find_unpublishable_edition != previous) end - def previous_items - @previous_items ||= ContentItem.where( - content_id: content_id, - locale: locale, - state: %w(published unpublished), - ).order(nil) - end - - def draft_exists? - ContentItem.where( - content_id: content_id, - locale: locale, - state: "draft", - ).exists? + def document + @document ||= Document.find_or_create_locked( + content_id: payload.fetch(:content_id), + locale: payload.fetch(:locale, Edition::DEFAULT_LOCALE), + ) end end end diff --git a/app/controllers/lookups_controller.rb b/app/controllers/lookups_controller.rb index 9b3dcde7a..ad01d4d6f 100644 --- a/app/controllers/lookups_controller.rb +++ b/app/controllers/lookups_controller.rb @@ -5,9 +5,9 @@ def by_base_path states = %w(published unpublished) base_paths = params.fetch(:base_paths) - base_paths_and_content_ids = ContentItem + base_paths_and_content_ids = Edition.with_document .where(state: states, base_path: base_paths) - .pluck(:base_path, :content_id) + .pluck(:base_path, 'documents.content_id') .uniq response = Hash[base_paths_and_content_ids] diff --git a/app/controllers/publish_intents_controller.rb b/app/controllers/publish_intents_controller.rb index 54171812d..060cc16da 100644 --- a/app/controllers/publish_intents_controller.rb +++ b/app/controllers/publish_intents_controller.rb @@ -4,7 +4,7 @@ def show end def create_or_update - response = Commands::PutPublishIntent.call(content_item) + response = Commands::PutPublishIntent.call(edition) render status: response.code, json: response end @@ -15,7 +15,7 @@ def destroy private - def content_item + def edition payload.merge(base_path: base_path) end end diff --git a/app/controllers/v2/content_items_controller.rb b/app/controllers/v2/content_items_controller.rb index 4fcc164e6..01da4e95c 100644 --- a/app/controllers/v2/content_items_controller.rb +++ b/app/controllers/v2/content_items_controller.rb @@ -31,28 +31,28 @@ def show end def put_content - response = Commands::V2::PutContent.call(content_item) + response = Commands::V2::PutContent.call(edition) render status: response.code, json: response end def publish - response = Commands::V2::Publish.call(content_item) + response = Commands::V2::Publish.call(edition) render status: response.code, json: response end def unpublish - response = Commands::V2::Unpublish.call(content_item) + response = Commands::V2::Unpublish.call(edition) render status: response.code, json: response end def discard_draft - response = Commands::V2::DiscardDraft.call(content_item) + response = Commands::V2::DiscardDraft.call(edition) render status: response.code, json: response end private - def content_item + def edition payload.merge(content_id: path_params[:content_id]) end diff --git a/app/models/access_limit.rb b/app/models/access_limit.rb index 1c8eeb556..d7339851a 100644 --- a/app/models/access_limit.rb +++ b/app/models/access_limit.rb @@ -1,5 +1,5 @@ class AccessLimit < ApplicationRecord - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" validate :user_uids_are_strings diff --git a/app/models/action.rb b/app/models/action.rb index 0f339a4e1..707791ca2 100644 --- a/app/models/action.rb +++ b/app/models/action.rb @@ -1,35 +1,35 @@ class Action < ActiveRecord::Base - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" belongs_to :link_set belongs_to :event - validate :one_of_content_item_link_set + validate :one_of_edition_link_set validates :action, presence: true - def self.create_put_content_action(content_item, locale, event) - create_publishing_action("PutContent", content_item, locale, event) + def self.create_put_content_action(edition, locale, event) + create_publishing_action("PutContent", edition, locale, event) end - def self.create_publish_action(content_item, locale, event) - create_publishing_action("Publish", content_item, locale, event) + def self.create_publish_action(edition, locale, event) + create_publishing_action("Publish", edition, locale, event) end - def self.create_unpublish_action(content_item, unpublishing_type, locale, event) + def self.create_unpublish_action(edition, unpublishing_type, locale, event) action = "Unpublish#{unpublishing_type.camelize}" - create_publishing_action(action, content_item, locale, event) + create_publishing_action(action, edition, locale, event) end - def self.create_discard_draft_action(content_item, locale, event) - create_publishing_action("DiscardDraft", content_item, locale, event) + def self.create_discard_draft_action(edition, locale, event) + create_publishing_action("DiscardDraft", edition, locale, event) end - def self.create_publishing_action(action, content_item, locale, event) + def self.create_publishing_action(action, edition, locale, event) create!( - content_id: content_item.content_id, + content_id: edition.document.content_id, locale: locale, action: action, user_uid: event.user_uid, - content_item: content_item, + edition: edition, event: event, ) end @@ -47,9 +47,9 @@ def self.create_patch_link_set_action(link_set, event) private - def one_of_content_item_link_set - if content_item_id && link_set_id || content_item && link_set - errors.add(:base, "can not be associated with both a content item and link set") + def one_of_edition_link_set + if content_item_id && link_set_id || edition && link_set + errors.add(:base, "can not be associated with both an edition and link set") end end end diff --git a/app/models/change_note.rb b/app/models/change_note.rb index 85a0d2f83..40682cf6b 100644 --- a/app/models/change_note.rb +++ b/app/models/change_note.rb @@ -1,25 +1,25 @@ class ChangeNote < ActiveRecord::Base - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" - def self.create_from_content_item(payload, content_item) - ChangeNoteFactory.new(payload, content_item).build + def self.create_from_edition(payload, edition) + ChangeNoteFactory.new(payload, edition).build end - def self.join_content_items(content_item_scope) - content_item_scope.joins( + def self.join_editions(edition_scope) + edition_scope.joins( "LEFT JOIN change_notes ON change_notes.content_item_id = content_items.id" ) end end class ChangeNoteFactory - def initialize(payload, content_item) + def initialize(payload, edition) @payload = payload - @content_item = content_item + @edition = edition end def build - return unless content_item.update_type == "major" + return unless edition.update_type == "major" create_from_top_level_change_note || create_from_details_hash_change_note || create_from_details_hash_change_history @@ -27,15 +27,15 @@ def build private - attr_reader :payload, :content_item + attr_reader :payload, :edition def create_from_top_level_change_note return unless change_note ChangeNote. - find_or_create_by!(content_item: content_item). + find_or_create_by!(edition: edition). update!( note: change_note, - content_id: content_item.content_id, + content_id: edition.document.content_id, public_timestamp: Time.zone.now ) end @@ -43,9 +43,9 @@ def create_from_top_level_change_note def create_from_details_hash_change_note return unless note ChangeNote.create!( - content_item: content_item, - content_id: content_item.content_id, - public_timestamp: content_item.updated_at, + edition: edition, + content_id: edition.document.content_id, + public_timestamp: edition.updated_at, note: note, ) end @@ -55,8 +55,8 @@ def create_from_details_hash_change_history history_element = change_history.max_by { |h| h[:public_timestamp] } ChangeNote.create!( history_element.merge( - content_item: content_item, - content_id: content_item.content_id + edition: edition, + content_id: edition.document.content_id ) ) end @@ -66,10 +66,10 @@ def change_note end def note - content_item.details[:change_note] + edition.details[:change_note] end def change_history - content_item.details[:change_history] + edition.details[:change_history] end end diff --git a/app/models/concerns/find_or_create_locked.rb b/app/models/concerns/find_or_create_locked.rb new file mode 100644 index 000000000..165337611 --- /dev/null +++ b/app/models/concerns/find_or_create_locked.rb @@ -0,0 +1,23 @@ +module FindOrCreateLocked + extend ActiveSupport::Concern + + class_methods do + def find_or_create_locked(params) + begin + retries ||= 0 + self.transaction(requires_new: true) do + entity = self.lock.find_by(params) + entity || self.create!(params).lock! + end + rescue ActiveRecord::RecordNotUnique + # This should never need more than 1 retry as the scenario this error + # would occur is: inbetween rails find_by and create SELECT & INSERT + # queries a concurrent request ran an INSERT. Thus on retry the + # SELECT would succeed. + # So if this actually throws an exception here we probably have a + # weird underlying problem. + (retries += 1) == 1 ? retry : raise + end + end + end +end diff --git a/app/models/content_item.rb b/app/models/content_item.rb deleted file mode 100644 index 8b2b81514..000000000 --- a/app/models/content_item.rb +++ /dev/null @@ -1,240 +0,0 @@ -class ContentItem < ApplicationRecord - include SymbolizeJSON - include DescriptionOverrides - - enum content_store: { - draft: 'draft', - live: 'live' - } - - DEFAULT_LOCALE = "en".freeze - - TOP_LEVEL_FIELDS = [ - :analytics_identifier, - :base_path, - :content_id, - :content_store, - :description, - :details, - :document_type, - :first_published_at, - :last_edited_at, - :locale, - :need_ids, - :phase, - :public_updated_at, - :publishing_app, - :redirects, - :rendering_app, - :routes, - :schema_name, - :state, - :title, - :user_facing_version, - :update_type, - ].freeze - - NON_RENDERABLE_FORMATS = %w(redirect gone).freeze - EMPTY_BASE_PATH_FORMATS = %w(contact government).freeze - - scope :renderable_content, -> { where.not(document_type: NON_RENDERABLE_FORMATS) } - - belongs_to :document - - validates :schema_name, presence: true - validates :document_type, presence: true - - validates :base_path, absolute_path: true, if: :base_path_present? - validates :content_id, presence: true, uuid: true - validates :publishing_app, presence: true - validates :title, presence: true, if: :renderable_content? - validates :rendering_app, presence: true, dns_hostname: true, if: :requires_rendering_app? - validates :phase, inclusion: { - in: %w(alpha beta live), - message: 'must be either alpha, beta, or live' - } - validates :description, well_formed_content_types: { must_include: "text/html" } - validates :details, well_formed_content_types: { must_include_one_of: %w(text/html text/govspeak) } - - validates :locale, inclusion: { - in: I18n.available_locales.map(&:to_s), - message: 'must be a supported locale' - } - - validate :user_facing_version_must_increase - validate :draft_cannot_be_behind_live - - validates_with VersionForLocaleValidator - validates_with BasePathForStateValidator - validates_with StateForLocaleValidator - validates_with RoutesAndRedirectsValidator - - # Temporary code until we kill Location, State, Translation, and - # UserFacing Version - after_save do - if changes[:base_path] && changes[:base_path].last - Location.find_or_initialize_by(content_item_id: id) - .update!(base_path: changes[:base_path].last) - end - - if changes[:base_path] && !changes[:base_path].last - Location.find_by(content_item_id: id).try(:destroy) - end - - if changes[:state] - State.find_or_initialize_by(content_item_id: id) - .update!(name: changes[:state].last) - end - - if changes[:locale] - Translation.find_or_initialize_by(content_item_id: id) - .update!(locale: changes[:locale].last) - end - - if changes[:user_facing_version] - UserFacingVersion.find_or_initialize_by(content_item_id: id) - .update!(number: changes[:user_facing_version].last) - end - end - - before_save { ensure_document } - - def document_requires_updating? - !document || document.locale != locale || document.content_id != content_id - end - - def ensure_document - if document_requires_updating? - self.document = Document.find_or_create_by(content_id: content_id, - locale: locale) - end - end - - def requires_base_path? - EMPTY_BASE_PATH_FORMATS.exclude?(document_type) - end - - def pathless? - !self.requires_base_path? && !base_path - end - - def base_path_present? - base_path.present? - end - - def draft_cannot_be_behind_live - if state == "draft" - draft_version = user_facing_version - live_version = ContentItem.where( - content_id: content_id, - locale: locale, - state: %w(published unpublished), - ).pluck(:user_facing_version).first - end - - if %w(published unpublished).include?(state) - draft_version = ContentItem.where( - content_id: content_id, - locale: locale, - state: "draft", - ).pluck(:user_facing_version).first - live_version = user_facing_version - end - - return unless draft_version && live_version - - if draft_version < live_version - mismatch = "(#{draft_version} < #{live_version})" - message = "draft content item cannot be behind the live content item #{mismatch}" - errors.add(:user_facing_version, message) - end - end - - def user_facing_version_must_increase - return unless persisted? - return unless user_facing_version_changed? && user_facing_version <= user_facing_version_was - - mismatch = "(#{user_facing_version} <= #{user_facing_version_was})" - message = "cannot be less than or equal to the previous user_facing_version #{mismatch}" - errors.add(:user_facing_version, message) - end - - # FIXME: This method is used to retrieve a version of .details that doesn't - # have text/html, thus this can be used to convert the item to HTML - # It is here for comparing our Govspeak output with that that was provided to - # us previously and can be removed once we have migrated most applications. - def details_for_govspeak_conversion - return details unless details.is_a?(Hash) - - value_without_html = lambda do |value| - wrapped = Array.wrap(value) - html = wrapped.find { |item| item.is_a?(Hash) && item[:content_type] == "text/html" } - govspeak = wrapped.find { |item| item.is_a?(Hash) && item[:content_type] == "text/govspeak" } - if html.present? && govspeak.present? - wrapped - [html] - else - value - end - end - - details.deep_dup.each_with_object({}) do |(key, value), memo| - memo[key] = value_without_html.call(value) - end - end - - def publish - update_attributes!(state: "published", content_store: "live") - end - - def supersede - update_attributes!(state: "superseded", content_store: nil) - end - - def unpublish(type:, explanation: nil, alternative_path: nil, unpublished_at: nil) - content_store = type == "substitute" ? nil : "live" - update_attributes!(state: "unpublished", content_store: content_store) - - unpublishing = Unpublishing.find_by(content_item: self) - - unpublished_at = nil unless type == "withdrawal" - - if unpublishing.present? - unpublishing.update_attributes( - type: type, - explanation: explanation, - alternative_path: alternative_path, - unpublished_at: unpublished_at, - ) - unpublishing - else - Unpublishing.create!( - content_item: self, - type: type, - explanation: explanation, - alternative_path: alternative_path, - unpublished_at: unpublished_at, - ) - end - end - - def substitute - unpublish( - type: "substitute", - explanation: "Automatically unpublished to make way for another content item", - ) - end - - def lock_version_number - LockVersion.find_by!(target: self).number - end - -private - - def renderable_content? - NON_RENDERABLE_FORMATS.exclude?(document_type) - end - - def requires_rendering_app? - renderable_content? && document_type != "contact" - end -end diff --git a/app/models/create_draft_content_item.rb b/app/models/create_draft_content_item.rb deleted file mode 100644 index d6d647371..000000000 --- a/app/models/create_draft_content_item.rb +++ /dev/null @@ -1,67 +0,0 @@ -class CreateDraftContentItem - def initialize(put_content, payload, previously_published_item) - @put_content = put_content - @payload = payload - @previously_published_item = previously_published_item - end - - def call - content_item.tap do - fill_out_new_content_item - end - end - -private - - attr_reader :payload, :put_content, :previously_published_item - - def content_item - @content_item ||= create_content_item - end - - def create_content_item - attributes = content_item_attributes_from_payload.merge( - locale: locale, - state: "draft", - content_store: "draft", - user_facing_version: user_facing_version_number_for_new_draft, - ) - ContentItem.create!(attributes) - end - - def lock_version_number_for_new_draft - previously_published_item.lock_version_number - end - - def user_facing_version_number_for_new_draft - previously_published_item.user_facing_version - end - - def locale - payload.fetch(:locale, ContentItem::DEFAULT_LOCALE) - end - - def fill_out_new_content_item - LockVersion.create!(target: content_item, number: lock_version_number_for_new_draft) - ensure_link_set_exists - - set_first_published_at - end - - def ensure_link_set_exists - link_set = LinkSet.find_or_create_by!(content_id: content_item.content_id) - LockVersion.find_or_create_by!(target: link_set, number: 1) - end - - def set_first_published_at - return unless previously_published_item.set_first_published_at? - return if content_item.first_published_at - content_item.update_attributes( - first_published_at: previously_published_item.first_published_at, - ) - end - - def content_item_attributes_from_payload - payload.slice(*ContentItem::TOP_LEVEL_FIELDS) - end -end diff --git a/app/models/create_draft_edition.rb b/app/models/create_draft_edition.rb new file mode 100644 index 000000000..8f7b77782 --- /dev/null +++ b/app/models/create_draft_edition.rb @@ -0,0 +1,64 @@ +class CreateDraftEdition + def initialize(put_content, payload, previously_published_item) + @put_content = put_content + @payload = payload + @previously_published_item = previously_published_item + end + + def call + edition.tap do + fill_out_new_edition + end + end + +private + + attr_reader :payload, :put_content, :previously_published_item + + def edition + @edition ||= create_edition + end + + def create_edition + attributes = edition_attributes_from_payload.merge( + state: "draft", + content_store: "draft", + user_facing_version: user_facing_version_number_for_new_draft, + ) + document.editions.create!(attributes) + end + + def user_facing_version_number_for_new_draft + previously_published_item.user_facing_version + end + + def document + put_content.document + end + + def fill_out_new_edition + document.increment!(:stale_lock_version) + ensure_link_set_exists + + set_first_published_at + end + + def ensure_link_set_exists + link_set = LinkSet.find_or_create_by!(content_id: document.content_id) + if link_set.stale_lock_version == 0 + link_set.increment!(:stale_lock_version) + end + end + + def set_first_published_at + return unless previously_published_item.set_first_published_at? + return if edition.first_published_at + edition.update_attributes( + first_published_at: previously_published_item.first_published_at, + ) + end + + def edition_attributes_from_payload + payload.slice(*Edition::TOP_LEVEL_FIELDS) + end +end diff --git a/app/models/document.rb b/app/models/document.rb index da4f10dd1..a60ed3013 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -1,7 +1,16 @@ class Document < ApplicationRecord - has_many :content_items + include FindOrCreateLocked - validates :content_id, presence: true + has_many :editions + has_one :draft, -> { where(content_store: "draft") }, class_name: Edition + has_one :live, -> { where(content_store: "live") }, class_name: Edition + + # Due to the scenario of unpublished type substitute we need a scope that + # can access published / unpublished which isn't tied to live content store + # FIXME - we should make this go away + has_one :published_or_unpublished, -> { where(state: %w(published unpublished)) }, class_name: Edition + + validates :content_id, presence: true, uuid: true validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s), diff --git a/app/models/edition.rb b/app/models/edition.rb index c614167c3..dd1e67bec 100644 --- a/app/models/edition.rb +++ b/app/models/edition.rb @@ -1,4 +1,272 @@ -# Needed to work around the fact that we use a polymorphic type and to avoid -# including a migration which changed the class name in the database, we simply -# make sure it is available under both names -Edition = ContentItem +class Edition < ApplicationRecord + self.table_name = "content_items" + + include SymbolizeJSON + include DescriptionOverrides + + enum content_store: { + draft: 'draft', + live: 'live' + } + + DEFAULT_LOCALE = "en".freeze + + TOP_LEVEL_FIELDS = [ + :analytics_identifier, + :base_path, + :content_store, + :description, + :details, + :document_type, + :first_published_at, + :last_edited_at, + :need_ids, + :phase, + :public_updated_at, + :publishing_app, + :redirects, + :rendering_app, + :routes, + :schema_name, + :state, + :title, + :user_facing_version, + :update_type, + ].freeze + + NON_RENDERABLE_FORMATS = %w(redirect gone).freeze + EMPTY_BASE_PATH_FORMATS = %w(contact government).freeze + + belongs_to :document + has_one :unpublishing, foreign_key: "content_item_id" + + scope :renderable_content, -> { where.not(document_type: NON_RENDERABLE_FORMATS) } + scope :with_document, -> { joins(:document) } + + validates :document, presence: true + + validates :schema_name, presence: true + validates :document_type, presence: true + + validates :base_path, absolute_path: true, if: :base_path_present? + validates :publishing_app, presence: true + validates :title, presence: true, if: :renderable_content? + validates :rendering_app, presence: true, dns_hostname: true, if: :requires_rendering_app? + validates :phase, inclusion: { + in: %w(alpha beta live), + message: 'must be either alpha, beta, or live' + } + validates :description, well_formed_content_types: { must_include: "text/html" } + validates :details, well_formed_content_types: { must_include_one_of: %w(text/html text/govspeak) } + + validate :user_facing_version_must_increase + validate :draft_cannot_be_behind_live + + validates_with VersionForDocumentValidator + validates_with BasePathForStateValidator + validates_with StateForDocumentValidator + validates_with RoutesAndRedirectsValidator + + # Temporary code until we remove content_id and locale fields + before_save do + next unless document + self.content_id = document.content_id unless content_id == document.content_id + self.locale = document.locale unless locale == document.locale + end + + after_save do + lock_version = LockVersion.find_or_create_by(target: self) + if document.stale_lock_version < lock_version.number + lock_version.update! number: document.stale_lock_version + end + end + + # Temporary code until we kill Location, State, Translation, and + # UserFacingVersion + after_save do + if changes[:base_path] && changes[:base_path].last + Location.find_or_initialize_by(content_item_id: id) + .update!(base_path: changes[:base_path].last) + end + + if changes[:base_path] && !changes[:base_path].last + Location.find_by(content_item_id: id).try(:destroy) + end + + if changes[:state] + State.find_or_initialize_by(content_item_id: id) + .update!(name: changes[:state].last) + end + + if changes[:locale] + Translation.find_or_initialize_by(content_item_id: id) + .update!(locale: changes[:locale].last) + end + + if changes[:user_facing_version] + UserFacingVersion.find_or_initialize_by(content_item_id: id) + .update!(number: changes[:user_facing_version].last) + end + end + + before_save { ensure_document } + + def document_requires_updating? + !document || document.locale != locale || document.content_id != content_id + end + + def ensure_document + if document_requires_updating? + self.document = Document.find_or_create_by(content_id: content_id, + locale: locale) + end + end + + def as_json(options = {}) + %i[content_id locale].each do |field| + next if Array.wrap(options[:methods]).include?(field) + only = Array.wrap(options[:only]) || [] + except = Array.wrap(options[:except]) || [] + if (only.empty? || only.include?(field)) && (except.empty? || !except.include?(field)) + options[:methods] = (options[:methods] || []) + [field] + end + end + super(options) + end + + def requires_base_path? + EMPTY_BASE_PATH_FORMATS.exclude?(document_type) + end + + def pathless? + !self.requires_base_path? && !base_path + end + + def base_path_present? + base_path.present? + end + + def draft_cannot_be_behind_live + return unless document + + if state == "draft" + draft_version = user_facing_version + published_unpublished_version = document.published_or_unpublished.try(:user_facing_version) + end + + if %w(published unpublished).include?(state) + draft_version = document.draft.user_facing_version if document.draft + published_unpublished_version = user_facing_version + end + + return unless draft_version && published_unpublished_version + + if draft_version < published_unpublished_version + mismatch = "(#{draft_version} < #{published_unpublished_version})" + message = "draft edition cannot be behind the published/unpublished edition #{mismatch}" + errors.add(:user_facing_version, message) + end + end + + def user_facing_version_must_increase + return unless persisted? + return unless user_facing_version_changed? && user_facing_version <= user_facing_version_was + + mismatch = "(#{user_facing_version} <= #{user_facing_version_was})" + message = "cannot be less than or equal to the previous user_facing_version #{mismatch}" + errors.add(:user_facing_version, message) + end + + # FIXME: This method is used to retrieve a version of .details that doesn't + # have text/html, thus this can be used to convert the item to HTML + # It is here for comparing our Govspeak output with that that was provided to + # us previously and can be removed once we have migrated most applications. + def details_for_govspeak_conversion + return details unless details.is_a?(Hash) + + value_without_html = lambda do |value| + wrapped = Array.wrap(value) + html = wrapped.find { |item| item.is_a?(Hash) && item[:content_type] == "text/html" } + govspeak = wrapped.find { |item| item.is_a?(Hash) && item[:content_type] == "text/govspeak" } + if html.present? && govspeak.present? + wrapped - [html] + else + value + end + end + + details.deep_dup.each_with_object({}) do |(key, value), memo| + memo[key] = value_without_html.call(value) + end + end + + def publish + update_attributes!(state: "published", content_store: "live") + end + + def supersede + update_attributes!(state: "superseded", content_store: nil) + end + + def unpublish(type:, explanation: nil, alternative_path: nil, unpublished_at: nil) + content_store = type == "substitute" ? nil : "live" + update_attributes!(state: "unpublished", content_store: content_store) + + unpublishing = Unpublishing.find_by(edition: self) + + unpublished_at = nil unless type == "withdrawal" + + if unpublishing.present? + unpublishing.update_attributes( + type: type, + explanation: explanation, + alternative_path: alternative_path, + unpublished_at: unpublished_at, + ) + unpublishing + else + Unpublishing.create!( + edition: self, + type: type, + explanation: explanation, + alternative_path: alternative_path, + unpublished_at: unpublished_at, + ) + end + end + + def substitute + unpublish( + type: "substitute", + explanation: "Automatically unpublished to make way for another document", + ) + end + +private + + # These are private whilst they are legacy attributes, that are currently + # kept for easy rollback to a previous iteration of the codebase + def content_id + self[:content_id] + end + + def content_id=(val) + write_attribute(:content_id, val) + end + + def locale + self[:locale] + end + + def locale=(val) + write_attribute(:locale, val) + end + + def renderable_content? + NON_RENDERABLE_FORMATS.exclude?(document_type) + end + + def requires_rendering_app? + renderable_content? && document_type != "contact" + end +end diff --git a/app/models/link.rb b/app/models/link.rb index b4df354e0..43a27e26b 100644 --- a/app/models/link.rb +++ b/app/models/link.rb @@ -6,9 +6,9 @@ class Link < ApplicationRecord validate :link_type_is_valid validate :content_id_is_valid - def self.filter_content_items(scope, filters) + def self.filter_editions(scope, filters) join_sql = <<-SQL.strip_heredoc - INNER JOIN link_sets ON link_sets.content_id = content_items.content_id + INNER JOIN link_sets ON link_sets.content_id = documents.content_id INNER JOIN links ON links.link_set_id = link_sets.id SQL diff --git a/app/models/link_set.rb b/app/models/link_set.rb index 77b05102e..c4bcbef6a 100644 --- a/app/models/link_set.rb +++ b/app/models/link_set.rb @@ -1,3 +1,10 @@ class LinkSet < ApplicationRecord + include FindOrCreateLocked + has_many :links, -> { order(link_type: :asc, position: :asc) }, dependent: :destroy + + after_save do + lock_version = LockVersion.find_or_create_by(target: self) + lock_version.update! number: stale_lock_version + end end diff --git a/app/models/location.rb b/app/models/location.rb index a18030d26..68d40d316 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -1,15 +1,12 @@ class Location < ApplicationRecord - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" - def self.filter(content_item_scope, base_path:) - join_content_items(content_item_scope) - .where("locations.base_path" => base_path) + def self.filter(edition_scope, base_path:) + join_editions(edition_scope).where("locations.base_path" => base_path) end - def self.join_content_items(content_item_scope) - content_item_scope.joins( - "INNER JOIN locations ON locations.content_item_id = content_items.id" - ) + def self.join_editions(edition_scope) + edition_scope.joins("INNER JOIN locations ON locations.content_item_id = content_items.id") end private diff --git a/app/models/lock_version.rb b/app/models/lock_version.rb index 727ca4a71..ee3d660fe 100644 --- a/app/models/lock_version.rb +++ b/app/models/lock_version.rb @@ -8,10 +8,10 @@ class LockVersion < ApplicationRecord item.update_column(:stale_lock_version, number) if number > (item.stale_lock_version || -1) end - def self.join_content_items(content_item_scope) - content_item_scope.joins( + def self.join_editions(edition_scope) + edition_scope.joins( "INNER JOIN lock_versions ON - lock_versions.target_id = content_items.id AND + lock_versions.target_id = editions.id AND lock_versions.target_type = 'ContentItem'" ) end @@ -23,7 +23,7 @@ def conflicts_with?(previous_version_number) end def lock_version_target - if content_item_target? + if edition_target? target.document else target @@ -41,7 +41,8 @@ def increment! private - def content_item_target? + def edition_target? + # The 'Edition' class used to be called the 'ContentItem' class. %w(ContentItem Edition).include? target_type end diff --git a/app/models/previously_published_item.rb b/app/models/previously_published_item.rb index 27cd5aa8d..b70f4fbc5 100644 --- a/app/models/previously_published_item.rb +++ b/app/models/previously_published_item.rb @@ -1,8 +1,7 @@ class PreviouslyPublishedItem - def initialize(content_id, base_path, locale, put_content) - @content_id = content_id + def initialize(document, base_path, put_content) + @document = document @base_path = base_path - @locale = locale @put_content = put_content end @@ -10,17 +9,10 @@ def call previously_published_item ? self : NoPreviousPublishedItem.new end - attr_reader :content_id, :base_path, :locale, :put_content + attr_reader :document, :base_path, :put_content def previously_published_item - @previously_published_item ||= - ContentItem.find_by(content_id: content_id, - state: %w(published unpublished), - locale: locale) - end - - def lock_version_number - previously_published_item.lock_version_number + 1 + document.published_or_unpublished end def user_facing_version @@ -48,10 +40,6 @@ def path_has_changed? end class NoPreviousPublishedItem - def lock_version_number - 1 - end - def user_facing_version 1 end diff --git a/app/models/state.rb b/app/models/state.rb index f9d79f4e3..fd4b45ddb 100644 --- a/app/models/state.rb +++ b/app/models/state.rb @@ -1,14 +1,11 @@ class State < ApplicationRecord - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" - def self.filter(content_item_scope, name:) - join_content_items(content_item_scope) - .where("states.name" => name) + def self.filter(edition_scope, name:) + join_editions(edition_scope).where("states.name" => name) end - def self.join_content_items(content_item_scope) - content_item_scope.joins( - "INNER JOIN states ON states.content_item_id = content_items.id" - ) + def self.join_editions(edition_scope) + edition_scope.joins("INNER JOIN states ON states.content_item_id = content_items.id") end end diff --git a/app/models/translation.rb b/app/models/translation.rb index 06b59f072..35b304ba7 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -1,14 +1,11 @@ class Translation < ApplicationRecord - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" - def self.filter(content_item_scope, locale:) - join_content_items(content_item_scope) - .where("translations.locale" => locale) + def self.filter(edition_scope, locale:) + join_editions(edition_scope).where("translations.locale" => locale) end - def self.join_content_items(content_item_scope) - content_item_scope.joins( - "INNER JOIN translations ON translations.content_item_id = content_items.id" - ) + def self.join_editions(edition_scope) + edition_scope.joins("INNER JOIN translations ON translations.content_item_id = content_items.id") end end diff --git a/app/models/unpublishing.rb b/app/models/unpublishing.rb index 8ce96a683..31ffc1ff3 100644 --- a/app/models/unpublishing.rb +++ b/app/models/unpublishing.rb @@ -1,7 +1,7 @@ class Unpublishing < ApplicationRecord self.inheritance_column = nil - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" VALID_TYPES = %w( gone @@ -11,7 +11,7 @@ class Unpublishing < ApplicationRecord withdrawal ).freeze - validates :content_item, presence: true, uniqueness: true + validates :edition, presence: true, uniqueness: true validates :type, presence: true, inclusion: { in: VALID_TYPES } validates :explanation, presence: true, if: :withdrawal? validates :alternative_path, presence: true, if: :redirect? @@ -25,7 +25,7 @@ def redirect? type == "redirect" end - def self.is_substitute?(content_item) - where(content_item: content_item).pluck(:type).first == "substitute" + def self.is_substitute?(edition) + where(edition: edition).pluck(:type).first == "substitute" end end diff --git a/app/models/update_existing_draft_content_item.rb b/app/models/update_existing_draft_content_item.rb deleted file mode 100644 index 4c01f5d69..000000000 --- a/app/models/update_existing_draft_content_item.rb +++ /dev/null @@ -1,59 +0,0 @@ -class UpdateExistingDraftContentItem - ATTRIBUTES_PROTECTED_FROM_RESET = [ - :id, - :created_at, - :updated_at, - :first_published_at, - :last_edited_at, - ].freeze - - attr_reader :payload, :put_content, :content_item - - def initialize(content_item, put_content, payload) - @content_item = content_item - @put_content = put_content - @payload = payload - end - - def call - update_lock_version - update_content_item - end - -private - - def update_lock_version - version = put_content.send(:check_version_and_raise_if_conflicting, content_item, payload[:previous_version]) - version.increment! - end - - def update_content_item - old_item = content_item.dup - assign_attributes_with_defaults - content_item.save! - [content_item, old_item] - end - - def assign_attributes_with_defaults - content_item.assign_attributes(new_attributes) - end - - def new_attributes - content_item.class.column_defaults.symbolize_keys - .merge(attributes.symbolize_keys) - .except(*ATTRIBUTES_PROTECTED_FROM_RESET) - end - - def attributes - content_item_attributes_from_payload.merge( - locale: payload.fetch(:locale, ContentItem::DEFAULT_LOCALE), - state: "draft", - content_store: "draft", - user_facing_version: content_item.user_facing_version, - ) - end - - def content_item_attributes_from_payload - payload.slice(*ContentItem::TOP_LEVEL_FIELDS) - end -end diff --git a/app/models/update_existing_draft_edition.rb b/app/models/update_existing_draft_edition.rb new file mode 100644 index 000000000..3c9dc83e4 --- /dev/null +++ b/app/models/update_existing_draft_edition.rb @@ -0,0 +1,65 @@ +class UpdateExistingDraftEdition + ATTRIBUTES_PROTECTED_FROM_RESET = [ + :id, + :document_id, + :content_id, + :locale, + :created_at, + :updated_at, + :first_published_at, + :last_edited_at, + ].freeze + + attr_reader :payload, :put_content, :edition + + def initialize(edition, put_content, payload) + @edition = edition + @put_content = put_content + @payload = payload + end + + def call + update_lock_version + update_edition + end + +private + + def update_lock_version + put_content.send(:check_version_and_raise_if_conflicting, document, payload[:previous_version]) + document.increment!(:stale_lock_version) + end + + def document + put_content.document + end + + def update_edition + old_edition = edition.dup + assign_attributes_with_defaults + edition.save! + [edition, old_edition] + end + + def assign_attributes_with_defaults + edition.assign_attributes(new_attributes) + end + + def new_attributes + edition.class.column_defaults.symbolize_keys + .merge(attributes.symbolize_keys) + .except(*ATTRIBUTES_PROTECTED_FROM_RESET) + end + + def attributes + edition_attributes_from_payload.merge( + state: "draft", + content_store: "draft", + user_facing_version: edition.user_facing_version, + ) + end + + def edition_attributes_from_payload + payload.slice(*Edition::TOP_LEVEL_FIELDS) + end +end diff --git a/app/models/user_facing_version.rb b/app/models/user_facing_version.rb index 09e8443f7..90bdfe7cb 100644 --- a/app/models/user_facing_version.rb +++ b/app/models/user_facing_version.rb @@ -1,24 +1,23 @@ class UserFacingVersion < ApplicationRecord - belongs_to :content_item + belongs_to :edition, foreign_key: "content_item_id" - def self.filter(content_item_scope, number:) - join_content_items(content_item_scope) - .where("user_facing_versions.number" => number) + def self.filter(edition_scope, number:) + join_editions(edition_scope).where("user_facing_versions.number" => number) end - def self.join_content_items(content_item_scope) - content_item_scope.joins( + def self.join_editions(edition_scope) + edition_scope.joins( "INNER JOIN user_facing_versions ON user_facing_versions.content_item_id = content_items.id" ) end private - def content_item_target? + def edition_target? true end def target - content_item + edition end end diff --git a/app/models/web_content_item.rb b/app/models/web_content_item.rb index cd3771e1d..9d573f65d 100644 --- a/app/models/web_content_item.rb +++ b/app/models/web_content_item.rb @@ -1,12 +1,14 @@ fields = %i{ id analytics_identifier + base_path content_id description details document_type first_published_at last_edited_at + locale need_ids phase public_updated_at @@ -14,14 +16,12 @@ redirects rendering_app routes + state schema_name title + unpublishing_type update_type - base_path - locale - state user_facing_version - unpublishing_type } WebContentItem = Struct.new(*fields) do @@ -61,4 +61,8 @@ def web_url def description self[:description]["value"] end + + def document + Document.find_by(content_id: content_id, locale: locale) + end end diff --git a/app/presenters/change_history_presenter.rb b/app/presenters/change_history_presenter.rb index bc29ffd58..c293a96d7 100644 --- a/app/presenters/change_history_presenter.rb +++ b/app/presenters/change_history_presenter.rb @@ -1,9 +1,9 @@ module Presenters class ChangeHistoryPresenter - attr_reader :content_item + attr_reader :web_content_item - def initialize(content_item) - @content_item = content_item + def initialize(web_content_item) + @web_content_item = web_content_item end def change_history @@ -13,12 +13,12 @@ def change_history private def details - SymbolizeJSON.symbolize(content_item.details) + SymbolizeJSON.symbolize(web_content_item.details) end def change_notes_for_content_item change_notes = ChangeNote - .where(content_id: content_item.content_id) + .where(content_id: content_id) .where("content_item_id IS NULL OR content_item_id IN (?)", content_item_ids) .order(:public_timestamp) .pluck(:note, :public_timestamp) @@ -27,13 +27,18 @@ def change_notes_for_content_item end def content_item_ids - ContentItem.where(content_id: content_item.content_id) - .where("user_facing_version <= ?", version_number) - .pluck(:id) + Edition.with_document + .where("documents.content_id": content_id) + .where("user_facing_version <= ?", version_number) + .pluck(:id) end def version_number - content_item.user_facing_version + web_content_item.user_facing_version + end + + def content_id + web_content_item.content_id end end end diff --git a/app/presenters/debug_presenter.rb b/app/presenters/debug_presenter.rb index bcce395c9..c6ed7f98a 100644 --- a/app/presenters/debug_presenter.rb +++ b/app/presenters/debug_presenter.rb @@ -10,20 +10,20 @@ def initialize(content_id) @content_id = content_id end - def content_items - @content_items ||= ContentItem.where(content_id: content_id) + def editions + @editions ||= Edition.with_document.where("documents.content_id": content_id) end def user_facing_versions - content_items.map(&:user_facing_version).sort.reverse + editions.map(&:user_facing_version).sort.reverse end - def latest_content_items - @latest_content_items ||= ::Queries::GetLatest.call(content_items) + def latest_editions + @latest_editions ||= ::Queries::GetLatest.call(editions) end def latest_state_with_locale - latest_content_items.map { |ci| [ci.locale, ci.state] } + latest_editions.map { |ci| [ci.document.locale, ci.state] } end def link_set @@ -52,11 +52,12 @@ def expanded_links end def states - content_items.group_by(&:locale).each_with_object([]) do |(locale, content_items), states| + grouped_editions = editions.group_by { |e| e.document.locale } + grouped_editions.each_with_object([]) do |(locale, editions), states| states << [locale, ''] - states << [{ v: content_items.first.id.to_s, f: content_items.first.state }, locale] - content_items[1..-1].each_with_index do |content_item, index| - states << [{ v: content_item.id.to_s, f: content_item.state }, content_items[index].try(:id).to_s] + states << [{ v: editions.first.id.to_s, f: editions.first.state }, locale] + editions[1..-1].each_with_index do |edition, index| + states << [{ v: edition.id.to_s, f: edition.state }, editions[index].try(:id).to_s] end end end @@ -69,7 +70,7 @@ def event_timeline def web_content_item @web_content_item ||= ::Queries::GetWebContentItems.find( - latest_content_items.last.id + latest_editions.last.id ) end diff --git a/app/presenters/downstream_presenter.rb b/app/presenters/downstream_presenter.rb index 04ae8faa4..b7ea88571 100644 --- a/app/presenters/downstream_presenter.rb +++ b/app/presenters/downstream_presenter.rb @@ -6,7 +6,7 @@ class DownstreamPresenter def self.present(web_content_item, state_fallback_order:) return {} unless web_content_item - if web_content_item.is_a?(ContentItem) + if web_content_item.is_a?(Edition) # TODO: Add deprecation notice here once we start to migrate other parts of # the app to use WebContentItem. Adding a notice now would be too noisy web_content_item = ::Queries::GetWebContentItems.(web_content_item.id).first @@ -87,7 +87,7 @@ def access_limit end def locale_fallback_order - [web_content_item.locale, ContentItem::DEFAULT_LOCALE].uniq + [web_content_item.locale, Edition::DEFAULT_LOCALE].uniq end def rendered_details diff --git a/app/presenters/queries/available_translations.rb b/app/presenters/queries/available_translations.rb index e412839ea..fc324674b 100644 --- a/app/presenters/queries/available_translations.rb +++ b/app/presenters/queries/available_translations.rb @@ -19,8 +19,9 @@ def translations attr_reader :content_id, :state_fallback_order, :expanded_translations def grouped_translations - ContentItem.where(content_id: content_id, state: state_fallback_order) - .pluck(:id, :locale, :state) + Edition.with_document + .where('documents.content_id': content_id, state: state_fallback_order) + .pluck(:id, 'documents.locale', :state) .sort_by { |(_, _, state)| state_fallback_order.index(state.to_sym) } .group_by { |(_, locale)| locale } end diff --git a/app/presenters/queries/content_item_presenter.rb b/app/presenters/queries/content_item_presenter.rb index 01bdc72c0..ab79821d3 100644 --- a/app/presenters/queries/content_item_presenter.rb +++ b/app/presenters/queries/content_item_presenter.rb @@ -7,10 +7,9 @@ class ContentItemPresenter :include_warnings DEFAULT_FIELDS = ([ - *ContentItem::TOP_LEVEL_FIELDS, + *Edition::TOP_LEVEL_FIELDS, :publication_state, - :user_facing_version, - :base_path, + :content_id, :locale, :lock_version, :updated_at, @@ -22,8 +21,8 @@ def self.present_many(scope, params = {}) new(scope, params).present_many end - def self.present(content_item, params = {}) - scope = ContentItem.where(id: content_item.id) + def self.present(edition, params = {}) + scope = Edition.where(id: edition.id) present_many(scope, params).first end @@ -61,13 +60,11 @@ def full_scope end def latest - ::Queries::GetLatest.call(self.scope) + ::Queries::GetLatest.call(self.scope.joins(:document)) end def join_supporting_objects(scope) - scope = ChangeNote.join_content_items(scope) - - LockVersion.join_content_items(scope) + ChangeNote.join_editions(scope) end def order_and_paginate @@ -86,7 +83,7 @@ def select_fields(scope) when :user_facing_version "content_items.user_facing_version AS user_facing_version" when :lock_version - "lock_versions.number AS lock_version" + "documents.stale_lock_version AS lock_version" when :description "description->>'value' AS description" when :last_edited_at @@ -102,7 +99,9 @@ def select_fields(scope) when :base_path "content_items.base_path as base_path" when :locale - "content_items.locale as locale" + "documents.locale as locale" + when :content_id + "documents.content_id as content_id" when :total "COUNT(*) OVER () as total" else @@ -122,8 +121,8 @@ def search(scope) ( SELECT json_agg((user_facing_version, state)) FROM content_items c - WHERE c.content_id = content_items.content_id - GROUP BY content_id + WHERE c.document_id = documents.id + GROUP BY documents.content_id ) SQL diff --git a/app/presenters/queries/content_item_warnings.rb b/app/presenters/queries/content_item_warnings.rb index 716afb2b2..0e20eb3d8 100644 --- a/app/presenters/queries/content_item_warnings.rb +++ b/app/presenters/queries/content_item_warnings.rb @@ -4,7 +4,7 @@ module ContentItemWarnings def self.call(content_id, state, base_path, document_type) return unless state == "draft" - blocking_content_item_id = ::Queries::CheckForContentItemPreventingDraftFromBeingPublished.call( + blocking_content_item_id = ::Queries::LiveEditionBlockingDraftEdition.call( content_id, base_path, document_type, diff --git a/app/presenters/queries/expanded_link_set.rb b/app/presenters/queries/expanded_link_set.rb index 1df737e37..fdd1eb128 100644 --- a/app/presenters/queries/expanded_link_set.rb +++ b/app/presenters/queries/expanded_link_set.rb @@ -3,7 +3,7 @@ module Queries class ExpandedLinkSet attr_reader :state_fallback_order - def initialize(content_id:, state_fallback_order:, locale_fallback_order: ContentItem::DEFAULT_LOCALE) + def initialize(content_id:, state_fallback_order:, locale_fallback_order: Edition::DEFAULT_LOCALE) @content_id = content_id @state_fallback_order = Array(state_fallback_order.freeze) @locale_fallback_order = Array(locale_fallback_order.freeze) @@ -16,7 +16,7 @@ def links def web_content_items(target_content_ids) return [] unless target_content_ids.present? ::Queries::GetWebContentItems.( - ::Queries::GetContentItemIdsWithFallbacks.( + ::Queries::GetEditionIdsWithFallbacks.( target_content_ids, locale_fallback_order: locale_fallback_order, state_fallback_order: state_fallback_order + [:withdrawn] diff --git a/app/presenters/queries/link_set_presenter.rb b/app/presenters/queries/link_set_presenter.rb index d7e2fdb11..d2fe08c32 100644 --- a/app/presenters/queries/link_set_presenter.rb +++ b/app/presenters/queries/link_set_presenter.rb @@ -2,26 +2,19 @@ module Presenters module Queries class LinkSetPresenter def self.present(link_set) - lock_version = LockVersion.find_by(target: link_set) - new(link_set, lock_version).present + new(link_set).present end - def initialize(link_set, lock_version = nil) + def initialize(link_set) self.link_set = link_set - self.lock_version = lock_version end def present - base = { + { content_id: link_set.content_id, links: links, + version: link_set.stale_lock_version, } - - if lock_version - base.merge(version: lock_version.number) - else - base - end end def links diff --git a/app/queries/base_path_for_state.rb b/app/queries/base_path_for_state.rb index 79c23d912..e1203bab1 100644 --- a/app/queries/base_path_for_state.rb +++ b/app/queries/base_path_for_state.rb @@ -2,37 +2,39 @@ module Queries module BasePathForState extend ArelHelpers - def self.conflict(content_item_id, state, base_path) + def self.conflict(edition_id, state, base_path) return if state == "superseded" - return if state == "unpublished" && Unpublishing.is_substitute?(content_item_id) + return if state == "unpublished" && Unpublishing.is_substitute?(edition_id) - content_items_table = ContentItem.arel_table + documents_table = Document.arel_table + editions_table = Edition.arel_table unpublishings_table = Unpublishing.arel_table allowed_states = state == "draft" ? %w(draft) : %w(published unpublished) - scope = content_items_table + scope = editions_table .project( - content_items_table[:id], - content_items_table[:content_id], - content_items_table[:locale] + editions_table[:id], + documents_table[:content_id], + documents_table[:locale], ) - .where(content_items_table[:id].not_eq(content_item_id)) - .where(content_items_table[:state].in(allowed_states)) - .where(content_items_table[:base_path].eq(base_path)) + .where(editions_table[:id].not_eq(edition_id)) + .where(editions_table[:state].in(allowed_states)) + .where(editions_table[:base_path].eq(base_path)) + .join(documents_table).on(documents_table[:id].eq(editions_table[:document_id])) if %w(published unpublished).include?(state) - unpublished_state = content_items_table[:state].eq("unpublished") - content_items_join = content_items_table[:id].eq(unpublishings_table[:content_item_id]) + unpublished_state = editions_table[:state].eq("unpublished") + editions_join = editions_table[:id].eq(unpublishings_table[:content_item_id]) nil_unpublishing = unpublishings_table[:type].eq(nil) non_substitute = unpublishings_table[:type].not_eq("substitute") scope = scope.outer_join(unpublishings_table) - .on(unpublished_state.and(content_items_join)) + .on(unpublished_state.and(editions_join)) .where(nil_unpublishing.or(non_substitute)) end - scope = scope.order(content_items_table[:created_at].desc).take(1) + scope = scope.order(editions_table[:created_at].desc).take(1) get_rows(scope).first.try(:symbolize_keys) end end diff --git a/app/queries/check_for_content_item_preventing_draft_from_being_published.rb b/app/queries/check_for_content_item_preventing_draft_from_being_published.rb deleted file mode 100644 index a71790a5b..000000000 --- a/app/queries/check_for_content_item_preventing_draft_from_being_published.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Queries - module CheckForContentItemPreventingDraftFromBeingPublished - extend ArelHelpers - - # Checks for any content item which would prevent a content item with the - # specified content_id and base_path from being published (from the draft - # state). - def self.call(content_id, base_path, document_type) - return unless base_path - - if SubstitutionHelper::SUBSTITUTABLE_DOCUMENT_TYPES.include?(document_type) - return # The SubstitionHelper will unpublish any item that is in the way - end - - content_items_table = ContentItem.arel_table - unpublishings_table = Unpublishing.arel_table - - scope = content_items_table - .project( - content_items_table[:id] - ) - .outer_join(unpublishings_table).on( # LEFT OUTER JOIN - content_items_table[:id].eq(unpublishings_table[:content_item_id]) - ) - .where(content_items_table[:content_id].not_eq(content_id)) - .where(content_items_table[:state].in(%w(published unpublished))) - .where(content_items_table[:document_type].not_in( - SubstitutionHelper::SUBSTITUTABLE_DOCUMENT_TYPES - )) - .where(content_items_table[:base_path].eq(base_path)) - .where( - unpublishings_table[:type].not_eq("substitute") - .or(unpublishings_table[:type].eq(nil)) - ) - - rows = get_rows(scope) - - if rows.length > 1 - raise "Multiple rows returned in CheckForContentItemPreventingDraftFromBeingPublished" - end - - rows.first["id"].to_i if rows.first - end - end -end diff --git a/app/queries/content_dependencies.rb b/app/queries/content_dependencies.rb index 04dffe134..ea988c2ae 100644 --- a/app/queries/content_dependencies.rb +++ b/app/queries/content_dependencies.rb @@ -1,12 +1,11 @@ module Queries - # This class resolves the depencies for a given subject content item with - # a provided content_id and locale + # This class resolves the depencies for a given content_id and locale # # There are 3 types of dependency this resolves: - # 1 - Content items that are linked to the subject of dependency resolution + # 1 - Documents that are linked to the subject of dependency resolution # (eg for a subject of a if b has a link to a b will be returned), # for certain link types these are recursed forming a tree structure. - # 2 - Content Items which have an automoatic reverse link to the subject. + # 2 - Documents which have an automoatic reverse link to the subject. # These are items this subject links to and is represented reciprocally # in the item linked to. eg if our subject (A) has a parent of B, B would # automatically have a link to A of type children. @@ -14,22 +13,22 @@ module Queries # added as dependencies, as well as all translations of the content_ids # found. class ContentDependencies - def initialize(content_id:, locale:, state_fallback_order:) + def initialize(content_id:, locale:, content_stores:) @content_id = content_id @locale = locale - @state_fallback_order = state_fallback_order + @content_stores = content_stores end def call content_ids = linked_to(content_id) + automatic_reverse_links(content_id) + [content_id] - with_locales = Queries::LocalesForContentItems.call(content_ids.uniq, state_fallback_order) + with_locales = Queries::LocalesForEditions.call(content_ids.uniq, content_stores) calling_item = locale ? [content_id, locale] : nil with_locales - [calling_item] end private - attr_reader :content_id, :locale, :state_fallback_order + attr_reader :content_id, :locale, :content_stores def linked_to(content_id) Queries::LinkedTo.new(content_id, DependeeExpansionRules).call diff --git a/app/queries/get_change_history.rb b/app/queries/get_change_history.rb index d95443f49..9aa57235c 100644 --- a/app/queries/get_change_history.rb +++ b/app/queries/get_change_history.rb @@ -2,17 +2,17 @@ module Queries class GetChangeHistory def self.call(publishing_app) Queries::GetLatest. - call(content_items(publishing_app)). + call(editions(publishing_app)). pluck(:id, "details->>'change_history'"). - flat_map do |content_item_id, item_history| + flat_map do |editions_id, item_history| JSON.parse(item_history).map do |item_history_element| - item_history_element.symbolize_keys.merge(content_item_id: content_item_id) + item_history_element.symbolize_keys.merge(content_item_id: editions_id) end end end - def self.content_items(publishing_app) - ContentItem. + def self.editions(publishing_app) + Edition. where(publishing_app: publishing_app). where("json_array_length(details->'change_history') > 0") end diff --git a/app/queries/get_content.rb b/app/queries/get_content.rb index dc16a4c7e..459ba5e01 100644 --- a/app/queries/get_content.rb +++ b/app/queries/get_content.rb @@ -1,13 +1,14 @@ module Queries module GetContent def self.call(content_id, locale = nil, version: nil, include_warnings: false) - locale_to_use = locale || ContentItem::DEFAULT_LOCALE + locale_to_use = locale || Edition::DEFAULT_LOCALE - content_items = ContentItem.where(content_id: content_id, locale: locale_to_use) - content_items = content_items.where(user_facing_version: version) if version + editions = Edition.with_document + .where(documents: { content_id: content_id, locale: locale_to_use }) + editions = editions.where(user_facing_version: version) if version response = Presenters::Queries::ContentItemPresenter.present_many( - content_items, + editions, include_warnings: include_warnings ).first @@ -31,14 +32,14 @@ def self.raise_not_found(message) end def self.not_found_message(content_id, locale, version) - if (locale || version) && ContentItem.exists?(content_id: content_id) + if (locale || version) && Document.exists?(content_id: content_id) locale_message = locale ? "locale: #{locale}" : nil version_message = version ? "version: #{version}" : nil reason = [locale_message, version_message].compact.join(" and ") - "Could not find #{reason} for content item with content_id: #{content_id}" + "Could not find #{reason} for document with content_id: #{content_id}" else - "Could not find content item with content_id: #{content_id}" + "Could not find document with content_id: #{content_id}" end end private_class_method :raise_not_found, :not_found_message diff --git a/app/queries/get_content_collection.rb b/app/queries/get_content_collection.rb index 47a3bdcae..fc11b2ef9 100644 --- a/app/queries/get_content_collection.rb +++ b/app/queries/get_content_collection.rb @@ -45,12 +45,12 @@ def total :states, ) - def content_items - scope = ContentItem.where(document_type: lookup_document_types) + def editions + scope = Edition.where(document_type: lookup_document_types) scope = scope.where(publishing_app: publishing_app) if publishing_app scope = scope.where(state: states) if states.present? - scope = scope.where(locale: locale) unless locale == "all" - scope = Link.filter_content_items(scope, link_filters) unless link_filters.blank? + scope = scope.with_document.where("documents.locale": locale) unless locale == "all" + scope = Link.filter_editions(scope, link_filters) unless link_filters.blank? scope end @@ -82,7 +82,7 @@ def default_fields def query @query ||= presenter.new( - content_items, + editions, fields: fields, order: pagination.order, offset: pagination.offset, diff --git a/app/queries/get_content_item_ids_with_fallbacks.rb b/app/queries/get_content_item_ids_with_fallbacks.rb deleted file mode 100644 index e7583e0de..000000000 --- a/app/queries/get_content_item_ids_with_fallbacks.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Queries - class GetContentItemIdsWithFallbacks - extend ArelHelpers - - def self.call(content_ids, locale_fallback_order: ContentItem::DEFAULT_LOCALE, state_fallback_order:) - state_fallback_order = Array(state_fallback_order).map(&:to_s) - locale_fallback_order = Array(locale_fallback_order).map(&:to_s) - - content_items = ContentItem.arel_table - unpublishings = Unpublishing.arel_table - - fallback_scope = content_items.project( - content_items[:id], - content_items[:content_id], - ) - .where(content_items[:content_id].in(content_ids)) - .where(content_items[:document_type].not_in(::ContentItem::NON_RENDERABLE_FORMATS)) - .where(content_items[:locale].in(locale_fallback_order)) - - if state_fallback_order.include?("withdrawn") - fallback_scope = fallback_scope.where(content_items[:state].in(state_fallback_order).or(content_items[:state] - .eq("unpublished") - .and(unpublishings[:type] - .eq("withdrawal")))) - .join(unpublishings, Arel::Nodes::OuterJoin).on(unpublishings[:content_item_id].eq(content_items[:id])) - else - fallback_scope = fallback_scope.where(content_items[:state].in(state_fallback_order)) - end - - fallback_scope = fallback_scope.order( - order_by_clause(:content_items, :state, state_fallback_order), - order_by_clause(:content_items, :locale, locale_fallback_order) - ) - - fallbacks = cte(fallback_scope, as: "fallbacks") - - aggregates = cte( - fallbacks.table - .project(Arel::Nodes::NamedFunction.new("array_agg", [fallbacks.table[:id]], "ids")) - .group(fallbacks.table[:content_id]) - .with(fallbacks.compiled_scope), - as: "aggregates" - ) - - get_column( - aggregates.table - .project(Arel::Nodes::SqlLiteral.new('aggregates.ids[1] AS id')) - .with(aggregates.compiled_scope) - .to_sql - ) - end - - # Arel::Nodes::Case is coming in arel master - def self.order_by_clause(table, attribute, values) - sql = %{CASE "#{table}"."#{attribute}" } - sql << values.map.with_index { |v, i| "WHEN '#{v}' THEN #{i}" }.join(" ") - sql << " ELSE #{values.size} END" - Arel::Nodes::SqlLiteral.new(sql) - end - private_class_method :order_by_clause - end -end diff --git a/app/queries/get_edition_ids_with_fallbacks.rb b/app/queries/get_edition_ids_with_fallbacks.rb new file mode 100644 index 000000000..c43b3d2eb --- /dev/null +++ b/app/queries/get_edition_ids_with_fallbacks.rb @@ -0,0 +1,57 @@ +module Queries + class GetEditionIdsWithFallbacks + def self.call(content_ids, state_fallback_order:, locale_fallback_order: Edition::DEFAULT_LOCALE) + state_fallback_order = Array.wrap(state_fallback_order).map(&:to_s) + locale_fallback_order = Array.wrap(locale_fallback_order).map(&:to_s) + + Edition.with_document.left_outer_joins(:unpublishing) + .where(documents: { content_id: content_ids }) + .where(where_state(state_fallback_order)) + .where(documents: { locale: locale_fallback_order }) + .where.not(document_type: Edition::NON_RENDERABLE_FORMATS) + .order("documents.content_id ASC") + .order(order_by_clause("content_items", "state", state_ordering(state_fallback_order))) + .order(order_by_clause("documents", "locale", locale_fallback_order)) + .pluck("DISTINCT ON (documents.content_id) documents.content_id, content_items.id") + .map(&:last) + end + + def self.where_state(state_fallback_order) + without_withdrawn = state_fallback_order - ["withdrawn"] + if without_withdrawn.present? + state_check = Edition.arel_table[:state].in(without_withdrawn) + else + state_check = nil + end + + if state_fallback_order.include?("withdrawn") + withdrawn_check = Edition.arel_table[:state].eq("unpublished") + .and(Unpublishing.arel_table[:type].eq("withdrawal")) + else + withdrawn_check = nil + end + + if state_check && withdrawn_check + state_check.or(withdrawn_check) + else + state_check || withdrawn_check + end + end + private_class_method :where_state + + def self.order_by_clause(table, attribute, values) + sql = %{CASE "#{table}"."#{attribute}" } + sql << values.map.with_index { |v, i| "WHEN '#{v}' THEN #{i}" }.join(" ") + sql << " ELSE #{values.size} END" + end + private_class_method :order_by_clause + + # We have a special case where a state of withdrawn can be passed in, + # this is not actually a state but an unpublishing type. So when this is + # passed in it is changed to "unpublished" for ordering purposes. + def self.state_ordering(state_fallback_order) + state_fallback_order.map { |state| state == "withdrawn" ? "unpublished" : state } + end + private_class_method :state_ordering + end +end diff --git a/app/queries/get_expanded_links.rb b/app/queries/get_expanded_links.rb index bde634a31..f4014dc50 100644 --- a/app/queries/get_expanded_links.rb +++ b/app/queries/get_expanded_links.rb @@ -2,17 +2,16 @@ module Queries class GetExpandedLinks def self.call(content_id, locale) link_set = find_link_set(content_id) - lock_version = LockVersion.find_by(target: link_set) expanded_link_set = Presenters::Queries::ExpandedLinkSet.new( content_id: content_id, state_fallback_order: [:draft, :published], - locale_fallback_order: [locale, ContentItem::DEFAULT_LOCALE].compact + locale_fallback_order: [locale, Edition::DEFAULT_LOCALE].compact ) { content_id: content_id, expanded_links: expanded_link_set.links, - version: lock_version ? lock_version.number : 0 + version: link_set.stale_lock_version, } end diff --git a/app/queries/get_latest.rb b/app/queries/get_latest.rb index 6866287cf..d185a25fd 100644 --- a/app/queries/get_latest.rb +++ b/app/queries/get_latest.rb @@ -1,23 +1,15 @@ module Queries module GetLatest class << self - # Returns a new scope for the content items with the highest user-facing - # version number per content_id and locale of the given scope. - def call(content_item_scope) - scope = content_item_scope.select(:id, :content_id, 'content_items.locale', :user_facing_version) + def call(edition_scope) + edition_scope.where(id: inner_scope(edition_scope)) + end - ContentItem.joins <<-SQL - INNER JOIN ( - WITH scope AS (#{scope.to_sql}) - SELECT s1.id FROM scope s1 - LEFT OUTER JOIN scope s2 ON - s1.content_id = s2.content_id AND - s1.locale = s2.locale AND - s1.user_facing_version < s2.user_facing_version - WHERE s2.content_id IS NULL - ) AS latest_versions - ON latest_versions.id = content_items.id - SQL + def inner_scope(edition_scope) + edition_scope + .reorder(:document_id, ["user_facing_version DESC"] + edition_scope.order_values) + .select("distinct on(content_items.document_id) content_items.document_id, content_items.id") + .map(&:id) end end end diff --git a/app/queries/get_linkables.rb b/app/queries/get_linkables.rb index e1f18b76b..f5d30a73f 100644 --- a/app/queries/get_linkables.rb +++ b/app/queries/get_linkables.rb @@ -18,7 +18,7 @@ def initialize(document_type:) end def call - latest_updated_at = ContentItem + latest_updated_at = Edition .where(document_type: [document_type, "placeholder_#{document_type}"]) .order('updated_at DESC') .limit(1) @@ -27,10 +27,10 @@ def call Rails.cache.fetch ["linkables", document_type, latest_updated_at] do Queries::GetWebContentItems.( - Queries::GetContentItemIdsWithFallbacks.( - ContentItem.distinct.where( + Queries::GetEditionIdsWithFallbacks.( + Edition.with_document.distinct.where( document_type: [document_type, "placeholder_#{document_type}"] - ).pluck(:content_id), + ).pluck('documents.content_id'), state_fallback_order: [:published, :draft] ), LinkablePresenter diff --git a/app/queries/get_linked.rb b/app/queries/get_linked.rb index 12c9d1e8e..cfb9e2ef7 100644 --- a/app/queries/get_linked.rb +++ b/app/queries/get_linked.rb @@ -18,9 +18,9 @@ def call .joins(:link_set) .pluck(:content_id) - content_items = ContentItem.where(content_id: content_ids) + editions = Edition.with_document.where("documents.content_id": content_ids) - presented = presenter.present_many(content_items, fields: fields) + presented = presenter.present_many(editions, fields: fields) presented.map { |p| filter_fields(p).as_json } end @@ -29,8 +29,8 @@ def call attr_accessor :target_content_id, :link_type, :fields def validate_presence_of_item! - return if ContentItem.exists?( - content_id: target_content_id, + return if Edition.joins(:document).exists?( + documents: { content_id: target_content_id }, state: %w(draft published), ) @@ -67,7 +67,7 @@ def filter_fields(hash) end def permitted_fields - ContentItem.column_names + %w(base_path locale publication_state) + Edition.column_names + %w(content_id base_path locale publication_state) end def presenter diff --git a/app/queries/get_web_content_items.rb b/app/queries/get_web_content_items.rb index 8719add7d..5a733c887 100644 --- a/app/queries/get_web_content_items.rb +++ b/app/queries/get_web_content_items.rb @@ -2,29 +2,29 @@ module Queries class GetWebContentItems extend ArelHelpers - def self.call(content_item_ids, presenter = WebContentItem) - content_items = ContentItem.arel_table - filtered = scope - .where(content_items[:id].in(content_item_ids)) + def self.call(edition_ids, presenter = WebContentItem) + editions = Edition.arel_table + filtered = scope.where(editions[:id].in(edition_ids)) get_rows(filtered).map do |row| presenter.from_hash(row) end end - def self.find(content_item_id) - call(content_item_id).first + def self.find(edition_id) + call(edition_id).first end def self.for_content_store(content_id, locale, include_draft = false) - content_items = ContentItem.arel_table + documents = Document.arel_table + editions = Edition.arel_table unpublishings = Unpublishing.arel_table allowed_states = [:published, :unpublished] allowed_states << :draft if include_draft - filtered = scope(content_items[:user_facing_version].desc) - .where(content_items[:content_id].eq(content_id)) - .where(content_items[:locale].eq(locale)) - .where(content_items[:state].in(allowed_states)) + filtered = scope(editions[:user_facing_version].desc) + .where(documents[:content_id].eq(content_id)) + .where(documents[:locale].eq(locale)) + .where(editions[:state].in(allowed_states)) .where( unpublishings[:type].eq(nil).or( unpublishings[:type].not_eq("substitute") @@ -38,40 +38,42 @@ def self.for_content_store(content_id, locale, include_draft = false) end def self.scope(order = nil) - content_items = ContentItem.arel_table + documents = Document.arel_table + editions = Edition.arel_table unpublishings = Unpublishing.arel_table - content_items + editions .project( - content_items[:id], - content_items[:analytics_identifier], - content_items[:content_id], - content_items[:description], - content_items[:details], - content_items[:document_type], - content_items[:first_published_at], - content_items[:last_edited_at], - content_items[:need_ids], - content_items[:phase], - content_items[:public_updated_at], - content_items[:publishing_app], - content_items[:redirects], - content_items[:rendering_app], - content_items[:routes], - content_items[:schema_name], - content_items[:title], - content_items[:update_type], - content_items[:base_path], - content_items[:state], - content_items[:locale], - content_items[:user_facing_version], + editions[:id], + editions[:analytics_identifier], + documents[:content_id], + editions[:description], + editions[:details], + editions[:document_type], + editions[:first_published_at], + editions[:last_edited_at], + editions[:need_ids], + editions[:phase], + editions[:public_updated_at], + editions[:publishing_app], + editions[:redirects], + editions[:rendering_app], + editions[:routes], + editions[:schema_name], + editions[:title], + editions[:update_type], + editions[:base_path], + editions[:state], + documents[:locale], + editions[:user_facing_version], unpublishings[:type].as("unpublishing_type") ) + .join(documents).on(editions[:document_id].eq(documents[:id])) .outer_join(unpublishings).on( - content_items[:id].eq(unpublishings[:content_item_id]) - .and(content_items[:state].eq("unpublished")) + editions[:id].eq(unpublishings[:content_item_id]) + .and(editions[:state].eq("unpublished")) ) - .order(order || content_items[:id].asc) + .order(order || editions[:id].asc) end end end diff --git a/app/queries/live_edition_blocking_draft_edition.rb b/app/queries/live_edition_blocking_draft_edition.rb new file mode 100644 index 000000000..8660c1900 --- /dev/null +++ b/app/queries/live_edition_blocking_draft_edition.rb @@ -0,0 +1,23 @@ +module Queries + module LiveEditionBlockingDraftEdition + # Checks for a live edition which would prevent a draft edition with the + # specified content_id and base_path from being published (from the draft + # state). + def self.call(content_id, base_path, document_type) + return unless base_path + + if SubstitutionHelper::SUBSTITUTABLE_DOCUMENT_TYPES.include?(document_type) + return # The SubstitionHelper will unpublish any item that is in the way + end + + conflicts = Edition.with_document + .where(base_path: base_path, content_store: :live) + .where.not( + "documents.content_id": content_id, + document_type: SubstitutionHelper::SUBSTITUTABLE_DOCUMENT_TYPES, + ).pluck(:id) + + conflicts.first unless conflicts.empty? + end + end +end diff --git a/app/queries/locales_for_content_items.rb b/app/queries/locales_for_content_items.rb deleted file mode 100644 index 0d8009826..000000000 --- a/app/queries/locales_for_content_items.rb +++ /dev/null @@ -1,44 +0,0 @@ -module Queries - module LocalesForContentItems - extend ArelHelpers - - # returns an array of form: - # [ - # [content_id, locale], - # [content_id, locale], - # ] - def self.call( - content_ids, - states = %w[draft published unpublished], - include_substitutes = false - ) - content_items_table = ContentItem.arel_table - - scope = content_items_table - .project( - content_items_table[:content_id], - content_items_table[:locale] - ) - .distinct - .where(content_items_table[:content_id].in(content_ids)) - .where(content_items_table[:state].in(states)) - .order(content_items_table[:content_id].asc, content_items_table[:locale].asc) - - unless include_substitutes - unpublishings_table = Unpublishing.arel_table - - scope = scope.outer_join(unpublishings_table).on( - content_items_table[:id].eq(unpublishings_table[:content_item_id]) - .and(content_items_table[:state].eq("unpublished")) - ) - .where( - unpublishings_table[:type].eq(nil).or( - unpublishings_table[:type].not_eq("substitute") - ) - ) - end - - get_rows(scope).map { |row| [row["content_id"], row["locale"]] } - end - end -end diff --git a/app/queries/locales_for_editions.rb b/app/queries/locales_for_editions.rb new file mode 100644 index 000000000..ed46ccdfc --- /dev/null +++ b/app/queries/locales_for_editions.rb @@ -0,0 +1,22 @@ +module Queries + module LocalesForEditions + # returns an array of form: + # [ + # [content_id, locale], + # [content_id, locale], + # ] + def self.call( + content_ids, + content_stores = %w[draft live] + ) + Document.joins(:editions) + .where( + content_id: content_ids, + content_items: { content_store: content_stores } + ) + .distinct + .order(:content_id, :locale) + .pluck(:content_id, :locale) + end + end +end diff --git a/app/services/downstream_service.rb b/app/services/downstream_service.rb index f05920b86..c84094455 100644 --- a/app/services/downstream_service.rb +++ b/app/services/downstream_service.rb @@ -53,12 +53,12 @@ def self.discard_from_draft_content_store(base_path) def self.draft_at_base_path?(base_path) return false unless base_path - ContentItem.exists?(base_path: base_path, state: "draft") + Edition.exists?(base_path: base_path, state: "draft") end def self.discard_draft_base_path_conflict?(base_path) return false unless base_path - ContentItem.exists?( + Edition.exists?( base_path: base_path, state: %w(draft published unpublished), ) diff --git a/app/substitution_helper.rb b/app/substitution_helper.rb index d835a191f..0687d0e3e 100644 --- a/app/substitution_helper.rb +++ b/app/substitution_helper.rb @@ -20,17 +20,18 @@ def clear!( ) raise NilBasePathError if base_path.nil? - blocking_items = ContentItem.where(base_path: base_path, locale: locale, state: state) + blocking_items = Edition.with_document + .where(base_path: base_path, state: state, "documents.locale": locale) blocking_items.each do |blocking_item| - mismatch = (blocking_item.content_id != new_item_content_id) + mismatch = (blocking_item.document.content_id != new_item_content_id) allowed_to_substitute = (substitute?(new_item_document_type) || substitute?(blocking_item.document_type)) if mismatch && allowed_to_substitute if state == "draft" Commands::V2::DiscardDraft.call({ - content_id: blocking_item.content_id, - locale: locale, + content_id: blocking_item.document.content_id, + locale: blocking_item.document.locale, }, downstream: downstream, nested: nested, diff --git a/app/validators/state_for_document_validator.rb b/app/validators/state_for_document_validator.rb new file mode 100644 index 000000000..d8869ddb4 --- /dev/null +++ b/app/validators/state_for_document_validator.rb @@ -0,0 +1,18 @@ +class StateForDocumentValidator < ActiveModel::Validator + def validate(record) + return unless record.state && record.document && %w(draft published unpublished).include?(record.state) + + criteria = { + document: record.document, + state: record.state == "draft" ? "draft" : %w(published unpublished), + } + + conflict = Edition.where(criteria).where.not(id: record.id).order(nil).first + + if conflict + error = "state=#{record.state} and document=#{record.document_id} "\ + "conflicts with edition id=#{conflict[:id]}" + record.errors.add(:base, error) + end + end +end diff --git a/app/validators/state_for_locale_validator.rb b/app/validators/state_for_locale_validator.rb deleted file mode 100644 index 1f9227621..000000000 --- a/app/validators/state_for_locale_validator.rb +++ /dev/null @@ -1,20 +0,0 @@ -class StateForLocaleValidator < ActiveModel::Validator - def validate(record) - return unless record.state && record.locale && %w(draft published unpublished).include?(record.state) - - criteria = { - content_id: record.content_id, - state: record.state == "draft" ? "draft" : %w(published unpublished), - locale: record.locale, - } - - conflict = ContentItem.where(criteria).where.not(id: record.id).order(nil).first - - if conflict - error = "state=#{record.state} and locale=#{record.locale} " - error << "for content item=#{record.content_id} conflicts with " - error << "content item id=#{conflict[:id]}" - record.errors.add(:base, error) - end - end -end diff --git a/app/validators/unpublishing_redirect_validator.rb b/app/validators/unpublishing_redirect_validator.rb index 2871cf6ec..b3896f4be 100644 --- a/app/validators/unpublishing_redirect_validator.rb +++ b/app/validators/unpublishing_redirect_validator.rb @@ -1,8 +1,8 @@ class UnpublishingRedirectValidator < ActiveModel::Validator def validate(unpublishing) - return unless unpublishing.content_item + return unless unpublishing.edition - base_path = unpublishing.content_item.base_path + base_path = unpublishing.edition.base_path if base_path == unpublishing.alternative_path unpublishing.errors.add( diff --git a/app/validators/version_for_document_validator.rb b/app/validators/version_for_document_validator.rb new file mode 100644 index 000000000..347a7a256 --- /dev/null +++ b/app/validators/version_for_document_validator.rb @@ -0,0 +1,19 @@ +class VersionForDocumentValidator < ActiveModel::Validator + def validate(record) + return unless record.document && record.user_facing_version + + criteria = { + document: record.document, + user_facing_version: record.user_facing_version, + } + + conflict = Edition.where(criteria).where.not(id: record.id).order(nil).first + + if conflict + error = "user_facing_version=#{record.user_facing_version} and "\ + "document=#{record.document_id} conflicts with edition "\ + "id=#{conflict[:id]}" + record.errors.add(:base, error) + end + end +end diff --git a/app/validators/version_for_locale_validator.rb b/app/validators/version_for_locale_validator.rb deleted file mode 100644 index 20c596b4e..000000000 --- a/app/validators/version_for_locale_validator.rb +++ /dev/null @@ -1,20 +0,0 @@ -class VersionForLocaleValidator < ActiveModel::Validator - def validate(record) - return unless record.locale && record.user_facing_version - - criteria = { - content_id: record.content_id, - user_facing_version: record.user_facing_version, - locale: record.locale, - } - - conflict = ContentItem.where(criteria).where.not(id: record.id).order(nil).first - - if conflict - error = "user_facing_version=#{record.user_facing_version} and " - error << "locale=#{record.locale} for content item=#{record.content_id} " - error << "conflicts with content item id=#{conflict[:id]}" - record.errors.add(:base, error) - end - end -end diff --git a/app/workers/dependency_resolution_worker.rb b/app/workers/dependency_resolution_worker.rb index 5150b0b2a..a8dc0a2b5 100644 --- a/app/workers/dependency_resolution_worker.rb +++ b/app/workers/dependency_resolution_worker.rb @@ -30,11 +30,10 @@ def assign_attributes(args) end def dependencies - states = draft? ? %w[draft published unpublished] : %w[published unpublished] Queries::ContentDependencies.new( content_id: content_id, locale: locale, - state_fallback_order: states, + content_stores: draft? ? %w[draft live] : %w[live], ).call end diff --git a/app/workers/downstream_draft_worker.rb b/app/workers/downstream_draft_worker.rb index 685ce4bfa..c5a7ccd36 100644 --- a/app/workers/downstream_draft_worker.rb +++ b/app/workers/downstream_draft_worker.rb @@ -27,7 +27,7 @@ def perform(args = {}) assign_attributes(args.symbolize_keys) unless web_content_item - raise AbortWorkerError.new("A downstreamable content item was not found for content_id: #{content_id} and locale: #{locale}") + raise AbortWorkerError.new("A downstreamable edition was not found for content_id: #{content_id} and locale: #{locale}") end unless dependency_resolution_source_content_id.nil? diff --git a/app/workers/downstream_live_worker.rb b/app/workers/downstream_live_worker.rb index 07b61fae9..e56ce1c3d 100644 --- a/app/workers/downstream_live_worker.rb +++ b/app/workers/downstream_live_worker.rb @@ -28,7 +28,7 @@ def perform(args = {}) assign_attributes(args.symbolize_keys) unless web_content_item - raise AbortWorkerError.new("A downstreamable content item was not found for content_id: #{content_id} and locale: #{locale}") + raise AbortWorkerError.new("A downstreamable edition was not found for content_id: #{content_id} and locale: #{locale}") end unless dependency_resolution_source_content_id.nil? diff --git a/bench/content_item_presenter.rb b/bench/content_item_presenter.rb index 572dbad6c..17bb015ad 100644 --- a/bench/content_item_presenter.rb +++ b/bench/content_item_presenter.rb @@ -10,7 +10,7 @@ end def present(number_of_items) - scope = ContentItem.limit(number_of_items).order("id DESC") + scope = Edition.limit(number_of_items).order(id: :desc) $queries = 0 puts "Presenting #{number_of_items} content items" diff --git a/bench/downstream_payload.rb b/bench/downstream_payload.rb index 29e175f60..9ed23b2be 100644 --- a/bench/downstream_payload.rb +++ b/bench/downstream_payload.rb @@ -56,7 +56,7 @@ benchmarks.each do |name, content_id| content_item_ids = Queries::GetLatest.( - ContentItem.where(content_id: content_id, state: :published) + Edition.where(content_id: content_id) ).pluck(:id) web_content_item = Queries::GetWebContentItems.(content_item_ids).first diff --git a/bench/downstream_presenter.rb b/bench/downstream_presenter.rb index b3eedd9ff..bcb4f264c 100644 --- a/bench/downstream_presenter.rb +++ b/bench/downstream_presenter.rb @@ -54,7 +54,7 @@ benchmarks.each do |name, content_id| content_item_ids = Queries::GetLatest.( - ContentItem.where(content_id: content_id, state: :published) + Edition.where(content_id: content_id, state: :published) ).pluck(:id) web_content_item = Queries::GetWebContentItems.(content_item_ids).first diff --git a/bench/get_content_items.rb b/bench/get_content_items.rb index e8341b655..58129fde8 100644 --- a/bench/get_content_items.rb +++ b/bench/get_content_items.rb @@ -10,7 +10,7 @@ queries = 0 ActiveSupport::Notifications.subscribe("sql.active_record") { |_| queries += 1 } -StackProf.run(mode: :wall, out: "tmp/get_content_items_wall.dump") do +StackProf.run(mode: :wall, out: "tmp/get_editions_wall.dump") do puts Benchmark.measure { 10.times do Presenters::ResultsPresenter.new( diff --git a/bench/get_expanded_links.rb b/bench/get_expanded_links.rb index 2f6d47fe3..b88002ddc 100644 --- a/bench/get_expanded_links.rb +++ b/bench/get_expanded_links.rb @@ -49,8 +49,8 @@ def get_content_id_and_locale(content_id) [ - content_id, - ContentItem.where(content_id: content_id, state: :published).pluck(:locale).first, + content_id, + Edition.where(content_id: content_id).pluck(:locale).first, ] end diff --git a/bench/linkables.rb b/bench/linkables.rb index 77e20a8a1..182d98171 100644 --- a/bench/linkables.rb +++ b/bench/linkables.rb @@ -8,7 +8,7 @@ abort "Refusing to run outside of development" unless Rails.env.development? -benchmarks = ContentItem.where(document_type: ['taxon', 'organisation', 'topic', 'mainstream_browse_page', 'policy']).pluck(:document_type).uniq +benchmarks = Edition.where(document_type: ['taxon', 'organisation', 'topic', 'mainstream_browse_page', 'policy']).pluck(:document_type).uniq benchmarks.each do |document_type| queries = 0 diff --git a/bench/lookup_by_base_path.rb b/bench/lookup_by_base_path.rb index e91ce4b65..121ae2adf 100644 --- a/bench/lookup_by_base_path.rb +++ b/bench/lookup_by_base_path.rb @@ -18,7 +18,7 @@ StackProf.run(mode: :wall, out: "tmp/lookup_by_base_path_#{base_path.gsub(/\//, '_').downcase}_wall.dump") do puts Benchmark.measure { 10.times do - ContentItem + Edition .where(state: states, base_path: [base_path]) .pluck(:base_path, :content_id) .uniq diff --git a/bench/patch_link_set.rb b/bench/patch_link_set.rb index 99ceefb38..d211caf33 100644 --- a/bench/patch_link_set.rb +++ b/bench/patch_link_set.rb @@ -14,7 +14,7 @@ $queries += 1 end -content_items = 100.times.map do +editions = 100.times.map do title = Faker::Company.catch_phrase { content_id: SecureRandom.uuid, @@ -31,9 +31,7 @@ redirects: [], publishing_app: "performance-testing", rendering_app: "performance-testing", - details: { - body: "
#{Faker::Lorem.paragraphs(10)}
" - }, + details: {}, phase: 'live', need_ids: [] } @@ -41,7 +39,7 @@ begin puts "Publishing content items..." - content_items.each do |item| + editions.each do |item| Commands::V2::PutContent.call(item) Commands::V2::Publish.call(content_id: item[:content_id], update_type: 'major') end @@ -50,8 +48,8 @@ puts "Patching links..." - content_ids = content_items.map { |ci| ci[:content_id] } - payloads = content_items.map do |ci| + content_ids = editions.map { |ci| ci[:content_id] } + payloads = editions.map do |ci| links = content_ids.sample(10).reject { |id| id == ci[:content_id] } {content_id: ci[:content_id], links: {foos: links}} end @@ -69,12 +67,12 @@ puts "#{$queries} SQL queries" ensure - scope = ContentItem.where(publishing_app: 'performance-testing') + scope = Edition.where(publishing_app: 'performance-testing') LinkSet.includes(:links).where(content_id: scope.pluck(:content_id)).destroy_all - Location.where(content_item: scope).delete_all - State.where(content_item: scope).delete_all - Translation.where(content_item: scope).delete_all - UserFacingVersion.where(content_item: scope).delete_all + Location.where(edition: scope).delete_all + State.where(edition: scope).delete_all + Translation.where(edition: scope).delete_all + UserFacingVersion.where(edition: scope).delete_all LockVersion.where(target: scope).delete_all PathReservation.where(publishing_app: 'performance-testing').delete_all scope.delete_all diff --git a/bench/publish.rb b/bench/publish.rb index 65175457e..9ae88dbf8 100644 --- a/bench/publish.rb +++ b/bench/publish.rb @@ -9,9 +9,9 @@ abort "Refusing to run outside of development" unless Rails.env.development? -def publish(content_items) +def publish(editions) puts Benchmark.measure { - content_items.each do |item| + editions.each do |item| Commands::V2::Publish.call(content_id: item[:content_id], update_type: 'major') print "." end @@ -24,7 +24,7 @@ def publish(content_items) $queries += 1 end -content_items = 100.times.map do +editions = 100.times.map do title = Faker::Company.catch_phrase { content_id: SecureRandom.uuid, @@ -41,9 +41,7 @@ def publish(content_items) redirects: [], publishing_app: "performance-testing", rendering_app: "performance-testing", - details: { - body: "#{Faker::Lorem.paragraphs(10)}
" - }, + details: {}, phase: 'live', need_ids: [] } @@ -51,7 +49,7 @@ def publish(content_items) begin puts "Creating drafts..." - content_items.each do |item| + editions.each do |item| Commands::V2::PutContent.call(item) end @@ -59,12 +57,12 @@ def publish(content_items) puts "Publishing..." - StackProf.run(mode: :wall, out: "tmp/publish_wall.dump") { publish(content_items) } + StackProf.run(mode: :wall, out: "tmp/publish_wall.dump") { publish(editions) } puts "#{$queries} SQL queries" puts "Creating new drafts..." - content_items.each do |item| + editions.each do |item| Commands::V2::PutContent.call(item.merge(title: Faker::Company.catch_phrase)) end @@ -72,17 +70,17 @@ def publish(content_items) puts "Superseding..." - StackProf.run(mode: :wall, out: "tmp/supersede_wall.dump") { publish(content_items) } + StackProf.run(mode: :wall, out: "tmp/supersede_wall.dump") { publish(editions) } puts "#{$queries} SQL queries" ensure - scope = ContentItem.where(publishing_app: 'performance-testing') + scope = Edition.where(publishing_app: 'performance-testing') LinkSet.includes(:links).where(content_id: scope.pluck(:content_id)).destroy_all - Location.where(content_item: scope).delete_all - State.where(content_item: scope).delete_all - Translation.where(content_item: scope).delete_all - UserFacingVersion.where(content_item: scope).delete_all + Location.where(edition: scope).delete_all + State.where(edition: scope).delete_all + Translation.where(edition: scope).delete_all + UserFacingVersion.where(edition: scope).delete_all LockVersion.where(target: scope).delete_all PathReservation.where(publishing_app: 'performance-testing').delete_all scope.delete_all diff --git a/bench/put_content.rb b/bench/put_content.rb index a7a705413..06cfefb7d 100644 --- a/bench/put_content.rb +++ b/bench/put_content.rb @@ -20,7 +20,7 @@ content_id = SecureRandom.uuid title = Faker::Company.catch_phrase -content_items = 100.times.map do +editions = 100.times.map do title = Faker::Company.catch_phrase if new_item content_id = SecureRandom.uuid if new_item { @@ -38,9 +38,7 @@ redirects: [], publishing_app: "performance-testing", rendering_app: "performance-testing", - details: { - body: "#{Faker::Lorem.paragraphs(10)}
" - }, + details: {}, phase: 'live', need_ids: [] } @@ -49,7 +47,7 @@ begin if redraft puts "Creating published items..." - content_items.each do |item| + editions.each do |item| Commands::V2::PutContent.call(item) Commands::V2::Publish.call(content_id: item[:content_id], update_type: 'major') end @@ -58,7 +56,7 @@ puts "Publishing..." StackProf.run(mode: :wall, out: "tmp/put_content_wall.dump") do puts Benchmark.measure { - content_items.each do |item| + editions.each do |item| Commands::V2::PutContent.call(item.merge(title: Faker::Company.catch_phrase)) print "." end @@ -68,7 +66,7 @@ else StackProf.run(mode: :wall, out: "tmp/put_content_wall.dump") do puts Benchmark.measure { - content_items.each do |item| + editions.each do |item| Commands::V2::PutContent.call(item) print "." end @@ -80,12 +78,12 @@ puts "#{$queries} SQL queries" ensure - scope = ContentItem.where(publishing_app: 'performance-testing') + scope = Edition.where(publishing_app: 'performance-testing') LinkSet.includes(:links).where(content_id: scope.pluck(:content_id)).destroy_all - Location.where(content_item: scope).delete_all - State.where(content_item: scope).delete_all - Translation.where(content_item: scope).delete_all - UserFacingVersion.where(content_item: scope).delete_all + Location.where(edition: scope).delete_all + State.where(edition: scope).delete_all + Translation.where(edition: scope).delete_all + UserFacingVersion.where(edition: scope).delete_all LockVersion.where(target: scope).delete_all scope.delete_all end diff --git a/config/initializers/lock_version_polymorphic_patch.rb b/config/initializers/lock_version_polymorphic_patch.rb new file mode 100644 index 000000000..47b8d8096 --- /dev/null +++ b/config/initializers/lock_version_polymorphic_patch.rb @@ -0,0 +1,44 @@ +# These are monkey patches to work around us changing one of our model classes +# from ContentItem to Edition. +# +# These are required because on LockVersion these are stored as a polymorphic +# association and thus the class is stored in the database. These monkey patches +# change the logic so that the database still stores a "ContentItem" string +# and loads Edition objects + +ActiveRecord::Associations::BelongsToPolymorphicAssociation.class_eval do + # When we load up an polymorphic association we will switch the type to be + # Edition when it is an instance of ContentItem + def klass + type = owner[reflection.foreign_type] + type = "Edition" if type == "ContentItem" + type.presence && type.constantize + end + + + # When saving a record we will replace the name of the target to be + # ContentItem if it is set to be Edition, this allows us to roll back. + alias_method :original_replace_keys, :replace_keys + + def replace_keys(record) + original_replace_keys(record) + owner[reflection.foreign_type] = "ContentItem" if owner[reflection.foreign_type] == "Edition" + end +end + +ActiveRecord::PredicateBuilder::AssociationQueryHandler.class_eval do + # This is a patch to have query scopes convert a find for a type of Edition + # to actually search for a type of "ContentItem" + def call(_, value) + queries = {} + + table = value.associated_table + if value.base_class + class_name = value.base_class == Edition ? "ContentItem" : value.base_class.name + queries[table.association_foreign_type.to_s] = class_name + end + + queries[table.association_foreign_key.to_s] = value.ids + predicate_builder.build_from_hash(queries) + end +end diff --git a/config/routes.rb b/config/routes.rb index f7cdda692..9dbca8615 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,8 @@ Rails.application.routes.draw do + def content_id_constraint(request) + UuidValidator.valid?(request.params[:content_id]) + end + scope format: false do put "/publish-intent(/*base_path)", to: "publish_intents#create_or_update" get "/publish-intent(/*base_path)", to: "publish_intents#show" @@ -10,18 +14,20 @@ namespace :v2 do get "/content", to: "content_items#index" - put "/content/:content_id", to: "content_items#put_content" - get "/content/:content_id", to: "content_items#show" - post "/content/:content_id/publish", to: "content_items#publish" - post "/content/:content_id/unpublish", to: "content_items#unpublish" - post "/content/:content_id/discard-draft", to: "content_items#discard_draft" - - get "/links/:content_id", to: "link_sets#get_links" - get "/expanded-links/:content_id", to: "link_sets#expanded_links" - patch "/links/:content_id", to: "link_sets#patch_links" - # put is provided for backwards compatibility. - put "/links/:content_id", to: "link_sets#patch_links" - get "/linked/:content_id", to: "link_sets#get_linked" + scope constraints: method(:content_id_constraint) do + put "/content/:content_id", to: "content_items#put_content" + get "/content/:content_id", to: "content_items#show" + post "/content/:content_id/publish", to: "content_items#publish" + post "/content/:content_id/unpublish", to: "content_items#unpublish" + post "/content/:content_id/discard-draft", to: "content_items#discard_draft" + + get "/links/:content_id", to: "link_sets#get_links" + get "/expanded-links/:content_id", to: "link_sets#expanded_links" + patch "/links/:content_id", to: "link_sets#patch_links" + # put is provided for backwards compatibility. + put "/links/:content_id", to: "link_sets#patch_links" + get "/linked/:content_id", to: "link_sets#get_linked" + end get "/linkables", to: "content_items#linkables" get "/new-linkables", to: "content_items#new_linkables" diff --git a/db/migrate/helpers/delete_content_item.rb b/db/migrate/helpers/delete_content.rb similarity index 58% rename from db/migrate/helpers/delete_content_item.rb rename to db/migrate/helpers/delete_content.rb index 87c38cf0a..9b1c2e5f5 100644 --- a/db/migrate/helpers/delete_content_item.rb +++ b/db/migrate/helpers/delete_content.rb @@ -1,17 +1,19 @@ module Helpers - module DeleteContentItem - def self.destroy_content_items_with_links(content_ids) + module DeleteContent + def self.destroy_document_with_links(content_ids) content_ids = Array(content_ids) - content_items = ContentItem.where(content_id: content_ids) - destroy_supporting_objects(content_items) - content_items.destroy_all + Document.where(content_id: content_ids).each do |document| + destroy_supporting_objects(documents.editions) + document.editions.destroy_all + document.destroy + end destroy_links(content_ids) end - def self.destroy_supporting_objects(content_items) - content_items = Array(content_items) + def self.destroy_edition_supporting_objects(editions) + editions = Array(editions) supporting_classes = [ AccessLimit, @@ -25,10 +27,10 @@ def self.destroy_supporting_objects(content_items) supporting_classes.each do |klass| next unless ActiveRecord::Base.connection.data_source_exists?(klass.table_name) - klass.where(content_item: content_items).destroy_all + klass.where(edition: editions).destroy_all end - LockVersion.where(target: content_items).destroy_all + LockVersion.where(target: editions).destroy_all end def self.destroy_links(content_ids) diff --git a/lib/data_hygiene/govspeak_compare.rb b/lib/data_hygiene/govspeak_compare.rb index 5dd7052b8..c1c08a234 100644 --- a/lib/data_hygiene/govspeak_compare.rb +++ b/lib/data_hygiene/govspeak_compare.rb @@ -2,10 +2,10 @@ module DataHygiene class GovspeakCompare - attr_reader :content_item + attr_reader :edition - def initialize(content_item) - @content_item = content_item + def initialize(edition) + @edition = edition end def published_html @@ -15,8 +15,8 @@ def published_html def generated_html @generated_html ||= html_from_details( Presenters::DetailsPresenter.new( - content_item.details_for_govspeak_conversion, - Presenters::ChangeHistoryPresenter.new(content_item) + edition.details_for_govspeak_conversion, + Presenters::ChangeHistoryPresenter.new(web_content_item) ).details ) end @@ -36,6 +36,10 @@ def pretty_much_same_html? private + def web_content_item + @web_content_item ||= Queries::GetWebContentItems.find(edition.id) + end + def calculate_diffs keys = (published_html.keys + generated_html.keys).uniq.sort keys.each_with_object({}) do |key, memo| @@ -66,7 +70,7 @@ def apply_old_html_common_changes(html) # This method strips out a lot of the common differences that are the result # of different versions of govspeak rendering. # It was wrote when testing Specialist Publisher documents and may need - # additional items when comparing content items published from different apps + # additional items when comparing editions published from different apps def basically_match(s) s = s.dup # strip span surrounding an inline-attachment as this element @@ -79,7 +83,7 @@ def basically_match(s) end def format_published_html - html_details = html_from_details(content_item.details) + html_details = html_from_details(edition.details) html_details.each_with_object({}) do |(key, value), memo| # pushed through nokogiri to catch minor html differences (Hello World
\n" } @@ -141,7 +141,7 @@ end describe 'same_html?' do - subject { described_class.new(content_item).same_html? } + subject { described_class.new(edition).same_html? } context "when govspeak will render to the same HTML" do let(:body_html) { "Foo
\n" } @@ -164,7 +164,7 @@ end describe 'pretty_much_same_html?' do - subject { described_class.new(content_item).pretty_much_same_html? } + subject { described_class.new(edition).pretty_much_same_html? } context "when govspeak will render to the same HTML" do let(:body_html) { "Foo
\n" } diff --git a/spec/lib/events/s3_exporter_spec.rb b/spec/lib/events/s3_exporter_spec.rb index d949a3e8c..3a5b38f41 100644 --- a/spec/lib/events/s3_exporter_spec.rb +++ b/spec/lib/events/s3_exporter_spec.rb @@ -10,8 +10,7 @@ let(:resource_double) { instance_double("Aws::S3::Resource", bucket: bucket_double) } let(:bucket_double) { instance_double("Aws::S3::Bucket", object: object_double) } let(:object_double) do - instance_double( - "Aws::S3::Object", + instance_double("Aws::S3::Object", exists?: object_exists?, put: Aws::S3::Types::PutObjectOutput.new ) @@ -80,29 +79,25 @@ def build_csv(expected_events) context "when there are items to export" do let(:created_before) { theresa_may_appointed } let!(:theresa_may_event) do - FactoryGirl.create( - :event, + FactoryGirl.create(:event, title: "Theresa May becomes Prime Minister", created_at: theresa_may_appointed, ) end let!(:david_cameron_event) do - FactoryGirl.create( - :event, + FactoryGirl.create(:event, title: "David Cameron becomes Prime Minister", created_at: david_cameron_appointed, ) end let!(:gordon_brown_event) do - FactoryGirl.create( - :event, + FactoryGirl.create(:event, title: "Gordon Brown becomes Prime Minister", created_at: gordon_brown_appointed, ) end let!(:tony_blair_event) do - FactoryGirl.create( - :event, + FactoryGirl.create(:event, title: "Tony Blair becomes Prime Minister", created_at: tony_blair_appointed, ) diff --git a/spec/lib/queue_publisher_spec.rb b/spec/lib/queue_publisher_spec.rb index 6132f235e..c73a36f82 100644 --- a/spec/lib/queue_publisher_spec.rb +++ b/spec/lib/queue_publisher_spec.rb @@ -69,7 +69,7 @@ queue_publisher.send_message(content_item) end - context "content item using string keys" do + context "edition using string keys" do let(:content_item) { super().stringify_keys } it "correctly calculates routing key" do diff --git a/spec/lib/requeue_content_spec.rb b/spec/lib/requeue_content_spec.rb index ad95a8d07..215eb0c63 100644 --- a/spec/lib/requeue_content_spec.rb +++ b/spec/lib/requeue_content_spec.rb @@ -2,15 +2,13 @@ RSpec.describe RequeueContent do before do - ContentItem.destroy_all + FactoryGirl.create(:live_edition, base_path: '/ci1') + FactoryGirl.create(:live_edition, base_path: '/ci2') + FactoryGirl.create(:live_edition, base_path: '/ci3') end - let!(:content_item1) { FactoryGirl.create(:live_content_item, base_path: '/ci1') } - let!(:content_item2) { FactoryGirl.create(:live_content_item, base_path: '/ci2') } - let!(:content_item3) { FactoryGirl.create(:live_content_item, base_path: '/ci3') } - describe "#call" do - it "by default, it republishes all content items" do + it "by default, it republishes all editions" do expect(PublishingAPI.service(:queue_publisher)).to receive(:send_message).exactly(3).times RequeueContent.new.call end diff --git a/spec/lib/tasks/data_sanitizer_spec.rb b/spec/lib/tasks/data_sanitizer_spec.rb index c2d303544..b53e6aa1d 100644 --- a/spec/lib/tasks/data_sanitizer_spec.rb +++ b/spec/lib/tasks/data_sanitizer_spec.rb @@ -2,24 +2,17 @@ RSpec.describe Tasks::DataSanitizer do let!(:non_limited_draft) do - FactoryGirl.create( - :draft_content_item, - base_path: "/non-limited-draft", - ) + FactoryGirl.create(:draft_edition, base_path: "/non-limited-draft") end let!(:limited_draft) do - FactoryGirl.create( - :access_limited_draft_content_item, + FactoryGirl.create(:access_limited_draft_edition, base_path: "/limited-draft", ) end - let!(:live_content_item) do - FactoryGirl.create( - :live_content_item, - base_path: "/live-item", - ) + let!(:live_edition) do + FactoryGirl.create(:live_edition, base_path: "/live-item") end let(:stdout) { double(:stdout, puts: nil) } @@ -31,9 +24,9 @@ it "deletes all access limited drafts" do Tasks::DataSanitizer.delete_access_limited(stdout) - expect(ContentItem.exists?(limited_draft.id)).to eq(false) - expect(ContentItem.exists?(non_limited_draft.id)).to eq(true) - expect(ContentItem.exists?(live_content_item.id)).to eq(true) + expect(Edition.exists?(limited_draft.id)).to eq(false) + expect(Edition.exists?(non_limited_draft.id)).to eq(true) + expect(Edition.exists?(live_edition.id)).to eq(true) end it "deletes access limited drafts from the draft content store" do @@ -46,9 +39,9 @@ it "removes the limited draft" do expect { Tasks::DataSanitizer.delete_access_limited(stdout) - }.to change(ContentItem, :count).by(-1) + }.to change(Edition, :count).by(-1) - expect(ContentItem.exists?(limited_draft.id)).to eq(false) + expect(Edition.exists?(limited_draft.id)).to eq(false) expect(AccessLimit.count).to be_zero end end diff --git a/spec/lib/tasks/version_validator_spec.rb b/spec/lib/tasks/version_validator_spec.rb index 03d3202f4..ec42760ed 100644 --- a/spec/lib/tasks/version_validator_spec.rb +++ b/spec/lib/tasks/version_validator_spec.rb @@ -2,20 +2,17 @@ RSpec.describe Tasks::VersionValidator do let(:content_id) { SecureRandom.uuid } + let(:document) { FactoryGirl.create(:document, content_id: content_id) } before do - FactoryGirl.create( - :superseded_content_item, - content_id: content_id, + FactoryGirl.create(:superseded_edition, + document: document, user_facing_version: 1, - locale: "en" ) - FactoryGirl.create( - :live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: document, user_facing_version: 2, - locale: "en" ) end @@ -29,22 +26,25 @@ context "when two items of the same content_id have a gap between versions" do before do - item = ContentItem.last + item = Edition.last item.user_facing_version = 3 item.save!(validate: false) end - it "outputs that the content item has an invalid version sequence" do + it "outputs that the edition has an invalid version sequence" do expect { subject.validate }.to output(/Invalid version sequence for #{content_id}/).to_stdout end end - context "when content items have the same version but different locale" do + context "when editions have the same version but different locale" do before do - item = ContentItem.last - item.locale = 'fr' + item = Edition.last + item.document = FactoryGirl.create(:document, + content_id: item.document.content_id, + locale: "fr" + ) item.user_facing_version = 1 item.save!(validate: false) end @@ -58,12 +58,12 @@ context "when the version sequence does not begin at zero" do before do - item = ContentItem.first + item = Edition.first item.user_facing_version = 3 item.save!(validate: false) end - it "outputs that the content item has an invalid version sequence" do + it "outputs that the edition has an invalid version sequence" do expect { subject.validate }.to output(/Invalid version sequence for #{content_id}/).to_stdout diff --git a/spec/models/action_spec.rb b/spec/models/action_spec.rb index f8d9ca15e..33d55354e 100644 --- a/spec/models/action_spec.rb +++ b/spec/models/action_spec.rb @@ -1,27 +1,27 @@ require "rails_helper" RSpec.describe Action do - describe "Content Item and Link Set presence" do - let(:content_item) { nil } + describe "edition and link set presence" do + let(:edition) { nil } let(:link_set) { nil } - subject { FactoryGirl.build(:action, content_item: content_item, link_set: link_set) } + subject { FactoryGirl.build(:action, edition: edition, link_set: link_set) } - context "No Content Item or Link Set" do + context "no edition or link set" do it { is_expected.to be_valid } end - context "Content Item and no Link Set" do - let(:content_item) { FactoryGirl.create(:content_item) } + context "edition and no link set" do + let(:edition) { FactoryGirl.create(:edition) } it { is_expected.to be_valid } end - context "Content Item and no Link Set" do + context "link set and no edition" do let(:link_set) { FactoryGirl.create(:link_set) } it { is_expected.to be_valid } end - context "Content Item and Link Set" do - let(:content_item) { FactoryGirl.create(:content_item) } + context "edition and link set" do + let(:edition) { FactoryGirl.create(:edition) } let(:link_set) { FactoryGirl.create(:link_set) } it { is_expected.not_to be_valid } end diff --git a/spec/models/change_note_spec.rb b/spec/models/change_note_spec.rb index dff2d15b4..42380cd06 100644 --- a/spec/models/change_note_spec.rb +++ b/spec/models/change_note_spec.rb @@ -5,17 +5,16 @@ let(:details) { {} } let(:payload_change_note) { nil } let(:update_type) { "major" } - let(:content_item) do - FactoryGirl.create( - :content_item, + let(:edition) do + FactoryGirl.create(:edition, update_type: update_type, details: details, change_note: nil, ) end - describe ".create_from_content_item" do - subject { described_class.create_from_content_item(payload, content_item) } + describe ".create_from_edition" do + subject { described_class.create_from_edition(payload, edition) } context "update_type is not major" do let(:update_type) { "minor" } @@ -35,17 +34,17 @@ end end - context "change note is entered for an existing content item" do + context "change note is entered for an existing edition" do it "updates the change note rather than creating a new one" do subject expect { - described_class.create_from_content_item(payload, content_item) + described_class.create_from_edition(payload, edition) }.to_not change { ChangeNote.count } end end end - context "content item has change_note entry in details hash" do + context "edition has change_note entry in details hash" do let(:details) { { change_note: "Marvellous" }.stringify_keys } it "populates change note from details hash" do expect { subject }.to change { ChangeNote.count }.by(1) @@ -53,21 +52,21 @@ end end - context "content item has change_note entry in details hash" do + context "edition has change_note entry in details hash" do let(:details) { { change_history: [] } } it "populates change note from details hash" do expect { subject }.to_not change { ChangeNote.count } end end - context "content item has change_note entry in details hash" do + context "edition has change_note entry in details hash" do let(:details) { { change_history: nil } } it "populates change note from details hash" do expect { subject }.to_not change { ChangeNote.count } end end - context "content item has change_history entry in details hash" do + context "edition has change_history entry in details hash" do let(:details) do { change_history: [ { public_timestamp: 3.day.ago.to_s, note: "note 3" }, diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb new file mode 100644 index 000000000..d0c25503f --- /dev/null +++ b/spec/models/document_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Document do + subject { FactoryGirl.build(:document) } + + describe "validations" do + it "is valid for the default factory" do + expect(subject).to be_valid + end + + it "requires a content_id" do + subject.content_id = nil + expect(subject).to be_invalid + end + + context "content_id" do + it "accepts a UUID" do + subject.content_id = "a7c48dac-f1c6-45a8-b5c1-5c407d45826f" + expect(subject).to be_valid + end + + it "does not accept an arbitrary string" do + subject.content_id = "bacon" + expect(subject).not_to be_valid + end + + it "does not accept an empty string" do + subject.content_id = "" + expect(subject).not_to be_valid + end + end + end +end diff --git a/spec/models/content_item_spec.rb b/spec/models/edition_spec.rb similarity index 65% rename from spec/models/content_item_spec.rb rename to spec/models/edition_spec.rb index f55b14b77..127e7ddc9 100644 --- a/spec/models/content_item_spec.rb +++ b/spec/models/edition_spec.rb @@ -1,14 +1,14 @@ require "rails_helper" -RSpec.describe ContentItem do - subject { FactoryGirl.build(:content_item) } +RSpec.describe Edition do + subject { FactoryGirl.build(:edition) } describe ".renderable_content" do - let!(:guide) { FactoryGirl.create(:content_item, schema_name: "guide") } - let!(:redirect) { FactoryGirl.create(:redirect_content_item) } - let!(:gone) { FactoryGirl.create(:gone_content_item) } + let!(:guide) { FactoryGirl.create(:edition, schema_name: "guide") } + let!(:redirect) { FactoryGirl.create(:redirect_edition) } + let!(:gone) { FactoryGirl.create(:gone_edition) } - it "returns content items that do not have a schema_name of 'redirect' or 'gone'" do + it "returns editions that do not have a schema_name of 'redirect' or 'gone'" do expect(described_class.renderable_content).to eq [guide] end end @@ -18,8 +18,8 @@ expect(subject).to be_valid end - it "requires a content_id" do - subject.content_id = nil + it "requires a document" do + subject.document = nil expect(subject).to be_invalid end @@ -33,7 +33,7 @@ expect(subject).to be_invalid end - context "when the content item is 'renderable'" do + context "when the edition is 'renderable'" do before do subject.document_type = "guide" end @@ -67,8 +67,8 @@ end end - context "when the content item is not 'renderable'" do - subject { FactoryGirl.build(:redirect_content_item) } + context "when the edition is not 'renderable'" do + subject { FactoryGirl.build(:redirect_edition) } it "does not require a title" do subject.title = "" @@ -81,8 +81,8 @@ end end - context "when the content item is optionally 'renderable'" do - subject { FactoryGirl.build(:content_item, document_type: "contact") } + context "when the edition is optionally 'renderable'" do + subject { FactoryGirl.build(:edition, document_type: "contact") } it "does not require a rendering_app" do subject.rendering_app = nil @@ -90,23 +90,6 @@ end end - context "content_id" do - it "accepts a UUID" do - subject.content_id = "a7c48dac-f1c6-45a8-b5c1-5c407d45826f" - expect(subject).to be_valid - end - - it "does not accept an arbitrary string" do - subject.content_id = "bacon" - expect(subject).not_to be_valid - end - - it "does not accept an empty string" do - subject.content_id = "" - expect(subject).not_to be_valid - end - end - context "base_path" do it "should be an absolute path" do subject.base_path = 'invalid//absolute/path/' @@ -115,18 +98,18 @@ end end - context "when another content item has the same base path" do - before { FactoryGirl.create(:draft_content_item, base_path: "/foo") } + context "when another edition has the same base path" do + before { FactoryGirl.create(:draft_edition, base_path: "/foo") } - let(:content_item) do - FactoryGirl.build(:content_item, base_path: "/foo", state: "draft") + let(:edition) do + FactoryGirl.build(:edition, base_path: "/foo", state: "draft") end - subject { content_item } + subject { edition } it { is_expected.to be_invalid } context "and the state is different" do - before { content_item.state = "published" } + before { edition.state = "published" } it { is_expected.to be_valid } end @@ -156,15 +139,14 @@ end end - context "when the state conflicts with another instance of this content item" do - subject { content_item } - let(:existing_content_item) do - FactoryGirl.create(:draft_content_item, user_facing_version: 2) + context "when the state conflicts with another instance of this edition" do + subject { edition } + let(:existing_edition) do + FactoryGirl.create(:draft_edition, user_facing_version: 2) end - let(:content_item) do - FactoryGirl.build( - :draft_content_item, - content_id: existing_content_item.content_id, + let(:edition) do + FactoryGirl.build(:draft_edition, + document: existing_edition.document, user_facing_version: 1 ) end @@ -172,47 +154,33 @@ it { is_expected.to be_invalid } context "and the states are different" do - before { content_item.state = "published" } - - it { is_expected.to be_valid } - end - - context "and the locales are different" do - before { content_item.locale = "fr" } + before { edition.state = "published" } it { is_expected.to be_valid } end end - context "when the user facing version conflicts with another instance of this content item" do - subject { content_item } - let(:existing_content_item) { FactoryGirl.create(:draft_content_item) } - let(:content_item) do - FactoryGirl.build( - :draft_content_item, - content_id: existing_content_item.content_id, + context "when the user facing version conflicts with another instance of this edition" do + subject { edition } + let(:existing_edition) { FactoryGirl.create(:draft_edition) } + let(:edition) do + FactoryGirl.build(:draft_edition, + document: existing_edition.document, user_facing_version: 1 ) end it { is_expected.to be_invalid } - - context "and the locales are different" do - before { content_item.locale = "fr" } - - it { is_expected.to be_valid } - end end context "when the draft user_facing_version is ahead of the live one" do - subject { content_item } - let(:existing_content_item) do - FactoryGirl.create(:live_content_item, user_facing_version: 1) + subject { edition } + let(:existing_edition) do + FactoryGirl.create(:live_edition, user_facing_version: 1) end - let(:content_item) do - FactoryGirl.build( - :draft_content_item, - content_id: existing_content_item.content_id, + let(:edition) do + FactoryGirl.build(:draft_edition, + document: existing_edition.document, user_facing_version: 2 ) end @@ -221,14 +189,13 @@ end context "when the draft user_facing_version is behind the live one" do - subject { content_item } - let(:existing_content_item) do - FactoryGirl.create(:draft_content_item, user_facing_version: 1) + subject { edition } + let(:existing_edition) do + FactoryGirl.create(:draft_edition, user_facing_version: 1) end - let(:content_item) do - FactoryGirl.build( - :live_content_item, - content_id: existing_content_item.content_id, + let(:edition) do + FactoryGirl.build(:live_edition, + document: existing_edition.document, user_facing_version: 2 ) end @@ -237,14 +204,13 @@ end context "when the live user_facing_version is ahead of the draft one" do - subject { content_item } - let(:existing_content_item) do - FactoryGirl.create(:live_content_item, user_facing_version: 2) + subject { edition } + let(:existing_edition) do + FactoryGirl.create(:live_edition, user_facing_version: 2) end - let(:content_item) do - FactoryGirl.build( - :draft_content_item, - content_id: existing_content_item.content_id, + let(:edition) do + FactoryGirl.build(:draft_edition, + document: existing_edition.document, user_facing_version: 1 ) end @@ -253,31 +219,31 @@ end context "when user_facing_version is incremented" do - subject { content_item } - let(:content_item) { FactoryGirl.create(:content_item) } + subject { edition } + let(:edition) { FactoryGirl.create(:edition) } - before { content_item.user_facing_version += 1 } + before { edition.user_facing_version += 1 } it { is_expected.to be_valid } end context "when user_facing_version is decremented" do - subject { content_item } - let(:content_item) { FactoryGirl.create(:content_item) } + subject { edition } + let(:edition) { FactoryGirl.create(:edition) } - before { content_item.user_facing_version -= 1 } + before { edition.user_facing_version -= 1 } it { is_expected.to be_invalid } end describe "routes and redirects" do - subject { content_item } - let(:content_item) { FactoryGirl.build(:content_item, base_path: "/vat-rates") } + subject { edition } + let(:edition) { FactoryGirl.build(:edition, base_path: "/vat-rates") } it_behaves_like RoutesAndRedirectsValidator end end context "EMPTY_BASE_PATH_FORMATS" do it "defines formats not requiring a base_path attibute" do - expect(ContentItem::EMPTY_BASE_PATH_FORMATS).to eq(%w(contact government)) + expect(Edition::EMPTY_BASE_PATH_FORMATS).to eq(%w(contact government)) end end @@ -300,7 +266,7 @@ describe "#details_for_govspeak_conversion" do subject do - FactoryGirl.build(:content_item, details: details) + FactoryGirl.build(:edition, details: details) .details_for_govspeak_conversion end @@ -370,7 +336,7 @@ end context "#unpublish" do - subject { FactoryGirl.build(:live_content_item) } + subject { FactoryGirl.build(:live_edition) } it "changes the content_store to nil when type substitute" do expect { subject.unpublish(type: "substitute") }.to change { subject.content_store }.from("live").to(nil) diff --git a/spec/models/link_spec.rb b/spec/models/link_spec.rb index 77f5a735a..456647721 100644 --- a/spec/models/link_spec.rb +++ b/spec/models/link_spec.rb @@ -41,15 +41,15 @@ end end - describe ".filter_content_items" do + describe ".filter_editions" do let(:scope) { double(:scope) } - it "modifies a scope to filter linked content items" do + it "modifies a scope to filter linked editions" do expect(scope).to receive(:joins).with(anything).and_return(scope) expect(scope).to receive(:where) .with("links.link_type": "organisations", "links.target_content_id": "12345") - described_class.filter_content_items(scope, "organisations" => "12345") + described_class.filter_editions(scope, "organisations" => "12345") end end end diff --git a/spec/models/lock_version_spec.rb b/spec/models/lock_version_spec.rb index 84ff11e9b..72c252057 100644 --- a/spec/models/lock_version_spec.rb +++ b/spec/models/lock_version_spec.rb @@ -2,17 +2,27 @@ RSpec.describe LockVersion do subject do - content_item = FactoryGirl.create(:content_item) - FactoryGirl.build(:lock_version, target: content_item) + edition = FactoryGirl.create(:edition) + FactoryGirl.build(:lock_version, target: edition) end it "starts version numbers at 0" do - content_item = FactoryGirl.create(:content_item) - lock_version = LockVersion.create!(target: content_item) + edition = FactoryGirl.create(:edition) + lock_version = LockVersion.create!(target: edition) expect(lock_version.number).to be_zero expect(lock_version).to be_valid end + it "works with a ContentItem target_type" do + edition = FactoryGirl.create(:edition) + lock_version = LockVersion.create!(target: edition) + lock_version.update_attributes!(target_type: "ContentItem") + + expect(lock_version.number).to be_zero + expect(lock_version).to be_valid + expect(lock_version.target).to_not be_nil + end + describe "#conflicts_with?(previous_version_number)" do before do subject.number = 2 diff --git a/spec/models/path_reservation_spec.rb b/spec/models/path_reservation_spec.rb index 293cac478..85a7f259d 100644 --- a/spec/models/path_reservation_spec.rb +++ b/spec/models/path_reservation_spec.rb @@ -46,8 +46,7 @@ describe ".reserve_base_path!(base_path, publishing_app)" do context "when the path reservation already exists" do before do - FactoryGirl.create( - :path_reservation, + FactoryGirl.create(:path_reservation, base_path: "/vat-rates", publishing_app: "something-else", ) diff --git a/spec/models/state_spec.rb b/spec/models/state_spec.rb index 9d382145b..69ce1085f 100644 --- a/spec/models/state_spec.rb +++ b/spec/models/state_spec.rb @@ -10,7 +10,7 @@ end describe ".unpublish" do - let(:live_item) { FactoryGirl.create(:live_content_item) } + let(:live_item) { FactoryGirl.create(:live_edition) } it "changes the state name to 'unpublished'" do expect { @@ -29,7 +29,7 @@ unpublishing = Unpublishing.last - expect(unpublishing.content_item).to eq(live_item) + expect(unpublishing.edition).to eq(live_item) expect(unpublishing.type).to eq("gone") expect(unpublishing.explanation).to eq("A test explanation") expect(unpublishing.alternative_path).to eq("/some-path") diff --git a/spec/models/symbolize_json_spec.rb b/spec/models/symbolize_json_spec.rb index 44f8c4d89..f49e78dff 100644 --- a/spec/models/symbolize_json_spec.rb +++ b/spec/models/symbolize_json_spec.rb @@ -1,18 +1,14 @@ require "rails_helper" RSpec.describe SymbolizeJSON do - subject { FactoryGirl.build(:draft_content_item) } + subject { FactoryGirl.build(:draft_edition) } it "doesn't affect non-json columns" do - content_id = SecureRandom.uuid - - subject.content_id = content_id subject.public_updated_at = Date.new(2000, 1, 1) subject.save! subject.reload - expect(subject.content_id).to eq(content_id) expect(subject.public_updated_at).to eq(Date.new(2000, 1, 1)) end diff --git a/spec/models/unpublishing_spec.rb b/spec/models/unpublishing_spec.rb index 7c976ee1d..ba6e47a24 100644 --- a/spec/models/unpublishing_spec.rb +++ b/spec/models/unpublishing_spec.rb @@ -59,14 +59,14 @@ context "when alternative_path is equal to base_path" do let(:base_path) { "/new-path" } - let(:content_item) do - FactoryGirl.create(:content_item, + let(:edition) do + FactoryGirl.create(:edition, base_path: base_path, ) end it "is invalid" do - subject.content_item = content_item + subject.edition = edition subject.type = "redirect" subject.alternative_path = base_path @@ -86,21 +86,21 @@ end describe ".is_subtitute?" do - subject { described_class.is_substitute?(content_item) } + subject { described_class.is_substitute?(edition) } context "when unpublished with type 'substitute'" do - let(:content_item) { FactoryGirl.create(:substitute_unpublished_content_item) } + let(:edition) { FactoryGirl.create(:substitute_unpublished_edition) } it { is_expected.to be true } end context "when unpublished with type 'gone'" do - let(:content_item) { FactoryGirl.create(:gone_unpublished_content_item) } + let(:edition) { FactoryGirl.create(:gone_unpublished_edition) } it { is_expected.to be false } end - context "when content item is published" do - let(:content_item) { FactoryGirl.create(:live_content_item) } + context "when edition is published" do + let(:edition) { FactoryGirl.create(:live_edition) } it { is_expected.to be false } end - context "when there isn't a content item" do - let(:content_item) { nil } + context "when there isn't an edition" do + let(:edition) { nil } it { is_expected.to be false } end end diff --git a/spec/pacts/content_store/put_endpoint_spec.rb b/spec/pacts/content_store/put_endpoint_spec.rb index fb3d94193..6b0d1097c 100644 --- a/spec/pacts/content_store/put_endpoint_spec.rb +++ b/spec/pacts/content_store/put_endpoint_spec.rb @@ -4,10 +4,9 @@ include Pact::Consumer::RSpec include RequestHelpers::Mocks - let!(:content_item) do - FactoryGirl.create( - :live_content_item, - content_id: content_id, + let!(:edition) do + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: "/vat-rates" ) end @@ -19,7 +18,7 @@ let(:body) { Presenters::ContentStorePresenter.present( Presenters::DownstreamPresenter.new( - Queries::GetWebContentItems.find(content_item.id), + Queries::GetWebContentItems.find(edition.id), state_fallback_order: [:published] ), event.id diff --git a/spec/presenters/change_history_presenter_spec.rb b/spec/presenters/change_history_presenter_spec.rb index fa129cd01..4e8de8d3b 100644 --- a/spec/presenters/change_history_presenter_spec.rb +++ b/spec/presenters/change_history_presenter_spec.rb @@ -2,15 +2,16 @@ RSpec.describe Presenters::ChangeHistoryPresenter do let(:content_id) { SecureRandom.uuid } - let(:content_item) do - FactoryGirl.create( - :content_item, + let(:document) { FactoryGirl.create(:document, content_id: content_id) } + let(:edition) do + FactoryGirl.create(:edition, + document: document, details: details.deep_stringify_keys, - content_id: content_id, ) end + let(:web_content_item) { Queries::GetWebContentItems.find(edition.id) } let(:details) { {} } - subject { described_class.new(content_item).change_history } + subject { described_class.new(web_content_item).change_history } describe "#change_history" do context "details hash includes content_history" do @@ -29,8 +30,8 @@ before do 2.times do |i| ChangeNote.create( - content_item: content_item, - content_id: content_item.content_id, + edition: edition, + content_id: content_id, note: i.to_s, public_timestamp: Time.now.utc ) @@ -44,8 +45,8 @@ it "orders change notes by public_timestamp (ascending)" do [1, 3, 2].to_a.each do |i| ChangeNote.create( - content_item: content_item, - content_id: content_item.content_id, + edition: edition, + content_id: content_id, note: i.to_s, public_timestamp: i.days.ago ) @@ -53,38 +54,38 @@ expect(subject.map { |item| item[:note] }).to eq %w(3 2 1) end - context "multiple content items for a single content id" do + context "multiple editions for a single content id" do let(:item1) do - FactoryGirl.create( - :superseded_content_item, + FactoryGirl.create(:superseded_edition, + document: document, details: details, - content_id: content_id, user_facing_version: 1, ) end + let(:web_content_item1) { Queries::GetWebContentItems.find(item1.id) } let(:item2) do - FactoryGirl.create( - :live_content_item, + FactoryGirl.create(:live_edition, + document: document, details: details, - content_id: content_id, user_facing_version: 2, ) end + let(:web_content_item2) { Queries::GetWebContentItems.find(item2.id) } before do - ChangeNote.create(content_item: item1, content_id: content_id) - ChangeNote.create(content_item: item2, content_id: content_id) + ChangeNote.create(edition: item1, content_id: content_id) + ChangeNote.create(edition: item2, content_id: content_id) ChangeNote.create(content_id: content_id) end - context "reviewing latest version of a content item" do + context "reviewing latest version of a edition" do it "constructs content history from all change notes for content id" do - expect(described_class.new(item2).change_history.count).to eq 3 + expect(described_class.new(web_content_item2).change_history.count).to eq 3 end end - context "reviewing older version of a content item" do + context "reviewing older version of a edition" do it "doesn't include change notes corresponding to newer versions" do - expect(described_class.new(item1).change_history.count).to eq 2 + expect(described_class.new(web_content_item1).change_history.count).to eq 2 end end end diff --git a/spec/presenters/content_store_presenter_spec.rb b/spec/presenters/content_store_presenter_spec.rb index c3fe86c6b..722a841a5 100644 --- a/spec/presenters/content_store_presenter_spec.rb +++ b/spec/presenters/content_store_presenter_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Presenters::ContentStorePresenter do let(:downstream_presenter) { - web_content_item = Queries::GetWebContentItems.find(FactoryGirl.create(:live_content_item).id) + web_content_item = Queries::GetWebContentItems.find(FactoryGirl.create(:live_edition).id) Presenters::DownstreamPresenter.new(web_content_item, nil, state_fallback_order: [:published]) } let(:event) { double(:event, id: 123) } diff --git a/spec/presenters/debug_presenter_spec.rb b/spec/presenters/debug_presenter_spec.rb new file mode 100644 index 000000000..a579ee7cf --- /dev/null +++ b/spec/presenters/debug_presenter_spec.rb @@ -0,0 +1,87 @@ +require "rails_helper" + +RSpec.describe Presenters::DebugPresenter do + let(:document) { FactoryGirl.create(:document) } + + let!(:edition) do + FactoryGirl.create(:draft_edition, + document: document, + user_facing_version: 3) + end + + let!(:link_set) { FactoryGirl.create(:link_set, content_id: document.content_id) } + + subject do + described_class.new(document.content_id) + end + + describe ".editions" do + it "has one entry" do + expect(subject.editions.length).to eq(1) + end + end + + describe ".user_facing_versions" do + it "has one entry" do + expect(subject.user_facing_versions.length).to eq(1) + end + + it "matches" do + expect(subject.user_facing_versions[0]).to eq(3) + end + end + + describe ".latest_editions" do + it "has one entry" do + expect(subject.latest_editions.length).to eq(1) + end + end + + describe ".latest_state_with_locale" do + it "matches" do + expect(subject.latest_state_with_locale[0]).to eq(%w(en draft)) + end + end + + describe ".web_content_item" do + it "matches" do + expect(subject.web_content_item.base_path).to match("/vat-rates-") + end + end + + describe ".title" do + it "matches" do + expect(subject.title).to eq("VAT rates") + end + end + + describe ".web_url" do + it "matches" do + expect(subject.web_url).to match("/vat-rates-") + end + end + + describe ".api_url" do + it "matches" do + expect(subject.api_url).to match("api/content/vat-rates-") + end + end + + describe ".link_set" do + it "is not nil" do + expect(subject.link_set).to_not be_nil + end + end + + describe ".expanded_links" do + it "has four entries" do + expect(subject.expanded_links.length).to eq(3) + end + end + + describe ".states" do + it "has four entries" do + expect(subject.states.length).to eq(2) + end + end +end diff --git a/spec/presenters/details_presenter_spec.rb b/spec/presenters/details_presenter_spec.rb index ed08c8116..d5274fcb5 100644 --- a/spec/presenters/details_presenter_spec.rb +++ b/spec/presenters/details_presenter_spec.rb @@ -6,33 +6,31 @@ instance_double(Presenters::ChangeHistoryPresenter, change_history: []) end subject do - described_class.new( - content_item_details, change_history_presenter - ).details + described_class.new(edition_details, change_history_presenter).details end context "when we're passed details without a body" do - let(:content_item_details) { {} } + let(:edition_details) { {} } it "matches original details" do - is_expected.to match(content_item_details) + is_expected.to match(edition_details) end end context "when we're passed a body which isn't enumerable" do - let(:content_item_details) do + let(:edition_details) do { body: "Something about VAT" } end it "matches original details" do - is_expected.to match(content_item_details) + is_expected.to match(edition_details) end end context "when we're passed details with govspeak and HTML" do - let(:content_item_details) do + let(:edition_details) do { body: [ { content_type: "text/html", content: "html" }, @@ -42,12 +40,12 @@ end it "matches original details" do - is_expected.to match(content_item_details) + is_expected.to match(edition_details) end end context "when we're passed govspeak without HTML" do - let(:content_item_details) do + let(:edition_details) do { body: [ { content_type: "text/govspeak", content: "**hello**" } @@ -68,7 +66,7 @@ end context "when we're passed multiple govspeak fields" do - let(:content_item_details) do + let(:edition_details) do { body: [ { content_type: "text/govspeak", content: "**hello**" } @@ -97,7 +95,7 @@ end context "when we're passed hashes rather than arrays" do - let(:content_item_details) do + let(:edition_details) do { body: { content_type: "text/govspeak", content: "**hello**" }, } @@ -116,17 +114,17 @@ end context "when we're passed an image hash" do - let(:content_item_details) do + let(:edition_details) do { image: { content_type: "image/png", content: "some content" } } end it "doesn't wrap the hash in an array" do - expect(subject).to eq content_item_details + expect(subject).to eq edition_details end end context "value contains nested array" do - let(:content_item_details) { { other: %w(an array of strings) } } + let(:edition_details) { { other: %w(an array of strings) } } it "doesn't try to convert to govspeak" do expect { subject }.to_not raise_error end diff --git a/spec/presenters/downstream_presenter_spec.rb b/spec/presenters/downstream_presenter_spec.rb index fd62adb34..387f2a2f7 100644 --- a/spec/presenters/downstream_presenter_spec.rb +++ b/spec/presenters/downstream_presenter_spec.rb @@ -1,12 +1,12 @@ require 'rails_helper' RSpec.describe Presenters::DownstreamPresenter do - def web_content_item_for(content_item) - Queries::GetWebContentItems.(content_item.id).first + def web_content_item_for(edition) + Queries::GetWebContentItems.(edition.id).first end let(:state_fallback_order) { [] } - let(:web_content_item) { web_content_item_for(content_item) } + let(:web_content_item) { web_content_item_for(edition) } let(:change_history) { { note: "Note", public_timestamp: 1.day.ago.to_s } } let(:details) { { body: "Text
\n", change_history: [change_history], } } @@ -17,7 +17,7 @@ def web_content_item_for(content_item) let(:expected) { { - content_id: content_item.content_id, + content_id: edition.document.content_id, base_path: base_path, analytics_identifier: "GDS01", description: "VAT rates for goods and services", @@ -40,45 +40,42 @@ def web_content_item_for(content_item) } } - context "for a live content item" do - let(:content_item) do - FactoryGirl.create( - :live_content_item, + context "for a live edition" do + let(:edition) do + FactoryGirl.create(:live_edition, base_path: base_path, details: details) end - let!(:link_set) { FactoryGirl.create(:link_set, content_id: content_item.content_id) } + let!(:link_set) { FactoryGirl.create(:link_set, content_id: edition.document.content_id) } it "presents the object graph for the content store" do expect(result).to eq(expected) end end - context "for a draft content item" do - let(:content_item) do - FactoryGirl.create( - :draft_content_item, + context "for a draft edition" do + let(:edition) do + FactoryGirl.create(:draft_edition, base_path: base_path, details: details) end - let!(:link_set) { FactoryGirl.create(:link_set, content_id: content_item.content_id) } + let!(:link_set) { FactoryGirl.create(:link_set, content_id: edition.document.content_id) } it "presents the object graph for the content store" do expect(result).to eq(expected) end end - context "for a withdrawn content item" do - let!(:content_item) do - FactoryGirl.create( - :withdrawn_unpublished_content_item, + context "for a withdrawn edition" do + let!(:edition) do + FactoryGirl.create(:withdrawn_unpublished_edition, base_path: base_path, details: details) end - let!(:link_set) { FactoryGirl.create(:link_set, content_id: content_item.content_id) } + let!(:link_set) { FactoryGirl.create(:link_set, content_id: edition.document.content_id) } it "merges in a withdrawal notice" do - unpublishing = Unpublishing.find_by(content_item: content_item) + unpublishing = Unpublishing.find_by(edition: edition) expect(result).to eq( expected.merge( @@ -91,9 +88,8 @@ def web_content_item_for(content_item) end context "with an overridden unpublished_at" do - let!(:content_item) do - FactoryGirl.create( - :withdrawn_unpublished_content_item, + let!(:edition) do + FactoryGirl.create(:withdrawn_unpublished_edition, base_path: base_path, details: details, unpublished_at: DateTime.new(2016, 9, 10, 4, 5, 6) @@ -101,7 +97,7 @@ def web_content_item_for(content_item) end it "merges in a withdrawal notice with the withdrawn_at set correctly" do - unpublishing = Unpublishing.find_by(content_item: content_item) + unpublishing = Unpublishing.find_by(edition: edition) expect(result).to eq( expected.merge( @@ -115,22 +111,22 @@ def web_content_item_for(content_item) end end - context "for a content item with dependencies" do - let(:a) { FactoryGirl.create(:content_item, base_path: "/a") } - let(:b) { FactoryGirl.create(:content_item, base_path: "/b") } + context "for a edition with dependencies" do + let(:a) { FactoryGirl.create(:edition, base_path: "/a") } + let(:b) { FactoryGirl.create(:edition, base_path: "/b") } before do - FactoryGirl.create(:link_set, content_id: a.content_id, links: [ - FactoryGirl.create(:link, link_type: "related", target_content_id: b.content_id) + FactoryGirl.create(:link_set, content_id: a.document.content_id, links: [ + FactoryGirl.create(:link, link_type: "related", target_content_id: b.document.content_id) ]) end - it "expands the links for the content item" do + it "expands the links for the edition" do result = described_class.present(web_content_item_for(a), state_fallback_order: [:draft]) expect(result[:expanded_links]).to eq( related: [{ - content_id: b.content_id, + content_id: b.document.content_id, api_path: "/api/content/b", base_path: "/b", title: "VAT rates", @@ -147,7 +143,7 @@ def web_content_item_for(content_item) analytics_identifier: "GDS01", api_path: "/api/content/a", base_path: "/a", - content_id: a.content_id, + content_id: a.document.content_id, description: "VAT rates for goods and services", schema_name: "guide", document_type: 'guide', @@ -160,15 +156,14 @@ def web_content_item_for(content_item) end end - context "for a content item with change notes" do - let(:content_item) do - FactoryGirl.create( - :draft_content_item, + context "for a edition with change notes" do + let(:edition) do + FactoryGirl.create(:draft_edition, base_path: base_path, details: details.slice(:body)) end before do - ChangeNote.create(change_history.merge(content_item: content_item, content_id: content_item.content_id)) + ChangeNote.create(change_history.merge(edition: edition, content_id: edition.document.content_id)) end it "constructs the change history" do @@ -177,8 +172,8 @@ def web_content_item_for(content_item) end describe "conditional attributes" do - let!(:content_item) { FactoryGirl.create(:live_content_item) } - let!(:link_set) { FactoryGirl.create(:link_set, content_id: content_item.content_id) } + let!(:edition) { FactoryGirl.create(:live_edition) } + let!(:link_set) { FactoryGirl.create(:link_set, content_id: edition.document.content_id) } context "when the link_set is not present" do before { link_set.destroy } @@ -189,7 +184,7 @@ def web_content_item_for(content_item) end context "when the public_updated_at is not present" do - let(:content_item) { FactoryGirl.create(:gone_draft_content_item) } + let(:edition) { FactoryGirl.create(:gone_draft_edition) } it "does not raise an error" do expect { result }.not_to raise_error @@ -199,11 +194,11 @@ def web_content_item_for(content_item) context "for an access-limited item" do let!(:access_limit) { - FactoryGirl.create(:access_limit, content_item: content_item) + FactoryGirl.create(:access_limit, edition: edition) } context "in draft" do - let(:content_item) { FactoryGirl.create(:draft_content_item) } + let(:edition) { FactoryGirl.create(:draft_edition) } it "populates the access_limited hash" do expect(result[:access_limited][:users].length).to eq(1) @@ -211,7 +206,7 @@ def web_content_item_for(content_item) end context "in live" do - let(:content_item) { FactoryGirl.create(:live_content_item) } + let(:edition) { FactoryGirl.create(:live_edition) } it "does not send an access_limited hash" do expect(result).not_to include(:access_limited) @@ -236,9 +231,8 @@ def web_content_item_for(content_item) } end - let(:content_item) do - FactoryGirl.create( - :live_content_item, + let(:edition) do + FactoryGirl.create(:live_edition, base_path: base_path, details: details, ) diff --git a/spec/presenters/message_queue_presenter_spec.rb b/spec/presenters/message_queue_presenter_spec.rb index 2ccefa083..da3d351b9 100644 --- a/spec/presenters/message_queue_presenter_spec.rb +++ b/spec/presenters/message_queue_presenter_spec.rb @@ -2,9 +2,9 @@ RSpec.describe Presenters::MessageQueuePresenter do let(:downstream_presenter) { - content_item = FactoryGirl.create(:live_content_item) - web_content_item = Queries::GetWebContentItems.find(content_item.id) - link_set = FactoryGirl.create(:link_set, content_id: content_item.content_id) + edition = FactoryGirl.create(:live_edition) + web_content_item = Queries::GetWebContentItems.find(edition.id) + link_set = FactoryGirl.create(:link_set, content_id: edition.document.content_id) FactoryGirl.create(:link, target_content_id: "d16216ce-7487-4bde-b817-ef68317fe3ab", link_set: link_set, link_type: 'taxons') Presenters::DownstreamPresenter.new(web_content_item, link_set, state_fallback_order: [:published]) } diff --git a/spec/presenters/queries/available_translations_spec.rb b/spec/presenters/queries/available_translations_spec.rb index b01d6887c..b5ed37e86 100644 --- a/spec/presenters/queries/available_translations_spec.rb +++ b/spec/presenters/queries/available_translations_spec.rb @@ -8,14 +8,12 @@ ).translations[:available_translations] } - def create_content_item(base_path, state = "published", locale = "en", version = 1) - FactoryGirl.create( - :content_item, - content_id: link_set.content_id, + def create_edition(base_path, state = "published", locale = "en", version = 1) + FactoryGirl.create(:edition, + document: Document.find_or_create_by(content_id: link_set.content_id, locale: locale), base_path: base_path, state: state, content_store: state == 'draft' ? 'draft' : 'live', - locale: locale, user_facing_version: version, ) end @@ -26,9 +24,9 @@ def create_content_item(base_path, state = "published", locale = "en", version = let(:state_fallback_order) { [:published] } before do - create_content_item("/a", "published") - create_content_item("/a.ar", "published", "ar") - create_content_item("/a.es", "published", "es") + create_edition("/a", "published") + create_edition("/a.ar", "published", "ar") + create_edition("/a.es", "published", "es") end it "returns all the items" do @@ -41,9 +39,9 @@ def create_content_item(base_path, state = "published", locale = "en", version = end context "with items in more than one state" do - let!(:en) { create_content_item("/a", "published") } - let!(:ar) { create_content_item("/a.ar", "draft", "ar") } - let!(:es) { create_content_item("/a.es", "published", "es") } + let!(:en) { create_edition("/a", "published") } + let!(:ar) { create_edition("/a.ar", "draft", "ar") } + let!(:es) { create_edition("/a.es", "published", "es") } context "with multiple states in the fallback order" do let(:state_fallback_order) { [:draft, :published] } @@ -58,7 +56,7 @@ def create_content_item(base_path, state = "published", locale = "en", version = it "takes the item in the first matching state" do es.update_attribute("title", "no habla español") - draft_es = create_content_item("/a.es", "draft", "es", 2) + draft_es = create_edition("/a.es", "draft", "es", 2) draft_es.update_attribute("title", "mais on parle français") expect(translations).to match_array([ a_hash_including(base_path: "/a", locale: "en"), diff --git a/spec/presenters/queries/content_item_presenter_spec.rb b/spec/presenters/queries/content_item_presenter_spec.rb index fe28c933a..3b1b07a9c 100644 --- a/spec/presenters/queries/content_item_presenter_spec.rb +++ b/spec/presenters/queries/content_item_presenter_spec.rb @@ -4,15 +4,20 @@ let(:content_id) { SecureRandom.uuid } let(:base_path) { "/vat-rates" } + let!(:document) { FactoryGirl.create(:document, content_id: content_id) } + let!(:fr_document) do + FactoryGirl.create(:document, content_id: content_id, locale: "fr") + end + describe "present" do - let!(:content_item) do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + let!(:edition) do + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path ) end - let(:result) { described_class.present(content_item) } + let(:result) { described_class.present(edition) } let(:payload) do { @@ -49,19 +54,19 @@ end end - it "presents content item attributes as a hash" do + it "presents edition attributes as a hash" do expect(result).to eq(payload) end - context "for a draft content item" do + context "for a draft edition" do it "has a publication state of draft" do expect(result.fetch("publication_state")).to eq("draft") end end - context "for a published content item" do + context "for a published edition" do before do - content_item.update_attributes!(state: 'published') + edition.update_attributes!(state: 'published') end it "has a publication state of published" do @@ -69,24 +74,24 @@ end end - context "when the content item exists in multiple locales" do + context "when the edition exists in multiple locales" do let!(:french_item) do - FactoryGirl.create(:draft_content_item, content_id: content_id, locale: "fr") + FactoryGirl.create(:draft_edition, document: fr_document) end it "presents the item with matching locale" do result = described_class.present(french_item) expect(result.fetch("locale")).to eq("fr") - result = described_class.present(content_item) + result = described_class.present(edition) expect(result.fetch("locale")).to eq("en") end end context "when a change note exists" do - let!(:content_item) do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + let!(:edition) do + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, update_type: "major" ) @@ -103,55 +108,50 @@ end describe "#present_many" do - let!(:content_item) do - FactoryGirl.create(:draft_content_item, - content_id: content_id, - ) + let!(:edition) do + FactoryGirl.create(:draft_edition, document: document) end context "when an array of fields is provided" do let(:fields) { %w(title phase publication_state) } it "returns the requested fields" do - content_items = ContentItem.where(content_id: content_id) + editions = Edition.with_document.where("documents.content_id": content_id) - results = described_class.present_many(content_items, fields: fields) + results = described_class.present_many(editions, fields: fields) expect(results.first.keys).to match_array(%w(title phase publication_state)) end end - context "when the content item exists in multiple locales" do + context "when the edition exists in multiple locales" do let!(:french_item) do - FactoryGirl.create(:content_item, content_id: content_id, locale: "fr") + FactoryGirl.create(:edition, document: fr_document) end - it "presents a content item for each locale" do - content_items = ContentItem.where(content_id: content_id) + it "presents a edition for each locale" do + editions = Edition.with_document.where("documents.content_id": content_id) - results = described_class.present_many(content_items) + results = described_class.present_many(editions) locales = results.map { |r| r.fetch("locale") } expect(locales).to match_array(%w(fr en)) end end - context "when there are other content items with that content_id" do + context "when there are other editions with that content_id" do before do - content_item.update_attributes(user_facing_version: 2) + edition.update_attributes(user_facing_version: 2) end let!(:published_item) do - FactoryGirl.create( - :live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: document, user_facing_version: 1, ) end - let(:content_items) { ContentItem.where(content_id: content_id) } - - it "returns a versioned history of states for the content item" do - results = described_class.present_many(content_items) + it "returns a versioned history of states for the edition" do + results = described_class.present_many(document.editions) expect(results.count).to eq(1) state_history = results.first.fetch("state_history") @@ -165,16 +165,14 @@ describe "#get_warnings" do before do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, user_facing_version: 2, ) end - let(:scope) do - ContentItem.where(content_id: content_id) - end + let(:scope) { document.editions } context "when include_warnings is false" do let(:result) do @@ -191,19 +189,17 @@ described_class.present_many(scope, include_warnings: true) end - context "without a blocking content item" do + context "without a blocking edition" do it "does not include warnings" do expect(result.first["warnings"]).to be_empty end end - context "with a blocking content item" do + context "with a blocking edition" do before do - @blocking_content_item = FactoryGirl.create(:live_content_item, - content_id: SecureRandom.uuid, + @blocking_edition = FactoryGirl.create(:live_edition, base_path: base_path, user_facing_version: 1, - locale: "en", ) end diff --git a/spec/presenters/queries/expanded_link_set_spec.rb b/spec/presenters/queries/expanded_link_set_spec.rb index a904ed6b8..4f9a912eb 100644 --- a/spec/presenters/queries/expanded_link_set_spec.rb +++ b/spec/presenters/queries/expanded_link_set_spec.rb @@ -21,10 +21,10 @@ ).links } - context "with content items that are non-renderable" do - let!(:draft_a) { create_content_item(a, "/a", "draft") } - let!(:redirect) { FactoryGirl.create(:redirect_draft_content_item, content_id: b, base_path: '/b') } - let!(:gone) { FactoryGirl.create(:gone_content_item, content_id: c, base_path: '/c') } + context "with editions that are non-renderable" do + let!(:draft_a) { create_edition(a, "/a", factory: :draft_edition) } + let!(:redirect) { create_edition(b, "/b", factory: :redirect_draft_edition) } + let!(:gone) { create_edition(c, "/c", factory: :gone_edition) } let(:state_fallback_order) { [:draft] } @@ -38,14 +38,14 @@ end end - context "with content items in a draft state" do - let!(:draft_a) { create_content_item(a, "/a", "draft") } - let!(:draft_b) { create_content_item(b, "/b", "draft") } - let!(:draft_c) { create_content_item(c, "/c", "draft") } - let!(:draft_d) { create_content_item(d, "/d", "draft") } - let!(:draft_e) { create_content_item(e, "/e", "draft") } - let!(:draft_f) { create_content_item(f, "/f", "draft") } - let!(:draft_g) { create_content_item(g, "/g", "draft") } + context "with editions in a draft state" do + let!(:draft_a) { create_edition(a, "/a", factory: :draft_edition) } + let!(:draft_b) { create_edition(b, "/b", factory: :draft_edition) } + let!(:draft_c) { create_edition(c, "/c", factory: :draft_edition) } + let!(:draft_d) { create_edition(d, "/d", factory: :draft_edition) } + let!(:draft_e) { create_edition(e, "/e", factory: :draft_edition) } + let!(:draft_f) { create_edition(f, "/f", factory: :draft_edition) } + let!(:draft_g) { create_edition(g, "/g", factory: :draft_edition) } let(:state_fallback_order) { [:draft] } @@ -73,7 +73,7 @@ base_path: "/c", links: { parent: [ - a_hash_including(base_path: "/d", details: {}, links: {}) + a_hash_including(base_path: "/d", links: {}) ] })] }) @@ -195,10 +195,10 @@ end end - context "when the depended on content item has no location" do + context "when the depended on edition has no location" do before do create_link(a, b, "parent") - ContentItem.find_by(base_path: '/b').update_attributes!(base_path: nil) + Edition.find_by(base_path: '/b').update_attributes!(base_path: nil) end it "has no web_url" do @@ -210,10 +210,10 @@ end end - context "when the depended on content item does not exist" do + context "when the depended on edition does not exist" do before do create_link(a, b, "parent") - ContentItem.find_by(content_id: b).destroy + Edition.with_document.find_by('documents.content_id': b).destroy end it "does not have a parent" do @@ -222,15 +222,15 @@ end end - context "with content items in different states" do - context "when a content item is in a state that does not match the provided state" do + context "with editions in different states" do + context "when a edition is in a state that does not match the provided state" do before do create_link(a, b, "related") create_link(a, c, "related") - create_content_item(a, "/a", "draft") - create_content_item(b, "/b", "draft") - create_content_item(c, "/c", "published") + create_edition(a, "/a", factory: :draft_edition) + create_edition(b, "/b", factory: :draft_edition) + create_edition(c, "/c") end context "when requested with a draft state" do @@ -254,11 +254,11 @@ end end - context "when a published content item is linked to content in draft" do + context "when a published edition is linked to content in draft" do before do create_link(a, b, "related") - create_content_item(a, "/a-published", "published") - create_content_item(b, "/b-draft", "draft") + create_edition(a, "/a-published") + create_edition(b, "/b-draft", factory: :draft_edition) end context "with a fallback to published" do @@ -278,18 +278,18 @@ end end - context "when one of the recursive content items does not match the provided state" do + context "when one of the recursive editions does not match the provided state" do before do create_link(a, b, "parent") create_link(b, c, "parent") create_link(c, d, "parent") - create_content_item(a, "/a-draft", "draft") - create_content_item(b, "/b-draft", "draft", "en", 2) - create_content_item(d, "/d-draft", "draft") + create_edition(a, "/a-draft", factory: :draft_edition) + create_edition(b, "/b-draft", factory: :draft_edition, version: 2) + create_edition(d, "/d-draft", factory: :draft_edition) - create_content_item(b, "/b-published", "published") - create_content_item(c, "/c-published", "published") + create_edition(b, "/b-published") + create_edition(c, "/c-published") end context "when requested with a draft state" do @@ -316,7 +316,7 @@ end # We need to support an array of states to cater for the DiscardDraft - # command which deletes the draft content item and sends the published item + # command which deletes the draft edition and sends the published item # to the draft content store. This means that we need to try to find a # draft, but fall back to the published item (if it exists). context "when an array of states is provided" do @@ -327,14 +327,14 @@ create_link(b, c, "parent") create_link(c, d, "parent") - create_content_item(a, "/a-draft", "draft") - create_content_item(b, "/b-published", "published") - create_content_item(c, "/c-draft", "draft", "en", 2) - create_content_item(c, "/c-published", "published") - create_content_item(d, "/d-published", "published") + create_edition(a, "/a-draft", factory: :draft_edition) + create_edition(b, "/b-published") + create_edition(c, "/c-draft", factory: :draft_edition, version: 2) + create_edition(c, "/c-published") + create_edition(d, "/d-published") end - it "expands for the content item of the first state that matches" do + it "expands for the edition of the first state that matches" do expect(expanded_links[:parent]).to match([ a_hash_including(base_path: "/b-published", links: { parent: [a_hash_including(base_path: "/c-draft", links: { @@ -352,12 +352,12 @@ before do create_link(a, b, "organisation") - create_content_item(a, "/a", "published", "en") - create_content_item(b, "/b", "published", "en") + create_edition(a, "/a", locale: "en") + create_edition(b, "/b", locale: "en") end context "when a linked item exists in multiple locales" do - let!(:arabic_b) { create_content_item(b, "/b.ar", "published", "ar") } + let!(:arabic_b) { create_edition(b, "/b.ar", locale: "ar") } it "links to the item in the matching locale" do expect(expanded_links[:organisation]).to match([ @@ -368,7 +368,7 @@ context "when the item exists in the matching locale but a fallback state" do let(:state_fallback_order) { [:draft, :published] } - let!(:arabic_b) { create_content_item(b, "/b.ar", "published", "ar") } + let!(:arabic_b) { create_edition(b, "/b.ar", locale: "ar") } it "links to the item in the matching locale" do expect(expanded_links[:organisation]).to match([ @@ -397,11 +397,11 @@ describe "expanding withdrawn dependents" do let(:state_fallback_order) { [:published] } - let!(:published) { FactoryGirl.create(:withdrawn_unpublished_content_item, content_id: a, base_path: '/a') } - let!(:withdrawn_child) { FactoryGirl.create(:withdrawn_unpublished_content_item, content_id: b, base_path: '/b') } - let!(:published_child) { create_content_item(c, "/c") } before do + create_edition(a, "/a", factory: :withdrawn_unpublished_edition) + create_edition(b, "/b", factory: :withdrawn_unpublished_edition) + create_edition(c, "/c") create_link(b, a, "parent") create_link(c, a, "parent") end @@ -422,10 +422,10 @@ let(:state_fallback_order) { [:draft, :published] } before do - create_content_item(a, "/a-draft", "draft") - create_content_item(b, "/b-published") - create_content_item(c, "/c-published") - create_content_item(d, "/d-published") + create_edition(a, "/a-draft", factory: :draft_edition) + create_edition(b, "/b-published") + create_edition(c, "/c-published") + create_edition(d, "/d-published") create_link(d, c, "parent") create_link(c, b, "parent") @@ -457,17 +457,22 @@ end end - context "with a withdrawn content item as a parent" do - let!(:published) { create_content_item(a, "/a", "published") } - let!(:unpublishing) { FactoryGirl.create(:withdrawn_unpublished_content_item, content_id: b, base_path: '/b') } - let!(:unpublishing_not_parent) { FactoryGirl.create(:withdrawn_unpublished_content_item, content_id: c, base_path: '/c') } - + context "with a withdrawn edition as a parent" do let(:state_fallback_order) { [:published, :withdrawn] } + before do + create_edition(a, "/a") + create_edition(b, "/b", factory: :withdrawn_unpublished_edition) + create_edition(c, "/c", factory: :withdrawn_unpublished_edition) + end + context "a simple non-recursive graph" do - it "expands the links for node a correctly" do + before do create_link(a, b, "parent") create_link(a, c, "related") + end + + it "expands the links for node a correctly" do expect(expanded_links[:parent]).to match([a_hash_including('base_path': '/b')]) expect(expanded_links[:related]).to match(nil) end diff --git a/spec/queries/base_path_for_state_spec.rb b/spec/queries/base_path_for_state_spec.rb index a4bbec0c1..6e793ff53 100644 --- a/spec/queries/base_path_for_state_spec.rb +++ b/spec/queries/base_path_for_state_spec.rb @@ -5,23 +5,20 @@ let(:no_conflict_base_path) { "/no-conflict" } describe ".conflict" do - subject { described_class.conflict(content_item_id, state, base_path) } + subject { described_class.conflict(edition_id, state, base_path) } let(:base_path) { conflict_base_path } - context "when content item is a draft" do - let!(:conflict_content_item) do - FactoryGirl.create( - :draft_content_item, - base_path: conflict_base_path, - ) + context "when edition is a draft" do + let!(:conflict_edition) do + FactoryGirl.create(:draft_edition, base_path: conflict_base_path) end - let(:content_item_id) { conflict_content_item.id + 1 } + let(:edition_id) { conflict_edition.id + 1 } let(:state) { "draft" } let(:collision_hash) do { - id: conflict_content_item.id, - content_id: conflict_content_item.content_id, + id: conflict_edition.id, + content_id: conflict_edition.document.content_id, locale: "en" } end @@ -31,20 +28,20 @@ it { is_expected.to be_nil } end - context "when we use this content items base path and content item id" do - let(:content_item_id) { conflict_content_item.id } + context "when we use this editions base path and edition id" do + let(:edition_id) { conflict_edition.id } it { is_expected.to be_nil } end - context "when we use this content items base path and a different content item id" do + context "when we use this editions base path and a different edition id" do it "should be a collision" do is_expected.to match(collision_hash) end %w(published unpublished superseded).each do |state| - context "when the content item is #{state}" do + context "when the edition is #{state}" do before do - conflict_content_item.update(state: state) + conflict_edition.update(state: state) end it { is_expected.to be_nil } @@ -54,20 +51,20 @@ end { - "published" => :live_content_item, - "unpublished" => :unpublished_content_item, + "published" => :live_edition, + "unpublished" => :unpublished_edition, }.each do |state_name, factory| - context "when content item is #{state_name}" do - let!(:conflict_content_item) do + context "when edition is #{state_name}" do + let!(:conflict_edition) do FactoryGirl.create(factory, base_path: conflict_base_path) end - let(:content_item_id) { conflict_content_item.id + 1 } + let(:edition_id) { conflict_edition.id + 1 } let(:state) { state_name } let(:collision_hash) do { - id: conflict_content_item.id, - content_id: conflict_content_item.content_id, + id: conflict_edition.id, + content_id: conflict_edition.document.content_id, locale: "en" } end @@ -77,20 +74,20 @@ it { is_expected.to be_nil } end - context "when we use this content items base path and content item id" do - let(:content_item_id) { conflict_content_item.id } + context "when we use this editions base path and edition id" do + let(:edition_id) { conflict_edition.id } it { is_expected.to be_nil } end - context "when we use this content items base path and a different content item id" do + context "when we use this editions base path and a different edition id" do it "should be a collision" do is_expected.to match(collision_hash) end end context "when the item we are checking against is unpublished with type substitute" do - let(:content_item_id) do - FactoryGirl.create(:substitute_unpublished_content_item).id + let(:edition_id) do + FactoryGirl.create(:substitute_unpublished_edition).id end let(:state) { "unpublished" } it { is_expected.to be_nil } @@ -98,29 +95,27 @@ end end - context "when content item is unpublished with substitute" do - let!(:conflict_content_item) do - FactoryGirl.create( - :substitute_unpublished_content_item, + context "when edition is unpublished with substitute" do + let!(:conflict_edition) do + FactoryGirl.create(:substitute_unpublished_edition, base_path: conflict_base_path ) end - let(:content_item_id) { conflict_content_item.id + 1 } + let(:edition_id) { conflict_edition.id + 1 } let(:state) { "unpublished" } it { is_expected.to be_nil } end - context "when content item is superseded" do - let!(:conflict_content_item) do - FactoryGirl.create( - :superseded_content_item, + context "when edition is superseded" do + let!(:conflict_edition) do + FactoryGirl.create(:superseded_edition, base_path: conflict_base_path ) end - let(:content_item_id) { conflict_content_item.id + 1 } + let(:edition_id) { conflict_edition.id + 1 } let(:state) { "superseded" } it { is_expected.to be_nil } diff --git a/spec/queries/content_dependencies_spec.rb b/spec/queries/content_dependencies_spec.rb index 30276697b..de07360ca 100644 --- a/spec/queries/content_dependencies_spec.rb +++ b/spec/queries/content_dependencies_spec.rb @@ -5,13 +5,13 @@ let(:content_id) { SecureRandom.uuid } let(:locale) { "en" } - let(:state_fallback_order) { %w[published unpublished] } + let(:content_stores) { %w[live] } let(:instance_options) do { content_id: content_id, locale: locale, - state_fallback_order: state_fallback_order, + content_stores: content_stores, } end @@ -22,11 +22,11 @@ it { is_expected.to be_empty } end - context "when there are translations of the content item" do + context "when there are translations of the edition" do before do - create_content_item(content_id, "/a", "published", "en") - create_content_item(content_id, "/a.fr", "published", "fr") - create_content_item(content_id, "/a.es", "published", "es") + create_edition(content_id, "/a", locale: "en") + create_edition(content_id, "/a.fr", locale: "fr") + create_edition(content_id, "/a.es", locale: "es") end context "and we specify locale as en" do @@ -67,19 +67,19 @@ end end - context "when there are draft translations of the content item" do + context "when there are draft translations of the edition" do before do - create_content_item(content_id, "/a", "published", "en") - create_content_item(content_id, "/a.cy", "draft", "cy") + create_edition(content_id, "/a", locale: "en") + create_edition(content_id, "/a.cy", factory: :draft_edition, locale: "cy") end let(:locale) { "en" } - let(:state_fallback_order) { %w[published unpublished] } + let(:content_stores) { %w[live] } it { is_expected.to be_empty } - context "but we requested drafts in state_fallback_order" do - let(:state_fallback_order) { %w[draft published unpublished] } + context "but we requested drafts in content_stores" do + let(:content_stores) { %w[draft live] } let(:translations) do [ [content_id, "cy"], @@ -90,10 +90,10 @@ end end - context "when items link to this content item" do + context "when items link to this edition" do before do - create_content_item(link_1_content_id, "/link-1", "published", "en") - create_content_item(link_2_content_id, "/link-2", "published", "en") + create_edition(link_1_content_id, "/link-1", locale: "en") + create_edition(link_2_content_id, "/link-2", locale: "en") create_link(link_1_content_id, content_id, "organisation") create_link(link_2_content_id, content_id, "organisation") end @@ -110,10 +110,10 @@ it { is_expected.to match_array(links) } end - context "when items in different translations link to this content item" do + context "when items in different translations link to this edition" do before do - create_content_item(link_content_id, "/link", "published", "en") - create_content_item(link_content_id, "/link.cy", "published", "cy") + create_edition(link_content_id, "/link", locale: "en") + create_edition(link_content_id, "/link.cy", locale: "cy") create_link(link_content_id, content_id, "organisation") end @@ -128,10 +128,10 @@ it { is_expected.to match_array(links) } end - context "when items in different states link to this content item" do + context "when items in different states link to this edition" do before do - create_content_item(link_1_content_id, "/link", "published") - create_content_item(link_2_content_id, "/link", "draft") + create_edition(link_1_content_id, "/link") + create_edition(link_2_content_id, "/link", factory: :draft_edition) create_link(link_1_content_id, content_id, "organisation") create_link(link_2_content_id, content_id, "organisation") end @@ -146,8 +146,8 @@ it { is_expected.to match_array(links) } - context "but we use a different state_fallback_order" do - let(:state_fallback_order) { %w[draft published unpublished] } + context "and we include drafts" do + let(:content_stores) { %w[draft live] } let(:links) do [ @@ -160,11 +160,11 @@ end end - context "when a graph of parent items link to this content item" do + context "when a graph of parent items link to this edition" do before do - create_content_item(great_grandparent_content_id, "/great") - create_content_item(grandparent_content_id, "/great/grand") - create_content_item(parent_content_id, "/great/grand/parent") + create_edition(great_grandparent_content_id, "/great") + create_edition(grandparent_content_id, "/great/grand") + create_edition(parent_content_id, "/great/grand/parent") create_link(parent_content_id, content_id, "parent") create_link(grandparent_content_id, parent_content_id, "parent") create_link(great_grandparent_content_id, grandparent_content_id, "parent") @@ -184,9 +184,9 @@ it { is_expected.to match_array(links) } end - context "when this content item has a link to an item with a reverse link type" do + context "when this edition has a link to an item with a reverse link type" do before do - create_content_item(reverse_link_content_id, "/reverse") + create_edition(reverse_link_content_id, "/reverse") create_link(content_id, reverse_link_content_id, "documents") end @@ -201,7 +201,7 @@ context "and there are translations of item that links" do before do - create_content_item(reverse_link_content_id, "/reverse.cy", "published", "cy") + create_edition(reverse_link_content_id, "/reverse.cy", locale: "cy") end let(:links) do @@ -217,17 +217,17 @@ context "when this content has a link to a draft item with a reverse link type" do before do - create_content_item(reverse_link_content_id, "/reverse", "draft") + create_edition(reverse_link_content_id, "/reverse", factory: :draft_edition) create_link(content_id, reverse_link_content_id, "documents") end let(:reverse_link_content_id) { SecureRandom.uuid } - let(:state_fallback_order) { %w[published unpublished] } + let(:content_stores) { %w[live] } it { is_expected.to be_empty } context "and we allow drafts" do - let(:state_fallback_order) { %w[draft published unpublished] } + let(:content_stores) { %w[draft live] } let(:links) do [ diff --git a/spec/queries/get_change_history_spec.rb b/spec/queries/get_change_history_spec.rb index 5eba8df87..182e12be6 100644 --- a/spec/queries/get_change_history_spec.rb +++ b/spec/queries/get_change_history_spec.rb @@ -14,16 +14,16 @@ subject { described_class } let!(:item1) do - FactoryGirl.create(:content_item, details: details, publishing_app: app1) + FactoryGirl.create(:edition, details: details, publishing_app: app1) end let!(:item2) do - FactoryGirl.create(:content_item, details: details, publishing_app: app1) + FactoryGirl.create(:edition, details: details, publishing_app: app1) end let!(:item3) do - FactoryGirl.create(:content_item, details: details, publishing_app: app2) + FactoryGirl.create(:edition, details: details, publishing_app: app2) end let!(:item4) do - FactoryGirl.create(:content_item, details: {}, publishing_app: app2) + FactoryGirl.create(:edition, details: {}, publishing_app: app2) end it "gets application-specific history data" do diff --git a/spec/queries/get_content_collection_spec.rb b/spec/queries/get_content_collection_spec.rb index 43016fdf1..ccfcb6759 100644 --- a/spec/queries/get_content_collection_spec.rb +++ b/spec/queries/get_content_collection_spec.rb @@ -3,35 +3,31 @@ RSpec.describe Queries::GetContentCollection do context "document_type" do before do - FactoryGirl.create( - :draft_content_item, - base_path: '/a', - document_type: 'topic', - schema_name: 'topic', + FactoryGirl.create(:draft_edition, + base_path: "/a", + document_type: "topic", + schema_name: "topic", ) - FactoryGirl.create( - :draft_content_item, - base_path: '/b', - document_type: 'topic', - schema_name: 'topic', + FactoryGirl.create(:draft_edition, + base_path: "/b", + document_type: "topic", + schema_name: "topic", ) - FactoryGirl.create( - :draft_content_item, - base_path: '/c', - document_type: 'mainstream_browse_page', - schema_name: 'mainstream_browse_page', + FactoryGirl.create(:draft_edition, + base_path: "/c", + document_type: "mainstream_browse_page", + schema_name: "mainstream_browse_page", ) - FactoryGirl.create( - :draft_content_item, - base_path: '/d', - document_type: 'another_type', - schema_name: 'another_type', + FactoryGirl.create(:draft_edition, + base_path: "/d", + document_type: "another_type", + schema_name: "another_type", ) end - it "returns the content items matching the type" do + it "returns the editions matching the type" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path locale publication_state), ).call).to match_array([ hash_including("base_path" => "/a", "publication_state" => "draft", "locale" => "en"), @@ -39,7 +35,7 @@ ]) end - it "returns the content items matching all types when given an array" do + it "returns the editions matching all types when given an array" do expect(Queries::GetContentCollection.new( document_types: %w(topic mainstream_browse_page), fields: %w(base_path locale publication_state), @@ -51,22 +47,20 @@ end end - it "returns the content items of the given format, and placeholder_format" do - FactoryGirl.create( - :draft_content_item, - base_path: '/a', - document_type: 'topic', - schema_name: 'topic', + it "returns the editions of the given format, and placeholder_format" do + FactoryGirl.create(:draft_edition, + base_path: "/a", + document_type: "topic", + schema_name: "topic", ) - FactoryGirl.create( - :draft_content_item, - base_path: '/b', - document_type: 'placeholder_topic', - schema_name: 'placeholder_topic' + FactoryGirl.create(:draft_edition, + base_path: "/b", + document_type: "placeholder_topic", + schema_name: "placeholder_topic" ) expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path publication_state), ).call).to match_array([ hash_including("base_path" => "/a", "publication_state" => "draft"), @@ -75,21 +69,19 @@ end it "includes the publishing state of the item" do - FactoryGirl.create( - :draft_content_item, - base_path: '/draft', - document_type: 'topic', - schema_name: 'topic', + FactoryGirl.create(:draft_edition, + base_path: "/draft", + document_type: "topic", + schema_name: "topic", ) - FactoryGirl.create( - :live_content_item, - base_path: '/live', - document_type: 'topic', - schema_name: 'topic', + FactoryGirl.create(:live_edition, + base_path: "/live", + document_type: "topic", + schema_name: "topic", ) expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path publication_state), ).call).to match_array([ hash_including("base_path" => "/draft", "publication_state" => "draft"), @@ -100,8 +92,8 @@ context "when there's no items for the format" do it "returns an empty array" do expect(Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['base_path'], + document_types: "topic", + fields: ["base_path"], ).call.to_a).to eq([]) end end @@ -110,8 +102,8 @@ it "raises an error" do expect { Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['not_existing'], + document_types: "topic", + fields: ["not_existing"], ).call }.to raise_error(CommandError) end @@ -119,34 +111,31 @@ context "filtering by publishing_app" do before do - FactoryGirl.create( - :draft_content_item, - base_path: '/a', - document_type: 'topic', - schema_name: 'topic', - publishing_app: 'publisher' + FactoryGirl.create(:draft_edition, + base_path: "/a", + document_type: "topic", + schema_name: "topic", + publishing_app: "publisher" ) - FactoryGirl.create( - :draft_content_item, - base_path: '/b', - document_type: 'topic', - schema_name: 'topic', - publishing_app: 'publisher' + FactoryGirl.create(:draft_edition, + base_path: "/b", + document_type: "topic", + schema_name: "topic", + publishing_app: "publisher" ) - FactoryGirl.create( - :draft_content_item, - base_path: '/c', - document_type: 'topic', - schema_name: 'topic', - publishing_app: 'whitehall' + FactoryGirl.create(:draft_edition, + base_path: "/c", + document_type: "topic", + schema_name: "topic", + publishing_app: "whitehall" ) end it "returns items corresponding to the publishing_app parameter if present" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(publishing_app publication_state), - filters: { publishing_app: 'publisher' } + filters: { publishing_app: "publisher" } ).call).to match_array([ hash_including("publishing_app" => "publisher", "publication_state" => "draft"), hash_including("publishing_app" => "publisher", "publication_state" => "draft") @@ -155,7 +144,7 @@ it "returns items for all apps if publishing_app is not present" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(publishing_app publication_state) ).call).to match_array([ hash_including("publishing_app" => "publisher", "publication_state" => "draft"), @@ -167,15 +156,35 @@ describe "the locale filter parameter" do before do - FactoryGirl.create(:draft_content_item, base_path: '/content.en', document_type: 'topic', schema_name: 'topic', locale: 'en') - FactoryGirl.create(:draft_content_item, base_path: '/content.ar', document_type: 'topic', schema_name: 'topic', locale: 'ar') - FactoryGirl.create(:live_content_item, base_path: '/content.en', document_type: 'topic', schema_name: 'topic', locale: 'en') - FactoryGirl.create(:live_content_item, base_path: '/content.ar', document_type: 'topic', schema_name: 'topic', locale: 'ar') + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, locale: "en"), + base_path: "/content.en", + document_type: "topic", + schema_name: "topic", + ) + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, locale: "ar"), + base_path: "/content.ar", + document_type: "topic", + schema_name: "topic", + ) + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, locale: "en"), + base_path: "/content.en", + document_type: "topic", + schema_name: "topic", + ) + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, locale: "ar"), + base_path: "/content.ar", + document_type: "topic", + schema_name: "topic", + ) end - it "returns the content items filtered by 'en' locale by default" do + it "returns the editions filtered by 'en' locale by default" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path publication_state), ).call).to match_array([ hash_including("base_path" => "/content.en", "publication_state" => "draft"), @@ -183,22 +192,22 @@ ]) end - it "returns the content items filtered by locale parameter" do + it "returns the editions filtered by locale parameter" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path publication_state), - filters: { locale: 'ar' }, + filters: { locale: "ar" }, ).call).to match_array([ hash_including("base_path" => "/content.ar", "publication_state" => "draft"), hash_including("base_path" => "/content.ar", "publication_state" => "published"), ]) end - it "returns all content items if the locale parameter is 'all'" do + it "returns all editions if the locale parameter is 'all'" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path publication_state), - filters: { locale: 'all' }, + filters: { locale: "all" }, ).call).to match_array([ hash_including("base_path" => "/content.en", "publication_state" => "draft"), hash_including("base_path" => "/content.ar", "publication_state" => "draft"), @@ -217,22 +226,19 @@ draft_2_content_id = SecureRandom.uuid live_1_content_id = SecureRandom.uuid - FactoryGirl.create( - :draft_content_item, - content_id: draft_1_content_id, + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: draft_1_content_id), base_path: "/foo", publishing_app: "specialist-publisher" ) - FactoryGirl.create( - :draft_content_item, - content_id: draft_2_content_id, + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: draft_2_content_id), base_path: "/bar" ) - FactoryGirl.create( - :live_content_item, - content_id: live_1_content_id, + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: live_1_content_id), base_path: "/baz" ) @@ -245,7 +251,7 @@ FactoryGirl.create(:link, link_set: link_set_3, target_content_id: someorg_content_id) end - it "filters content items by organisation" do + it "filters editions by organisation" do result = Queries::GetContentCollection.new( document_types: "guide", filters: { links: { organisations: someorg_content_id } }, @@ -258,7 +264,7 @@ ]) end - it "filters content items by organisation and other filters" do + it "filters editions by organisation and other filters" do result = Queries::GetContentCollection.new( document_types: "guide", filters: { @@ -274,9 +280,9 @@ describe "filtering by state" do before do - FactoryGirl.create(:draft_content_item, base_path: "/draft") - FactoryGirl.create(:live_content_item, base_path: "/published") - FactoryGirl.create(:unpublished_content_item, base_path: "/unpublished") + FactoryGirl.create(:draft_edition, base_path: "/draft") + FactoryGirl.create(:live_edition, base_path: "/published") + FactoryGirl.create(:unpublished_edition, base_path: "/unpublished") end it "returns all content if no filter is provided" do @@ -316,13 +322,27 @@ end context "when details hash is requested" do + before do + FactoryGirl.create(:draft_edition, + base_path: "/z", + details: { foo: :bar }, + document_type: "topic", + schema_name: "topic", + publishing_app: "publisher" + ) + FactoryGirl.create(:draft_edition, + base_path: "/b", + details: { baz: :bat }, + document_type: "placeholder_topic", + schema_name: "placeholder_topic", + publishing_app: "publisher" + ) + end it "returns the details hash" do - FactoryGirl.create(:draft_content_item, base_path: '/z', details: { foo: :bar }, document_type: 'topic', schema_name: 'topic', publishing_app: 'publisher') - FactoryGirl.create(:draft_content_item, base_path: '/b', details: { baz: :bat }, document_type: 'placeholder_topic', schema_name: 'placeholder_topic', publishing_app: 'publisher') expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(details publication_state), - filters: { publishing_app: 'publisher' } + filters: { publishing_app: "publisher" } ).call).to match_array([ hash_including("details" => { "foo" => "bar" }, "publication_state" => "draft"), hash_including("details" => { "baz" => "bat" }, "publication_state" => "draft"), @@ -331,156 +351,180 @@ end describe "search_fields" do - let!(:content_item_foo) { FactoryGirl.create(:live_content_item, base_path: '/bar/foo', document_type: 'topic', schema_name: 'topic', title: "Baz") } - let!(:content_item_zip) { FactoryGirl.create(:live_content_item, base_path: '/baz', document_type: 'topic', schema_name: 'topic', title: 'zip') } + before do + FactoryGirl.create(:live_edition, + base_path: "/bar/foo", + document_type: "topic", + schema_name: "topic", + title: "Baz", + ) + FactoryGirl.create(:live_edition, + base_path: "/baz", + document_type: "topic", + schema_name: "topic", + title: "zip" + ) + end + subject do Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['base_path'], + document_types: "topic", + fields: ["base_path"], search_query: search_query ) end context "base_path and title" do let(:search_query) { "baz" } - it "finds the content item" do + it "finds the edition" do expect(subject.call.map(&:to_hash)).to match_array([{ "base_path" => "/bar/foo" }, { "base_path" => "/baz" }]) end end context "title" do let(:search_query) { "zip" } - it "finds the content item" do + it "finds the edition" do expect(subject.call.map(&:to_hash)).to eq([{ "base_path" => "/baz" }]) end end end describe "pagination" do - context "with multiple content items" do + context "with multiple editions" do before do - FactoryGirl.create(:draft_content_item, base_path: '/a', document_type: 'topic', schema_name: 'topic', public_updated_at: "2010-01-06") - FactoryGirl.create(:draft_content_item, base_path: '/b', document_type: 'topic', schema_name: 'topic', public_updated_at: "2010-01-05") - FactoryGirl.create(:draft_content_item, base_path: '/c', document_type: 'topic', schema_name: 'topic', public_updated_at: "2010-01-04") - FactoryGirl.create(:draft_content_item, base_path: '/d', document_type: 'topic', schema_name: 'topic', public_updated_at: "2010-01-03") - FactoryGirl.create(:live_content_item, base_path: '/live1', document_type: 'topic', schema_name: 'topic', public_updated_at: "2010-01-02") - FactoryGirl.create(:live_content_item, base_path: '/live2', document_type: 'topic', schema_name: 'topic', public_updated_at: "2010-01-01") + [ + ["/a", "2010-01-06"], ["/b", "2010-01-05"], ["/c", "2010-01-04"], ["/d", "2010-01-03"] + ].each do |(base_path, public_updated_at)| + FactoryGirl.create(:draft_edition, + base_path: base_path, + document_type: "topic", + schema_name: "topic", + public_updated_at: public_updated_at, + ) + end + [ + ["/live1", "2010-01-02"], ["/live2", "2010-01-01"] + ].each do |(base_path, public_updated_at)| + FactoryGirl.create(:live_edition, + base_path: base_path, + document_type: "topic", + schema_name: "topic", + public_updated_at: public_updated_at, + ) + end end it "limits the results returned" do - content_items = Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['publishing_app'], + editions = Queries::GetContentCollection.new( + document_types: "topic", + fields: ["publishing_app"], pagination: Pagination.new(offset: 0, per_page: 3) ).call - expect(content_items.count).to eq(3) + expect(editions.count).to eq(3) end it "fetches results from a specified index" do - content_items = Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['base_path'], + editions = Queries::GetContentCollection.new( + document_types: "topic", + fields: ["base_path"], pagination: Pagination.new(offset: 1, per_page: 2) ).call - expect(content_items.first['base_path']).to eq('/b') + expect(editions.first["base_path"]).to eq("/b") end - it "when per_page is higher than results we only receive remaining content items" do - content_items = Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['base_path'], + it "when per_page is higher than results we only receive remaining editions" do + editions = Queries::GetContentCollection.new( + document_types: "topic", + fields: ["base_path"], pagination: Pagination.new(offset: 3, per_page: 8) ).call.to_a - expect(content_items.first['base_path']).to eq('/d') - expect(content_items.last['base_path']).to eq('/live2') + expect(editions.first["base_path"]).to eq("/d") + expect(editions.last["base_path"]).to eq("/live2") end it "returns all items when no pagination params are specified" do - content_items = Queries::GetContentCollection.new( - document_types: 'topic', - fields: ['publishing_app'], + editions = Queries::GetContentCollection.new( + document_types: "topic", + fields: ["publishing_app"], ).call - expect(content_items.count).to eq(6) + expect(editions.count).to eq(6) end end end describe "result order" do before do - FactoryGirl.create(:content_item, base_path: "/c4", title: 'D', public_updated_at: DateTime.parse('2014-06-14')) - FactoryGirl.create(:content_item, base_path: "/c1", title: 'A', public_updated_at: DateTime.parse('2014-06-13')) - FactoryGirl.create(:content_item, base_path: "/c3", title: 'C', public_updated_at: DateTime.parse('2014-06-17')) - FactoryGirl.create(:content_item, base_path: "/c2", title: 'B', public_updated_at: DateTime.parse('2014-06-15')) + FactoryGirl.create(:edition, base_path: "/c4", title: "D", public_updated_at: "2014-06-14") + FactoryGirl.create(:edition, base_path: "/c1", title: "A", public_updated_at: "2014-06-13") + FactoryGirl.create(:edition, base_path: "/c3", title: "C", public_updated_at: "2014-06-17") + FactoryGirl.create(:edition, base_path: "/c2", title: "B", public_updated_at: "2014-06-15") end - it "returns content items in default order" do - content_items = Queries::GetContentCollection.new( - document_types: 'guide', + it "returns editions in default order" do + editions = Queries::GetContentCollection.new( + document_types: "guide", fields: %w(public_updated_at), ).call.to_a - expect(content_items.count).to eq(4) - expect(content_items.first['public_updated_at']).to eq('2014-06-17T00:00:00Z') - expect(content_items.last['public_updated_at']).to eq('2014-06-13T00:00:00Z') + expect(editions.count).to eq(4) + expect(editions.first["public_updated_at"]).to eq("2014-06-17T00:00:00Z") + expect(editions.last["public_updated_at"]).to eq("2014-06-13T00:00:00Z") end - it "returns paginated content items in default order" do - content_items = Queries::GetContentCollection.new( - document_types: 'guide', + it "returns paginated editions in default order" do + editions = Queries::GetContentCollection.new( + document_types: "guide", fields: %w(public_updated_at), pagination: Pagination.new(offset: 2, per_page: 4) ).call.to_a - expect(content_items.first['public_updated_at']).to eq('2014-06-14T00:00:00Z') - expect(content_items.last['public_updated_at']).to eq('2014-06-13T00:00:00Z') + expect(editions.first["public_updated_at"]).to eq("2014-06-14T00:00:00Z") + expect(editions.last["public_updated_at"]).to eq("2014-06-13T00:00:00Z") end end describe "#total" do - it "returns the number of content items" do - FactoryGirl.create(:content_item, base_path: '/a', schema_name: 'topic', document_type: 'topic') - FactoryGirl.create(:content_item, base_path: '/b', schema_name: 'topic', document_type: 'topic') + it "returns the number of editions" do + FactoryGirl.create(:edition, base_path: "/a", schema_name: "topic", document_type: "topic") + FactoryGirl.create(:edition, base_path: "/b", schema_name: "topic", document_type: "topic") expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path locale publication_state), ).total).to eq(2) end - context "when there are multiple versions of the same content item" do + context "when there are multiple versions of the same edition" do before do - content_id = SecureRandom.uuid + document = FactoryGirl.create(:document) - FactoryGirl.create( - :live_content_item, - content_id: content_id, - document_type: 'topic', - schema_name: 'topic', + FactoryGirl.create(:live_edition, + document: document, + document_type: "topic", + schema_name: "topic", user_facing_version: 1, ) - FactoryGirl.create( - :content_item, - content_id: content_id, - document_type: 'topic', - schema_name: 'topic', - state: "draft", + FactoryGirl.create(:draft_edition, + document: document, + document_type: "topic", + schema_name: "topic", user_facing_version: 2, ) end it "returns the latest item only" do expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path locale publication_state), ).total).to eq(1) expect(Queries::GetContentCollection.new( - document_types: 'topic', + document_types: "topic", fields: %w(base_path locale publication_state), ).call.first["publication_state"]).to eq("draft") end diff --git a/spec/queries/get_content_spec.rb b/spec/queries/get_content_spec.rb index 5b89e8a7e..16207f44e 100644 --- a/spec/queries/get_content_spec.rb +++ b/spec/queries/get_content_spec.rb @@ -2,8 +2,10 @@ RSpec.describe Queries::GetContent do let(:content_id) { SecureRandom.uuid } + let(:document) { FactoryGirl.create(:document, content_id: content_id) } + let(:fr_document) { FactoryGirl.create(:document, content_id: content_id, locale: "fr") } - context "when no content item exists for the content_id" do + context "when no edition exists for the content_id" do it "raises a command error" do expect { subject.call(content_id) @@ -11,20 +13,19 @@ end end - context "when a content item exists for the content_id" do + context "when a edition exists for the content_id" do let(:incorrect_version) { 2 } let(:incorrect_locale) { "fr" } before do - FactoryGirl.create(:content_item, - content_id: content_id, + FactoryGirl.create(:edition, + document: document, base_path: "/vat-rates", user_facing_version: 1, - locale: "en", ) end - it "presents the content item" do + it "presents the edition" do result = subject.call(content_id) expect(result).to include( @@ -41,23 +42,23 @@ ) end - context "when a content item for the requested version does not exist" do + context "when a edition for the requested version does not exist" do it "raises a command error" do expect { subject.call(content_id, version: incorrect_version) - }.to raise_error(CommandError, /version: #{incorrect_version} for content item/) + }.to raise_error(CommandError, /version: #{incorrect_version} for document/) end end - context "when a content item for the requested locale does not exist" do + context "when a edition for the requested locale does not exist" do it "raises a command error" do expect { subject.call(content_id, incorrect_locale) - }.to raise_error(CommandError, /locale: #{incorrect_locale} for content item/) + }.to raise_error(CommandError, /locale: #{incorrect_locale} for document/) end end - context "when a content item for the requested version and locale does not exist" do + context "when a edition for the requested version and locale does not exist" do it "raises a command error" do expect { subject.call(content_id, incorrect_locale, version: incorrect_version) @@ -66,72 +67,70 @@ end end - context "when a draft and a live content item exists for the content_id" do + context "when a draft and a live edition exists for the content_id" do before do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, title: "Draft Title", user_facing_version: 2, ) - FactoryGirl.create(:live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: document, title: "Live Title", user_facing_version: 1, ) end - it "presents the draft content item" do + it "presents the draft edition" do result = subject.call(content_id) expect(result.fetch("title")).to eq("Draft Title") end end - context "when content items exist in non-draft, non-live states" do + context "when editions exist in non-draft, non-live states" do before do - FactoryGirl.create(:content_item, - content_id: content_id, + FactoryGirl.create(:edition, + document: document, user_facing_version: 1, title: "Published Title", state: "published", ) - FactoryGirl.create(:superseded_content_item, - content_id: content_id, + FactoryGirl.create(:superseded_edition, + document: document, user_facing_version: 2, title: "Submitted Title", ) end - it "includes these content items" do + it "includes these editions" do result = subject.call(content_id) expect(result.fetch("title")).to eq("Submitted Title") end end - context "when content items exist in multiple locales" do + context "when editions exist in multiple locales" do before do - FactoryGirl.create(:content_item, - content_id: content_id, + FactoryGirl.create(:edition, + document: fr_document, user_facing_version: 2, title: "French Title", - locale: "fr", ) - FactoryGirl.create(:content_item, - content_id: content_id, + FactoryGirl.create(:edition, + document: document, user_facing_version: 1, title: "English Title", - locale: "en", ) end - it "returns the english content item by default" do + it "returns the english edition by default" do result = subject.call(content_id) expect(result.fetch("title")).to eq("English Title") end - it "filters content items by the specified locale" do + it "filters editions by the specified locale" do result = subject.call(content_id, "fr") expect(result.fetch("title")).to eq("French Title") end @@ -139,13 +138,13 @@ describe "requesting specific versions" do before do - FactoryGirl.create(:superseded_content_item, - content_id: content_id, + FactoryGirl.create(:superseded_edition, + document: document, user_facing_version: 1, ) - FactoryGirl.create(:live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: document, user_facing_version: 2, ) end diff --git a/spec/queries/get_edition_ids_with_fallbacks_spec.rb b/spec/queries/get_edition_ids_with_fallbacks_spec.rb new file mode 100644 index 000000000..6887a51d8 --- /dev/null +++ b/spec/queries/get_edition_ids_with_fallbacks_spec.rb @@ -0,0 +1,222 @@ +require "rails_helper" + +RSpec.describe Queries::GetEditionIdsWithFallbacks do + describe ".call" do + let(:state_fallback_order) { %w(published) } + let(:locale_fallback_order) { %w(en) } + let(:content_ids) { [] } + let(:options) do + { + state_fallback_order: state_fallback_order, + locale_fallback_order: locale_fallback_order, + } + end + subject { described_class.call(content_ids, options) } + + it { is_expected.to be_a(Array) } + + context "when a edition is in a draft state" do + let(:content_ids) { [SecureRandom.uuid] } + let(:document) { FactoryGirl.create(:document, content_id: content_ids.first) } + let!(:draft_edition) do + FactoryGirl.create(:draft_edition, document: document) + end + + context "and the state_fallback order is [draft]" do + let(:state_fallback_order) { %w(draft) } + it { is_expected.to match_array([draft_edition.id]) } + end + + context "and the state_fallback order is [published]" do + let(:state_fallback_order) { %w(published) } + it { is_expected.to be_empty } + end + + context "and the state_fallback order is [published, draft]" do + let(:state_fallback_order) { %w(published draft) } + it { is_expected.to match_array([draft_edition.id]) } + end + end + + context "when a edition is in draft and unpublished (withdrawn) states" do + let(:content_ids) { [SecureRandom.uuid] } + let(:document) { FactoryGirl.create(:document, content_id: content_ids.first) } + let!(:draft_edition) do + FactoryGirl.create(:draft_edition, + document: document, + user_facing_version: 2, + ) + end + let!(:withdrawn_edition) do + FactoryGirl.create(:withdrawn_unpublished_edition, + document: document, + user_facing_version: 1, + ) + end + + context "and the state_fallback order is [draft, withdrawn]" do + let(:state_fallback_order) { %w(draft withdrawn) } + it { is_expected.to match_array([draft_edition.id]) } + end + + context "and the state_fallback order is [withdrawn, draft]" do + let(:state_fallback_order) { %w(withdrawn draft) } + it { is_expected.to match_array([withdrawn_edition.id]) } + end + end + + context "when a edition is in multiple locales" do + let(:content_ids) { [SecureRandom.uuid] } + let(:fr_document) do + FactoryGirl.create(:document, + content_id: content_ids.first, + locale: "fr", + ) + end + let(:en_document) do + FactoryGirl.create(:document, + content_id: content_ids.first, + locale: "en", + ) + end + let!(:fr_draft_edition) do + FactoryGirl.create(:draft_edition, document: fr_document) + end + let!(:en_draft_edition) do + FactoryGirl.create(:draft_edition, + document: en_document, + user_facing_version: 2, + ) + end + let!(:en_published_edition) do + FactoryGirl.create(:live_edition, + document: en_document, + user_facing_version: 1, + ) + end + + context "and the locale_fallback_order is [fr]" do + let(:locale_fallback_order) { %w(fr) } + + context "and the state_fallback_order is [draft]" do + let(:state_fallback_order) { %w(draft) } + it { is_expected.to match_array(fr_draft_edition.id) } + end + + context "and the state_fallback_order is [published]" do + let(:state_fallback_order) { %w(published) } + it { is_expected.to be_empty } + end + end + + context "and the locale_fallback_order is [fr, en]" do + let(:locale_fallback_order) { %w(fr en) } + + context "and the state_fallback_order is [draft]" do + let(:state_fallback_order) { %w(draft) } + it { is_expected.to match_array(fr_draft_edition.id) } + end + + context "and the state_fallback_order is [published]" do + let(:state_fallback_order) { %w(published) } + it { is_expected.to match_array(en_published_edition.id) } + end + end + + context "and the locale_fallback_order is [en, fr]" do + let(:locale_fallback_order) { %w(en fr) } + + context "and the state_fallback_order is [draft]" do + let(:state_fallback_order) { %w(draft) } + it { is_expected.to match_array(en_draft_edition.id) } + end + + context "and the state_fallback_order is [published]" do + let(:state_fallback_order) { %w(published) } + it { is_expected.to match_array(en_published_edition.id) } + end + end + end + + context "when multiple editions are requested" do + let(:content_ids) { [SecureRandom.uuid, SecureRandom.uuid] } + let(:vat_document) { FactoryGirl.create(:document, content_id: content_ids.first) } + let(:tax_rates_document) { FactoryGirl.create(:document, content_id: content_ids.last) } + let!(:vat_draft_edition) do + FactoryGirl.create(:draft_edition, + document: vat_document, + user_facing_version: 2, + ) + end + let!(:vat_published_edition) do + FactoryGirl.create(:live_edition, + document: vat_document, + user_facing_version: 1, + ) + end + let!(:tax_rates_draft_edition) do + FactoryGirl.create(:draft_edition, + document: tax_rates_document, + user_facing_version: 2, + ) + end + let!(:tax_rates_withdrawn_edition) do + FactoryGirl.create(:withdrawn_unpublished_edition, + document: tax_rates_document, + user_facing_version: 1, + ) + end + + context "and the state_fallback order is [draft, published]" do + let(:state_fallback_order) { %w(draft published) } + let(:expected) { [vat_draft_edition.id, tax_rates_draft_edition.id] } + it { is_expected.to match_array(expected) } + end + + context "and the state_fallback order is [published, draft]" do + let(:state_fallback_order) { %w(published draft) } + let(:expected) { [vat_published_edition.id, tax_rates_draft_edition.id] } + it { is_expected.to match_array(expected) } + end + + context "and the state_fallback order is [withdrawn, draft]" do + let(:state_fallback_order) { %w(withdrawn draft) } + let(:expected) { [vat_draft_edition.id, tax_rates_withdrawn_edition.id] } + it { is_expected.to match_array(expected) } + end + end + + context "when there is a non-renderable document type" do + let(:content_ids) { [SecureRandom.uuid] } + let(:document) { FactoryGirl.create(:document, content_id: content_ids.first) } + let!(:draft_edition) do + FactoryGirl.create(:draft_edition, + document: document, + document_type: "gone", + user_facing_version: 2, + ) + end + let!(:published_edition) do + FactoryGirl.create(:live_edition, + document: document, + user_facing_version: 1, + ) + end + + context "and the state_fallback order is [draft]" do + let(:state_fallback_order) { %w(draft) } + it { is_expected.to be_empty } + end + + context "and the state_fallback order is [published]" do + let(:state_fallback_order) { %w(published) } + it { is_expected.to match_array([published_edition.id]) } + end + + context "and the state_fallback order is [draft, published]" do + let(:state_fallback_order) { %w(draft published) } + it { is_expected.to match_array([published_edition.id]) } + end + end + end +end diff --git a/spec/queries/get_latest_spec.rb b/spec/queries/get_latest_spec.rb index 18fba61d9..f76c01fa8 100644 --- a/spec/queries/get_latest_spec.rb +++ b/spec/queries/get_latest_spec.rb @@ -1,36 +1,40 @@ require "rails_helper" RSpec.describe Queries::GetLatest do - let(:a) { SecureRandom.uuid } - let(:b) { SecureRandom.uuid } - let(:c) { SecureRandom.uuid } + let(:document_a) { FactoryGirl.create(:document) } + let(:document_b) { FactoryGirl.create(:document) } + let(:document_b_fr) do + FactoryGirl.create(:document, content_id: document_b.content_id, locale: "fr") + end + let(:document_c) { FactoryGirl.create(:document) } before do - FactoryGirl.create(:live_content_item, content_id: a, user_facing_version: 2, base_path: "/a2", state: "published") - FactoryGirl.create(:superseded_content_item, content_id: a, user_facing_version: 1, base_path: "/a1", state: "superseded") - FactoryGirl.create(:content_item, content_id: a, user_facing_version: 3, base_path: "/a3") + FactoryGirl.create(:live_edition, document: document_a, user_facing_version: 2, base_path: "/a2") + FactoryGirl.create(:superseded_edition, document: document_a, user_facing_version: 1, base_path: "/a1") + FactoryGirl.create(:draft_edition, document: document_a, user_facing_version: 3, base_path: "/a3") - FactoryGirl.create(:live_content_item, content_id: b, user_facing_version: 1, base_path: "/b1", state: "published") - FactoryGirl.create(:content_item, content_id: b, user_facing_version: 2, locale: "fr", base_path: "/b2", state: "published") + FactoryGirl.create(:live_edition, document: document_b, user_facing_version: 1, base_path: "/b1") + FactoryGirl.create(:live_edition, document: document_b_fr, user_facing_version: 2, base_path: "/b2") - FactoryGirl.create(:live_content_item, content_id: c, user_facing_version: 1, base_path: "/c1", state: "published") - FactoryGirl.create(:content_item, content_id: c, user_facing_version: 2, base_path: "/c2", state: "draft") + FactoryGirl.create(:live_edition, document: document_c, user_facing_version: 1, base_path: "/c1") + FactoryGirl.create(:draft_edition, document: document_c, user_facing_version: 2, base_path: "/c2") end def base_paths(result) result.map(&:base_path) end - it "returns a scope of the latest content_items for the given scope" do - scope = ContentItem.all + it "returns a scope of the latest editions for the given scope" do + scope = Edition.all result = subject.call(scope) expect(base_paths(result)).to match_array(["/a3", "/b1", "/b2", "/c2"]) - scope = scope.where(content_id: [a, b]) + scope = scope.with_document + .where('documents.content_id': [document_a.content_id, document_b.content_id]) result = subject.call(scope) expect(base_paths(result)).to match_array(["/a3", "/b1", "/b2"]) - scope = scope.where(locale: 'fr') + scope = scope.where('documents.locale': 'fr') result = subject.call(scope) expect(base_paths(result)).to match_array(["/b2"]) end diff --git a/spec/queries/get_link_set_spec.rb b/spec/queries/get_link_set_spec.rb index c5d80dead..f2d4066fe 100644 --- a/spec/queries/get_link_set_spec.rb +++ b/spec/queries/get_link_set_spec.rb @@ -17,22 +17,19 @@ let(:related) { [SecureRandom.uuid, SecureRandom.uuid] } before do - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_set: link_set, link_type: "parent", target_content_id: parent.first ) - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_set: link_set, link_type: "related", target_content_id: related.first ) - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_set: link_set, link_type: "related", target_content_id: related.last diff --git a/spec/queries/get_linked_spec.rb b/spec/queries/get_linked_spec.rb index ce5de0950..7fe4a09a6 100644 --- a/spec/queries/get_linked_spec.rb +++ b/spec/queries/get_linked_spec.rb @@ -32,16 +32,16 @@ end end - context "when a content item with no draft exists" do + context "when a edition with no draft exists" do before do - FactoryGirl.create(:live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: "/vat-rules-2020", title: "VAT rules 2020", ) - FactoryGirl.create(:live_content_item, - content_id: target_content_id, + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: target_content_id), base_path: "/vat-org", ) end @@ -56,7 +56,7 @@ ).to eq([]) end - context "where another content item is linked to it" do + context "where another edition is linked to it" do before do link_set = FactoryGirl.create(:link_set, content_id: content_id) FactoryGirl.create(:link, link_set: link_set, target_content_id: target_content_id) @@ -72,16 +72,16 @@ end end - context "when a content item with draft exists "do + context "when a document with draft exists "do before do - FactoryGirl.create(:live_content_item, + FactoryGirl.create(:live_edition, :with_draft, - content_id: target_content_id, + document: FactoryGirl.create(:document, content_id: target_content_id), base_path: "/pay-now" ) end - context "but no content item links to it" do + context "but no edition links to it" do it "returns an empty array" do expect( Queries::GetLinked.new( @@ -105,31 +105,29 @@ end end - context "content items link to the wanted content item" do + context "editions link to the wanted edition" do before do - FactoryGirl.create(:live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: content_id), title: "VAT and VATy things", base_path: "/vat-rates", ) FactoryGirl.create(:link_set, content_id: content_id, links: [ - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_type: "organisations", target_content_id: target_content_id ) ] ) - content_item = FactoryGirl.create(:live_content_item, + edition = FactoryGirl.create(:live_edition, base_path: '/vatty', - content_id: SecureRandom.uuid, title: "Another VATTY thing" ) FactoryGirl.create(:link_set, - content_id: content_item.content_id, + content_id: edition.document.content_id, links: [ FactoryGirl.create(:link, link_type: "organisations", @@ -185,55 +183,47 @@ context "draft items linking to the wanted draft item" do before do - FactoryGirl.create( - :live_content_item, + FactoryGirl.create(:live_edition, :with_draft, - content_id: another_target_content_id, + document: FactoryGirl.create(:document, content_id: another_target_content_id), base_path: "/send-now" ) - FactoryGirl.create( - :draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: content_id), title: "HMRC documents" ) - FactoryGirl.create( - :link_set, + FactoryGirl.create(:link_set, content_id: content_id, links: [ - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_type: "organisations", target_content_id: another_target_content_id ), ] ) - content_item = FactoryGirl.create( - :draft_content_item, + edition = FactoryGirl.create(:draft_edition, base_path: '/other-hmrc-document', - content_id: SecureRandom.uuid, title: "Another HMRC document" ) - FactoryGirl.create( - :link_set, - content_id: content_item.content_id, + FactoryGirl.create(:link_set, + content_id: edition.document.content_id, links: [ - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_type: "organisations", target_content_id: another_target_content_id ), - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_type: "related_links", target_content_id: SecureRandom.uuid ) ] ) end + it "returns array of hashes, with requested fields" do expect( Queries::GetLinked.new( diff --git a/spec/queries/linked_to_spec.rb b/spec/queries/linked_to_spec.rb index 575046195..a677a1292 100644 --- a/spec/queries/linked_to_spec.rb +++ b/spec/queries/linked_to_spec.rb @@ -19,13 +19,13 @@ def create_link_set(content_id, links_hash) it { is_expected.to be_empty } end - context "when our content item links to items but they don't link back" do + context "when our document links to items but they don't link back" do before { create_link_set(content_id, organistion: [SecureRandom.uuid]) } it { is_expected.to be_empty } end - context "when there are links to our content item" do + context "when there are links to our document" do let(:links_to_content_id) { SecureRandom.uuid } let(:linked_to) { [links_to_content_id] } before { create_link_set(links_to_content_id, organistion: [content_id]) } diff --git a/spec/queries/check_for_content_item_preventing_draft_from_being_published_spec.rb b/spec/queries/live_edition_blocking_draft_edition_spec.rb similarity index 59% rename from spec/queries/check_for_content_item_preventing_draft_from_being_published_spec.rb rename to spec/queries/live_edition_blocking_draft_edition_spec.rb index 8885166fc..8931dda4e 100644 --- a/spec/queries/check_for_content_item_preventing_draft_from_being_published_spec.rb +++ b/spec/queries/live_edition_blocking_draft_edition_spec.rb @@ -1,7 +1,8 @@ require "rails_helper" -RSpec.describe Queries::CheckForContentItemPreventingDraftFromBeingPublished do +RSpec.describe Queries::LiveEditionBlockingDraftEdition do let(:content_id) { SecureRandom.uuid } + let(:document) { FactoryGirl.create(:document, content_id: content_id) } let(:base_path) { "/vat-rates" } let(:document_type) { "guide" } @@ -14,36 +15,33 @@ end end - context "with a single content item" do + context "with a single edition" do before do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end include_examples "check succeeds" end - context "with two content items of different locales" do + context "with two editions of different locales" do before do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path + ".en", document_type: document_type, user_facing_version: 1, - locale: "en", ) - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: document.content_id, locale: "es"), base_path: base_path + ".es", document_type: document_type, user_facing_version: 1, - locale: "es", ) end @@ -52,21 +50,16 @@ context "with a unpublished item, of type \"substitute\", and a draft at the same base path" do before do - FactoryGirl.create(:unpublished_content_item, - content_id: SecureRandom.uuid, + FactoryGirl.create(:substitute_unpublished_edition, base_path: base_path, document_type: document_type, - unpublishing_type: "substitute", user_facing_version: 1, - locale: "en", ) - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end @@ -75,20 +68,17 @@ context "with a published item, with a substitutable document_type, and a draft at the same base path" do before do - FactoryGirl.create(:live_content_item, - content_id: SecureRandom.uuid, + FactoryGirl.create(:live_edition, base_path: base_path, document_type: "unpublishing", user_facing_version: 1, - locale: "en", ) - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end @@ -99,20 +89,17 @@ let(:document_type) { "unpublishing" } before do - FactoryGirl.create(:live_content_item, - content_id: SecureRandom.uuid, + FactoryGirl.create(:live_edition, base_path: base_path, document_type: "guide", user_facing_version: 1, - locale: "en", ) - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end @@ -121,82 +108,71 @@ context "with a unpublished item, and a draft at the same base path" do before do - @blocking_content_item = FactoryGirl.create(:gone_unpublished_content_item, - content_id: SecureRandom.uuid, + @blocking_edition = FactoryGirl.create(:gone_unpublished_edition, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) - FactoryGirl.create(:content_item, - content_id: content_id, + FactoryGirl.create(:edition, + document: document, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end - it "fails, returning the id of the content item" do - expect(subject).to eq(@blocking_content_item.id) + it "fails, returning the id of the edition" do + expect(subject).to eq(@blocking_edition.id) end end context "with a published item, and a draft at the same base path" do before do - @blocking_content_item = FactoryGirl.create(:live_content_item, - content_id: SecureRandom.uuid, + @blocking_edition = FactoryGirl.create(:live_edition, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end - it "fails, returning the id of the content item" do - expect(subject).to eq(@blocking_content_item.id) + it "fails, returning the id of the edition" do + expect(subject).to eq(@blocking_edition.id) end end context "with no base_path" do let(:base_path) { nil } - let!(:blocking_content_item) do - FactoryGirl.create(:live_content_item, - content_id: SecureRandom.uuid, + let!(:blocking_edition) do + FactoryGirl.create(:live_edition, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end - let!(:blocking_content_item_2) do - FactoryGirl.create(:live_content_item, - content_id: SecureRandom.uuid, + let!(:blocking_edition_2) do + FactoryGirl.create(:live_edition, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end let!(:content) do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, document_type: document_type, user_facing_version: 1, - locale: "en", ) end diff --git a/spec/queries/locales_for_content_items_spec.rb b/spec/queries/locales_for_content_items_spec.rb deleted file mode 100644 index 8158088ad..000000000 --- a/spec/queries/locales_for_content_items_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -require "rails_helper" - -RSpec.describe Queries::LocalesForContentItems do - def create_content_item( - content_id, - type = :live_content_item, - user_facing_version = 1, - locale = "en", - base_path_prefix = "vat" - ) - FactoryGirl.create( - type, - content_id: content_id, - locale: locale, - base_path: "/#{base_path_prefix}-#{locale}", - user_facing_version: user_facing_version, - ) - end - - describe ".call" do - let(:content_id_1) { SecureRandom.uuid } - let(:content_id_2) { SecureRandom.uuid } - let(:base_content_ids) { [content_id_1, content_id_2] } - let(:content_ids) { base_content_ids } - let(:states) { %w[draft published unpublished] } - let(:include_substitutes) { false } - - subject { described_class.call(content_ids, states, include_substitutes) } - - it { is_expected.to be_a(Array) } - - context "when there are no content items" do - it { is_expected.to be_empty } - end - - context "when there are two live content items in english" do - before do - create_content_item(content_id_1, :live_content_item, 1, "en", "path-1") - create_content_item(content_id_2, :live_content_item, 1, "en", "path-2") - end - - let(:results) do - [ - [content_id_1, "en"], - [content_id_2, "en"], - ] - end - - it { is_expected.to match_array(results) } - end - - context "when there are live content items in multiple locales" do - before do - create_content_item(content_id_1, :live_content_item, 1, "en", "path-1") - create_content_item(content_id_1, :live_content_item, 1, "cy", "path-1") - create_content_item(content_id_1, :live_content_item, 1, "fr", "path-1") - create_content_item(content_id_2, :live_content_item, 1, "es", "path-2") - create_content_item(content_id_2, :live_content_item, 1, "cy", "path-2") - create_content_item(content_id_2, :live_content_item, 1, "de", "path-2") - end - - let(:results) do - [ - [content_id_1, "cy"], - [content_id_1, "en"], - [content_id_1, "fr"], - [content_id_2, "cy"], - [content_id_2, "de"], - [content_id_2, "es"], - ] - end - - it { is_expected.to match_array(results) } - end - - context "when some of the items are drafts" do - before do - create_content_item(content_id_1, :live_content_item, 1, "en", "path-1") - create_content_item(content_id_2, :draft_content_item, 1, "en", "path-2") - end - - let(:results) do - [ - [content_id_1, "en"], - [content_id_2, "en"], - ] - end - - it { is_expected.to match_array(results) } - - context "but we're only filtering on published / unpublished" do - let(:states) { %w[published unpublished] } - - let(:results) do - [ - [content_id_1, "en"], - ] - end - - it { is_expected.to match_array(results) } - end - end - - context "when some of the items are superseded" do - before do - create_content_item(content_id_1, :live_content_item, 1, "en", "path-1") - create_content_item(content_id_2, :superseded_content_item, 1, "en", "path-2") - end - - let(:results) do - [ - [content_id_1, "en"], - ] - end - - it { is_expected.to match_array(results) } - end - - context "when some of the items are unpublished type substite" do - before do - create_content_item(content_id_1, :live_content_item, 1, "en", "path-1") - create_content_item(content_id_2, :substitute_unpublished_content_item, 1, "en", "path-2") - end - - let(:results) do - [ - [content_id_1, "en"], - ] - end - - it { is_expected.to match_array(results) } - - context "and we're including substitutes" do - let(:include_substitutes) { true } - - let(:results) do - [ - [content_id_1, "en"], - [content_id_2, "en"], - ] - end - - it { is_expected.to match_array(results) } - end - end - end -end diff --git a/spec/queries/locales_for_editions_spec.rb b/spec/queries/locales_for_editions_spec.rb new file mode 100644 index 000000000..26894e89e --- /dev/null +++ b/spec/queries/locales_for_editions_spec.rb @@ -0,0 +1,116 @@ +require "rails_helper" + +RSpec.describe Queries::LocalesForEditions do + def create_edition( + content_id, + type = :live_edition, + user_facing_version = 1, + locale = "en", + base_path_prefix = "vat" + ) + FactoryGirl.create(type, + document: Document.find_or_create_by(content_id: content_id, locale: locale), + base_path: "/#{base_path_prefix}-#{locale}", + user_facing_version: user_facing_version, + ) + end + + describe ".call" do + let(:content_id_1) { SecureRandom.uuid } + let(:content_id_2) { SecureRandom.uuid } + let(:base_content_ids) { [content_id_1, content_id_2] } + let(:content_ids) { base_content_ids } + let(:content_stores) { %w[draft live] } + + subject { described_class.call(content_ids, content_stores) } + + it { is_expected.to be_a(Array) } + + context "when there are no editions" do + it { is_expected.to be_empty } + end + + context "when there are two live editions in english" do + before do + create_edition(content_id_1, :live_edition, 1, "en", "path-1") + create_edition(content_id_2, :live_edition, 1, "en", "path-2") + end + + let(:results) do + [ + [content_id_1, "en"], + [content_id_2, "en"], + ] + end + + it { is_expected.to match_array(results) } + end + + context "when there are live editions in multiple locales" do + before do + create_edition(content_id_1, :live_edition, 1, "en", "path-1") + create_edition(content_id_1, :live_edition, 1, "cy", "path-1") + create_edition(content_id_1, :live_edition, 1, "fr", "path-1") + create_edition(content_id_2, :live_edition, 1, "es", "path-2") + create_edition(content_id_2, :live_edition, 1, "cy", "path-2") + create_edition(content_id_2, :live_edition, 1, "de", "path-2") + end + + let(:results) do + [ + [content_id_1, "cy"], + [content_id_1, "en"], + [content_id_1, "fr"], + [content_id_2, "cy"], + [content_id_2, "de"], + [content_id_2, "es"], + ] + end + + it { is_expected.to match_array(results) } + end + + context "when some of the items are drafts" do + before do + create_edition(content_id_1, :live_edition, 1, "en", "path-1") + create_edition(content_id_2, :draft_edition, 1, "en", "path-2") + end + + let(:results) do + [ + [content_id_1, "en"], + [content_id_2, "en"], + ] + end + + it { is_expected.to match_array(results) } + + context "but we're only filtering on live " do + let(:content_stores) { %w[live] } + + let(:results) do + [ + [content_id_1, "en"], + ] + end + + it { is_expected.to match_array(results) } + end + end + + context "when some of the items are superseded" do + before do + create_edition(content_id_1, :live_edition, 1, "en", "path-1") + create_edition(content_id_2, :superseded_edition, 1, "en", "path-2") + end + + let(:results) do + [ + [content_id_1, "en"], + ] + end + + it { is_expected.to match_array(results) } + end + end +end diff --git a/spec/requests/content_item_requests/downstream_requests_spec.rb b/spec/requests/content_item_requests/downstream_requests_spec.rb index 252b1f08b..811c3770b 100644 --- a/spec/requests/content_item_requests/downstream_requests_spec.rb +++ b/spec/requests/content_item_requests/downstream_requests_spec.rb @@ -27,15 +27,15 @@ expect(response).to be_ok, response.body end - context "when a link set exists for the content item" do + context "when a link set exists for the edition" do let(:link_set) do FactoryGirl.create(:link_set, content_id: v2_content_item[:content_id] ) end - let(:target_content_item) { FactoryGirl.create(:content_item, base_path: "/foo", title: "foo") } - let!(:links) { FactoryGirl.create(:link, link_set: link_set, link_type: "parent", target_content_id: target_content_item.content_id) } + let(:target_edition) { FactoryGirl.create(:edition, base_path: "/foo", title: "foo") } + let!(:links) { FactoryGirl.create(:link, link_set: link_set, link_type: "parent", target_content_id: target_edition.document.content_id) } let(:content_item_for_draft_content_store) do v2_content_item.except(:update_type).merge( @@ -73,10 +73,10 @@ .except(:access_limited, :update_type) } - context "when only a draft content item exists for the link set" do + context "when only a draft edition exists for the link set" do before do - draft = FactoryGirl.create(:draft_content_item, - content_id: content_id, + draft = FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: base_path, ) @@ -84,7 +84,7 @@ FactoryGirl.create(:access_limit, users: access_limit_params.fetch(:users), - content_item: draft, + edition: draft, ) end @@ -106,10 +106,10 @@ end end - context "when only a live content item exists for the link set" do + context "when only a live edition exists for the link set" do before do - FactoryGirl.create(:live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: base_path, ) end @@ -138,21 +138,23 @@ end end - context "when draft and live content items exists for the link set" do + context "when draft and live editions exists for the link set" do before do - draft = FactoryGirl.create(:draft_content_item, - content_id: content_id, + document = FactoryGirl.create(:document, content_id: content_id) + + draft = FactoryGirl.create(:draft_edition, + document: document, base_path: base_path, user_facing_version: 2, ) FactoryGirl.create(:access_limit, users: access_limit_params.fetch(:users), - content_item: draft, + edition: draft, ) - FactoryGirl.create(:live_content_item, - content_id: content_id, + FactoryGirl.create(:live_edition, + document: document, base_path: base_path, ) end @@ -181,7 +183,7 @@ end end - context "when a content item does not exist for the link set" do + context "when an edition does not exist for the link set" do it "does not send to either content store" do expect(WebMock).not_to have_requested(:any, /.*content-store.*/) expect(PublishingAPI.service(:draft_content_store)).not_to receive(:put_content_item) @@ -200,11 +202,10 @@ let(:a) { create_link_set } let(:b) { create_link_set } - let!(:draft_a) { create_content_item(a, "/a", "superseded", "en", 1) } - let!(:published_a) { create_content_item(a, "/a", "published", 'en', 2) } - let!(:draft_b) { create_content_item(b, "/b", "draft") } - before do + create_edition(a, "/a", version: 1) + create_edition(a, "/a", factory: :draft_edition, version: 2) + create_edition(b, "/b", factory: :draft_edition) create_link(a, b, "parent") end @@ -219,7 +220,6 @@ end it "doesn't send draft dependencies to the live content store" do - published_a.update_attributes!(state: 'draft', content_store: 'draft') expect(PublishingAPI.service(:live_content_store)).to receive(:put_content_item) .with(a_hash_including(base_path: '/a')) expect(PublishingAPI.service(:live_content_store)).to_not receive(:put_content_item) @@ -229,7 +229,6 @@ end it "doesn't send draft dependencies to the message queue" do - published_a.update_attributes!(state: 'draft', content_store: 'draft') allow(PublishingAPI.service(:live_content_store)).to receive(:put_content_item) expect(PublishingAPI.service(:queue_publisher)).to receive(:send_message) .with(a_hash_including(base_path: '/a')) @@ -243,8 +242,8 @@ context "/v2/publish" do let(:content_id) { SecureRandom.uuid } let!(:draft) { - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: base_path, ) } diff --git a/spec/requests/content_item_requests/downstream_timeouts_spec.rb b/spec/requests/content_item_requests/downstream_timeouts_spec.rb index 6d6aecc2d..e847184d0 100644 --- a/spec/requests/content_item_requests/downstream_timeouts_spec.rb +++ b/spec/requests/content_item_requests/downstream_timeouts_spec.rb @@ -28,20 +28,22 @@ let(:base_path) { "/vat-rates" } before do - FactoryGirl.create(:live_content_item, + document = FactoryGirl.create(:document, content_id: content_id) + + FactoryGirl.create(:live_edition, v2_content_item - .slice(*ContentItem::TOP_LEVEL_FIELDS) - .merge(base_path: base_path, user_facing_version: 1) + .slice(*Edition::TOP_LEVEL_FIELDS) + .merge(base_path: base_path, user_facing_version: 1, document: document) ) - draft = FactoryGirl.create(:draft_content_item, + draft = FactoryGirl.create(:draft_edition, v2_content_item - .slice(*ContentItem::TOP_LEVEL_FIELDS) - .merge(base_path: base_path, user_facing_version: 2) + .slice(*Edition::TOP_LEVEL_FIELDS) + .merge(base_path: base_path, user_facing_version: 2, document: document) ) FactoryGirl.create(:access_limit, - content_item: draft, + edition: draft, users: access_limit_params.fetch(:users), ) end diff --git a/spec/requests/content_item_requests/endpoint_behaviour_spec.rb b/spec/requests/content_item_requests/endpoint_behaviour_spec.rb index 5c5fe7648..09fb73b19 100644 --- a/spec/requests/content_item_requests/endpoint_behaviour_spec.rb +++ b/spec/requests/content_item_requests/endpoint_behaviour_spec.rb @@ -20,12 +20,12 @@ expect(response.status).to eq(200) end - it "responds with the presented content item" do + it "responds with the presented edition" do put "/v2/content/#{content_id}", params: content_item.to_json - updated_content_item = ContentItem.find_by!(content_id: content_id) + updated_edition = Edition.with_document.find_by!("documents.content_id": content_id) presented_content_item = Presenters::Queries::ContentItemPresenter.present( - updated_content_item, + updated_edition, include_warnings: true, ) @@ -76,7 +76,7 @@ context "with the root path as a base_path" do let(:base_path) { "/" } - it "creates the content item" do + it "creates the edition" do put "/v2/content/#{content_id}", params: content_item.to_json expect(response.status).to eq(200) @@ -96,55 +96,47 @@ context "GET /v2/content/:content_id" do let(:content_id) { SecureRandom.uuid } - context "when the content item exists" do - let!(:content_item) { - FactoryGirl.create( - :draft_content_item, - content_id: content_id, - ) - } + context "when the document exists" do + let(:document) { FactoryGirl.create(:document, content_id: content_id) } + let!(:edition) { FactoryGirl.create(:draft_edition, document: document) } it "responds with 200" do get "/v2/content/#{content_id}" expect(response.status).to eq(200) end - it "responds with the presented content item" do + it "responds with the presented edition" do get "/v2/content/#{content_id}" - updated_content_item = ContentItem.find_by!(content_id: content_id) - presented_content_item = Presenters::Queries::ContentItemPresenter.present( - updated_content_item, - include_warnings: true, - ) - - expect(response.body).to eq(presented_content_item.to_json) - end - - it "responds with the presented content item for the correct locale" do - FactoryGirl.create(:draft_content_item, content_id: content_id, locale: "ar") + updated_edition = Edition.with_document.find_by!("documents.content_id": content_id) presented_content_item = Presenters::Queries::ContentItemPresenter.present( - content_item, + updated_edition, include_warnings: true, ) - get "/v2/content/#{content_id}" - expect(response.body).to eq(presented_content_item.to_json) end end - context "when the content item does not exist" do + context "when the document does not exist" do it "responds with 404" do get "/v2/content/#{SecureRandom.uuid}" expect(response.status).to eq(404) end end + + context "when an invalid UUID is used as content_id" do + it "responds with 404" do + expect { + get "/v2/content/INVALID_UUID" + }.to raise_error(ActionController::RoutingError) + end + end end context "/links" do context "PATCH /v2/links/:content_id" do - context "when creating a link set for a content item to be added later" do + context "when creating a link set for a document to be added later" do it "responds with 200" do patch "/v2/links/#{SecureRandom.uuid}", params: { links: {} }.to_json diff --git a/spec/requests/content_item_requests/message_bus_spec.rb b/spec/requests/content_item_requests/message_bus_spec.rb index 9d2588dbd..468380903 100644 --- a/spec/requests/content_item_requests/message_bus_spec.rb +++ b/spec/requests/content_item_requests/message_bus_spec.rb @@ -16,13 +16,13 @@ let(:request_body) { patch_links_attributes.to_json } let(:request_path) { "/v2/links/#{content_id}" } - context "with a live content item" do - let!(:live_content_item) { - FactoryGirl.create(:live_content_item, - content_id: content_id, + context "with a live edition" do + before do + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: base_path, ) - } + end it "sends a message with a 'links' routing key" do expect(DownstreamService).to receive(:broadcast_to_message_queue).with(anything, 'links') @@ -32,13 +32,13 @@ end end - context "with a draft content item" do - let!(:draft_content_item) { - FactoryGirl.create(:draft_content_item, - content_id: content_id, + context "with a draft edition" do + before do + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: content_id), base_path: base_path, ) - } + end it "doesn't send any messages" do expect(DownstreamService).to_not receive(:broadcast_to_message_queue) @@ -53,8 +53,8 @@ context "/v2/publish" do before do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, content_id: content_id), document_type: "guide", schema_name: "guide", base_path: base_path, diff --git a/spec/requests/content_item_requests/reallocating_base_path_spec.rb b/spec/requests/content_item_requests/reallocating_base_path_spec.rb index a86c58bde..1536c9cac 100644 --- a/spec/requests/content_item_requests/reallocating_base_path_spec.rb +++ b/spec/requests/content_item_requests/reallocating_base_path_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe "Reallocating base paths of content items" do +RSpec.describe "Reallocating base paths of editions" do let(:content_id) { SecureRandom.uuid } let(:base_path) { "/vat-rates" } @@ -9,46 +9,43 @@ end let(:regular_payload) do - FactoryGirl.build(:draft_content_item, - content_id: content_id, + FactoryGirl.build(:draft_edition, + document: FactoryGirl.create(:document, content_id: content_id), ).as_json.deep_symbolize_keys.merge(base_path: base_path) end describe "/v2/content" do - context "when a base path is occupied by a 'regular' content item" do + context "when a base path is occupied by a 'regular' edition" do before do - FactoryGirl.create( - :draft_content_item, + FactoryGirl.create(:draft_edition, base_path: base_path, ) end - it "cannot be replaced by another 'regular' content item" do + it "cannot be replaced by another 'regular' edition" do put "/v2/content/#{content_id}", params: regular_payload.to_json expect(response.status).to eq(422) end end end - describe "publishing a draft which has a different content_id to the published content item on the same base_path" do - let(:draft_content_id) { SecureRandom.uuid } - let(:live_content_id) { SecureRandom.uuid } + describe "publishing a draft which has a different content_id to the published edition on the same base_path" do + let(:draft_document) { FactoryGirl.create(:document) } + let(:live_document) { FactoryGirl.create(:document) } before do stub_request(:put, %r{.*content-store.*/content/.*}) end - context "when both content items are 'regular' content items" do + context "when both editions are 'regular' editions" do before do - draft = FactoryGirl.create( - :draft_content_item, - content_id: draft_content_id, + draft = FactoryGirl.create(:draft_edition, + document: draft_document, base_path: base_path ) - live = FactoryGirl.create( - :live_content_item, - content_id: live_content_id, + live = FactoryGirl.create(:live_edition, + document: live_document, base_path: base_path ) @@ -57,8 +54,8 @@ end it "raises an error" do - post "/v2/content/#{draft_content_id}/publish", - params: { update_type: "major", content_id: draft_content_id }.to_json + post "/v2/content/#{draft_document.content_id}/publish", + params: { update_type: "major", content_id: draft_document.content_id }.to_json expect(response.status).to eq(422) end diff --git a/spec/requests/discard_draft_request_spec.rb b/spec/requests/discard_draft_request_spec.rb index 78a01db5d..991f8f735 100644 --- a/spec/requests/discard_draft_request_spec.rb +++ b/spec/requests/discard_draft_request_spec.rb @@ -3,12 +3,14 @@ RSpec.describe "Discard draft requests", type: :request do let(:content_id) { SecureRandom.uuid } let(:base_path) { "/vat-rates" } + let(:document) { FactoryGirl.create(:document, content_id: content_id) } + let(:fr_document) { FactoryGirl.create(:document, content_id: content_id, locale: "fr") } describe "POST /v2/content/:content_id/discard-draft" do - context "when a draft content item exists" do - let!(:draft_content_item) do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + context "when a draft edition exists" do + let!(:draft_edition) do + FactoryGirl.create(:draft_edition, + document: document, title: "draft", base_path: base_path, ) @@ -23,7 +25,7 @@ expect(response.status).to eq(200) end - it "deletes the content item from the draft content store" do + it "deletes the edition from the draft content store" do expect(PublishingAPI.service(:draft_content_store)).to receive(:delete_content_item) .with(base_path) @@ -35,11 +37,10 @@ describe "optional locale parameter" do let(:french_base_path) { "/tva-tarifs" } - let!(:french_draft_content_item) do - FactoryGirl.create(:draft_content_item, - content_id: content_id, + let!(:french_draft_edition) do + FactoryGirl.create(:draft_edition, + document: fr_document, title: "draft", - locale: "fr", base_path: french_base_path, ) end @@ -48,7 +49,7 @@ stub_request(:delete, Plek.find('draft-content-store') + "/content#{french_base_path}") end - it "only deletes the French content item from the draft content store" do + it "only deletes the French edition from the draft content store" do expect(PublishingAPI.service(:draft_content_store)).to receive(:delete_content_item) .with(french_base_path) @@ -60,7 +61,7 @@ end end - context "when a draft content item does not exist" do + context "when a draft edition does not exist" do it "responds with 404" do post "/v2/content/#{content_id}/discard-draft", params: {}.to_json @@ -75,11 +76,9 @@ post "/v2/content/#{content_id}/discard-draft", params: {}.to_json end - context "and a live content item exists" do + context "and a live edition exists" do before do - FactoryGirl.create(:live_content_item, - content_id: content_id, - ) + FactoryGirl.create(:live_edition, document: document) end it "returns a 422" do diff --git a/spec/requests/expanded_links_endpoint_spec.rb b/spec/requests/expanded_links_endpoint_spec.rb index d17f608ee..9e347b8cc 100644 --- a/spec/requests/expanded_links_endpoint_spec.rb +++ b/spec/requests/expanded_links_endpoint_spec.rb @@ -19,38 +19,42 @@ ] } - let(:content_item) { - FactoryGirl.create(:content_item, + let(:edition) { + FactoryGirl.create(:edition, state: "published", document_type: "placeholder", schema_name: "placeholder", title: "Some title", base_path: "/some-path", description: "Some description", - content_id: "10529c0d-f4b3-4c7d-9589-35ba6a6d1a12" + document: FactoryGirl.create(:document, content_id: "10529c0d-f4b3-4c7d-9589-35ba6a6d1a12") ) } it "returns expanded links" do - organisation = FactoryGirl.create(:content_item, + organisation = FactoryGirl.create(:edition, state: "published", document_type: "organisation", schema_name: "organisation", base_path: "/my-super-org", - content_id: "9b5ae6f5-f127-4843-9333-c157a404dd2d", + document: FactoryGirl.create(:document, content_id: "9b5ae6f5-f127-4843-9333-c157a404dd2d") ) link_set = FactoryGirl.create(:link_set, - content_id: content_item.content_id, + content_id: edition.document.content_id, ) - FactoryGirl.create(:link, link_set: link_set, target_content_id: organisation.content_id, link_type: 'organisations') + FactoryGirl.create(:link, + link_set: link_set, + target_content_id: organisation.document.content_id, + link_type: "organisations", + ) - get "/v2/expanded-links/#{content_item.content_id}" + get "/v2/expanded-links/#{edition.document.content_id}" expect(parsed_response).to eql( "version" => 0, - "content_id" => content_item.content_id, + "content_id" => edition.document.content_id, "expanded_links" => { "organisations" => [ { @@ -76,14 +80,14 @@ it "returns only translations if there are no links" do link_set = FactoryGirl.create(:link_set, - content_id: content_item.content_id, + content_id: edition.document.content_id, ) get "/v2/expanded-links/#{link_set.content_id}" expect(parsed_response).to eql( "version" => 0, - "content_id" => content_item.content_id, + "content_id" => edition.document.content_id, "expanded_links" => { "available_translations" => translations, }, @@ -92,7 +96,7 @@ it "returns a version if the link set has a version" do link_set = FactoryGirl.create(:link_set, - content_id: content_item.content_id, + content_id: edition.document.content_id, ) FactoryGirl.create(:lock_version, target: link_set, number: 11) @@ -101,7 +105,7 @@ expect(parsed_response).to eql( "version" => 11, - "content_id" => content_item.content_id, + "content_id" => edition.document.content_id, "expanded_links" => { "available_translations" => translations, }, @@ -109,12 +113,13 @@ end it "returns 404 if the link set is not found" do - get "/v2/expanded-links/I-DO-NOT-EXIST" + content_id = SecureRandom.uuid + get "/v2/expanded-links/#{content_id}" expect(parsed_response).to eql( "error" => { "code" => 404, - "message" => "Could not find link set with content_id: I-DO-NOT-EXIST", + "message" => "Could not find link set with content_id: #{content_id}", } ) end diff --git a/spec/requests/index_spec.rb b/spec/requests/index_spec.rb index b6148e1f4..5ac698693 100644 --- a/spec/requests/index_spec.rb +++ b/spec/requests/index_spec.rb @@ -2,7 +2,7 @@ RSpec.describe "GET /v2/content", type: :request do let!(:policy_1) { - FactoryGirl.create(:content_item, + FactoryGirl.create(:edition, state: "draft", document_type: "policy", schema_name: "policy", @@ -12,7 +12,7 @@ } let!(:policy_2) { - FactoryGirl.create(:content_item, + FactoryGirl.create(:edition, state: "published", document_type: "policy", schema_name: "policy", diff --git a/spec/requests/linkables_spec.rb b/spec/requests/linkables_spec.rb index c6be7ea06..047fe2270 100644 --- a/spec/requests/linkables_spec.rb +++ b/spec/requests/linkables_spec.rb @@ -2,7 +2,7 @@ RSpec.describe "GET /v2/linkables", type: :request do let!(:policy_1) { - FactoryGirl.create(:content_item, + FactoryGirl.create(:edition, state: "draft", document_type: "policy", title: "Policy 1", @@ -14,7 +14,7 @@ } let!(:policy_2) { - FactoryGirl.create(:content_item, + FactoryGirl.create(:edition, state: "published", document_type: "policy", title: "Policy 2", @@ -28,19 +28,19 @@ end end - it "returns the title, content ID, state, internal name and base path for all content items of a given format" do + it "returns the title, content ID, state, internal name and base path for all editions of a given format" do get "/v2/linkables", params: { document_type: "policy" } expect(JSON.parse(response.body, symbolize_names: true)).to match_array([ hash_including( - content_id: policy_1.content_id, + content_id: policy_1.document.content_id, title: "Policy 1", publication_state: "draft", base_path: "/cat-rates", internal_name: "Cat rates (do not use for actual cats)", ), hash_including( - content_id: policy_2.content_id, + content_id: policy_2.document.content_id, title: "Policy 2", publication_state: "published", base_path: "/vat-rates", diff --git a/spec/requests/logging_requests_spec.rb b/spec/requests/logging_requests_spec.rb index 834587757..776f37181 100644 --- a/spec/requests/logging_requests_spec.rb +++ b/spec/requests/logging_requests_spec.rb @@ -21,12 +21,12 @@ end it "adds a request uuid to the message bus" do - draft_content_item = FactoryGirl.create(:draft_content_item, base_path: base_path) + draft_edition = FactoryGirl.create(:draft_edition, base_path: base_path) expect(PublishingAPI.service(:queue_publisher)).to receive(:send_message) .with(hash_including(govuk_request_id: govuk_request_id)) - post("/v2/content/#{draft_content_item.content_id}/publish", params: { update_type: "minor" }.to_json, + post("/v2/content/#{draft_edition.document.content_id}/publish", params: { update_type: "minor" }.to_json, headers: { "HTTP_GOVUK_REQUEST_ID" => "12345-67890" } ) end @@ -37,9 +37,6 @@ let(:a) { create_link_set } let(:b) { create_link_set } - let!(:draft_a) { create_content_item(a, "/a", "draft", "en", 2) } - let!(:draft_b) { create_content_item(b, "/b", "draft") } - let(:params) do v2_content_item.merge( content_id: a, @@ -49,6 +46,8 @@ end before do + create_edition(a, "/a") + create_edition(b, "/b") create_link(a, b, "parent") end @@ -67,7 +66,7 @@ expect(WebMock).to have_requested(:put, /draft-content-store.*content\/b/) .with(headers: { - "GOVUK-Dependency-Resolution-Source-Content-Id" => draft_a.content_id, + "GOVUK-Dependency-Resolution-Source-Content-Id" => a, }) end end diff --git a/spec/requests/lookups_spec.rb b/spec/requests/lookups_spec.rb index 18bca8761..a2e715c99 100644 --- a/spec/requests/lookups_spec.rb +++ b/spec/requests/lookups_spec.rb @@ -25,11 +25,15 @@ end def create_test_content - FactoryGirl.create(:live_content_item, state: "published", base_path: "/published-and-draft-page", content_id: "aa491126-77ed-4e81-91fa-8dc7f74e9657", user_facing_version: 1) - FactoryGirl.create(:content_item, state: "draft", base_path: "/published-and-draft-page", content_id: "aa491126-77ed-4e81-91fa-8dc7f74e9657", user_facing_version: 2) - FactoryGirl.create(:live_content_item, state: "published", base_path: "/only-published-page", content_id: "bbabcd3c-7c45-4403-8490-db51e4bfc4f6") - FactoryGirl.create(:content_item, state: "draft", base_path: "/draft-and-superseded-page", content_id: "dd1bf833-f91c-4e45-9f97-87b165808176", user_facing_version: 2) - FactoryGirl.create(:superseded_content_item, state: "superseded", base_path: "/draft-and-superseded-page", content_id: "dd1bf833-f91c-4e45-9f97-87b165808176", user_facing_version: 1) + doc1 = FactoryGirl.create(:document, content_id: "aa491126-77ed-4e81-91fa-8dc7f74e9657") + doc2 = FactoryGirl.create(:document, content_id: "bbabcd3c-7c45-4403-8490-db51e4bfc4f6") + doc3 = FactoryGirl.create(:document, content_id: "dd1bf833-f91c-4e45-9f97-87b165808176") + + FactoryGirl.create(:live_edition, state: "published", base_path: "/published-and-draft-page", document: doc1, user_facing_version: 1) + FactoryGirl.create(:edition, state: "draft", base_path: "/published-and-draft-page", document: doc1, user_facing_version: 2) + FactoryGirl.create(:live_edition, state: "published", base_path: "/only-published-page", document: doc2) + FactoryGirl.create(:edition, state: "draft", base_path: "/draft-and-superseded-page", document: doc3, user_facing_version: 2) + FactoryGirl.create(:superseded_edition, state: "superseded", base_path: "/draft-and-superseded-page", document: doc3, user_facing_version: 1) end def test_base_paths diff --git a/spec/requests/publish_intent_requests_spec.rb b/spec/requests/publish_intent_requests_spec.rb index 4c101a9d9..646641ab3 100644 --- a/spec/requests/publish_intent_requests_spec.rb +++ b/spec/requests/publish_intent_requests_spec.rb @@ -65,7 +65,7 @@ context "with the root path as a base_path" do let(:base_path) { "/" } - it "creates the content item" do + it "creates the reservation" do put "/publish-intent#{base_path}", params: content_item.to_json expect(response.status).to eq(200) diff --git a/spec/requests/unpublishing_spec.rb b/spec/requests/unpublishing_spec.rb index 1af586fc5..2fe3c9772 100644 --- a/spec/requests/unpublishing_spec.rb +++ b/spec/requests/unpublishing_spec.rb @@ -9,12 +9,13 @@ RSpec.describe "POST /v2/content/:content_id/unpublish", type: :request do let(:content_id) { SecureRandom.uuid } let(:base_path) { "/vat-rates" } - let!(:content_item) { - FactoryGirl.create(:live_content_item, - content_id: content_id, + let!(:document) { FactoryGirl.create(:document, content_id: content_id) } + let!(:edition) do + FactoryGirl.create(:live_edition, + document: document, base_path: base_path, ) - } + end describe "withdrawing" do let(:withdrawal_params) { @@ -40,7 +41,7 @@ expect(response.status).to eq(200), response.body - unpublishing = Unpublishing.find_by(content_item: content_item) + unpublishing = Unpublishing.find_by(edition: edition) expect(unpublishing.type).to eq("withdrawal") expect(unpublishing.explanation).to eq("Test withdrawal") end @@ -92,7 +93,7 @@ document_type: "redirect", schema_name: "redirect", base_path: base_path, - publishing_app: content_item.publishing_app, + publishing_app: edition.publishing_app, public_updated_at: Time.zone.now.iso8601, redirects: [ { @@ -111,7 +112,7 @@ expect(response.status).to eq(200), response.body - unpublishing = Unpublishing.find_by(content_item: content_item) + unpublishing = Unpublishing.find_by(edition: edition) expect(unpublishing.type).to eq("redirect") expect(unpublishing.alternative_path).to eq("/new-path") end @@ -164,7 +165,7 @@ base_path: base_path, document_type: "gone", schema_name: "gone", - publishing_app: content_item.publishing_app, + publishing_app: edition.publishing_app, details: { explanation: "Test gone", alternative_path: "/new-path", @@ -185,7 +186,7 @@ expect(response.status).to eq(200), response.body - unpublishing = Unpublishing.find_by(content_item: content_item) + unpublishing = Unpublishing.find_by(edition: edition) expect(unpublishing.type).to eq("gone") expect(unpublishing.explanation).to eq("Test gone") expect(unpublishing.alternative_path).to eq("/new-path") @@ -236,7 +237,7 @@ expect(response.status).to eq(200), response.body - unpublishing = Unpublishing.find_by(content_item: content_item) + unpublishing = Unpublishing.find_by(edition: edition) expect(unpublishing.type).to eq("vanish") end diff --git a/spec/service_consumers/pact_helper.rb b/spec/service_consumers/pact_helper.rb index 73b338fd6..335b8d7df 100644 --- a/spec/service_consumers/pact_helper.rb +++ b/spec/service_consumers/pact_helper.rb @@ -67,10 +67,11 @@ provider_state "a content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - draft = FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + + FactoryGirl.create(:draft_edition, base_path: "/robots.txt", - content_id: "bed722e6-db68-43e5-9079-063f623335a7", + document: document, title: "Instructions for crawler robots", description: "robots.txt provides rules for which parts of GOV.UK are permitted to be crawled by different bots.", document_type: "special_route", @@ -85,65 +86,57 @@ }, ], ) - - FactoryGirl.create(:lock_version, target: draft, number: 1) end end provider_state "a draft content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - draft = FactoryGirl.create( - :draft_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", - ) - FactoryGirl.create(:lock_version, target: draft, number: 1) + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + + FactoryGirl.create(:draft_edition, document: document) end end provider_state "a draft content item exists with content_id bed722e6-db68-43e5-9079-063f623335a7 with a blocking live item at the same path" do set_up do - live = FactoryGirl.create( - :live_content_item, + FactoryGirl.create(:live_edition, + document: FactoryGirl.create(:document), base_path: "/blocking_path", ) - FactoryGirl.create(:lock_version, target: live, number: 1) - draft = FactoryGirl.create( - :draft_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", + + draft_document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + + FactoryGirl.create(:draft_edition, + document: draft_document, base_path: "/blocking_path", ) - FactoryGirl.create(:lock_version, target: draft, number: 1) end end provider_state "a French content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - draft = FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", locale: "fr", ) - FactoryGirl.create(:lock_version, target: draft, number: 1) + + FactoryGirl.create(:draft_edition, document: document) end end provider_state "a published content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - live = FactoryGirl.create( - :live_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7" - ) + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") - FactoryGirl.create(:lock_version, target: live, number: 1) + FactoryGirl.create(:live_edition, document: document) end end provider_state "an unpublished content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - FactoryGirl.create( - :unpublished_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7" - ) + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + + FactoryGirl.create(:unpublished_edition, document: document) end end @@ -151,19 +144,21 @@ set_up do link_set = FactoryGirl.create(:link_set, content_id: "bed722e6-db68-43e5-9079-063f623335a7", + stale_lock_version: 2, ) - linked_organisation = FactoryGirl.create(:content_item, content_id: "20583132-1619-4c68-af24-77583172c070") - FactoryGirl.create(:lock_version, target: link_set, number: 2) - FactoryGirl.create(:link, link_set: link_set, link_type: "organisations", target_content_id: linked_organisation.content_id) + + document = FactoryGirl.create(:document, content_id: "20583132-1619-4c68-af24-77583172c070") + FactoryGirl.create(:edition, document: document) + FactoryGirl.create(:link, link_set: link_set, link_type: "organisations", target_content_id: document.content_id) end end provider_state "empty links exist for content_id bed722e6-db68-43e5-9079-063f623335a7" do set_up do - link_set = FactoryGirl.create(:link_set, + FactoryGirl.create(:link_set, content_id: "bed722e6-db68-43e5-9079-063f623335a7", + stale_lock_version: 2, ) - FactoryGirl.create(:lock_version, target: link_set, number: 2) end end @@ -175,42 +170,42 @@ provider_state "a draft content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7 and locale: fr" do set_up do - draft = FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", locale: "fr", ) - FactoryGirl.create(:lock_version, target: draft, number: 1) + + FactoryGirl.create(:draft_edition, document: document) end end provider_state "a content item exists in multiple locales with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - FactoryGirl.create( - :draft_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", - locale: "en", + en_doc = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + fr_doc = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", locale: "fr") + ar_doc = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", locale: "ar") + + FactoryGirl.create(:draft_edition, + document: en_doc, document_type: "topic", schema_name: "topic", - public_updated_at: '2015-01-03', + public_updated_at: "2015-01-03", user_facing_version: 1, ) - FactoryGirl.create( - :draft_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", - locale: "fr", + + FactoryGirl.create(:draft_edition, + document: fr_doc, document_type: "topic", schema_name: "topic", - public_updated_at: '2015-01-02', + public_updated_at: "2015-01-02", user_facing_version: 1, ) - FactoryGirl.create( - :draft_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", - locale: "ar", + + FactoryGirl.create(:draft_edition, + document: ar_doc, document_type: "topic", schema_name: "topic", - public_updated_at: '2015-01-01', + public_updated_at: "2015-01-01", user_facing_version: 1, ) end @@ -218,21 +213,21 @@ provider_state "a content item exists in with a superseded version with content_id: bed722e6-db68-43e5-9079-063f623335a7" do set_up do - FactoryGirl.create(:superseded_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", - locale: "en", + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + + FactoryGirl.create(:superseded_edition, + document: document, document_type: "topic", schema_name: "topic", - public_updated_at: '2015-01-03', + public_updated_at: "2015-01-03", user_facing_version: 1, ) - FactoryGirl.create(:live_content_item, - content_id: "bed722e6-db68-43e5-9079-063f623335a7", - locale: "en", + FactoryGirl.create(:live_edition, + document: document, document_type: "topic", schema_name: "topic", - public_updated_at: '2015-01-03', + public_updated_at: "2015-01-03", user_facing_version: 2, ) end @@ -240,11 +235,12 @@ provider_state "the content item bed722e6-db68-43e5-9079-063f623335a7 is at lock version 3" do set_up do - draft = FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", + stale_lock_version: 3, ) - FactoryGirl.create(:lock_version, target: draft, number: 3) + + FactoryGirl.create(:draft_edition, document: document) stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("content-store")) + "/content")) stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("draft-content-store")) + "/content")) @@ -253,18 +249,18 @@ provider_state "the linkset for bed722e6-db68-43e5-9079-063f623335a7 is at lock version 3" do set_up do - draft = FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", + stale_lock_version: 1, ) - FactoryGirl.create(:lock_version, target: draft, number: 1) - linkset = FactoryGirl.create(:link_set, + FactoryGirl.create(:draft_edition, document: document) + + FactoryGirl.create(:link_set, content_id: "bed722e6-db68-43e5-9079-063f623335a7", + stale_lock_version: 3, ) - FactoryGirl.create(:lock_version, target: linkset, number: 3) - stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("content-store")) + "/content")) stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("draft-content-store")) + "/content")) end @@ -272,9 +268,13 @@ provider_state "there is content with document_type 'topic'" do set_up do - FactoryGirl.create(:draft_content_item, + document_a = FactoryGirl.create(:document, + content_id: "aaaaaaaa-aaaa-1aaa-aaaa-aaaaaaaaaaaa", + ) + + FactoryGirl.create(:draft_edition, title: 'Content Item A', - content_id: 'aaaaaaaa-aaaa-1aaa-aaaa-aaaaaaaaaaaa', + document: document_a, base_path: '/a-base-path', document_type: "topic", schema_name: "topic", @@ -284,9 +284,13 @@ }, ) - FactoryGirl.create(:live_content_item, + document_b = FactoryGirl.create(:document, + content_id: "bbbbbbbb-bbbb-2bbb-bbbb-bbbbbbbbbbbb", + ) + + FactoryGirl.create(:live_edition, title: 'Content Item B', - content_id: 'bbbbbbbb-bbbb-2bbb-bbbb-bbbbbbbbbbbb', + document: document_b, base_path: '/another-base-path', public_updated_at: '2015-01-01', document_type: "topic", @@ -297,33 +301,34 @@ provider_state "there is content with document_type 'topic' for multiple publishing apps" do set_up do - content_item = FactoryGirl.create( - :draft_content_item, + document_a = FactoryGirl.create(:document) + document_b = FactoryGirl.create(:document) + document_c = FactoryGirl.create(:document) + + FactoryGirl.create(:draft_edition, + document: document_a, title: 'Content Item A', base_path: '/a-base-path', document_type: "topic", schema_name: "topic", ) - FactoryGirl.create(:lock_version, target: content_item, number: 1) - content_item = FactoryGirl.create( - :draft_content_item, + FactoryGirl.create(:draft_edition, + document: document_b, title: 'Content Item B', base_path: '/another-base-path', document_type: "topic", schema_name: "topic", ) - FactoryGirl.create(:lock_version, target: content_item, number: 1) - content_item = FactoryGirl.create( - :draft_content_item, + FactoryGirl.create(:draft_edition, + document: document_c, title: 'Content Item C', base_path: '/yet-another-base-path', document_type: "topic", schema_name: "topic", publishing_app: 'whitehall', ) - FactoryGirl.create(:lock_version, target: content_item, number: 1) end end @@ -333,28 +338,29 @@ content_id2 = "08dfd5c3-d935-4e81-88fd-cfe65b78893d" content_id3 = "e2961462-bc37-48e9-bb98-c981ef1a2d59" - FactoryGirl.create( - :live_content_item, - content_id: content_id1, + document_1 = FactoryGirl.create(:document, content_id: content_id1) + document_2 = FactoryGirl.create(:document, content_id: content_id2) + document_3 = FactoryGirl.create(:document, content_id: content_id3) + + FactoryGirl.create(:live_edition, + document: document_1, user_facing_version: 1, ) - FactoryGirl.create( - :draft_content_item, - content_id: content_id1, + + FactoryGirl.create(:draft_edition, + document: document_1, user_facing_version: 2 ) - FactoryGirl.create( - :live_content_item, - content_id: content_id3, + FactoryGirl.create(:live_edition, + document: document_3, base_path: '/item-b', public_updated_at: '2015-01-02', user_facing_version: 1, ) - FactoryGirl.create( - :live_content_item, - content_id: content_id2, + FactoryGirl.create(:live_edition, + document: document_2, base_path: '/item-a', public_updated_at: '2015-01-01', user_facing_version: 1, @@ -370,9 +376,10 @@ provider_state "a content item exists with content_id: bed722e6-db68-43e5-9079-063f623335a7 and it has details" do set_up do - FactoryGirl.create( - :draft_content_item, - content_id: 'bed722e6-db68-43e5-9079-063f623335a7', + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7") + + FactoryGirl.create(:draft_edition, + document: document, document_type: "topic", schema_name: "topic", details: { foo: :bar }, @@ -382,12 +389,13 @@ provider_state "the content item bed722e6-db68-43e5-9079-063f623335a7 is at version 3" do set_up do - FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", - lock_version: 3 + stale_lock_version: 3, ) + FactoryGirl.create(:draft_edition, document: document) + stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("content-store")) + "/content")) stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("draft-content-store")) + "/content")) end @@ -395,12 +403,13 @@ provider_state "the published content item bed722e6-db68-43e5-9079-063f623335a7 is at version 3" do set_up do - FactoryGirl.create( - :live_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", - lock_version: 3 + stale_lock_version: 3 ) + FactoryGirl.create(:live_edition, document: document) + stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("content-store")) + "/content")) stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("draft-content-store")) + "/content")) end @@ -408,19 +417,18 @@ provider_state "the linkset for bed722e6-db68-43e5-9079-063f623335a7 is at version 3" do set_up do - FactoryGirl.create( - :draft_content_item, + document = FactoryGirl.create(:document, content_id: "bed722e6-db68-43e5-9079-063f623335a7", - lock_version: 1 + stale_lock_version: 1, ) - link_set = FactoryGirl.create( - :link_set, + FactoryGirl.create(:draft_edition, document: document) + + FactoryGirl.create(:link_set, content_id: "bed722e6-db68-43e5-9079-063f623335a7", + stale_lock_version: 3, ) - FactoryGirl.create(:lock_version, target: link_set, number: 3) - stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("content-store")) + "/content")) stub_request(:put, Regexp.new('\A' + Regexp.escape(Plek.find("draft-content-store")) + "/content")) end @@ -428,17 +436,13 @@ provider_state "there are live content items with base_paths /foo and /bar" do set_up do - FactoryGirl.create( - :live_content_item, - base_path: '/foo', - content_id: '08f86d00-e95f-492f-af1d-470c5ba4752e', - ) + document_1 = FactoryGirl.create(:document, content_id: "08f86d00-e95f-492f-af1d-470c5ba4752e") - FactoryGirl.create( - :live_content_item, - base_path: '/bar', - content_id: 'ca6c58a6-fb9d-479d-b3e6-74908781cb18', - ) + FactoryGirl.create(:live_edition, base_path: '/foo', document: document_1) + + document_2 = FactoryGirl.create(:document, content_id: "ca6c58a6-fb9d-479d-b3e6-74908781cb18") + + FactoryGirl.create(:live_edition, base_path: '/bar', document: document_2) end end end diff --git a/spec/services/downstream_service_spec.rb b/spec/services/downstream_service_spec.rb index a577f7303..3c217bc6d 100644 --- a/spec/services/downstream_service_spec.rb +++ b/spec/services/downstream_service_spec.rb @@ -258,7 +258,7 @@ context "conflict" do before do - FactoryGirl.create(:unpublished_content_item, base_path: "/test") + FactoryGirl.create(:unpublished_edition, base_path: "/test") end it "returns true" do diff --git a/spec/substitution_helper_spec.rb b/spec/substitution_helper_spec.rb index 86409b36b..784d4179f 100644 --- a/spec/substitution_helper_spec.rb +++ b/spec/substitution_helper_spec.rb @@ -6,7 +6,7 @@ let(:existing_base_path) { "/vat-rates" } let!(:existing_item) { - FactoryGirl.create(:draft_content_item, + FactoryGirl.create(:draft_edition, document_type: existing_document_type, base_path: existing_base_path, ) @@ -31,15 +31,15 @@ end context "when the content_id is the same as the existing item" do - let(:new_content_id) { existing_item.content_id } + let(:new_content_id) { existing_item.document.content_id } it "does not discard the existing draft" do - expect(ContentItem.exists?(id: existing_item.id)).to eq(true) + expect(Edition.exists?(id: existing_item.id)).to eq(true) end context "when the existing item is published" do let!(:existing_item) { - FactoryGirl.create(:live_content_item, + FactoryGirl.create(:live_edition, document_type: existing_document_type, base_path: existing_base_path, ) @@ -58,22 +58,22 @@ let(:existing_document_type) { "gone" } it "discards the existing draft" do - expect(ContentItem.exists?(id: existing_item.id)).to eq(false) + expect(Edition.exists?(id: existing_item.id)).to eq(false) end it "doesn't unpublish any other items" do - live_item = FactoryGirl.create(:live_content_item, + live_item = FactoryGirl.create(:live_edition, document_type: existing_document_type, base_path: existing_base_path, ) - french_item = FactoryGirl.create(:draft_content_item, + french_item = FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, locale: "fr"), document_type: existing_document_type, base_path: existing_base_path, - locale: "fr", ) - item_elsewhere = FactoryGirl.create(:draft_content_item, + item_elsewhere = FactoryGirl.create(:draft_edition, document_type: existing_document_type, base_path: "/somewhere-else", ) @@ -85,7 +85,7 @@ context "when the existing item is published" do let!(:existing_item) { - FactoryGirl.create(:live_content_item, + FactoryGirl.create(:live_edition, document_type: existing_document_type, base_path: existing_base_path, ) @@ -101,22 +101,22 @@ let(:new_document_type) { "gone" } it "discards the existing draft" do - expect(ContentItem.exists?(id: existing_item.id)).to eq(false) + expect(Edition.exists?(id: existing_item.id)).to eq(false) end it "doesn't unpublish any other items" do - live_item = FactoryGirl.create(:live_content_item, + live_item = FactoryGirl.create(:live_edition, document_type: existing_document_type, base_path: existing_base_path, ) - french_item = FactoryGirl.create(:draft_content_item, + french_item = FactoryGirl.create(:draft_edition, + document: FactoryGirl.create(:document, locale: "fr"), document_type: existing_document_type, base_path: existing_base_path, - locale: "fr", ) - item_elsewhere = FactoryGirl.create(:draft_content_item, + item_elsewhere = FactoryGirl.create(:draft_edition, document_type: existing_document_type, base_path: "/somewhere-else", ) @@ -128,7 +128,7 @@ context "when the existing item is published" do let!(:existing_item) { - FactoryGirl.create(:live_content_item, + FactoryGirl.create(:live_edition, document_type: existing_document_type, base_path: existing_base_path, ) @@ -142,12 +142,12 @@ context "when neither item has a document_type that is substitutable" do it "does not discard the existing draft" do - expect(ContentItem.exists?(id: existing_item.id)).to eq(true) + expect(Edition.exists?(id: existing_item.id)).to eq(true) end context "when the existing item is published" do let!(:existing_item) { - FactoryGirl.create(:live_content_item, + FactoryGirl.create(:live_edition, document_type: existing_document_type, base_path: existing_base_path, ) diff --git a/spec/support/dependency_resolution_helper.rb b/spec/support/dependency_resolution_helper.rb index 4539acb5a..dbd631f61 100644 --- a/spec/support/dependency_resolution_helper.rb +++ b/spec/support/dependency_resolution_helper.rb @@ -4,36 +4,24 @@ def create_link_set link_set.content_id end - def create_content_item( + def create_edition( content_id, base_path, - state = "published", - locale = "en", - version = 1 + factory: :live_edition, + locale: "en", + version: 1 ) - FactoryGirl.create( - :content_item, - content_id: content_id, + FactoryGirl.create(factory, + document: Document.find_or_create_by(content_id: content_id, locale: locale), base_path: base_path, - state: state, - content_store: content_store_for(state), - locale: locale, - document_type: 'topical_event', - details: {}, user_facing_version: version, ) end - def content_store_for(state) - return if state == 'superseded' - state == 'draft' ? 'draft' : 'live' - end - def create_link(from, to, link_type, link_position = 0) link_set = LinkSet.find_or_create_by(content_id: from) - FactoryGirl.create( - :link, + FactoryGirl.create(:link, link_set: link_set, target_content_id: to, link_type: link_type, diff --git a/spec/support/routes_and_redirects_validator.rb b/spec/support/routes_and_redirects_validator.rb index c34cdc0e0..b6a627fa1 100644 --- a/spec/support/routes_and_redirects_validator.rb +++ b/spec/support/routes_and_redirects_validator.rb @@ -1,7 +1,7 @@ RSpec.shared_examples_for RoutesAndRedirectsValidator do describe "routes validations" do it "is invalid when a route is not below the base path" do - content_item.routes = [ + edition.routes = [ { path: subject.base_path, type: "exact" }, { path: "/wrong-path", type: "exact" }, ] @@ -11,7 +11,7 @@ end it "must have unique paths" do - content_item.routes = [ + edition.routes = [ { path: subject.base_path, type: "exact" }, { path: subject.base_path, type: "exact" }, ] @@ -21,29 +21,29 @@ end it "must have a type" do - content_item.routes = [{ path: subject.base_path }] + edition.routes = [{ path: subject.base_path }] expect(subject).to be_invalid end it "must have a path" do - content_item.routes = [{ type: "exact" }] + edition.routes = [{ type: "exact" }] expect(subject).to be_invalid end it "must have a valid type" do - content_item.routes = [{ path: subject.base_path, type: "unsupported" }] + edition.routes = [{ path: subject.base_path, type: "unsupported" }] expect(subject).to be_invalid end it "cannot have extra keys" do - content_item.routes = [{ path: subject.base_path, type: "exact", foo: "bar" }] + edition.routes = [{ path: subject.base_path, type: "exact", foo: "bar" }] expect(subject).to be_invalid expect(subject.errors[:routes]).to eq(["unsupported keys: foo"]) end it "is valid with a dashed locale" do - content_item.routes = [ + edition.routes = [ { path: subject.base_path, type: "exact" }, { path: "#{subject.base_path}.es-419", type: "exact" }, ] @@ -52,7 +52,7 @@ end it "must contain valid absolute paths" do - content_item.routes = [ + edition.routes = [ { path: subject.base_path, type: "exact" }, { path: "#{subject.base_path}/ not valid", type: "exact" }, ] @@ -62,39 +62,39 @@ end it "does not throw an error when routes is nil rather than an empty array" do - content_item.routes = nil + edition.routes = nil expect { subject.valid? }.to_not raise_error end context "for a redirect item" do before do - content_item.document_type = "redirect" - content_item.routes = [] + edition.document_type = "redirect" + edition.routes = [] end it "must not have routes" do - content_item.routes = [{ path: "#{subject.base_path}/foo", type: "exact" }] + edition.routes = [{ path: "#{subject.base_path}/foo", type: "exact" }] expect(subject).to be_invalid end end context "for a non-redirect item" do before do - content_item.document_type = "guide" + edition.document_type = "guide" end it "must have routes" do - content_item.routes = [{ path: subject.base_path, type: "exact" }] + edition.routes = [{ path: subject.base_path, type: "exact" }] expect(subject).to be_valid end it "must include the base path" do - content_item.routes = [{ path: "#{subject.base_path}/foo", type: "exact" }] + edition.routes = [{ path: "#{subject.base_path}/foo", type: "exact" }] expect(subject).to be_invalid end it "does not throw an error when routes is nil rather than an empty array" do - content_item.routes = nil + edition.routes = nil expect { subject.valid? }.to_not raise_error end end @@ -102,7 +102,7 @@ describe "redirects validations" do it "is invalid when a redirect is not below the base path" do - content_item.redirects = [ + edition.redirects = [ { path: "/wrong-path", type: "exact", destination: "/bar" }, ] @@ -111,7 +111,7 @@ end it "must have unique paths" do - content_item.redirects = [ + edition.redirects = [ { path: "#{subject.base_path}/foo", type: "exact", destination: "/bar" }, { path: "#{subject.base_path}/foo", type: "exact", destination: "/bar" }, ] @@ -121,38 +121,38 @@ end it "must have a valid type" do - content_item.redirects = [{ path: subject.base_path, type: "unsupported", destination: "/foo" }] + edition.redirects = [{ path: subject.base_path, type: "unsupported", destination: "/foo" }] expect(subject).to be_invalid end it "cannot have extra keys" do - content_item.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: "/foo", foo: "bar" }] + edition.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: "/foo", foo: "bar" }] expect(subject).to be_invalid expect(subject.errors[:redirects]).to eq(["unsupported keys: foo"]) end it "is valid with a dashed locale" do - content_item.redirects = [{ path: "#{subject.base_path}.es-419", type: "exact", destination: "/foo" }] + edition.redirects = [{ path: "#{subject.base_path}.es-419", type: "exact", destination: "/foo" }] expect(subject).to be_valid end it "must contain valid absolute paths" do - content_item.redirects = [{ path: "#{subject.base_path}/ not valid", type: "exact", destination: "/foo" }] + edition.redirects = [{ path: "#{subject.base_path}/ not valid", type: "exact", destination: "/foo" }] expect(subject).to be_invalid expect(subject.errors[:redirects]).to eq(["is not a valid absolute URL path"]) end it "does not throw an error when redirects is nil rather than an empty array" do - content_item.redirects = nil + edition.redirects = nil expect { subject.valid? }.to_not raise_error end context "when destination is external url" do it "is invalid if it is not an external gov.uk campaign url" do ["https://www.example.com/foo/bar", "https://www.gov.uk/foo/bar"].each do |destination| - content_item.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] + edition.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] expect(subject).to be_invalid expect(subject.errors[:redirects]).to eq(["is not a valid redirect destination"]) @@ -161,7 +161,7 @@ it "is invalid if the url is a malformed gov.uk campaign external url" do ["://new-vat-rates.campaign.gov.uk/", "http:new-vat-rates.campaign.gov.uk/", "httpsnew-vat-rates.campaign.gov.uk/", "https://new_vat-rates.campaign.gov.uk/", "http://.campaign.gov.uk/", "http://new-vat-rates.campaign.gov.uk/path/to/your/new/vat-rates", "http://new-vat-rates.campaignjservicepgov.uk/path/to/your/new/vat-rates", "https://fakesite.net/.new-vat-rates.campaign.gov.uk/path/to/your/new/vat-rates", "ftp://new-vat-rates.campaign.gov.uk/"].each do |destination| - content_item.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] + edition.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] expect(subject).to be_invalid expect(subject.errors[:redirects]).to eq(["is not a valid redirect destination"]) @@ -170,7 +170,7 @@ it "is valid if the url is a wellformed gov.uk campaign external url" do ["https://new-vat-rates.campaign.gov.uk/", "https://new-vat-rates.campaign.gov.uk/path/to/your/new/vat-rates", "https://new-vat-rates.campaign.gov.uk/path/to/your/new/vat-rates?q=123&&a=23344"].each do |destination| - content_item.redirects = [{ path: "#{subject.base_path}/new", type: "exact", destination: destination }] + edition.redirects = [{ path: "#{subject.base_path}/new", type: "exact", destination: destination }] expect(subject).to be_valid expect(subject.errors[:redirects]).to eq([]) @@ -180,7 +180,7 @@ context "when the type is 'prefix'" do it "must contain valid absolute paths for destinations" do - content_item.redirects = [{ path: "#{subject.base_path}/foo", type: "prefix", destination: "not valid" }] + edition.redirects = [{ path: "#{subject.base_path}/foo", type: "prefix", destination: "not valid" }] expect(subject).to be_invalid expect(subject.errors[:redirects]).to eq(["is not a valid absolute URL path"]) @@ -190,14 +190,14 @@ context "when the type is 'exact'" do it "is valid with an optional query string and fragment in destination" do %w(/foo/bar /foo?bar=baz /foo/bar#baz).each do |destination| - content_item.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] + edition.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] expect(subject).to be_valid end end it "is invalid with an non-absolute url" do ["foo/bar", "/url with spaces", "fdjkdfjkljsdaf"].each do |destination| - content_item.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] + edition.redirects = [{ path: "#{subject.base_path}/foo", type: "exact", destination: destination }] expect(subject).to be_invalid expect(subject.errors[:redirects]).to eq(["is not a valid redirect destination"]) @@ -207,34 +207,34 @@ context "for a redirect item" do before do - content_item.schema_name = "redirect" - content_item.document_type = "redirect" - content_item.routes = [] + edition.schema_name = "redirect" + edition.document_type = "redirect" + edition.routes = [] end it "must have redirects" do - content_item.redirects = [{ path: subject.base_path, type: "exact", destination: "/foo" }] + edition.redirects = [{ path: subject.base_path, type: "exact", destination: "/foo" }] expect(subject).to be_valid end it "must include the base path" do - content_item.redirects = [{ path: "#{subject.base_path}/bar", type: "exact", destination: "/bar" }] + edition.redirects = [{ path: "#{subject.base_path}/bar", type: "exact", destination: "/bar" }] expect(subject).to be_invalid end end context "for a non-redirect item" do before do - content_item.document_type = "guide" + edition.document_type = "guide" end it "can have redirects" do - content_item.redirects = [{ path: "#{subject.base_path}/bar", type: "exact", destination: "/bar" }] + edition.redirects = [{ path: "#{subject.base_path}/bar", type: "exact", destination: "/bar" }] expect(subject).to be_valid end it "does not throw an error when redirects is nil rather than an empty array" do - content_item.redirects = nil + edition.redirects = nil expect { subject.valid? }.to_not raise_error end end @@ -242,8 +242,8 @@ describe "validations that cross-over between routes and redirects" do it "does not allow redirects to duplicate any of the routes" do - content_item.routes = [{ path: subject.base_path, type: "exact" }] - content_item.redirects = [{ path: subject.base_path, type: "exact", destination: "/foo" }] + edition.routes = [{ path: subject.base_path, type: "exact" }] + edition.redirects = [{ path: subject.base_path, type: "exact", destination: "/foo" }] expect(subject).to be_invalid end diff --git a/spec/support/transactional_command.rb b/spec/support/transactional_command.rb index c0c5a58d5..c2b0222da 100644 --- a/spec/support/transactional_command.rb +++ b/spec/support/transactional_command.rb @@ -15,17 +15,17 @@ module TransactionalCommand it "wraps the command in a transaction" do allow_any_instance_of(described_class).to receive(:call) do - FactoryGirl.create(:content_item, state: "published") + FactoryGirl.create(:live_edition) raise "Uh oh, command failed half-way through processing" end - previous_count = ContentItem.count + previous_count = Edition.count expect { described_class.call(payload) }.to raise_error(/half-way through/) - new_count = ContentItem.count + new_count = Edition.count expect(new_count).to eq(previous_count), "The transaction should have been rolled back" diff --git a/spec/validators/base_path_for_state_validator_spec.rb b/spec/validators/base_path_for_state_validator_spec.rb index b208d2683..924648731 100644 --- a/spec/validators/base_path_for_state_validator_spec.rb +++ b/spec/validators/base_path_for_state_validator_spec.rb @@ -4,16 +4,15 @@ let(:state_name) { "draft" } let(:base_path) { "/vat-rates" } - let(:content_item) do - FactoryGirl.build( - :content_item, + let(:edition) do + FactoryGirl.build(:edition, state: state_name, base_path: base_path, ) end describe ".validate" do - subject(:validate) { described_class.new.validate(content_item) } + subject(:validate) { described_class.new.validate(edition) } context "when state is nil" do let(:state_name) { nil } @@ -25,24 +24,29 @@ it { is_expected.to be_nil } end - context "when there are multiple content items" do + context "when there are multiple editions" do let(:conflict_content_id) { SecureRandom.uuid } let(:conflict_state_name) { "draft" } let(:conflict_base_path) { "/vat-rates-2016" } let(:conflict_locale) { "en" } - let!(:conflict_content_item) do - FactoryGirl.create( - :content_item, + let(:conflict_document) do + FactoryGirl.create(:document, content_id: conflict_content_id, + locale: conflict_locale, + ) + end + + let!(:conflict_edition) do + FactoryGirl.create(:edition, + document: conflict_document, state: conflict_state_name, base_path: conflict_base_path, - locale: conflict_locale, user_facing_version: 2, ) end - before { content_item.base_path = conflict_base_path } + before { edition.base_path = conflict_base_path } context "when state is draft" do let(:state_name) { "draft" } @@ -54,8 +58,8 @@ end before { validate } - it "adds the error to content_item attribute" do - expect(content_item.errors[:base]).to eq([expected_error]) + it "adds the error to edition attribute" do + expect(edition.errors[:base]).to eq([expected_error]) end end end @@ -77,8 +81,8 @@ end before { validate } - it "adds the error to content_item attribute" do - expect(content_item.errors[:base]).to eq([expected_error]) + it "adds the error to edition attribute" do + expect(edition.errors[:base]).to eq([expected_error]) end end end diff --git a/spec/validators/state_for_document_validator_spec.rb b/spec/validators/state_for_document_validator_spec.rb new file mode 100644 index 000000000..892795e22 --- /dev/null +++ b/spec/validators/state_for_document_validator_spec.rb @@ -0,0 +1,66 @@ +require "rails_helper" + +RSpec.describe StateForDocumentValidator do + let(:state_name) { "draft" } + let(:document) { FactoryGirl.create(:document) } + + let(:edition) do + FactoryGirl.build(:edition, + state: state_name, + document: document, + ) + end + + describe "#validate" do + subject(:validate) { described_class.new.validate(edition) } + + context "when document is nil" do + before { edition.document = nil } + it { is_expected.to be_nil } + end + + context "when state is nil" do + before { edition.state = nil } + it { is_expected.to be_nil } + end + + context "when state and document can conflict" do + [ + { factory: :draft_edition, state: "draft", scenario: "both items are drafts" }, + { factory: :live_edition, state: "published", scenario: "both items are published" }, + { factory: :live_edition, state: "unpublished", scenario: "existing item is published and new item is unpublished" }, + { factory: :unpublished_edition, state: "published", name: "existing item is unpublished and new item is published" }, + ].each do |hash| + context "when #{hash[:scenario]}" do + let!(:conflict_edition) { + FactoryGirl.create(hash[:factory], + document: document, + ) + } + let(:state_name) { hash[:state] } + let(:expected_error) do + "state=#{hash[:state]} and document=#{document.id} conflicts " + + "with edition id=#{conflict_edition.id}" + end + + before do + validate + end + + it "adds the error to the base attribute" do + expect(edition.errors[:base]).to eq([expected_error]) + end + end + end + + context "when state is superseded" do + let!(:conflict_edition) { + FactoryGirl.create(:superseded_edition, document: document) + } + let(:state_name) { "superseded" } + + it { is_expected.to be_nil } + end + end + end +end diff --git a/spec/validators/state_for_locale_validator_spec.rb b/spec/validators/state_for_locale_validator_spec.rb deleted file mode 100644 index 670f3fd0b..000000000 --- a/spec/validators/state_for_locale_validator_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require "rails_helper" - -RSpec.describe StateForLocaleValidator do - let(:state_name) { "draft" } - let(:locale) { "en" } - - let(:content_item) do - FactoryGirl.build( - :content_item, - state: state_name, - locale: locale, - ) - end - - describe "#validate" do - subject(:validate) { described_class.new.validate(content_item) } - - context "when locale is nil" do - before { content_item.locale = nil } - it { is_expected.to be_nil } - end - - context "when state is nil" do - before { content_item.state = nil } - it { is_expected.to be_nil } - end - - context "when version, state and content_id can conflict" do - [ - { factory: :draft_content_item, state: "draft", scenario: "both items are drafts" }, - { factory: :live_content_item, state: "published", scenario: "both items are published" }, - { factory: :live_content_item, state: "unpublished", scenario: "existing item is published and new item is unpublished" }, - { factory: :unpublished_content_item, state: "published", name: "existing item is unpublished and new item is published" }, - ].each do |hash| - context "when #{hash[:scenario]}" do - let!(:conflict_content_item) { - FactoryGirl.create( - hash[:factory], - content_id: content_item.content_id, - locale: "fr", - ) - } - let(:state_name) { hash[:state] } - let(:expected_error) do - "state=#{hash[:state]} and locale=fr for content " + - "item=#{content_item.content_id} conflicts with content item " + - "id=#{conflict_content_item.id}" - end - - before do - content_item.locale = "fr" - validate - end - - it "adds the error to the base attribute" do - expect(content_item.errors[:base]).to eq([expected_error]) - end - end - end - - context "when state is superseded" do - let!(:conflict_content_item) { - FactoryGirl.create( - :superseded_content_item, - content_id: content_item.content_id, - locale: "fr", - ) - } - let(:state_name) { "superseded" } - - before { content_item.locale = "fr" } - - it { is_expected.to be_nil } - end - end - end -end diff --git a/spec/validators/version_for_document_validator_spec.rb b/spec/validators/version_for_document_validator_spec.rb new file mode 100644 index 000000000..43523db73 --- /dev/null +++ b/spec/validators/version_for_document_validator_spec.rb @@ -0,0 +1,52 @@ +require "rails_helper" + +RSpec.describe VersionForDocumentValidator do + let(:version) { 5 } + let(:document) { FactoryGirl.create(:document) } + + let(:edition) do + FactoryGirl.build(:edition, + document: document, + user_facing_version: version, + ) + end + + describe "#validate" do + subject(:validate) { described_class.new.validate(edition) } + + context "when it's missing a edition" do + it { is_expected.to be_nil } + end + + context "when document is nil" do + before { edition.document_id = nil } + it { is_expected.to be_nil } + end + + context "when version number is nil" do + before { edition.user_facing_version = nil } + it { is_expected.to be_nil } + end + + context "when version and document are the same" do + let!(:conflict_edition) { + FactoryGirl.create(:edition, + document: document, + user_facing_version: version, + ) + } + let(:expected_error) do + "user_facing_version=#{version} and document=#{document.id} " + + "conflicts with edition id=#{conflict_edition.id}" + end + + before do + validate + end + + it "adds the error to the base attribute" do + expect(edition.errors[:base]).to eq([expected_error]) + end + end + end +end diff --git a/spec/validators/version_for_locale_validator_spec.rb b/spec/validators/version_for_locale_validator_spec.rb deleted file mode 100644 index beb4a68d3..000000000 --- a/spec/validators/version_for_locale_validator_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require "rails_helper" - -RSpec.describe VersionForLocaleValidator do - let(:version) { 5 } - let(:locale) { "en" } - - let(:content_item) do - FactoryGirl.create( - :content_item, - user_facing_version: version, - locale: locale, - ) - end - - describe "#validate" do - subject(:validate) { described_class.new.validate(content_item) } - - context "when it's missing a content item" do - it { is_expected.to be_nil } - end - - context "when locale is nil" do - before { content_item.locale = nil } - it { is_expected.to be_nil } - end - - context "when version number is nil" do - before { content_item.user_facing_version = nil } - it { is_expected.to be_nil } - end - - context "when version, locale and content_id are the same" do - let!(:conflict_content_item) { - FactoryGirl.create( - :content_item, - content_id: content_item.content_id, - locale: "fr", - user_facing_version: version, - ) - } - let(:expected_error) do - "user_facing_version=#{version} and locale=fr for content item=" + - "#{content_item.content_id} conflicts with content item " + - "id=#{conflict_content_item.id}" - end - before do - content_item.locale = "fr" - validate - end - - it "adds the error to the base attribute" do - expect(content_item.errors[:base]).to eq([expected_error]) - end - end - end -end diff --git a/spec/workers/dependency_resolution_worker_spec.rb b/spec/workers/dependency_resolution_worker_spec.rb index 145ea9ec3..24e137f8d 100644 --- a/spec/workers/dependency_resolution_worker_spec.rb +++ b/spec/workers/dependency_resolution_worker_spec.rb @@ -1,9 +1,10 @@ require "rails_helper" RSpec.describe DependencyResolutionWorker, :perform do - let(:live_content_item) { FactoryGirl.create(:live_content_item, locale: "en") } let(:content_id) { SecureRandom.uuid } let(:locale) { "en" } + let(:document) { FactoryGirl.create(:document, content_id: content_id, locale: locale) } + let(:live_edition) { FactoryGirl.create(:live_edition, document: document) } subject(:worker_perform) do described_class.new.perform( @@ -14,10 +15,10 @@ ) end - let(:content_item_dependee) { double(:content_item_dependent, call: []) } + let(:edition_dependee) { double(:edition_dependent, call: []) } let(:dependencies) do [ - [live_content_item.content_id, "en"], + [content_id, "en"], ] end @@ -26,12 +27,12 @@ allow_any_instance_of(Queries::ContentDependencies).to receive(:call).and_return(dependencies) end - it "finds the content item dependees" do + it "finds the edition dependees" do expect(Queries::ContentDependencies).to receive(:new).with( content_id: content_id, locale: locale, - state_fallback_order: %w[published unpublished], - ).and_return(content_item_dependee) + content_stores: %w[live], + ).and_return(edition_dependee) worker_perform end @@ -50,10 +51,9 @@ end context "with a draft version available" do - let!(:draft_content_item) do - FactoryGirl.create(:draft_content_item, - content_id: live_content_item.content_id, - locale: "en", + let!(:draft_edition) do + FactoryGirl.create(:draft_edition, + document: document, user_facing_version: 2, ) end @@ -62,7 +62,7 @@ expect(DownstreamLiveWorker).to receive(:perform_async_in_queue).with( anything, a_hash_including( - content_id: live_content_item.content_id, + content_id: content_id, locale: "en", ) ) @@ -78,7 +78,7 @@ expect(DownstreamDraftWorker).to receive(:perform_async_in_queue).with( anything, a_hash_including( - content_id: draft_content_item.content_id, + content_id: content_id, locale: "en", ) ) @@ -91,7 +91,7 @@ end end - context "when there are translations of a content item" do + context "when there are translations of an edition" do context "and locale is specified" do let(:dependencies) do [ diff --git a/spec/workers/downstream_discard_draft_worker_spec.rb b/spec/workers/downstream_discard_draft_worker_spec.rb index 5fc9eb660..f7586effe 100644 --- a/spec/workers/downstream_discard_draft_worker_spec.rb +++ b/spec/workers/downstream_discard_draft_worker_spec.rb @@ -3,10 +3,9 @@ RSpec.describe DownstreamDiscardDraftWorker do let(:base_path) { "/foo" } - let(:content_item) do - FactoryGirl.create(:draft_content_item, + let(:edition) do + FactoryGirl.create(:draft_edition, base_path: base_path, - locale: "en", title: "Draft", ) end @@ -14,7 +13,7 @@ let(:arguments) do { "base_path" => base_path, - "content_id" => content_item.content_id, + "content_id" => edition.document.content_id, "locale" => "en", "payload_version" => 1, "update_dependencies" => true, @@ -23,7 +22,7 @@ end before do - content_item.destroy + edition.destroy stub_request(:put, %r{.*content-store.*/content/.*}) stub_request(:delete, %r{.*content-store.*/content/.*}) end @@ -72,21 +71,21 @@ end end - context "has a live content item with same base_path" do - let!(:live_content_item) do - FactoryGirl.create(:live_content_item, + context "has a live edition with same base_path" do + let!(:live_edition) do + FactoryGirl.create(:live_edition, base_path: base_path, - content_id: content_item.content_id, + document: edition.document, title: "live", ) end let(:live_content_item_arguments) do - arguments.merge("live_content_item_id" => live_content_item.id) + arguments.merge("live_content_item_id" => live_edition.id) end - it "adds the live content item to the draft content store" do + it "adds the live edition to the draft content store" do expect(Adapters::DraftContentStore).to receive(:put_content_item) - .with(base_path, a_hash_including(title: live_content_item.title)) + .with(base_path, a_hash_including(title: live_edition.title)) subject.perform(live_content_item_arguments) end @@ -96,21 +95,21 @@ end end - context "has a live content item with a different base_path" do - let(:live_content_item) do - FactoryGirl.create(:live_content_item, + context "has a live edition with a different base_path" do + let(:live_edition) do + FactoryGirl.create(:live_edition, base_path: "/bar", - content_id: content_item.content_id, + document: edition.document, title: "Live", ) end let(:live_content_item_arguments) do - arguments.merge("live_content_item_id" => live_content_item.id) + arguments.merge("live_content_item_id" => live_edition.id) end - it "adds the live content item to the draft content store" do + it "adds the live edition to the draft content store" do expect(Adapters::DraftContentStore).to receive(:put_content_item) - .with("/bar", a_hash_including(title: live_content_item.title)) + .with("/bar", a_hash_including(title: live_edition.title)) subject.perform(live_content_item_arguments) end @@ -121,7 +120,7 @@ end end - context "doesn't have a live content item" do + context "doesn't have a live edition" do it "doesn't add to live draft content store" do expect(Adapters::DraftContentStore).to_not receive(:put_content_item) subject.perform(arguments) @@ -149,15 +148,14 @@ end it "wont send to content store without a base_path" do - pathless = FactoryGirl.create( - :draft_content_item, + pathless = FactoryGirl.create(:draft_edition, base_path: nil, document_type: "contact", schema_name: "contact" ) expect(Adapters::DraftContentStore).to_not receive(:delete_content_item) subject.perform( - arguments.merge("content_id" => pathless.content_id, "base_path" => nil) + arguments.merge("content_id" => pathless.document.content_id, "base_path" => nil) ) end end @@ -179,14 +177,14 @@ end describe "conflict protection" do - let(:content_id) { content_item.content_id } + let(:content_id) { edition.content_id } let(:logger) { Sidekiq::Logging.logger } before do - FactoryGirl.create(:live_content_item, base_path: "/foo") + FactoryGirl.create(:live_edition, base_path: "/foo") end - it "doesn't delete content item from content store" do + it "doesn't delete edition from content store" do expect(Adapters::DraftContentStore).to_not receive(:delete_content_item) subject.perform(arguments) end diff --git a/spec/workers/downstream_draft_worker_spec.rb b/spec/workers/downstream_draft_worker_spec.rb index f02a102be..c0a99cb7c 100644 --- a/spec/workers/downstream_draft_worker_spec.rb +++ b/spec/workers/downstream_draft_worker_spec.rb @@ -1,13 +1,13 @@ require "rails_helper" RSpec.describe DownstreamDraftWorker do - let(:content_item) do - FactoryGirl.create(:draft_content_item, base_path: "/foo", locale: "en") + let(:edition) do + FactoryGirl.create(:draft_edition, base_path: "/foo") end let(:base_arguments) do { - "content_id" => content_item.content_id, + "content_id" => edition.document.content_id, "locale" => "en", "payload_version" => 1, "update_dependencies" => true, @@ -26,7 +26,7 @@ subject.perform(arguments.except("content_id")) }.to raise_error(KeyError) expect { - subject.perform(arguments.merge("content_item_id" => content_item.id)) + subject.perform(arguments.merge("content_item_id" => edition.id)) }.not_to raise_error end @@ -44,31 +44,30 @@ end describe "sends to draft content store" do - context "content item has a base path" do + context "edition has a base path" do it "sends put content to draft content store" do expect(Adapters::DraftContentStore).to receive(:put_content_item) subject.perform(arguments) end it "receives the base path" do - base_path = ContentItem.where(id: content_item.id).pluck(:base_path).first + base_path = Edition.where(id: edition.id).pluck(:base_path).first expect(Adapters::DraftContentStore).to receive(:put_content_item) .with(base_path, anything) subject.perform(arguments) end end - context "content item has a nil base path" do + context "edition has a nil base path" do it "doesn't send the item to the draft content store" do - pathless = FactoryGirl.create( - :draft_content_item, + pathless = FactoryGirl.create(:draft_edition, base_path: nil, document_type: "contact", schema_name: "contact", ) expect(Adapters::DraftContentStore).to_not receive(:put_content_item) - subject.perform(arguments.merge("content_id" => pathless.content_id)) + subject.perform(arguments.merge("content_id" => pathless.document.content_id)) end end end diff --git a/spec/workers/downstream_live_worker_spec.rb b/spec/workers/downstream_live_worker_spec.rb index cba0ae151..8e235c432 100644 --- a/spec/workers/downstream_live_worker_spec.rb +++ b/spec/workers/downstream_live_worker_spec.rb @@ -1,13 +1,13 @@ require "rails_helper" RSpec.describe DownstreamLiveWorker do - let(:content_item) do - FactoryGirl.create(:live_content_item, base_path: "/foo", locale: "en") + let(:edition) do + FactoryGirl.create(:live_edition, base_path: "/foo") end let(:base_arguments) do { - "content_id" => content_item.content_id, + "content_id" => edition.document.content_id, "locale" => "en", "payload_version" => 1, "message_queue_update_type" => "major", @@ -27,7 +27,7 @@ subject.perform(arguments.except("content_id")) }.to raise_error(KeyError) expect { - subject.perform(arguments.merge("content_item_id" => content_item.id)) + subject.perform(arguments.merge("content_item_id" => edition.id)) }.not_to raise_error end @@ -51,16 +51,16 @@ end describe "send to live content store" do - context "published content item" do + context "published edition" do it "sends content to live content store" do expect(Adapters::ContentStore).to receive(:put_content_item) subject.perform(arguments) end end - context "unpublished content item" do - let(:unpublished_content_item) { FactoryGirl.create(:unpublished_content_item) } - let(:unpublished_arguments) { arguments.merge(content_id: unpublished_content_item.content_id) } + context "unpublished edition" do + let(:unpublished_edition) { FactoryGirl.create(:unpublished_edition) } + let(:unpublished_arguments) { arguments.merge(content_id: unpublished_edition.document.content_id) } it "sends content to live content store" do expect(Adapters::ContentStore).to receive(:put_content_item) @@ -68,9 +68,9 @@ end end - context "superseded content item" do - let(:superseded_content_item) { FactoryGirl.create(:live_content_item, state: "superseded") } - let(:superseded_arguments) { arguments.merge(content_id: superseded_content_item.content_id) } + context "superseded edition" do + let(:superseded_edition) { FactoryGirl.create(:superseded_edition) } + let(:superseded_arguments) { arguments.merge(content_id: superseded_edition.document.content_id) } it "doesn't send to live content store" do expect(Adapters::ContentStore).to_not receive(:put_content_item) @@ -85,14 +85,13 @@ end it "wont send to content store without a base_path" do - pathless = FactoryGirl.create( - :live_content_item, + pathless = FactoryGirl.create(:live_edition, base_path: nil, document_type: "contact", schema_name: "contact" ) expect(Adapters::ContentStore).to_not receive(:put_content_item) - subject.perform(arguments.merge("content_id" => pathless.content_id)) + subject.perform(arguments.merge("content_id" => pathless.document.content_id)) end end @@ -128,23 +127,23 @@ end describe "draft-to-live protection" do - it "rejects draft content items" do - draft = FactoryGirl.create(:draft_content_item) + it "rejects draft editions" do + draft = FactoryGirl.create(:draft_edition) expect(Airbrake).to receive(:notify) .with(an_instance_of(AbortWorkerError), a_hash_including(:parameters)) - subject.perform(arguments.merge("content_id" => draft.content_id)) + subject.perform(arguments.merge("content_id" => draft.document.content_id)) end - it "allows live content items" do - live = FactoryGirl.create(:live_content_item) + it "allows live editions" do + live = FactoryGirl.create(:live_edition) expect(Airbrake).to_not receive(:notify) - subject.perform(arguments.merge("content_id" => live.content_id)) + subject.perform(arguments.merge("content_id" => live.document.content_id)) end end - describe "no content item" do + describe "no edition" do it "swallows the error" do expect(Airbrake).to receive(:notify) .with(an_instance_of(AbortWorkerError), a_hash_including(:parameters))