diff --git a/Gemfile b/Gemfile index 1883d68ce..b83703353 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem 'google_drive' gem 'groupdate' gem 'honeybadger', '~> 5.2.0' gem 'html-pipeline', '~> 2.14.3' -gem 'commonmarker', '~> 0.23.8' +gem 'commonmarker', '~> 0.23.10' gem 'net-sftp', '~> 4.0' gem 'octicons_helper' gem 'omniauth-orcid', '~> 2.1.1' @@ -18,7 +18,7 @@ gem 'octokit', '~> 6.0' gem 'pdf-reader', '~> 2.11.0' gem 'pg', '~> 1.4.6' gem 'pagy' -gem 'rails', '7.0.6' +gem 'rails', '7.0.7' gem "importmap-rails" gem "turbo-rails" gem "stimulus-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 2f2259f4c..dc4f83a20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,47 +4,47 @@ GEM Ascii85 (1.1.0) aasm (5.5.0) concurrent-ruby (~> 1.0) - actioncable (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + actioncable (7.0.7) + actionpack (= 7.0.7) + activesupport (= 7.0.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + actionmailbox (7.0.7) + actionpack (= 7.0.7) + activejob (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.6) - actionpack (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activesupport (= 7.0.6) + actionmailer (7.0.7) + actionpack (= 7.0.7) + actionview (= 7.0.7) + activejob (= 7.0.7) + activesupport (= 7.0.7) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.6) - actionview (= 7.0.6) - activesupport (= 7.0.6) + actionpack (7.0.7) + actionview (= 7.0.7) + activesupport (= 7.0.7) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.6) - actionpack (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + actiontext (7.0.7) + actionpack (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.6) - activesupport (= 7.0.6) + actionview (7.0.7) + activesupport (= 7.0.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -52,22 +52,22 @@ GEM active_link_to (1.0.5) actionpack addressable - activejob (7.0.6) - activesupport (= 7.0.6) + activejob (7.0.7) + activesupport (= 7.0.7) globalid (>= 0.3.6) - activemodel (7.0.6) - activesupport (= 7.0.6) - activerecord (7.0.6) - activemodel (= 7.0.6) - activesupport (= 7.0.6) - activestorage (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activesupport (= 7.0.6) + activemodel (7.0.7) + activesupport (= 7.0.7) + activerecord (7.0.7) + activemodel (= 7.0.7) + activesupport (= 7.0.7) + activestorage (7.0.7) + actionpack (= 7.0.7) + activejob (= 7.0.7) + activerecord (= 7.0.7) + activesupport (= 7.0.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.6) + activesupport (7.0.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -91,7 +91,7 @@ GEM xpath (~> 3.2) chartkick (5.0.2) coderay (1.1.3) - commonmarker (0.23.9) + commonmarker (0.23.10) concurrent-ruby (1.2.2) connection_pool (2.4.1) crack (0.4.5) @@ -204,16 +204,16 @@ GEM matrix (0.4.2) memoist (0.16.2) method_source (1.0.0) - mini_mime (1.1.2) + mini_mime (1.1.5) mini_portile2 (2.8.4) mini_racer (0.8.0) libv8-node (~> 18.16.0.0) - minitest (5.18.1) + minitest (5.19.0) msgpack (1.7.2) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.3.0) - net-imap (0.3.6) + net-imap (0.3.7) date net-protocol net-pop (0.1.2) @@ -227,12 +227,12 @@ GEM net-ssh (7.1.0) newrelic_rpm (9.3.1) nio4r (2.5.9) - nokogiri (1.15.3) + nokogiri (1.15.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.15.3-x86_64-darwin) + nokogiri (1.15.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.15.3-x86_64-linux) + nokogiri (1.15.4-x86_64-linux) racc (~> 1.4) oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) @@ -282,39 +282,39 @@ GEM puma (6.3.0) nio4r (~> 2.0) racc (1.7.1) - rack (2.2.7) + rack (2.2.8) rack-protection (3.0.6) rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.6) - actioncable (= 7.0.6) - actionmailbox (= 7.0.6) - actionmailer (= 7.0.6) - actionpack (= 7.0.6) - actiontext (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activemodel (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + rails (7.0.7) + actioncable (= 7.0.7) + actionmailbox (= 7.0.7) + actionmailer (= 7.0.7) + actionpack (= 7.0.7) + actiontext (= 7.0.7) + actionview (= 7.0.7) + activejob (= 7.0.7) + activemodel (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) bundler (>= 1.15.0) - railties (= 7.0.6) + railties (= 7.0.7) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.1.1) + rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + railties (7.0.7) + actionpack (= 7.0.7) + activesupport (= 7.0.7) method_source rake (>= 12.2) thor (~> 1.0) @@ -423,12 +423,12 @@ GEM hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) websocket (1.2.9) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.8) + zeitwerk (2.6.11) PLATFORMS ruby @@ -441,7 +441,7 @@ DEPENDENCIES bootsnap capybara (~> 3.38) chartkick - commonmarker (~> 0.23.8) + commonmarker (~> 0.23.10) dotenv (~> 2.8.1) elasticsearch (< 7.14) factory_bot_rails (~> 6.2.0) @@ -464,7 +464,7 @@ DEPENDENCIES pg (~> 1.4.6) pry-byebug puma - rails (= 7.0.6) + rails (= 7.0.7) rails-controller-testing (~> 1.0.5) redis (~> 5.0) responders diff --git a/app/controllers/dispatch_controller.rb b/app/controllers/dispatch_controller.rb index 03a72980e..c11d49289 100644 --- a/app/controllers/dispatch_controller.rb +++ b/app/controllers/dispatch_controller.rb @@ -145,4 +145,53 @@ def api_deposit head :forbidden end end + + def api_retract + if params[:secret] == ENV['BOT_SECRET'] + paper = Paper.find_by_doi!(params[:doi]) + return head :unprocessable_entity if paper.retracted? + + if params[:metadata] + metadata = JSON.parse(Base64.decode64(params[:metadata])) + else + metadata = {} + end + + retraction_paper = Paper.new + retraction_paper.doi = metadata[:doi] || "#{paper.doi}R" + retraction_paper.retraction_for_id = paper.id + retraction_paper.title = metadata[:title] || "Retraction notice for: #{paper.title}" + retraction_paper.body = "Retraction notice for: #{paper.title}" + retraction_paper.authors = "Editorial Board" + retraction_paper.repository_url = paper.repository_url + retraction_paper.software_version = paper.software_version + retraction_paper.track_id = paper.track_id + retraction_paper.citation_string = params[:citation_string] + retraction_paper.submission_kind = "new" + retraction_paper.state = "accepted" + retraction_paper.metadata = metadata + retraction_paper.accepted_at = Time.now + retraction_paper.review_issue_id = paper.review_issue_id + + if paper.track.nil? + submitting_author = Editor.includes(:user).board.select {|e| e.user.present? }.first.user + else + submitting_author = paper.track.aeics.select {|e| e.user.present? }.first.user + end + submitting_author = User.where(admin: true).first if submitting_author.nil? + + retraction_paper.submitting_author = submitting_author + + if retraction_paper.save! && retraction_paper.accept! + paper.update(retraction_notice: params[:retraction_notice]) if params[:retraction_notice].present? + paper.retract! + render json: retraction_paper.to_json, status: '201' + else + head :unprocessable_entity + end + else + head :forbidden + end + end + end diff --git a/app/models/paper.rb b/app/models/paper.rb index 7366c8e52..5dd0a1f78 100644 --- a/app/models/paper.rb +++ b/app/models/paper.rb @@ -19,6 +19,16 @@ class Paper < ApplicationRecord optional: true, foreign_key: "eic_id" + belongs_to :retracted_paper, + class_name: 'Paper', + optional: true, + foreign_key: "retraction_for_id" + + has_one :retraction_paper, + class_name: 'Paper', + foreign_key: "retraction_for_id", + inverse_of: :retracted_paper + has_many :invitations has_many :notes has_many :votes @@ -62,6 +72,10 @@ class Paper < ApplicationRecord event :withdraw do transitions to: :withdrawn end + + event :retract do + transitions to: :retracted + end end VISIBLE_STATES = [ @@ -128,14 +142,14 @@ class Paper < ApplicationRecord validates_presence_of :track_id, on: :create, message: "You must select a valid subject for the paper", if: Proc.new { JournalFeatures.tracks? } validates :kind, inclusion: { in: Rails.application.settings["paper_types"] }, allow_nil: true validates :submission_kind, inclusion: { in: SUBMISSION_KINDS, message: "You must select a submission type" }, allow_nil: false - validate :check_repository_address, on: :create + validate :check_repository_address, on: :create, unless: Proc.new {|paper| paper.is_a_retraction_notice?} def notify_editors - Notifications.submission_email(self).deliver_now + Notifications.submission_email(self).deliver_now unless self.is_a_retraction_notice? end def notify_author - Notifications.author_submission_email(self).deliver_now + Notifications.author_submission_email(self).deliver_now unless self.is_a_retraction_notice? end # Only index papers that are visible @@ -172,6 +186,10 @@ def published? accepted? || retracted? end + def is_a_retraction_notice? + retraction_for_id.present? + end + def invite_editor(editor_handle) return false unless editor = Editor.find_by_login(editor_handle) Notifications.editor_invite_email(self, editor).deliver_now @@ -296,8 +314,12 @@ def archive_doi_url # A 5-figure integer used to produce the JOSS DOI def joss_id - id = "%05d" % review_issue_id - "#{setting(:abbreviation).downcase}.#{id}" + if self.is_a_retraction_notice? + return retracted_paper.joss_id + "R" + else + id = "%05d" % review_issue_id + return "#{setting(:abbreviation).downcase}.#{id}" + end end # This URL returns the 'DOI optimized' representation of a URL for a paper diff --git a/app/views/papers/_show_published.html.erb b/app/views/papers/_show_published.html.erb index c400bcff5..fd89188f5 100644 --- a/app/views/papers/_show_published.html.erb +++ b/app/views/papers/_show_published.html.erb @@ -1,5 +1,5 @@
- <%= render partial: "shared/retraction_notice" if @paper.retracted? %> + <%= render partial: "shared/retraction_info" %>
@@ -22,58 +22,6 @@
- -
-
- <%= link_to @paper.repository_url, class: 'btn paper-btn' do %> - <%= image_tag "gh-icon.svg" %> - Software repository - <% end %> - - <%= link_to @paper.review_url, class: 'btn paper-btn' do %> - <%= image_tag "icon_docs.svg" %> - Paper review - <% end %> - - <%= link_to @paper.pdf_url, class: 'btn paper-btn' do %> - <%= image_tag "dl-icon.svg" %> - Download paper - <% end %> - - <%= link_to @paper.archive_doi_url, class: 'btn paper-btn' do %> - <%= image_tag "hist-icon.svg" %> - Software archive - <% end %> -
- -
Review
-

