From bcc75129cb139de19260668fde73d00538850437 Mon Sep 17 00:00:00 2001 From: Justin Coyne Date: Thu, 30 Mar 2023 15:56:53 -0500 Subject: [PATCH] Cache virtual object membership This is required for adding relatedItem entries on the public xml. Fixes #585 --- app/models/cocina_data.rb | 9 ++ app/models/purl.rb | 1 + app/models/virtual_object_constituent.rb | 4 + app/services/purl_cocina_updater.rb | 7 +- ...2410_create_virtual_object_constituents.rb | 12 +++ db/schema.rb | 58 ++++++++----- spec/consumers/purl_updates_consumer_spec.rb | 87 ++++++++++++++----- spec/factories/purl.rb | 4 +- 8 files changed, 135 insertions(+), 47 deletions(-) create mode 100644 app/models/virtual_object_constituent.rb create mode 100644 db/migrate/20230330202410_create_virtual_object_constituents.rb diff --git a/app/models/cocina_data.rb b/app/models/cocina_data.rb index 60ee419d..6ab368b5 100644 --- a/app/models/cocina_data.rb +++ b/app/models/cocina_data.rb @@ -45,6 +45,15 @@ def collections cocina_object.dro? ? cocina_object.structural.isMemberOf : [] end + # Find virtual objects in argo via: + # https://argo-stage.stanford.edu/catalog?f%5Bhas_constituents_ssim%5D%5B%5D=has_constituents + # @return [Array] The members of this virtual object + def virtual_object_constituents + return [] unless cocina_object.dro? && cocina_object.structural.hasMemberOrders.present? + + cocina_object.structural.hasMemberOrders.first.members + end + private # from https://github.com/sul-dlss/dor-services-app/blob/f51bbcea710b7612f251a3922c5164ec69ba39aa/app/services/publish/public_xml_service.rb#L99 diff --git a/app/models/purl.rb b/app/models/purl.rb index 7b8f2be0..00ebed6b 100644 --- a/app/models/purl.rb +++ b/app/models/purl.rb @@ -2,6 +2,7 @@ class Purl < ApplicationRecord has_and_belongs_to_many :collections has_many :release_tags, dependent: :destroy has_one :public_xml, dependent: :destroy + has_many :virtual_object_constituents, dependent: :destroy accepts_nested_attributes_for :public_xml, update_only: true paginates_per 100 diff --git a/app/models/virtual_object_constituent.rb b/app/models/virtual_object_constituent.rb new file mode 100644 index 00000000..2547ca4e --- /dev/null +++ b/app/models/virtual_object_constituent.rb @@ -0,0 +1,4 @@ +class VirtualObjectConstituent < ApplicationRecord + default_scope { order(ordinal: :asc) } + belongs_to :purl +end diff --git a/app/services/purl_cocina_updater.rb b/app/services/purl_cocina_updater.rb index 6d8bea22..cc76fa92 100644 --- a/app/services/purl_cocina_updater.rb +++ b/app/services/purl_cocina_updater.rb @@ -9,7 +9,7 @@ def initialize(active_record, cocina_object) attr_reader :active_record, :cocina_data - delegate :collections, :releases, to: :cocina_data + delegate :collections, :releases, :virtual_object_constituents, to: :cocina_data def attributes { @@ -30,6 +30,11 @@ def update # public xml active_record.refresh_collections(collections) + active_record.virtual_object_constituents = [] + virtual_object_constituents.each.with_index do |member, i| + active_record.virtual_object_constituents.build(has_member: member, ordinal: i) + end + # add the release tags, and reuse tags if already associated with this PURL active_record.refresh_release_tags(releases) diff --git a/db/migrate/20230330202410_create_virtual_object_constituents.rb b/db/migrate/20230330202410_create_virtual_object_constituents.rb new file mode 100644 index 00000000..a80bd292 --- /dev/null +++ b/db/migrate/20230330202410_create_virtual_object_constituents.rb @@ -0,0 +1,12 @@ +class CreateVirtualObjectConstituents < ActiveRecord::Migration[7.0] + def change + create_table :virtual_object_constituents do |t| + t.references :purl, null: false, foreign_key: true + t.string :has_member, null: false, index: true + t.integer :ordinal, null: false + t.index [:purl_id, :has_member], unique: true + t.index [:purl_id, :ordinal], unique: true + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4f7aa162..f3a4b0e1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,14 +10,13 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_01_03_184105) do - +ActiveRecord::Schema[7.0].define(version: 2023_03_30_202410) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false - t.integer "record_id", null: false - t.integer "blob_id", null: false - t.datetime "created_at", precision: 6, null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end @@ -30,20 +29,20 @@ t.string "service_name", null: false t.integer "byte_size", null: false t.string "checksum" - t.datetime "created_at", precision: 6, null: false + t.datetime "created_at", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end create_table "active_storage_variant_records", force: :cascade do |t| - t.integer "blob_id", null: false + t.bigint "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end create_table "collections", force: :cascade do |t| t.string "druid", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["druid"], name: "index_collections_on_druid", unique: true end @@ -56,30 +55,30 @@ create_table "listener_logs", force: :cascade do |t| t.integer "process_id", null: false - t.datetime "started_at", precision: 6, null: false - t.datetime "active_at", precision: 6 - t.datetime "ended_at", precision: 6 - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "started_at", precision: nil, null: false + t.datetime "active_at", precision: nil + t.datetime "ended_at", precision: nil + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["process_id"], name: "index_listener_logs_on_process_id" t.index ["started_at"], name: "index_listener_logs_on_started_at" end create_table "public_xmls", force: :cascade do |t| t.integer "purl_id" - t.binary "data" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.binary "data", limit: 16777216 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["purl_id"], name: "index_public_xmls_on_purl_id" end create_table "purls", force: :cascade do |t| t.string "druid", null: false t.string "object_type" - t.datetime "deleted_at", precision: 6 - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.datetime "published_at", precision: 6 + t.datetime "deleted_at", precision: nil + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false + t.datetime "published_at", precision: nil t.text "title" t.string "catkey" t.index ["deleted_at"], name: "index_purls_on_deleted_at" @@ -94,13 +93,26 @@ t.string "name", null: false t.boolean "release_type", null: false t.integer "purl_id", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["name", "purl_id"], name: "index_release_tags_on_name_and_purl_id", unique: true t.index ["purl_id"], name: "index_release_tags_on_purl_id" t.index ["release_type"], name: "index_release_tags_on_release_type" end + create_table "virtual_object_constituents", force: :cascade do |t| + t.integer "purl_id", null: false + t.string "has_member", null: false + t.integer "ordinal", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["has_member"], name: "index_virtual_object_constituents_on_has_member" + t.index ["purl_id", "has_member"], name: "index_virtual_object_constituents_on_purl_id_and_has_member", unique: true + t.index ["purl_id", "ordinal"], name: "index_virtual_object_constituents_on_purl_id_and_ordinal", unique: true + t.index ["purl_id"], name: "index_virtual_object_constituents_on_purl_id" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "virtual_object_constituents", "purls" end diff --git a/spec/consumers/purl_updates_consumer_spec.rb b/spec/consumers/purl_updates_consumer_spec.rb index 19834b75..be1af17a 100644 --- a/spec/consumers/purl_updates_consumer_spec.rb +++ b/spec/consumers/purl_updates_consumer_spec.rb @@ -4,30 +4,75 @@ let(:message) { instance_double(Racecar::Message, value: message_value) } let!(:purl_object) { create(:purl) } - let(:message_value) do - build(:dro, id: purl_object.druid, - title: "The Information Paradox for Black Holes", - collection_ids: ['druid:xb432gf1111']) - .new(administrative: { - hasAdminPolicy: "druid:hv992ry2431", - releaseTags: [ - { to: 'Searchworks', release: true }, - { to: 'Earthworks', release: false } - ] - }) - .to_json - end - before do described_class.new.process(message) end - it "updates the purl record with the provided data" do - purl_object.reload - expect(purl_object.title).to eq "The Information Paradox for Black Holes" - expect(purl_object.true_targets).to eq ["Searchworks", "SearchWorksPreview", "ContentSearch"] - expect(purl_object.false_targets).to eq ['Earthworks'] - expect(purl_object.collections.size).to eq 1 - expect(purl_object.collections.first.druid).to eq 'druid:xb432gf1111' + context 'with a DRO' do + let(:message_value) do + build(:dro, id: purl_object.druid, + title: "The Information Paradox for Black Holes", + collection_ids: ['druid:xb432gf1111']) + .new(administrative: { + hasAdminPolicy: "druid:hv992ry2431", + releaseTags: [ + { to: 'Searchworks', release: true }, + { to: 'Earthworks', release: false } + ] + }) + .to_json + end + + it "updates the purl record with the provided data" do + purl_object.reload + expect(purl_object.title).to eq "The Information Paradox for Black Holes" + expect(purl_object.true_targets).to eq ["Searchworks", "SearchWorksPreview", "ContentSearch"] + expect(purl_object.false_targets).to eq ['Earthworks'] + expect(purl_object.collections.size).to eq 1 + expect(purl_object.collections.first.druid).to eq 'druid:xb432gf1111' + end + end + + context 'with a virtual object' do + let(:message_value) do + build(:dro, id: purl_object.druid, title: "The Information Paradox for Black Holes") + .new(administrative: { + hasAdminPolicy: "druid:hv992ry2431", + releaseTags: [ + { to: 'Searchworks', release: true }, + { to: 'Earthworks', release: false } + ] + }, + structural: { + isMemberOf: ['druid:xb432gf1111'], + hasMemberOrders: [{ + members: [ + 'druid:kq126jw7402', + 'druid:cv761kr7119', + 'druid:kn300wd1779', + 'druid:rz617vr4473', + 'druid:sd322dt2118', + 'druid:hp623ch4433', + 'druid:sq217qj5005', + 'druid:vd823mb5658', + 'druid:zp230ft8517', + 'druid:xx933wk5286', + 'druid:qf828rv2163' + ] + }] + }) + .to_json + end + + it "updates the purl record with the provided data" do + purl_object.reload + expect(purl_object.title).to eq "The Information Paradox for Black Holes" + expect(purl_object.true_targets).to eq ["Searchworks", "SearchWorksPreview", "ContentSearch"] + expect(purl_object.false_targets).to eq ['Earthworks'] + expect(purl_object.collections.size).to eq 1 + expect(purl_object.collections.first.druid).to eq 'druid:xb432gf1111' + expect(purl_object.virtual_object_constituents.first.has_member).to eq 'druid:kq126jw7402' + expect(purl_object.virtual_object_constituents.size).to eq 11 + end end end diff --git a/spec/factories/purl.rb b/spec/factories/purl.rb index 92bb5404..665bd2f0 100644 --- a/spec/factories/purl.rb +++ b/spec/factories/purl.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :purl do - sequence :druid do |n| - "druid:zz#{n.to_s * 3}yy#{n.to_s * 4}" + sequence :druid do + "druid:zz#{format('%03d', rand(1000))}yy#{format('%04d', rand(10_000))}" end end end