diff --git a/app/components/works/edit/access_settings_component.html.erb b/app/components/works/edit/access_settings_component.html.erb index 72187a45..93270991 100644 --- a/app/components/works/edit/access_settings_component.html.erb +++ b/app/components/works/edit/access_settings_component.html.erb @@ -11,3 +11,11 @@

<%= render Elements::HorizontalRuleComponent.new(classes: 'my-3') %> + +<%= render Works::Edit::SectionHeaderComponent.new(title: 'Download access') %> +<% if depositor_selects_access? %> + <%= render Elements::Forms::SelectFieldComponent.new(form:, field_name: :access, options: access_options, width: 250, label: 'Who can download the files?') %> +<% else %> + <%= collection_selects_access_message %> + <%= form.hidden_field :access %> +<% end %> diff --git a/app/components/works/edit/access_settings_component.rb b/app/components/works/edit/access_settings_component.rb index 1fc60bf0..0bc88b6a 100644 --- a/app/components/works/edit/access_settings_component.rb +++ b/app/components/works/edit/access_settings_component.rb @@ -4,10 +4,27 @@ module Works module Edit # Component for rendering the access settings pane. class AccessSettingsComponent < ApplicationComponent - def initialize(form:) + def initialize(form:, collection:) @form = form + @collection = collection super() end + + attr_reader :form + + delegate :depositor_selects_access?, to: :@collection + + def access_options + [ + [I18n.t('access.stanford'), 'stanford'], + [I18n.t('access.world'), 'world'] + ] + end + + def collection_selects_access_message + access_text = form.object.access == 'stanford' ? 'the Stanford Community' : 'anyone' + "The files in your deposit will be downloadable by #{access_text}." + end end end end diff --git a/app/controllers/works_controller.rb b/app/controllers/works_controller.rb index 47159dfe..7cd24cb4 100644 --- a/app/controllers/works_controller.rb +++ b/app/controllers/works_controller.rb @@ -18,14 +18,13 @@ def show end def new - collection = Collection.find_by!(druid: params.expect(:collection_druid)) + @collection = Collection.find_by!(druid: params.expect(:collection_druid)) - authorize! collection, with: WorkPolicy + authorize! @collection, with: WorkPolicy - @collection_title = collection.title @content = Content.create!(user: current_user) - @work_form = WorkForm.new(collection_druid: collection.druid, content_id: @content.id, license: collection.license) - @license_presenter = LicensePresenter.new(work_form: @work_form, collection:) + @work_form = new_work_form + @license_presenter = LicensePresenter.new(work_form: @work_form, collection: @collection) render :form end @@ -40,7 +39,7 @@ def edit # This updates the Work with the latest metadata from the Cocina object. ModelSync::Work.call(work: @work, cocina_object: @cocina_object) - @collection_title = @work.collection.title + @collection = @work.collection @license_presenter = LicensePresenter.new(work_form: @work_form, collection: @work.collection) render :form @@ -48,21 +47,20 @@ def edit def create # rubocop:disable Metrics/AbcSize @work_form = WorkForm.new(**work_params) - collection = Collection.find_by!(druid: @work_form.collection_druid) - @collection_title = collection.title - authorize! collection, with: WorkPolicy + @collection = Collection.find_by!(druid: @work_form.collection_druid) + authorize! @collection, with: WorkPolicy # The validation_context param determines whether extra validations are applied, e.g., for deposits. if @work_form.valid?(validation_context) # Setting the deposit_job_started_at to the current time to indicate that the deposit job has started and user # should be "waiting". work = Work.create!(title: @work_form.title, user: current_user, deposit_job_started_at: Time.zone.now, - collection:) + collection: @collection) DepositWorkJob.perform_later(work:, work_form: @work_form, deposit: deposit?) redirect_to wait_works_path(work.id) else @content = Content.find(@work_form.content_id) - @license_presenter = LicensePresenter.new(work_form: @work_form, collection:) + @license_presenter = LicensePresenter.new(work_form: @work_form, collection: @collection) render :form, status: :unprocessable_entity end end @@ -80,6 +78,7 @@ def update # rubocop:disable Metrics/AbcSize redirect_to wait_works_path(@work.id) else @content = Content.find(@work_form.content_id) + @collection = @work.collection @license_presenter = LicensePresenter.new(work_form: @work_form, collection: @work.collection) render :form, status: :unprocessable_entity end @@ -153,4 +152,13 @@ def editable? def set_presenter @work_presenter = WorkPresenter.new(work: @work, work_form: @work_form, version_status: @version_status) end + + def new_work_form + WorkForm.new( + collection_druid: @collection.druid, + content_id: @content.id, + license: @collection.license, + access: @collection.stanford_access? ? 'stanford' : 'world' + ) + end end diff --git a/app/forms/work_form.rb b/app/forms/work_form.rb index 69a046d5..fb024257 100644 --- a/app/forms/work_form.rb +++ b/app/forms/work_form.rb @@ -56,6 +56,9 @@ def persisted? # Other requires a work subtype string validates :other_work_subtype, presence: true, if: -> { work_type == WorkType::OTHER } + attribute :access, :string, default: 'world' + validates :access, inclusion: { in: %w[world stanford] } + def content_file_presence return if content_id.nil? # This makes test configuration easier. return if Content.find(content_id).content_files.exists? diff --git a/app/presenters/work_presenter.rb b/app/presenters/work_presenter.rb index 947881e6..73ade151 100644 --- a/app/presenters/work_presenter.rb +++ b/app/presenters/work_presenter.rb @@ -48,6 +48,10 @@ def publication_date super.to_s end + def access_label + I18n.t("access.#{access}") + end + private delegate :collection, :created_at, :user, to: :work diff --git a/app/services/cocina_support.rb b/app/services/cocina_support.rb index 24fefe01..de90abbf 100644 --- a/app/services/cocina_support.rb +++ b/app/services/cocina_support.rb @@ -179,4 +179,12 @@ def self.event_date_for(cocina_object:, type:) # rubocop:disable Metrics/AbcSize def self.orcid_for(contributor:) contributor.identifier&.find { |id| id.type == 'ORCID' }&.value&.presence end + + def self.access_for(cocina_object:) + cocina_object.access.view + end + + def self.license_for(cocina_object:) + cocina_object.access.license + end end diff --git a/app/services/to_cocina/work/access_mapper.rb b/app/services/to_cocina/work/access_mapper.rb new file mode 100644 index 00000000..efbd19b5 --- /dev/null +++ b/app/services/to_cocina/work/access_mapper.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module ToCocina + module Work + # Maps WorkForm to Cocina access parameters + class AccessMapper + def self.call(...) + new(...).call + end + + # @param [WorkForm] work_form + def initialize(work_form:) + @work_form = work_form + end + + # @return [Hash] the Cocina access parameters + def call + { + view: access, + download: access, + license: license.presence, + useAndReproductionStatement: I18n.t('license.terms_of_use') + }.compact + end + + private + + attr_reader :work_form + + delegate :access, :license, to: :work_form + end + end +end diff --git a/app/services/to_cocina/work/mapper.rb b/app/services/to_cocina/work/mapper.rb index d8f44fdb..7f98faba 100644 --- a/app/services/to_cocina/work/mapper.rb +++ b/app/services/to_cocina/work/mapper.rb @@ -37,18 +37,13 @@ def params label: work_form.title, description: DescriptionMapper.call(work_form:), version: work_form.version, - access:, + access: AccessMapper.call(work_form:), identification: { sourceId: source_id }, administrative: { hasAdminPolicy: Settings.apo }, structural: StructuralMapper.call(work_form:, content:) }.compact end - def access - { view: 'world', download: 'world', license: work_form.license.presence, - useAndReproductionStatement: I18n.t('license.terms_of_use') }.compact - end - def request_params params.tap do |params_hash| params_hash[:administrative][:partOfProject] = Settings.project_tag diff --git a/app/services/to_cocina/work/structural_mapper.rb b/app/services/to_cocina/work/structural_mapper.rb index 9969d0e2..bcbaf307 100644 --- a/app/services/to_cocina/work/structural_mapper.rb +++ b/app/services/to_cocina/work/structural_mapper.rb @@ -29,6 +29,8 @@ def call attr_reader :work_form, :content + delegate :access, to: :work_form + def params { contains: content.content_files.map { |content_file| fileset_params_for(content_file) }, @@ -55,7 +57,7 @@ def file_params_for(content_file) version: work_form.version, label: content_file.label, filename: content_file.filepath, - access: { view: 'world', download: 'world' }, + access: { view: access, download: access }, administrative: { publish: !content_file.hidden?, sdrPreserve: true, shelve: !content_file.hidden? }, hasMimeType: content_file.mime_type, hasMessageDigests: message_digests_for(content_file), diff --git a/app/services/to_collection_form/roundtrip_validator.rb b/app/services/to_collection_form/roundtrip_validator.rb index 0b9cc386..f1002e19 100644 --- a/app/services/to_collection_form/roundtrip_validator.rb +++ b/app/services/to_collection_form/roundtrip_validator.rb @@ -27,6 +27,9 @@ def call Rails.logger.info("Roundtripped: #{pretty_roundtripped}") false end + rescue Cocina::Models::ValidationError + # Generating the roundtripped cocina object may create an invalid object + false end private diff --git a/app/services/to_work_form/mapper.rb b/app/services/to_work_form/mapper.rb index 511856b5..4330c8ae 100644 --- a/app/services/to_work_form/mapper.rb +++ b/app/services/to_work_form/mapper.rb @@ -32,7 +32,8 @@ def params # rubocop:disable Metrics/AbcSize related_works_attributes: CocinaSupport.related_works_for(cocina_object:), related_links_attributes: CocinaSupport.related_links_for(cocina_object:), keywords_attributes: CocinaSupport.keywords_for(cocina_object:), - license: cocina_object.access.license, + license: CocinaSupport.license_for(cocina_object:), + access: CocinaSupport.access_for(cocina_object:), version: cocina_object.version, collection_druid: CocinaSupport.collection_druid_for(cocina_object:), publication_date_attributes: CocinaSupport.event_date_for(cocina_object:, type: 'publication') diff --git a/app/services/to_work_form/roundtrip_validator.rb b/app/services/to_work_form/roundtrip_validator.rb index 4e9d52ed..408fcb2c 100644 --- a/app/services/to_work_form/roundtrip_validator.rb +++ b/app/services/to_work_form/roundtrip_validator.rb @@ -29,6 +29,9 @@ def call Rails.logger.info("Roundtripped: #{pretty_roundtripped}") false end + rescue Cocina::Models::ValidationError + # Generating the roundtripped cocina object may create an invalid object + false end private diff --git a/app/views/works/form.html.erb b/app/views/works/form.html.erb index 08204dec..46708217 100644 --- a/app/views/works/form.html.erb +++ b/app/views/works/form.html.erb @@ -1,6 +1,6 @@ <% content_for :breadcrumbs do %> <%= render Elements::BreadcrumbNavComponent.new(dashboard: true, admin: false) do |component| %> - <% component.with_breadcrumb(text: @collection_title, link: collection_path(druid: @work_form.collection_druid)) %> + <% component.with_breadcrumb(text: @collection.title, link: collection_path(druid: @work_form.collection_druid)) %> <% if (current_page?(action: :new) || (controller.action_name == 'create')) %> <% component.with_breadcrumb(text: I18n.t('works.edit.no_title')) %> <% else %> @@ -74,7 +74,7 @@ <% end %> <% component.with_pane(tab_name: :access, label: t('works.edit.panes.access.label'), form_id:) do %> - <%= render Works::Edit::AccessSettingsComponent.new(form:) %> + <%= render Works::Edit::AccessSettingsComponent.new(form:, collection: @collection) %> <% end %> <% component.with_pane(tab_name: :license, label: t('works.edit.panes.license.label'), form_id:) do %> diff --git a/app/views/works/show.html.erb b/app/views/works/show.html.erb index ba3457f1..a4fcc7e5 100644 --- a/app/views/works/show.html.erb +++ b/app/views/works/show.html.erb @@ -49,6 +49,10 @@ <% component.with_row(label: 'Related links', values: @work_presenter.related_links) %> <% end %> +<%= render Elements::Tables::TableComponent.new(id: 'access-table', classes: 'mb-5', label: 'Access settings') do |component| %> + <% component.with_row(label: 'Access', values: [@work_presenter.access_label]) %> +<% end %> + <%= render Elements::Tables::TableComponent.new(id: 'license-table', classes: 'mb-5', label: 'License and additional terms of use') do |component| %> <% component.with_row(label: 'License', values: [@work_presenter.license_label]) %> <% component.with_row(label: 'Terms of use', values: [t('license.terms_of_use')]) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index b8014159..c9cef1fc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -53,6 +53,9 @@ en: validation: > Required fields have not been filled out. Click on the section(s) highlighted in red on the left to correct the error(s) and then try depositing again. + access: + stanford: 'Stanford Community' + world: 'Everyone' publication_date: edit: legend: 'Previous publication date' diff --git a/spec/components/works/edit/access_settings_component_spec.rb b/spec/components/works/edit/access_settings_component_spec.rb new file mode 100644 index 00000000..438d4b6b --- /dev/null +++ b/spec/components/works/edit/access_settings_component_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Works::Edit::AccessSettingsComponent, type: :component do + let(:form) { ActionView::Helpers::FormBuilder.new(nil, work_form, vc_test_controller.view_context, {}) } + let(:work_form) { WorkForm.new(access: 'stanford') } + let(:collection) { instance_double(Collection, depositor_selects_access?: depositor_selects) } + + context 'when the depositor selects access' do + let(:depositor_selects) { true } + + it 'renders the select' do + render_inline(described_class.new(form:, collection:)) + + expect(page).to have_select('Who can download the files?', options: ['Stanford Community', 'Everyone']) + end + end + + context 'when the collection specifies access' do + let(:depositor_selects) { false } + + it 'states the access and renders a hidden field' do + render_inline(described_class.new(form:, collection:)) + + expect(page).to have_text('The files in your deposit will be downloadable by the Stanford Community.') + expect(page).to have_field('access', type: 'hidden', with: 'stanford') + end + end +end diff --git a/spec/serializers/work_form_serializer_spec.rb b/spec/serializers/work_form_serializer_spec.rb index 4d3624e4..7710e60f 100644 --- a/spec/serializers/work_form_serializer_spec.rb +++ b/spec/serializers/work_form_serializer_spec.rb @@ -25,7 +25,8 @@ 'work_type' => work_type_fixture, 'work_subtypes' => work_subtypes_fixture, 'other_work_subtype' => nil, - 'content_id' => 5 + 'content_id' => 5, + 'access' => 'stanford' } end let(:work_form) do @@ -44,7 +45,8 @@ keywords_attributes: keywords_fixture, work_type: work_type_fixture, work_subtypes: work_subtypes_fixture, - content_id: 5) + content_id: 5, + access: 'stanford') end describe '.serialize?' do diff --git a/spec/services/to_cocina/work/access_mapper_spec.rb b/spec/services/to_cocina/work/access_mapper_spec.rb new file mode 100644 index 00000000..eb0984e4 --- /dev/null +++ b/spec/services/to_cocina/work/access_mapper_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ToCocina::Work::AccessMapper do + subject(:access) { described_class.call(work_form:) } + + context 'when stanford access' do + let(:work_form) { WorkForm.new(access: 'stanford') } + + it 'maps to cocina' do + expect(access).to match( + view: 'stanford', + download: 'stanford', + useAndReproductionStatement: String + ) + end + end + + context 'when world access' do + let(:work_form) { WorkForm.new } + + it 'maps to cocina' do + expect(access).to match( + view: 'world', + download: 'world', + useAndReproductionStatement: String + ) + end + end + + context 'when license' do + let(:work_form) { WorkForm.new(license: 'http://creativecommons.org/licenses/by/3.0/us/') } + + it 'maps to cocina' do + expect(access).to match( + view: 'world', + download: 'world', + license: 'http://creativecommons.org/licenses/by/3.0/us/', + useAndReproductionStatement: String + ) + end + end + + context 'when none license' do + let(:work_form) { WorkForm.new(license: '') } + + it 'maps to cocina' do + expect(access).to match( + view: 'world', + download: 'world', + useAndReproductionStatement: String + ) + end + end +end diff --git a/spec/support/work_mapping_fixtures.rb b/spec/support/work_mapping_fixtures.rb index 6baeaf97..ad7cc6a1 100644 --- a/spec/support/work_mapping_fixtures.rb +++ b/spec/support/work_mapping_fixtures.rb @@ -18,7 +18,8 @@ def new_work_form_fixture contact_emails_attributes: contact_emails_fixture, keywords_attributes: keywords_fixture, work_type: work_type_fixture, - work_subtypes: work_subtypes_fixture + work_subtypes: work_subtypes_fixture, + access: 'stanford' ) end @@ -87,7 +88,7 @@ def request_dro_fixture # rubocop:disable Metrics/AbcSize version: 1, identification: { sourceId: source_id_fixture }, administrative: { hasAdminPolicy: Settings.apo, partOfProject: Settings.project_tag }, - access: { view: 'world', download: 'world', license: license_fixture, + access: { view: 'stanford', download: 'stanford', license: license_fixture, useAndReproductionStatement: I18n.t('license.terms_of_use') }, structural: { isMemberOf: [collection_druid_fixture] } } @@ -184,8 +185,8 @@ def request_dro_with_structural_fixture } ], access: { - view: 'world', - download: 'world', + view: 'stanford', + download: 'stanford', controlledDigitalLending: false }, administrative: { @@ -226,8 +227,8 @@ def dro_fixture # rubocop:disable Metrics/AbcSize version: 2, identification: { sourceId: source_id_fixture }, administrative: { hasAdminPolicy: Settings.apo }, - access: { view: 'world', - download: 'world', + access: { view: 'stanford', + download: 'stanford', license: license_fixture, useAndReproductionStatement: I18n.t('license.terms_of_use') }, structural: { isMemberOf: [collection_druid_fixture] } @@ -266,8 +267,8 @@ def dro_with_structural_fixture(hide: false) } ], access: { - view: 'world', - download: 'world', + view: 'stanford', + download: 'stanford', controlledDigitalLending: false }, administrative: { diff --git a/spec/system/create_work_deposit_spec.rb b/spec/system/create_work_deposit_spec.rb index ce7e77eb..88681095 100644 --- a/spec/system/create_work_deposit_spec.rb +++ b/spec/system/create_work_deposit_spec.rb @@ -148,6 +148,8 @@ click_link_or_button('Next') expect(page).to have_css('.nav-link.active', text: 'Access settings') expect(page).to have_css('.h5', text: 'Individual file visibility') + expect(page).to have_css('.h5', text: 'Download access') + select('Everyone', from: 'work_access') # Clicking on Next to go to license tab click_link_or_button('Next') diff --git a/spec/system/show_work_spec.rb b/spec/system/show_work_spec.rb index b1a8a2b2..cc55ed83 100644 --- a/spec/system/show_work_spec.rb +++ b/spec/system/show_work_spec.rb @@ -204,6 +204,13 @@ expect(page).to have_css('td', text: work_subtypes_fixture.join(', ')) end + # Dates table + within('table#dates-table') do + expect(page).to have_css('caption', text: 'Dates') + expect(page).to have_css('tr', text: 'Publication date') + expect(page).to have_css('td', text: '2024-12') + end + # Preferred citation table within('table#citation-table') do expect(page).to have_css('caption', text: 'Citation') @@ -221,6 +228,13 @@ expect(page).to have_css('td', text: 'doi:10.7710/2162-3309.1059 (has part)') end + # Access settings table + within('table#access-table') do + expect(page).to have_css('caption', text: 'Access settings') + expect(page).to have_css('tr', text: 'Access') + expect(page).to have_css('td', text: 'Stanford Community') + end + # License table within('table#license-table') do expect(page).to have_css('caption', text: 'License') @@ -231,13 +245,6 @@ text: 'Content distributed via the Stanford Digital Repository may be subject to ' \ 'additional license and use restrictions applied by the depositor.') end - - # Dates table - within('table#dates-table') do - expect(page).to have_css('caption', text: 'Dates') - expect(page).to have_css('tr', text: 'Publication date') - expect(page).to have_css('td', text: '2024-12') - end end end