Skip to content
Open
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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ python-magic==0.4.27
pytz==2024.1
requests==2.32.4
six==1.16.0
django-sortedm2m~=3.1
sqlparse==0.4.4
swapper==1.3.0
tqdm==4.66.3
Expand Down
1 change: 1 addition & 0 deletions src/identifiers/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ def create_crossref_article_context(article, identifier=None):
"other_pages": article.page_numbers,
"scheduled": article.scheduled_for_publication,
"object": article,
"erratum_of": article.erratum_of(),
}

# append citations for i4oc compatibility
Expand Down
2 changes: 2 additions & 0 deletions src/identifiers/tests/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ def test_create_crossref_article_context_published(self):
"date_accepted": None,
"date_published": self.article_published.date_published,
"doi": f"10.0000/TST.{self.article_published.id}",
"erratum_of": None,
"id": self.article_published.id,
"license": "",
"object": self.article_published,
Expand All @@ -267,6 +268,7 @@ def test_create_crossref_article_context_not_published(self):
"date_accepted": None,
"date_published": None,
"doi": self.doi_one.identifier,
"erratum_of": None,
"id": self.article_one.id,
"license": submission_models.Licence.objects.filter(
journal=self.journal_one,
Expand Down
16 changes: 16 additions & 0 deletions src/submission/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ def _answer(self, obj):
return truncatewords_html(obj.answer, 10) if obj else ""


class GenealogyAdmin(admin.ModelAdmin):
list_display = ("pk", "parent_id", "_parent_title")
list_filter = ("parent__journal",)
search_fields = (
"parent__pk",
"parent__title",
"children__pk",
"children__title",
)
raw_id_fields = ("parent", "children")

def _parent_title(self, obj):
return truncatewords_html(obj.parent.title, 10)


class SubmissionConfigAdmin(admin.ModelAdmin):
list_display = (
"pk",
Expand Down Expand Up @@ -295,6 +310,7 @@ class SubmissionConfigAdmin(admin.ModelAdmin):
(models.Keyword, KeywordAdmin),
(models.SubmissionConfiguration, SubmissionConfigAdmin),
(models.CreditRecord, CreditRecordAdmin),
(models.Genealogy, GenealogyAdmin),
]

[admin.site.register(*t) for t in admin_list]
46 changes: 46 additions & 0 deletions src/submission/migrations/0090_genealogy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 4.2.29 on 2026-04-27 11:51

from django.db import migrations, models
import django.db.models.deletion
import sortedm2m.fields


class Migration(migrations.Migration):

dependencies = [
("submission", "0089_merge_20260226_1524"),
]

operations = [
migrations.CreateModel(
name="Genealogy",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"children",
sortedm2m.fields.SortedManyToManyField(
help_text=None,
related_name="ancestors",
to="submission.article",
),
),
(
"parent",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="genealogy",
to="submission.article",
verbose_name="Original or main paper",
),
),
],
),
]
43 changes: 43 additions & 0 deletions src/submission/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
from utils.orcid import validate_orcid, COMPILED_ORCID_REGEX
from utils.forms import plain_text_validator
from journal import models as journal_models
from sortedm2m.fields import SortedManyToManyField
from review.const import (
ReviewerDecisions as RD,
)
Expand Down Expand Up @@ -2630,6 +2631,26 @@ def best_large_image_alt_text(self):
)
return default_text

def erratum_of(self):
"""
Return the "parent" article for which this article is an erratum.

This is intended to be used in
templates/common/identifiers/crossref_article.xml
"""
if self.section.name != "Erratum":
return None
if not self.ancestors.exists():
return None

# We can safely assume that an erratum refers to only one other paper
# so we just return the first "ancestor".
#
# Also, there is no need to check if the "parent" was published:
# the business logic should ensure that we cannot publish an erratum
# to a non-published paper.
return self.ancestors.first().parent


class FrozenAuthorQueryset(model_utils.AffiliationCompatibleQueryset):
AFFILIATION_RELATED_NAME = "frozen_author"
Expand Down Expand Up @@ -3400,6 +3421,28 @@ def handle_defaults(self, article):
article.save()


class Genealogy(models.Model):
"""
Maintain relations of type parent/children between articles.

This can be used, for instance, to link erratum to the original paper.
"""

parent = models.OneToOneField(
Article,
verbose_name=_("Original or main paper"),
on_delete=models.CASCADE,
related_name="genealogy",
)
children = SortedManyToManyField(
Article,
related_name="ancestors",
)

def __str__(self):
return f"Genealogy: {self.parent} has {self.children.count()} kids"


# Signals


Expand Down
28 changes: 26 additions & 2 deletions src/templates/common/identifiers/crossref_article.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@
<item_number item_number_type="article_number">{{ article.object.pk }}</item_number>
</publisher_item>

{% if article.erratum_of %}
<crossmark>
<crossmark_policy>{{ article.object.journal|setting:'crossref_prefix' }}/not-used</crossmark_policy>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ according to XML specs, when recording an <update>, then a <crossmark_policy> DOI should also be provided, but, according to crossref support, that element is not used and any DOI would do. They suggested to use the article DOI, but I'm hardcoding a 10.11111/no-used fake DOI (I feel it's less confusing...)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@mauromsl please feel free to resolve this thread if you have no objections

<updates>
<update type="erratum" date="{{ now|date:"Y-m-d" }}">{{ article.erratum_of.get_doi }}</update>
</updates>
{% if article.object.funders.exists %}
<custom_metadata>
<fr:program name="fundref">
{% for funder in article.object.funders.all %}
<fr:assertion name="fundgroup">
<fr:assertion name="funder_name">{{ funder.name }}</fr:assertion>
{% if funder.fundref_id %}
<fr:assertion name="funder_identifier">{{ funder.fundref_id }}</fr:assertion>
{% endif %}
{% if funder.funding_id %}
<fr:assertion name="award_number">{{ funder.funding_id }}</fr:assertion>
{% endif %}
</fr:assertion>
{% endfor %}
</fr:program>
</custom_metadata>
{% endif %}
</crossmark>
{% else %}
{% if article.object.funders.exists %}
<fr:program name="fundref">
{% for funder in article.object.funders.all %}
Expand All @@ -69,8 +94,7 @@
{% endfor %}
</fr:program>
{% endif %}


{% endif %}

<doi_data>
<doi>{{ article.doi }}</doi>
Expand Down
Loading