diff --git a/docs/source/manager/articlesissues/index.rst b/docs/source/manager/articlesissues/index.rst index c321ad0bd8..1376c0e6d2 100644 --- a/docs/source/manager/articlesissues/index.rst +++ b/docs/source/manager/articlesissues/index.rst @@ -84,6 +84,9 @@ To create an issue select *Create Issue* in the top right and in the modal that New issue form +- Issue DOI + - Issues can have a DOI, which will be registered with all of its articles in crossref. If you are using Janeway's autoregistration (recommended) or if you are not interested on registering DOIs for issues, you can leave this field blank. + Issue Articles ~~~~~~~~~~~~~~ You can manage the article associated with a given issue by selecting the *View* option, the data of the issue will be displayed along with a list of articles grouped by section. diff --git a/docs/source/manager/identifiers/index.rst b/docs/source/manager/identifiers/index.rst index 3b09a7c46b..bd1abf5fe0 100644 --- a/docs/source/manager/identifiers/index.rst +++ b/docs/source/manager/identifiers/index.rst @@ -100,10 +100,28 @@ DOI Display Suffix Text to append to DOIs -- also used to generate DOI URLs DOI Pattern - The pattern for auto-generating DOIs. Defaults to using the journal code and article ID (e.g. orbit.123): + The pattern for auto-generating DOIs. Defaults to using the journal code and article ID (e.g. ``orbit.123``): ``{{ article.journal.code }}.{{ article.pk }}`` +Title DOI + The DOI (not in URL format) registered for this journal (e.g. ``10.001/my-journal``). It is included on all deposits for this journal. It must be registered ahead of time. + + It is mandatory for a journal to have a DOI registered **only** when an ISSN is not available for a journal, as Crossref requires at least one unique identifier for every journal. + + However, even if your journal has an ISSN, Crossref still recommends registering a DOI for your journal. We recommend using your journal code as the DOI. For example, with the prefix of ``10.0001`` and the journal code of ``abcd``, you could set the journal DOI to ``10.0001/abcd``. + +Issue DOI Pattern + Janeway supports minting DOIs for journal issues automatically. With this setting, you can define the pattern used to generate the issue-level DOI that will be used for registration. + + With the default pattern, an issue with ID ``1`` (and prefix ``10.0001``, and journal code ``abcd``) will have a generated DOI of ``10.0001/abcd.issue.1``. + + A collection with an ID of ``2`` would have a generated DOI of ``10.0001/abcd.collection.2``. + +Auto-register issue-level DOIs + When enabled, issues will have a DOI assigned and registered as soon as the first article in the issue is scheduled for publication. If an issue DOI has not been entered manually, Janeway will use the pattern defined in the setting above to generate one automatically. + + Crosscheck Settings ------------------- Janeway also has support for Crosscheck (also called Similarity Check), which is provided by iThenticate. You can sign up for an account via Crossref and this will allow you to send submitted manuscripts for originality checking. diff --git a/src/core/logic.py b/src/core/logic.py index 457669df54..43e3e03503 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -390,7 +390,7 @@ def get_settings_to_edit(display_group, journal): xref_settings = [ 'use_crossref', 'crossref_test', 'crossref_username', 'crossref_password', 'crossref_email', 'crossref_name', 'crossref_prefix', 'crossref_registrant', 'doi_display_prefix', 'doi_display_suffix', - 'doi_pattern', 'doi_manager_action_maximum_size', + 'doi_pattern', 'doi_manager_action_maximum_size', 'title_doi', 'issue_doi_pattern', 'register_issue_dois' ] settings = process_setting_list(xref_settings, 'Identifiers', journal) diff --git a/src/events/logic.py b/src/events/logic.py index 48343aabcc..86137d1a86 100755 --- a/src/events/logic.py +++ b/src/events/logic.py @@ -208,7 +208,11 @@ class Events: # raised when proofing is complete ON_PROOFING_COMPLETE = 'on_proofing_complete' - #kwargs: request, article + # kwargs: article, issue, user + # raised when an article is assigned to an issue + ON_ARTICLE_ASSIGNED_TO_ISSUE = 'on_article_assigned_to_issue' + + # kwargs: request, article # raised when an article is marked as published ON_ARTICLE_PUBLISHED = 'on_article_published' diff --git a/src/events/registration.py b/src/events/registration.py index 8ed839fb87..95ec988270 100755 --- a/src/events/registration.py +++ b/src/events/registration.py @@ -7,6 +7,7 @@ from utils import transactional_emails, workflow_tasks from events import logic as event_logic from journal import logic as journal_logic +from identifiers import logic as id_logic # wire up event notifications @@ -181,3 +182,8 @@ # N.B. this is critical to the operation of the task framework. It automatically tears down tasks that have registered # for event listeners event_logic.Events.register_for_event('destroy_tasks', core_models.Task.destroyer) + +event_logic.Events.register_for_event( + event_logic.Events.ON_ARTICLE_ASSIGNED_TO_ISSUE, + id_logic.on_article_assign_to_issue, +) diff --git a/src/identifiers/logic.py b/src/identifiers/logic.py index f3eb1cd811..0959c0d3bd 100755 --- a/src/identifiers/logic.py +++ b/src/identifiers/logic.py @@ -311,12 +311,17 @@ def create_crossref_journal_context( ISSN_override=None, publication_title=None ): - return { + journal_data = { 'title': publication_title or journal.name, 'journal_issn': ISSN_override or journal.issn, 'print_issn': journal.print_issn, 'press': journal.press, } + if journal.doi: + journal_data["doi"] = journal.doi + journal_data["url"] = journal.site_url() + + return journal_data def create_crossref_article_context(article, identifier=None): template_context = { @@ -625,3 +630,28 @@ def register_preprint_doi(request, crossref_enabled, identifier): util_models.LogEntry.add_entry('Submission', "Deposited {0}. Status: {1}".format(token, status), 'Info', target=identifier.article) logger.info("Status of {} in {}: {}".format(token, identifier.identifier, status)) + + +def generate_issue_doi_from_logic(issue): + doi_prefix = setting_handler.get_setting( + 'Identifiers', 'crossref_prefix', issue.journal).value + doi_suffix = render_template.get_requestless_content( + {'issue': issue}, + issue.journal, + 'issue_doi_pattern', + group_name='Identifiers') + return '{0}/{1}'.format(doi_prefix, doi_suffix) + + +def auto_assign_issue_doi(issue): + auto_assign_enabled = setting_handler.get_setting( + 'Identifiers', 'use_crossref', issue.journal, + default=True, + ).processed_value + if auto_assign_enabled and not issue.doi: + issue.doi = generate_issue_doi_from_logic(issue) + issue.save() + + +def on_article_assign_to_issue(article, issue, user): + auto_assign_issue_doi(issue) diff --git a/src/identifiers/tests/test_logic.py b/src/identifiers/tests/test_logic.py index 879e0f90e0..17c08f4a45 100644 --- a/src/identifiers/tests/test_logic.py +++ b/src/identifiers/tests/test_logic.py @@ -1,5 +1,10 @@ import datetime +from io import BytesIO, StringIO +import json +import mock import pytz +import os + from django.test import TestCase from django.conf import settings @@ -7,6 +12,7 @@ from identifiers import logic, models from core.models import SettingGroup +from journal import logic as journal_logic from submission import models as submission_models from utils.testing import helpers from utils.setting_handler import save_setting @@ -14,10 +20,6 @@ from lxml import etree from bs4 import BeautifulSoup import requests -from io import BytesIO, StringIO -import os -import json - class TestLogic(TestCase): @classmethod @@ -353,6 +355,72 @@ def test_conference_deposit_xml_document_has_basically_correct_components(self): # There should be two conference papers (articles) self.assertEqual(2, len(soup.find_all('conference_paper'))) + def test_issue_doi_deposited_correctly(self): + template = 'common/identifiers/crossref_doi_batch.xml' + issue = self.article_one.issue + issue.doi = issue_doi = "10.0001/issue" + issue.save() + identifier = self.article_one.get_doi_object + clear_cache() + + template_context = logic.create_crossref_doi_batch_context( + self.article_one.journal, + {identifier}, + ) + deposit = logic.render_to_string(template, template_context) + + soup = BeautifulSoup(deposit, 'lxml') + # There should be one doi_batch + issue_soup = soup.find('journal_issue') + found = False + if issue_soup: + issue_doi_soup = issue_soup.find("doi_data") + if issue_doi_soup: + self.assertEqual(issue_doi_soup.find("doi").string, issue_doi) + self.assertEqual(issue_doi_soup.find("resource").string, issue.url) + found = True + if not found: + raise AssertionError("No Issue DOI found on article deposit") + + def test_journal_doi_deposited_correctly(self): + template = 'common/identifiers/crossref_doi_batch.xml' + issue = self.article_one.issue + journal_doi = "10.0001/journal" + save_setting('Identifiers', 'title_doi', issue.journal, journal_doi) + identifier = self.article_one.get_doi_object + clear_cache() + + template_context = logic.create_crossref_doi_batch_context( + self.article_one.journal, + {identifier}, + ) + deposit = logic.render_to_string(template, template_context) + + soup = BeautifulSoup(deposit, 'lxml') + # There should be one doi_batch + journal_soup = soup.find('journal_metadata') + found = False + if journal_soup: + doi_soup = journal_soup.find("doi_data") + if doi_soup: + self.assertEqual(doi_soup.find("doi").string, journal_doi) + self.assertEqual( + doi_soup.find("resource").string, issue.journal.site_url()) + found = True + if not found: + raise AssertionError("No Issue DOI found on article deposit") + + def test_issue_doi_auto_assigned(self): + issue = helpers.create_issue(self.journal_one, vol=99, number=99) + self.request.POST = {"assign_issue": issue.pk} + mock_messages = mock.patch('journal.logic.messages').start() + mock_messages.messages = mock.MagicMock() + save_setting('Identifiers', 'register_issue_dois', self.journal_one, '') + from events import registration # Forces events to load into memory + journal_logic.handle_assign_issue(self.request, self.article_one, issue) + issue.refresh_from_db() + self.assertTrue(issue.doi) + def test_check_crossref_settings(self): # Missing settings diff --git a/src/journal/issue_forms.py b/src/journal/issue_forms.py index 5f3215f704..0a4a1ccbbf 100755 --- a/src/journal/issue_forms.py +++ b/src/journal/issue_forms.py @@ -29,7 +29,7 @@ class Meta: fields = ( 'issue_title', 'volume', 'issue', 'date', 'issue_description', 'short_description', 'cover_image', 'large_image', 'issue_type', - 'code', + 'code', 'doi', ) diff --git a/src/journal/logic.py b/src/journal/logic.py index 772f754b94..2f86f01390 100755 --- a/src/journal/logic.py +++ b/src/journal/logic.py @@ -18,6 +18,7 @@ from django.conf import settings from django.shortcuts import redirect, get_object_or_404 from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ from django.template.loader import get_template from django.core.validators import validate_email, ValidationError from django.utils.timezone import make_aware @@ -202,19 +203,30 @@ def handle_new_issue(request): return [form, 'issue', new_issue] -def handle_assign_issue(request, article, issues): - try: - issue_to_assign = journal_models.Issue.objects.get(pk=request.POST.get('assign_issue', None)) - - if issue_to_assign in issues: - issue_to_assign.articles.add(article) - issue_to_assign.save() - messages.add_message(request, messages.SUCCESS, 'Article assigned to issue.') - else: - - messages.add_message(request, messages.WARNING, 'Issue not in this journals issue list.') - except journal_models.Issue.DoesNotExist: - messages.add_message(request, messages.WARNING, 'Issue does no exist.') +def handle_assign_issue(request, article, issue): + added = False + if not article.section: + messages.add_message( + request, + messages.WARNING, + _('Articles without a section cannot be added to an issue.'), + ) + elif issue not in article.journal.issues: + messages.add_message( + request, messages.WARNING, 'Issue not in this journal’s issue list.') + else: + issue.articles.add(article) + issue.save() + messages.add_message( + request, messages.SUCCESS, 'Article assigned to issue.') + event_logic.Events.raise_event( + event_logic.Events.ON_ARTICLE_ASSIGNED_TO_ISSUE, + article=article, + issue=issue, + user=request.user, + ) + added = True + return added def handle_unassign_issue(request, article, issues): @@ -224,12 +236,12 @@ def handle_unassign_issue(request, article, issues): if issue_to_unassign in issues: issue_to_unassign.articles.remove(article) issue_to_unassign.save() - messages.add_message(request, messages.SUCCESS, 'Article unassigned from Issue.') + messages.add_message(request, messages.SUCCESS, 'Article unassigned from issue.') else: - messages.add_message(request, messages.WARNING, 'Issue not in this journals issue list.') + messages.add_message(request, messages.WARNING, 'Issue not in this journal’s issue list.') except journal_models.Issue.DoesNotExist: - messages.add_message(request, messages.WARNING, 'Issue does no exist.') + messages.add_message(request, messages.WARNING, 'Issue does not exist.') def handle_set_pubdate(request, article): diff --git a/src/journal/migrations/0053_issue_doi.py b/src/journal/migrations/0053_issue_doi.py new file mode 100644 index 0000000000..8b3fc9f733 --- /dev/null +++ b/src/journal/migrations/0053_issue_doi.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2022-09-08 14:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journal', '0052_issue_display_title'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='doi', + field=models.CharField(blank=True, help_text='The DOI (not URL) to be registered for the issue when registering articles that are part of this issue. If you have enabled issue autoregistration in your settings, this field should not be entered manually.', max_length=255, null=True, verbose_name='DOI'), + ), + ] diff --git a/src/journal/models.py b/src/journal/models.py index b82d8278d1..ee4bfee423 100755 --- a/src/journal/models.py +++ b/src/journal/models.py @@ -34,7 +34,7 @@ from press import models as press_models from submission import models as submission_models from utils import setting_handler, logic, install, shared -from utils.function_cache import cache +from utils.function_cache import cache, mutable_cached_property from utils.logger import get_logger logger = get_logger(__name__) @@ -265,6 +265,14 @@ def publisher(self, value): def issn(self): return setting_handler.get_setting('general', 'journal_issn', self, default=True).value + @mutable_cached_property + def doi(self): + return setting_handler.get_setting('Identifiers', 'title_doi', self, default=True).value or None + + @doi.setter + def doi(self, value): + setting_handler.save_setting('Identifiers', 'title_doi', self, value) + @property @cache(120) def print_issn(self): @@ -567,6 +575,17 @@ class Issue(AbstractLastModifiedModel): " url for this issue. e.g: 'winter-special-issue'." ), ) + doi = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name='DOI', + help_text='The DOI (not URL) to be registered for the issue when registering ' + 'articles that are part of this issue. If you have enabled issue ' + 'autoregistration in your settings, this field should not be ' + 'entered manually.', + ) + @property def hero_image_url(self): diff --git a/src/journal/views.py b/src/journal/views.py index 82568f4918..cd56008161 100755 --- a/src/journal/views.py +++ b/src/journal/views.py @@ -975,16 +975,15 @@ def publish_article(request, article_id): if request.POST: if 'assign_issue' in request.POST: try: - logic.handle_assign_issue(request, article, issues) - except IntegrityError as integrity_error: - if not article.section: - messages.add_message( - request, - messages.ERROR, - _('Your article must have a section assigned.'), - ) - else: - raise integrity_error + issue = models.Issue.objects.get( + pk=request.POST['assign_issue'], + ) + logic.handle_assign_issue(request, article, issue) + except models.Issue.DoesNotExist: + messages.add_message( + request, messages.WARNING, + _('Issue not in this journal’s issue list.') + ) return redirect( '{0}?m=issue'.format( @@ -1422,18 +1421,15 @@ def issue_add_article(request, issue_id): if request.POST.get('article'): article_id = request.POST.get('article') - article = get_object_or_404(submission_models.Article, pk=article_id, journal=request.journal) - - if not article.section: - messages.add_message( - request, - messages.WARNING, - _('Articles without a section cannot be added to an issue.'), - ) - return redirect(reverse('issue_add_article', kwargs={'issue_id': issue.pk})) + article = get_object_or_404( + submission_models.Article, pk=article_id, journal=request.journal) + added = logic.handle_assign_issue(request, article, issue) + if added: + return redirect(reverse( + 'manage_issues_id', kwargs={'issue_id': issue.pk})) else: - issue.articles.add(article) - return redirect(reverse('manage_issues_id', kwargs={'issue_id': issue.pk})) + return redirect(reverse( + 'issue_add_article', kwargs={'issue_id': issue.pk})) template = 'journal/manage/issue_add_article.html' context = { diff --git a/src/templates/admin/elements/issue/issue_modal.html b/src/templates/admin/elements/issue/issue_modal.html index 8c0217415f..8858232914 100644 --- a/src/templates/admin/elements/issue/issue_modal.html +++ b/src/templates/admin/elements/issue/issue_modal.html @@ -47,9 +47,11 @@