Editor: <%= github_link @paper.metadata_editor %> (<%= link_to "all papers", papers_by_editor_path(@paper.metadata_editor) %>)
Reviewers: <%= pretty_reviewers(@paper.metadata_reviewers) %>

- -
Authors
-

<%= pretty_authors(@paper.metadata_authors) %>

- -
Citation
-

<%= @paper.citation_string %>

- -
<%= render partial: "bibtex", locals: { paper: @paper } %>
-
<%= link_to "Copy citation string".html_safe, "#", class: "clipboard-btn", "data-clipboard-action": "copy", "data-clipboard-target": "#citationstring" %> · <%= link_to "Copy BibTeX".html_safe, "#", class: "clipboard-btn", "data-clipboard-action": "copy", "data-clipboard-target": "#bibtex" %>  <%= octicon "paste", height: 16, class: "", "aria-label": "Copy" %>
- -
Tags
-

- <% @paper.author_tags.compact.each do |tag| %> - <%= link_to tag, papers_by_tag_path(tag: tag) %> - <% end %> -

-
Altmetrics
-
- -
Markdown badge
-

<%= image_tag @paper.status_badge_url %>   <%= octicon "paste", height: 16, class: "", "aria-label": "Copy" %>

- -
License
-

Authors of <%= Rails.application.settings['abbreviation'] %> papers retain copyright.

