Skip to content

Commit

Permalink
Adds support for DOIs.
Browse files Browse the repository at this point in the history
closes #429
  • Loading branch information
justinlittman committed Jan 15, 2025
1 parent 2b83ed7 commit de23077
Show file tree
Hide file tree
Showing 32 changed files with 506 additions and 19 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ gem 'bootsnap', require: false
gem 'action_policy'
gem 'cocina-models'
gem 'config'
gem 'datacite'
gem 'dor-services-client', '>= 15.3.0'
gem 'dor-workflow-client', '>= 7.6.1'
gem 'druid-tools'
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ GEM
listen (~> 3)
parser (~> 3)
webrick-websocket (~> 0.0.4)
datacite (0.4.0)
dry-monads (~> 1.3)
faraday (~> 2.0)
json_schema (~> 0.21.0)
zeitwerk (~> 2.4)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
Expand Down Expand Up @@ -277,6 +282,7 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.9.1)
json_schema (0.21.0)
jsonpath (1.1.5)
multi_json
kaminari (1.2.2)
Expand Down Expand Up @@ -565,6 +571,7 @@ DEPENDENCIES
config
cssbundling-rails
cyperful
datacite
debug
dlss-capistrano
dor-services-client (>= 15.3.0)
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/application.bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ $container-max-widths: (
.tabbable-panes {
.nav-pills {
@include media-breakpoint-up(md) {
height: 650px;
height: 700px;
}

background-color: var(--stanford-10-black);
Expand Down
24 changes: 24 additions & 0 deletions app/components/works/edit/doi_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<% if assigned? %>
<p>The DOI assigned to this work is <%= doi_link %>.</p>
<%= form.hidden_field :doi_option, value: 'assigned' %>
<% elsif yes_doi_option? %>
<p>A DOI will be assigned.</p>
<%= form.hidden_field :doi_option, value: 'yes' %>
<% elsif no_doi_option? %>
<p>DOI assignment is turned off for this collection.</p>
<%= form.hidden_field :doi_option, value: 'no' %>
<% else %>
<p>Getting a DOI may improve discovery of your work in web searches and will enable Altmetric reporting.</p>

<fieldset class="row">
<legend class="form-label col-md-5">Do you want a DOI assigned to this work?</legend>
<div class="form-check col-md-2">
<%= form.radio_button :doi_option, 'yes', class: 'form-check-input' %>
<%= form.label :doi_option_yes, 'Yes', class: 'form-check-label' %>
</div>
<div class="form-check col-md-2">
<%= form.radio_button :doi_option, 'no', class: 'form-check-input' %>
<%= form.label :doi_option_no, 'No', class: 'form-check-label' %>
</div>
</fieldset>
<% end %>
26 changes: 26 additions & 0 deletions app/components/works/edit/doi_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Works
module Edit
# Component for rendering the DOI form
class DoiComponent < ApplicationComponent
def initialize(form:, collection:)
@form = form
@collection = collection
super
end

attr_reader :form

delegate :yes_doi_option?, :no_doi_option?, to: :@collection

def assigned?
form.object.doi_option == 'assigned'
end

def doi_link
link_to(nil, Doi.url(druid: form.object.druid))
end
end
end
end
7 changes: 6 additions & 1 deletion app/controllers/works_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ def check_deposit_job_started

def set_work_form_from_cocina
@cocina_object = Sdr::Repository.find(druid: params[:druid])
@work_form = ToWorkForm::Mapper.call(cocina_object: @cocina_object)
@work_form = ToWorkForm::Mapper.call(cocina_object: @cocina_object,
doi_assigned: doi_assigned?)
end

def doi_assigned?
DoiAssignedService.call(cocina_object: @cocina_object, work: @work)
end

def set_status
Expand Down
3 changes: 3 additions & 0 deletions app/forms/work_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def persisted?
attribute :access, :string, default: 'world'
validates :access, inclusion: { in: %w[world stanford] }

attribute :doi_option, :string, default: 'yes'
validates :doi_option, inclusion: { in: %w[yes no assigned] }

def content_file_presence
return if content_id.nil? # This makes test configuration easier.
return if Content.find(content_id).content_files.exists?
Expand Down
6 changes: 5 additions & 1 deletion app/jobs/deposit_work_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ def perform_persist
Sdr::Repository.open_if_needed(cocina_object:)
.then { |cocina_object| Sdr::Repository.update(cocina_object:) }
else
Sdr::Repository.register(cocina_object:)
Sdr::Repository.register(cocina_object:, assign_doi:)
end
end

def assign_doi
work_form.doi_option == 'yes'
end
end
44 changes: 44 additions & 0 deletions app/models/doi.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

# Model for DOIs
class Doi
def self.id(...)
new(...).id
end

def self.url(...)
new(...).url
end

def self.assigned?(...)
new(...).assigned?
end

def initialize(druid:)
@druid = druid
end

def id
@id ||= "#{prefix}/#{druid.delete_prefix('druid:')}"
end

def url
@url ||= "https://doi.org/#{id}"
end

def assigned?
@assigned ||= client.exists?(id:)
end

private

attr_reader :druid

def prefix
Settings.datacite.prefix
end

def client
Datacite::Client.new(host: Settings.datacite.host)
end
end
4 changes: 4 additions & 0 deletions app/models/work.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ def deposit_job_started?
def deposit_job_finished?
deposit_job_started_at.nil?
end

def doi_assigned?
doi_assigned
end
end
4 changes: 4 additions & 0 deletions app/services/cocina_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,8 @@ def self.access_for(cocina_object:)
def self.license_for(cocina_object:)
cocina_object.access.license
end

def self.doi_for(cocina_object:)
cocina_object.identification.doi
end
end
44 changes: 44 additions & 0 deletions app/services/doi_assigned_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

# Efficiently determines if a DOI has been assigned to a work.
class DoiAssignedService
def self.call(...)
new(...).call
end

def initialize(cocina_object:, work:)
@cocina_object = cocina_object
@work = work
end

# return true if the work has a DOI assigned
def call
# If there is no DOI in the Cocina object, then a DOI definitely has not been assigned.
return false unless doi_in_cocina?

# Once a DOI is assigned (as determined by checking Datacite),
# the Work record's doi_assigned attribute is set to true.
# This is done to avoid checking Datacite every time the Work is accessed (slow!).
# The way DOI assignment works is:
# 1. The Cocina object gets a DOI. This may be done when the object is registered or by an update.
# 2. When the object is accessioned, the update DOI step registers the DOI with Datacite.
# Thus, having a DOI in the Cocina object does not mean it is registered with Datacite.
# The only way to know for sure is to check Datacite.
return true if work.doi_assigned?

# This checks Datacite.
assigned = Doi.assigned?(druid: work.druid)

# So that next time don't have to check Datacite.
work.update!(doi_assigned: true) if assigned
assigned
end

private

attr_reader :cocina_object, :work

def doi_in_cocina?
CocinaSupport.doi_for(cocina_object:).present?
end
end
45 changes: 45 additions & 0 deletions app/services/to_cocina/work/identification_mapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module ToCocina
module Work
# Maps WorkForm to Cocina identification parameters
class IdentificationMapper
def self.call(...)
new(...).call
end

# @param [WorkForm] work_form
# @param [source_id] source_id
def initialize(work_form:, source_id:)
@work_form = work_form
@source_id = source_id
end

# @return [Hash] the Cocina identification parameters
def call
{
sourceId: source_id
}.tap do |params|
params[:doi] = doi if doi?
end
end

private

attr_reader :work_form, :source_id

delegate :doi, to: :work_form

def doi?
# If a work has not yet been registered, the DOI is assigned as part of the registration request.
# If the work already has a DOI, it should continue to be added here.
# If the work does not have a DOI, but one should be assigned then it should be added here.
work_form.persisted? && %w[assigned yes].include?(work_form.doi_option)
end

def doi
Doi.id(druid: work_form.druid)
end
end
end
end
4 changes: 2 additions & 2 deletions app/services/to_cocina/work/mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ def call

attr_reader :work_form, :source_id, :content

def params
def params # rubocop:disable Metrics/AbcSize
{
externalIdentifier: work_form.druid,
type: Cocina::Models::ObjectType.object,
label: work_form.title,
description: DescriptionMapper.call(work_form:),
version: work_form.version,
access: AccessMapper.call(work_form:),
identification: { sourceId: source_id },
identification: IdentificationMapper.call(work_form:, source_id:),
administrative: { hasAdminPolicy: Settings.apo },
structural: StructuralMapper.call(work_form:, content:)
}.compact
Expand Down
23 changes: 20 additions & 3 deletions app/services/to_work_form/mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ def self.call(...)
new(...).call
end

def initialize(cocina_object:)
def initialize(cocina_object:, doi_assigned:)
@cocina_object = cocina_object
@doi_assigned = doi_assigned
end

def call
Expand All @@ -17,7 +18,7 @@ def call

private

attr_reader :cocina_object
attr_reader :cocina_object, :doi_assigned

def params # rubocop:disable Metrics/AbcSize
{
Expand All @@ -36,7 +37,8 @@ def params # rubocop:disable Metrics/AbcSize
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')
publication_date_attributes: CocinaSupport.event_date_for(cocina_object:, type: 'publication'),
doi_option:
}.merge(work_type_params)
end

Expand All @@ -49,5 +51,20 @@ def work_type_params

{ work_type:, work_subtypes: }
end

def doi_option
# If the work has a DOI and that DOI exists in DataCite, then already assigned.
# If the work has a DOI and that DOI does not exist in DataCite, then yes.
# (It will be assigned as part of the registration request or when deposited.)
# If the work does not have a DOI, then no.
doi = CocinaSupport.doi_for(cocina_object:)
if doi.nil?
'no'
elsif doi_assigned
'assigned'
else
'yes'
end
end
end
end
5 changes: 5 additions & 0 deletions app/views/works/form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<% component.with_tab(label: t('works.edit.panes.authors.tab_label'), tab_name: :authors) %>
<% component.with_tab(label: t('works.edit.panes.abstract.tab_label'), tab_name: :abstract) %>
<% component.with_tab(label: t('works.edit.panes.types.tab_label'), tab_name: :types) %>
<% component.with_tab(label: t('works.edit.panes.doi.tab_label'), tab_name: :doi) %>
<% component.with_tab(label: t('works.edit.panes.dates.tab_label'), tab_name: :dates) %>
<% component.with_tab(label: t('works.edit.panes.citation.tab_label'), tab_name: :citation) %>
<% component.with_tab(label: t('works.edit.panes.related_content.tab_label'), tab_name: :related_content) %>
Expand Down Expand Up @@ -60,6 +61,10 @@
<%= render Works::Edit::WorkTypeComponent.new(form:) %>
<% end %>

<% component.with_pane(tab_name: :doi, form_id:, label: t('works.edit.panes.doi.label')) do %>
<%= render Works::Edit::DoiComponent.new(form:, collection: @collection) %>
<% end %>

<% component.with_pane(tab_name: :dates, label: t('works.edit.panes.dates.label'), form_id:) do %>
<%= render NestedComponentPresenter.for(form:, field_name: :publication_date, model_class: DateForm, form_component: PublicationDate::EditComponent) %>
<% end %>
Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ en:
dates:
tab_label: 'Dates (optional)'
label: 'Enter dates related to your deposit'
doi:
tab_label: 'DOI'
label: 'DOI assignment'
deposit:
tab_label: 'Deposit'
label: 'Submit your deposit'
Expand Down
4 changes: 4 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ file_upload:
hierarchical_files_limit: 1000
max_filesize: 10000 # 10GB
max_files: 25000

datacite:
prefix: '10.80343'
host: api.test.datacite.org
Loading

0 comments on commit de23077

Please sign in to comment.