Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 53 additions & 31 deletions app/controllers/publications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,41 +51,54 @@ def edit

# POST /publications
def create
@publication = Publication.by_doi(params['publication']['doi'])
if @publication.new_record?
render('new')
if params['publication']['doi'].blank? && current_user.try(:curator?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Rails 8 we could have used except_on[1] in a validation to create a specific context skipping the doi validation, but I can't think of a nicer way to do it now, so I think this is fine.

[1] https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate-label-Options

@publication = Publication.create_without_doi(
publication_manual_params,
authors: params[:authors_given].to_a.zip(params[:authors_family].to_a)
)
return render('new') unless @publication.persisted?
else
if params[:link_name] && params[:link_name][:id]
@name = Name.find(params[:link_name][:id])
par = { publication: @publication, name: @name }
pn = PublicationName.find_or_create_by(par)
doi = params['publication']['doi']
# by_autocomplete resolves either a raw DOI (falling through to
# by_doi, so "type a fresh DOI to register it" keeps working
# unchanged) or a doi_title-style "key: title" string from selecting
# an existing publication in the autocomplete dropdown — including a
# DOI-less one via its "id:<id>" key. Only falls back to by_doi
# directly for blank input, matching its pre-existing error message.
@publication = Publication.by_autocomplete(doi) || Publication.by_doi(doi)
return render('new') if @publication.new_record?
end

if params[:link_name] && params[:link_name][:id]
@name = Name.find(params[:link_name][:id])
par = { publication: @publication, name: @name }
pn = PublicationName.find_or_create_by(par)

case params[:link_name][:as]
when 'propose'
@name.update(proposed_in: @publication)
flash[:notice] = 'Effective publication registered'
when 'not_valid_proposal'
pn.update(not_valid_proposal: true)
flash[:notice] =
'Original (not valid) publication registered'
when 'corrig'
redirect_to(
corrigendum_in_name_url(@name, publication_id: @publication.id)
)
return
when 'assign'
@name.update(assigned_in: @publication)
flash[:notice] = 'Taxonomic assignment publication registered'
when 'emend'
pn.update(emends: true)
flash[:notice] = 'Emending publication registered'
else
flash[:notice] = 'Publication registered'
end
redirect_to(@name)
case params[:link_name][:as]
when 'propose'
@name.update(proposed_in: @publication)
flash[:notice] = 'Effective publication registered'
when 'not_valid_proposal'
pn.update(not_valid_proposal: true)
flash[:notice] =
'Original (not valid) publication registered'
when 'corrig'
redirect_to(
corrigendum_in_name_url(@name, publication_id: @publication.id)
)
return
when 'assign'
@name.update(assigned_in: @publication)
flash[:notice] = 'Taxonomic assignment publication registered'
when 'emend'
pn.update(emends: true)
flash[:notice] = 'Emending publication registered'
else
redirect_to(@publication)
flash[:notice] = 'Publication registered'
end
redirect_to(@name)
else
redirect_to(@publication)
end
end

Expand Down Expand Up @@ -128,6 +141,15 @@ def set_publication
params[:doi] ? Publication.find_by(doi: params[:doi]) : nil
end

# Fields a curator may hand-enter to register a publication without a
# DOI (e.g. an older paper never issued one). Deliberately excludes
# crossref_json/datacite_json, which only ever hold verbatim external-API
# payloads and have no place in a manually-entered record.
def publication_manual_params
params.require(:publication)
.permit(*%i[title journal journal_loc journal_date doi url pub_type abstract])
end

# Never trust parameters from the scary internet, only allow the white list through.
def publication_params
params.require(:publication)
Expand Down
13 changes: 13 additions & 0 deletions app/models/name_paratype.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,17 @@ class NameParatype < ApplicationRecord
# Per the SeqCode paratype amendment (DOI:10.1093/ismeco/ycaf238), only
# isolated strains can serve as paratypes
validates(:nomenclatural_type_type, inclusion: { in: %w[Strain] })

after_create(:link_publication_to_name)

private

# A paratype's registering publication should also be recognized as one
# of the name's own publications, same as propose/assign/emend/corrig
# already ensure (see PublicationsController#create). Uses find_or_create
# so this holds regardless of which code path creates the NameParatype,
# and is a no-op if the publication is already linked.
def link_publication_to_name
PublicationName.find_or_create_by(publication: publication, name: name)
end
end
90 changes: 82 additions & 8 deletions app/models/publication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
class Publication < ApplicationRecord
default_scope { order(journal_date: :desc) }

# An empty string isn't the same as NULL for the DB's unique index on doi
# (two blank-string dois would collide; two NULLs don't), so normalize
# blank input before it ever reaches a validation or a save.
before_validation { self.doi = nil if doi.blank? }

validates(:title, presence: true)
validates(:journal_date, presence: true)
validates(:pub_type, presence: true)
validates(:doi, uniqueness: true, allow_nil: true)

has_many(
:publication_authors, -> { order('publication_authors.id ASC') },
dependent: :destroy
Expand Down Expand Up @@ -34,8 +44,35 @@ class Publication < ApplicationRecord
class << self

def by_autocomplete(query)
doi = query&.sub(/: .*/, '')
doi.present? ? by_doi(doi) : nil
key = query&.sub(/: .*/, '')
return nil unless key.present?

key.start_with?('id:') ?
by_id(key.delete_prefix('id:')) :
by_doi(key)
end

def by_id(id)
Publication.find_by(id: id) ||
Publication.new.tap { |i| i.errors.add(:doi, 'publication not found') }
end

# Creates a Publication without going through DOI lookup (e.g. a
# curator-entered legacy paper that never received one), with its
# authors, atomically: if either the publication itself is invalid or
# an author fails to save, nothing is persisted — avoids ever leaving
# behind an author-less publication because add_authors partially
# failed after the publication itself saved fine.
def create_without_doi(params, authors: [])
p = new(params)
transaction do
if p.save
p.add_authors(authors)
else
raise ActiveRecord::Rollback
end
end
p
end

def by_doi(doi, force_update = false)
Expand Down Expand Up @@ -200,6 +237,39 @@ def journals
where.not(journal: ['', nil]).select(:journal).reorder(:journal).distinct
end

# Curated subset of the Crossref/DataCite `type` vocabulary offered in
# the manual-entry dropdown. Values match what by_serrano_work/
# by_datacite_work already store, so existing display code (e.g.
# #citation, #journal_html) keeps working unchanged either way.
def pub_types
{
'journal-article' => 'Journal article',
'book' => 'Book',
'book-chapter' => 'Book chapter',
'proceedings-article' => 'Conference proceedings',
'report' => 'Report',
'other' => 'Other'
}
end

end

##
# Attach authors given as an array of [given, family] pairs, in
# authorship order. Mirrors the author-creation loop in
# +by_uniform_hash_work+, so the same dedup/standardization rules
# (Author.find_or_create) apply regardless of whether the author list
# came from Crossref/DataCite or was hand-entered.
def add_authors(pairs)
pairs.each_with_index do |(given, family), i|
next if family.blank?
author = Author.find_or_create(given, family)
next if authors.include?(author)
PublicationAuthor.create!(
publication_id: id, author_id: author.id,
sequence: i.zero? ? 'first' : 'additional'
)
end
end

def authors_et_al(format = :text)
Expand Down Expand Up @@ -248,17 +318,20 @@ def journal_raw
def long_citation(format = :text)
case format.to_sym
when :html
doi_html = doi.present? ?
%(<a href="#{link}" target="_blank">DOI: #{doi}</a>) : ''
<<~HTML.html_safe
#{authors_et_al(format)} (#{journal_date.year}). #{title_html}.
#{authors_et_al(format)} (#{journal_date.year}). #{title_html}.
<i>#{journal_html}</i>.
<a href="#{link}" target="_blank">DOI: #{doi}</a>
#{doi_html}
HTML
when :wikispecies
"#{authors_array(format).join(', ')}. #{journal_date.year}: " \
"#{title.gsub(/[\n\r\s]+/, ' ')}. #{journal_raw}. {{Doi|#{doi}}}"
"#{title.gsub(/[\n\r\s]+/, ' ')}. #{journal_raw}" \
"#{doi.present? ? ". {{Doi|#{doi}}}" : '.'}"
else
"#{authors_et_al(format)} (#{journal_date.year}). " \
"#{title}. #{journal_raw}. DOI: #{doi}"
"#{title}. #{journal_raw}#{doi.present? ? ". DOI: #{doi}" : '.'}"
end
end

Expand All @@ -283,11 +356,12 @@ def prepub?
end

def link
"https://doi.org/#{doi}"
"https://doi.org/#{doi}" if doi.present?
end

def doi_title(html = true)
html ? "#{doi}: #{title_html}" : "#{doi}: #{title}"
key = doi.presence || "id:#{id}"
html ? "#{key}: #{title_html}" : "#{key}: #{title}"
end

def include_term?(term)
Expand Down
5 changes: 3 additions & 2 deletions app/models/register/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,10 @@ def endorse(user)
#
# user: The user notifying the Registry (the current user)
# params: The notification parameters
# doi: DOI of the effective publication
# doi: DOI of the effective publication, or a doi_title-style "key: title"
# round-trip string identifying a DOI-less publication
def notify(user, params, doi)
publication = Publication.by_doi(doi)
publication = Publication.by_autocomplete(doi) || Publication.by_doi(doi)
params[:publication] = publication

if publication.new_record?
Expand Down
11 changes: 7 additions & 4 deletions app/views/contacts/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@
<dd>
<%= link_to(@publication) do %>
<%= fa_icon('arrow-circle-right') %> <%= @publication.citation %>
<% end %> /
<%= link_to(@publication.link, target: '_blank') do %>
DOI: <%= @publication.doi %>
<%= fa_icon('external-link-alt') %>
<% end %>
<% if @publication.doi.present? %>
/
<%= link_to(@publication.link, target: '_blank') do %>
DOI: <%= @publication.doi %>
<%= fa_icon('external-link-alt') %>
<% end %>
<% end %>
</dd>

Expand Down
11 changes: 7 additions & 4 deletions app/views/contacts/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
<dd>
<%= link_to(@publication) do %>
<%= fa_icon('arrow-circle-right') %> <%= @publication.citation %>
<% end %><br/>
<%= link_to(@publication.link, target: '_blank') do %>
DOI: <%= @publication.doi %>
<%= fa_icon('external-link-alt') %>
<% end %>
<% if @publication.doi.present? %>
<br/>
<%= link_to(@publication.link, target: '_blank') do %>
DOI: <%= @publication.doi %>
<%= fa_icon('external-link-alt') %>
<% end %>
<% end %>
</dd>

Expand Down
20 changes: 18 additions & 2 deletions app/views/publications/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@

<div class="field">
<%= form.label :journal_date %>
<%= form.date_select :journal_date, id: :publication_journal_date %>
<%= form.date_select(
:journal_date, { start_year: 1900, end_year: Date.current.year + 1 },
id: :publication_journal_date
) %>
</div>

<div class="field">
Expand All @@ -43,7 +46,20 @@

<div class="field">
<%= form.label :pub_type %>
<%= form.text_field :pub_type, id: :publication_pub_type %>
<%
pub_type_options = Publication.pub_types.to_a.map(&:reverse)
if publication.pub_type.present? &&
!Publication.pub_types.key?(publication.pub_type)
# Preserve an existing value outside the curated list (e.g. a
# Crossref/DataCite type like "dataset" or "posted-content") as
# its own option, instead of silently losing it on the next save.
pub_type_options << [publication.pub_type, publication.pub_type]
end
%>
<%= form.select(
:pub_type, pub_type_options, { include_blank: true },
id: :publication_pub_type
) %>
</div>

<div class="field">
Expand Down
Loading
Loading