-

This work is licensed under a Creative Commons Attribution 4.0 International License.

-

Creative Commons License

-
+ <%= render partial: "sidebar_published" %>
diff --git a/app/views/papers/_sidebar_published.html.erb b/app/views/papers/_sidebar_published.html.erb new file mode 100644 index 000000000..900732a1d --- /dev/null +++ b/app/views/papers/_sidebar_published.html.erb @@ -0,0 +1,73 @@ +
+
+ <% if @paper.is_a_retraction_notice? %> + <%= link_to @paper.pdf_url, class: 'btn paper-btn' do %> + <%= image_tag "dl-icon.svg" %> + Download Retraction Notice + <% end %> + + <%= link_to @paper.retracted_paper.seo_url, class: 'btn paper-btn danger' do %> + <%= image_tag "icon_docs.svg" %> + Retracted Paper + <% end %> + <% else %> + + <% if @paper.retracted? && @paper.retraction_paper.present? %> + <%= link_to @paper.retraction_paper.seo_url, class: 'btn paper-btn' do %> + <%= image_tag "icon_docs.svg" %> + Retraction notice + <% end %> + <% end %> + <%= link_to @paper.repository_url, class: 'btn paper-btn' do %> + <%= image_tag "gh-icon.svg" %> + Software repository + <% end %> + + <%= link_to @paper.review_url, class: 'btn paper-btn' do %> + <%= image_tag "icon_docs.svg" %> + Paper review + <% end %> + + <%= link_to @paper.pdf_url, class: 'btn paper-btn' do %> + <%= image_tag "dl-icon.svg" %> + Download paper + <% end %> + + <%= link_to @paper.archive_doi_url, class: 'btn paper-btn' do %> + <%= image_tag "hist-icon.svg" %> + Software archive + <% end %> + <% end %> +
+ + <% unless @paper.is_a_retraction_notice? %> +
Review
+

