Skip to content

Commit

Permalink
Merge pull request #3128 from BirkbeckCTP/2817-register-issue-journal…
Browse files Browse the repository at this point in the history
…-dois

Adds support for Issue and Journal DOIs to Crossref Integration
  • Loading branch information
joemull authored Sep 14, 2022
2 parents 799cf18 + 24fcf12 commit bcf7617
Show file tree
Hide file tree
Showing 16 changed files with 307 additions and 49 deletions.
3 changes: 3 additions & 0 deletions docs/source/manager/articlesissues/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 19 additions & 1 deletion docs/source/manager/identifiers/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 # for an account via Crossref and this will allow you to send submitted manuscripts for originality checking.
Expand Down
2 changes: 1 addition & 1 deletion src/core/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion src/events/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
6 changes: 6 additions & 0 deletions src/events/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)
32 changes: 31 additions & 1 deletion src/identifiers/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
76 changes: 72 additions & 4 deletions src/identifiers/tests/test_logic.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
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
from django.utils import timezone

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
from utils.shared import clear_cache
from lxml import etree
from bs4 import BeautifulSoup
import requests
from io import BytesIO, StringIO
import os
import json

class TestLogic(TestCase):

@classmethod
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/journal/issue_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)


Expand Down
44 changes: 28 additions & 16 deletions src/journal/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
20 changes: 20 additions & 0 deletions src/journal/migrations/0053_issue_doi.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
21 changes: 20 additions & 1 deletion src/journal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
Loading

0 comments on commit bcf7617

Please # to comment.