Editor: <%= github_link @paper.metadata_editor %> (<%= link_to "all papers", papers_by_editor_path(@paper.metadata_editor) %>)
Reviewers: <%= pretty_reviewers(@paper.metadata_reviewers) %>

+ <% end %> + +
Authors
+

<%= pretty_authors(@paper.metadata_authors) %>

+ +
Citation
+

<%= @paper.citation_string %>

+ +
<%= render partial: "bibtex", locals: { paper: @paper } %>
+
<%= link_to "Copy citation string".html_safe, "#", class: "clipboard-btn", "data-clipboard-action": "copy", "data-clipboard-target": "#citationstring" %> · <%= link_to "Copy BibTeX".html_safe, "#", class: "clipboard-btn", "data-clipboard-action": "copy", "data-clipboard-target": "#bibtex" %>  <%= octicon "paste", height: 16, class: "", "aria-label": "Copy" %>
+ +
Tags
+

+ <% @paper.author_tags.compact.each do |tag| %> + <%= link_to tag, papers_by_tag_path(tag: tag) %> + <% end %> +

+
Altmetrics
+
+ +
Markdown badge
+

<%= image_tag @paper.status_badge_url %>   <%= octicon "paste", height: 16, class: "", "aria-label": "Copy" %>

+ +
License
+

Authors of <%= Rails.application.settings['abbreviation'] %> papers retain copyright.

+

This work is licensed under a Creative Commons Attribution 4.0 International License.

+

Creative Commons License

+
diff --git a/app/views/papers/show.json.jbuilder b/app/views/papers/show.json.jbuilder index d216c1fb6..23403bf31 100644 --- a/app/views/papers/show.json.jbuilder +++ b/app/views/papers/show.json.jbuilder @@ -17,8 +17,8 @@ if @paper.published? json.editor_orcid @paper.editor.orcid if @paper.editor.orcid end json.reviewers @paper.metadata_reviewers - json.languages @paper.language_tags.join(', ') - json.tags @paper.author_tags.join(', ') + json.languages @paper.language_tags + json.tags @paper.author_tags json.paper_review @paper.review_url json.meta_review_issue_id @paper.meta_review_issue_id json.pdf_url @paper.seo_pdf_url diff --git a/app/views/shared/_retraction_info.html.erb b/app/views/shared/_retraction_info.html.erb new file mode 100644 index 000000000..4363e73ca --- /dev/null +++ b/app/views/shared/_retraction_info.html.erb @@ -0,0 +1,17 @@ +<% if @paper.retracted? %> +
+ <% if @paper.retraction_notice.present? %> + <%= @paper.retraction_notice.html_safe %> + <% elsif @paper.retraction_paper.present? %> + This paper has been retracted, <%= link_to "read details here", @paper.retraction_paper.seo_url %> + <% else %> + This paper has been retracted + <% end %> +
+<% end %> + +<% if @paper.is_a_retraction_notice? %> +
+ This paper is a retraction notice for: <%= link_to @paper.retracted_paper.title, @paper.retracted_paper.seo_url %> +
+<% end %> diff --git a/app/views/shared/_retraction_notice.html.erb b/app/views/shared/_retraction_notice.html.erb deleted file mode 100644 index e0c3f1f37..000000000 --- a/app/views/shared/_retraction_notice.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
- <%= @paper.retraction_notice.html_safe %> -
diff --git a/config/routes.rb b/config/routes.rb index 6eb1156b9..44da0e53d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,9 +67,9 @@ get '/papers/:id/status.svg', to: "papers#status", format: "svg", as: 'status_badge' doi_prefix_name = Rails.application.settings[:abbreviation].downcase || "joss" - get '/papers/:doi/status.svg', to: "papers#status", format: "svg", constraints: { doi: /10.21105\/#{doi_prefix_name}\.\d{5}/} - get '/papers/:doi', to: "papers#show", constraints: { doi: /10.21105\/#{doi_prefix_name}\.\d{5}/} - get '/papers/:doi.:format', to: "papers#show", constraints: { doi: /10.21105\/#{doi_prefix_name}\.\d{5}/} + get '/papers/:doi/status.svg', to: "papers#status", format: "svg", constraints: { doi: /10.21105\/#{doi_prefix_name}\.\d{5}R?/} + get '/papers/:doi', to: "papers#show", constraints: { doi: /10.21105\/#{doi_prefix_name}\.\d{5}R?/} + get '/papers/:doi.:format', to: "papers#show", constraints: { doi: /10.21105\/#{doi_prefix_name}\.\d{5}R?/} get '/editor_profile', to: 'editors#profile', as: 'editor_profile' patch '/update_editor_profile', to: 'editors#update_profile', as: 'update_editor_profile' @@ -94,6 +94,7 @@ post '/papers/api_editor_invite', to: 'dispatch#api_editor_invite' post '/papers/api_start_review', to: 'dispatch#api_start_review' post '/papers/api_deposit', to: 'dispatch#api_deposit' + post '/papers/api_retract', to: 'dispatch#api_retract' post '/papers/api_assign_editor', to: 'dispatch#api_assign_editor' post '/papers/api_update_paper_info', to: 'dispatch#api_update_paper_info' post '/papers/api_assign_reviewers', to: 'dispatch#api_assign_reviewers' diff --git a/db/migrate/20230609104144_add_retraction_for_id_to_papers.rb b/db/migrate/20230609104144_add_retraction_for_id_to_papers.rb new file mode 100644 index 000000000..af60e6b4b --- /dev/null +++ b/db/migrate/20230609104144_add_retraction_for_id_to_papers.rb @@ -0,0 +1,5 @@ +class AddRetractionForIdToPapers < ActiveRecord::Migration[7.0] + def change + add_reference :papers, :retraction_for, foreign_key: { to_table: :papers }, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index fc27cbd69..2df47d9d8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_06_16_085520) do +ActiveRecord::Schema[7.0].define(version: 2023_06_09_104144) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" enable_extension "plpgsql" @@ -110,10 +110,12 @@ t.string "git_branch" t.bigint "track_id" t.string "suggested_subject" + t.bigint "retraction_for_id" t.index ["editor_id"], name: "index_papers_on_editor_id" t.index ["eic_id"], name: "index_papers_on_eic_id" t.index ["labels"], name: "index_papers_on_labels", using: :gin t.index ["last_activity"], name: "index_papers_on_last_activity" + t.index ["retraction_for_id"], name: "index_papers_on_retraction_for_id" t.index ["reviewers"], name: "index_papers_on_reviewers", using: :gin t.index ["sha"], name: "index_papers_on_sha" t.index ["track_id"], name: "index_papers_on_track_id" @@ -178,4 +180,5 @@ t.index ["paper_id"], name: "index_votes_on_paper_id" end + add_foreign_key "papers", "papers", column: "retraction_for_id" end diff --git a/docs/submitting.md b/docs/submitting.md index 8a3f7209f..8d8954e87 100644 --- a/docs/submitting.md +++ b/docs/submitting.md @@ -108,7 +108,9 @@ As this short list shows, JOSS papers are only expected to contain a limited set ## Example paper and bibliography -This example `paper.md` is adapted from _Gala: A Python package for galactic dynamics_ by Adrian M. Price-Whelan [http://doi.org/10.21105/joss.00388](http://doi.org/10.21105/joss.00388): +This example `paper.md` is adapted from _Gala: A Python package for galactic dynamics_ by Adrian M. Price-Whelan [http://doi.org/10.21105/joss.00388](http://doi.org/10.21105/joss.00388). + +For a complete description of available options to describe author names [see here](https://github.com/openjournals/inara/blob/main/docs/names.md). ``` --- @@ -130,6 +132,10 @@ authors: - name: Author with no affiliation corresponding: true # (This is how to denote the corresponding author) affiliation: 3 + - given-names: Ludwig + dropping-particle: van + surname: Beethoven + affiliation: 3 affiliations: - name: Lyman Spitzer, Jr. Fellow, Princeton University, USA index: 1 diff --git a/spec/controllers/dispatch_controller_spec.rb b/spec/controllers/dispatch_controller_spec.rb index ee00d1d60..81dd6f1a5 100644 --- a/spec/controllers/dispatch_controller_spec.rb +++ b/spec/controllers/dispatch_controller_spec.rb @@ -522,4 +522,65 @@ def headers(event, payload) expect(paper.accepted_at).to eql(initial_accepted_at) end end + + describe "POST #api_retract" do + + it "with no API key" do + post :api_retract + expect(response).to be_forbidden + end + + it "with the correct API key" do + user = create(:user) + paper = create(:accepted_paper, title: "Bad paper", review_issue_id: 1234, doi: "10.21105/test.00042") + track_editor = paper.track.aeics.first + track_editor.update(user: user) + + expect(paper.accepted_at).to be_present + expect(paper.state).to eql('accepted') + encoded_metadata = "eyJwYXBlciI6eyJ0aXRsZSI6IkZpZGdpdDogQW4gdW5nb2RseSB1bmlvbiBv\nZiBHaXRIdWIgYW5kIGZpZ3NoYXJlIiwidGFncyI6WyJleGFtcGxlIiwidGFn\ncyIsImZvciB0aGUgcGFwZXIiXSwibGFuZ3VhZ2VzIjpbIlB5dGhvbiIsIlJ1\nc3QiLCJQZXJsIl0sImF1dGhvcnMiOlt7ImdpdmVuX25hbWUiOiJBcmZvbiIs\nIm1pZGRsZV9uYW1lIjoiTS4iLCJsYXN0X25hbWUiOiJTbWl0aCIsIm9yY2lk\nIjoiMDAwMC0wMDAyLTM5NTctMjQ3NCIsImFmZmlsaWF0aW9uIjoiR2l0SHVi\nIEluYy4sIERpc25leSBJbmMuIn0seyJnaXZlbl9uYW1lIjoiSmFtZXMiLCJt\naWRkbGVfbmFtZSI6IlAuIiwibGFzdF9uYW1lIjoidmFuIERpc2hvZWNrIiwi\nb3JjaWQiOiIwMDAwLTAwMDItMzk1Ny0yNDc0IiwiYWZmaWxpYXRpb24iOiJE\naXNuZXkgSW5jLiJ9XSwiZG9pIjoiMTAuMjExMDUvam9zcy4wMDAxNyIsImFy\nY2hpdmVfZG9pIjoiaHR0cDovL2R4LmRvaS5vcmcvMTAuNTI4MS96ZW5vZG8u\nMTM3NTAiLCJyZXBvc2l0b3J5X2FkZHJlc3MiOiJodHRwczovL2dpdGh1Yi5j\nb20vYXBwbGljYXRpb25za2VsZXRvbi9Ta2VsZXRvbiIsImVkaXRvciI6ImFy\nZm9uIiwicmV2aWV3ZXJzIjpbIkBqaW0iLCJAYm9iIl19fQ==\n" + + post :api_retract, params: {secret: "testBOTsecret", + doi: "10.21105/test.00042", + citation_string: "Editorial Board, 2023, JOSS, Retraction etc.", + metadata: encoded_metadata + } + + expect(response).to be_successful + expect(paper.reload.state).to eql("retracted") + expect(paper.retraction_paper).to be_present + + retraction_notice = paper.retraction_paper + + expect(retraction_notice.state).to eql("accepted") + expect(retraction_notice.retracted_paper).to eql(paper) + expect(retraction_notice.submitting_author).to eql(user) + expect(retraction_notice.doi).to eql("10.21105/test.00042R") + expect(retraction_notice.track_id).to eql(paper.track_id) + expect(Base64.encode64(retraction_notice.metadata.to_json)).to eql(encoded_metadata) + expect(retraction_notice.review_issue_id).to eql(paper.review_issue_id) + expect(retraction_notice.citation_string).to eql("Editorial Board, 2023, JOSS, Retraction etc.") + expect(retraction_notice.title).to eql("Retraction notice for: Bad paper") + end + + it "should not retract papers twice" do + paper = create(:retracted_paper, title: "Bad paper", review_issue_id: 1234, doi: "10.21105/test.00042") + retraction_notice = paper.retraction_paper + expect(paper.accepted_at).to be_present + expect(paper.state).to eql('retracted') + encoded_metadata = "eyJwYXBlciI6eyJ0aXRsZSI6IkZpZGdpdDogQW4gdW5nb2RseSB1bmlvbiBv\nZiBHaXRIdWIgYW5kIGZpZ3NoYXJlIiwidGFncyI6WyJleGFtcGxlIiwidGFn\ncyIsImZvciB0aGUgcGFwZXIiXSwibGFuZ3VhZ2VzIjpbIlB5dGhvbiIsIlJ1\nc3QiLCJQZXJsIl0sImF1dGhvcnMiOlt7ImdpdmVuX25hbWUiOiJBcmZvbiIs\nIm1pZGRsZV9uYW1lIjoiTS4iLCJsYXN0X25hbWUiOiJTbWl0aCIsIm9yY2lk\nIjoiMDAwMC0wMDAyLTM5NTctMjQ3NCIsImFmZmlsaWF0aW9uIjoiR2l0SHVi\nIEluYy4sIERpc25leSBJbmMuIn0seyJnaXZlbl9uYW1lIjoiSmFtZXMiLCJt\naWRkbGVfbmFtZSI6IlAuIiwibGFzdF9uYW1lIjoidmFuIERpc2hvZWNrIiwi\nb3JjaWQiOiIwMDAwLTAwMDItMzk1Ny0yNDc0IiwiYWZmaWxpYXRpb24iOiJE\naXNuZXkgSW5jLiJ9XSwiZG9pIjoiMTAuMjExMDUvam9zcy4wMDAxNyIsImFy\nY2hpdmVfZG9pIjoiaHR0cDovL2R4LmRvaS5vcmcvMTAuNTI4MS96ZW5vZG8u\nMTM3NTAiLCJyZXBvc2l0b3J5X2FkZHJlc3MiOiJodHRwczovL2dpdGh1Yi5j\nb20vYXBwbGljYXRpb25za2VsZXRvbi9Ta2VsZXRvbiIsImVkaXRvciI6ImFy\nZm9uIiwicmV2aWV3ZXJzIjpbIkBqaW0iLCJAYm9iIl19fQ==\n" + total_papers = Paper.count + + post :api_retract, params: {secret: "testBOTsecret", + doi: "10.21105/test.00042", + citation_string: "Editorial Board, 2023, JOSS, Retraction etc.", + metadata: encoded_metadata + } + + expect(response.status).to eql(422) + expect(paper.reload.state).to eql("retracted") + expect(paper.reload.retraction_paper).to eql(retraction_notice) + expect(Paper.count).to eql(total_papers) + end + end end diff --git a/spec/factories/papers_factory.rb b/spec/factories/papers_factory.rb index 3cc7d895a..fb2fee098 100644 --- a/spec/factories/papers_factory.rb +++ b/spec/factories/papers_factory.rb @@ -9,8 +9,8 @@ submission_kind { 'new' } track { create(:track) } - created_at { Time.now } - updated_at { Time.now } + created_at { Time.now } + updated_at { Time.now } factory :paper_with_sha do sha { '48d24b0158528e85ac7706aecd8cddc4' } @@ -35,7 +35,7 @@ end factory :resubmission_paper do - submission_kind { 'resubmission' } + submission_kind { 'resubmission' } end factory :rejected_paper do @@ -47,7 +47,7 @@ state { 'retracted' } accepted_at { Time.now } review_issue_id { 0 } - sequence(:doi) {|n| "10.21105/jose.0000#{n}" } + sequence(:doi) {|n| "10.21105/jose.4000#{n}" } end factory :submitted_paper_with_sha do diff --git a/spec/models/paper_spec.rb b/spec/models/paper_spec.rb index 47137d7e3..906df57f7 100644 --- a/spec/models/paper_spec.rb +++ b/spec/models/paper_spec.rb @@ -21,6 +21,14 @@ expect(association.macro).to eq(:belongs_to) end + it "retraction paper belongs to a retracted paper" do + association = Paper.reflect_on_association(:retracted_paper) + expect(association.macro).to eq(:belongs_to) + + association = Paper.reflect_on_association(:retraction_paper) + expect(association.macro).to eq(:has_one) + end + it "has many invitations" do association = Paper.reflect_on_association(:invitations) expect(association.macro).to eq(:has_many) @@ -73,43 +81,43 @@ end end - # Scopes - - it "should return recent" do - old_paper = create(:paper, created_at: 2.weeks.ago) - new_paper = create(:paper) + describe "Scopes" do + it "should return recent" do + old_paper = create(:paper, created_at: 2.weeks.ago) + new_paper = create(:paper) - expect(Paper.recent).to eq([new_paper]) - end + expect(Paper.recent).to eq([new_paper]) + end - it "should return only visible papers" do - hidden_paper = create(:paper, state: "submitted") - visible_paper_1 = create(:accepted_paper) - visible_paper_2 = create(:paper, state: "superceded") + it "should return only visible papers" do + hidden_paper = create(:paper, state: "submitted") + visible_paper_1 = create(:accepted_paper) + visible_paper_2 = create(:paper, state: "superceded") - expect(Paper.visible).to contain_exactly(visible_paper_1, visible_paper_2) - assert hidden_paper.invisible? - end + expect(Paper.visible).to contain_exactly(visible_paper_1, visible_paper_2) + assert hidden_paper.invisible? + end - it "should exclude withdrawn and rejected papers" do - rejected_paper = create(:paper, state: "rejected") - withdrawn_paper = create(:paper, state: "withdrawn") - paper = create(:accepted_paper) + it "should exclude withdrawn and rejected papers" do + rejected_paper = create(:paper, state: "rejected") + withdrawn_paper = create(:paper, state: "withdrawn") + paper = create(:accepted_paper) - expect(Paper.everything).to contain_exactly(paper) - expect(Paper.invisible).to contain_exactly(rejected_paper, withdrawn_paper) - end + expect(Paper.everything).to contain_exactly(paper) + expect(Paper.invisible).to contain_exactly(rejected_paper, withdrawn_paper) + end - it "should filter by track" do - track_A, track_B = create_list(:track, 2) - paper_A1, paper_A2 = create_list(:paper, 2, track: track_A) - paper_B1, paper_B2 = create_list(:paper, 2, track: track_B) - paper_C = create(:paper) + it "should filter by track" do + track_A, track_B = create_list(:track, 2) + paper_A1, paper_A2 = create_list(:paper, 2, track: track_A) + paper_B1, paper_B2 = create_list(:paper, 2, track: track_B) + paper_C = create(:paper) - track_A_papers = Paper.by_track(track_A.id) - expect(track_A_papers.size).to eq(2) - expect(track_A_papers.include?(paper_A1)).to be true - expect(track_A_papers.include?(paper_A2)).to be true + track_A_papers = Paper.by_track(track_A.id) + expect(track_A_papers.size).to eq(2) + expect(track_A_papers.include?(paper_A1)).to be true + expect(track_A_papers.include?(paper_A2)).to be true + end end # GitHub stuff @@ -201,6 +209,25 @@ end end + describe "#is_a_retraction_notice?" do + it "should return true if paper is a retraction notice for another paper" do + retracted_paper = create(:paper) + paper = create(:paper, retracted_paper: retracted_paper) + + expect(paper.retraction_for_id).to eq(retracted_paper.id) + expect(paper.retracted_paper).to eq(retracted_paper) + expect(paper.is_a_retraction_notice?).to be true + end + + it "should return false oherwise" do + paper = create(:paper) + + expect(paper.retraction_for_id).to be_nil + expect(paper.retracted_paper).to be_nil + expect(paper.is_a_retraction_notice?).to be false + end + end + context "when accepted" do it "should know how to generate a PDF URL for Google Scholar" do paper = create(:accepted_paper) diff --git a/spec/system/papers/show_published_spec.rb b/spec/system/papers/show_published_spec.rb new file mode 100644 index 000000000..2dd31d20f --- /dev/null +++ b/spec/system/papers/show_published_spec.rb @@ -0,0 +1,73 @@ +require "rails_helper" + +feature "Published paper's show page" do + before do + @accepted_paper = create(:accepted_paper, title: "Astronomy paper", doi: "10.21105/jose.00001", review_issue_id: 1) + @accepted_paper.metadata['paper']['title'] = "Astronomy paper" + @accepted_paper.metadata['paper']['authors'] = [{'given_name' => "Vera", 'last_name' => "Rubin"}] + @accepted_paper.metadata['paper']['tags'] = ["Galaxy rotation curves"] + @accepted_paper.save! + + @retracted_paper = create(:retracted_paper, title: "Bad paper", doi: "10.21105/jose.00002", review_issue_id: 2) + @retracted_paper.metadata['paper']['title'] = "Bad paper" + @retracted_paper.save! + + @retraction_notice = create(:accepted_paper, title: "Retraction notice for: Bad paper", doi: "10.21105/jose.00002R") + @retraction_notice.update(retracted_paper: @retracted_paper) + end + + scenario "Accepted paper" do + visit paper_path(@accepted_paper) + + expect(page).to have_content("Astronomy paper") + expect(page).to_not have_content("This paper has been retracted") + expect(page).to_not have_content("This paper is a retraction notice") + + expect(page).to have_link("Software repository") + expect(page).to have_link("Paper review") + expect(page).to have_link("Download paper") + expect(page).to have_link("Software archive") + + expect(page).to_not have_link("Retraction notice") + expect(page).to_not have_link("Retracted Paper") + expect(page).to_not have_link("Download Retraction Notice") + end + + scenario "Retracted paper" do + visit paper_path(@retracted_paper) + + expect(page).to have_content("Bad paper") + expect(page).to have_content("This paper has been retracted") + expect(page).to_not have_content("This paper is a retraction notice") + + expect(page).to have_link("Software repository") + expect(page).to have_link("Paper review") + expect(page).to have_link("Download paper") + expect(page).to have_link("Software archive") + + expect(page).to have_link("Retraction notice") + expect(page).to have_link("read details here") + + expect(page).to_not have_link("Retracted Paper") + expect(page).to_not have_link("Download Retraction Notice") + end + + scenario "Retraction notice" do + visit paper_path(@retraction_notice) + + expect(page).to have_content("Retraction notice for: Bad paper") + expect(page).to have_content("This paper is a retraction notice for: Bad paper") + expect(page).to_not have_content("This paper has been retracted") + + expect(page).to_not have_link("Software repository") + expect(page).to_not have_link("Paper review") + expect(page).to_not have_link("Download paper") + expect(page).to_not have_link("Software archive") + + expect(page).to_not have_link("Retraction notice") + expect(page).to_not have_link("read details here") + + expect(page).to have_link("Retracted Paper") + expect(page).to have_link("Download Retraction Notice") + end +end