Skip to content

Criar funcionalidade de depósito de DOI no Crossref#872

Open
Copilot wants to merge 3 commits intomainfrom
copilot/create-crossref-doi-deposit-feature
Open

Criar funcionalidade de depósito de DOI no Crossref#872
Copilot wants to merge 3 commits intomainfrom
copilot/create-crossref-doi-deposit-feature

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 5, 2026

Implementa o fluxo completo de depósito de DOI no Crossref, desde a configuração por periódico até o rastreamento dos depósitos realizados, usando packtools para geração do XML.

O que esse PR faz?

Adiciona a funcionalidade de depósito de DOI no Crossref com três componentes principais:

Modelos (doi/models.py)

  • CrossrefConfiguration — configuração por periódico: depositor_name, depositor_email, registrant, crossmark_policy_url, crossmark_policy_doi, login_id, login_password
  • CrossrefDeposit — rastreamento de cada depósito com status (pending/submitted/success/error), resposta HTTP e referência ao XML gerado

Controller (doi/controller.py)

  • get_crossref_xml() — gera XML via packtools.sps.formats.crossref.pipeline_crossref()
  • deposit_xml_to_crossref() — POST multipart para https://doi.crossref.org/servlet/deposit
  • deposit_article_doi() — orquestra o fluxo completo, protegendo contra re-depósito acidental sem force=True

Tasks Celery (doi/tasks.py)

  • task_deposit_doi_to_crossref — depósito de 1 artigo
  • task_batch_deposit_doi_to_crossref — depósito em lote por periódico ou lista de artigos

Interface

  • Admin Wagtail (doi/wagtail_hooks.py) com grupo "Crossref" para gerenciar configurações e consultar histórico de depósitos
  • View + URL (doi/views.py, doi/urls.py) para disparar depósito via botão HTTP
  • Seção "Crossref DOI Deposit" adicionada ao template de inspeção do artigo, com botões de depósito e histórico dos últimos 5 depósitos

Onde a revisão poderia começar?

Começar em doi/controller.py (lógica principal) → doi/models.py (estrutura de dados) → doi/tasks.py (integração Celery) → doi/wagtail_hooks.py (admin).

Como este poderia ser testado manualmente?

  1. Rodar python manage.py migrate doi para criar as novas tabelas
  2. No admin Wagtail, acessar Crossref → Crossref Configuration e criar uma configuração para um periódico (preencher depositor, registrant, credenciais da API Crossref)
  3. Acessar um artigo com sps_pkg associado via Articles → inspect
  4. Clicar no botão "Deposit DOI to Crossref" — a task Celery será enfileirada
  5. Verificar o resultado em Crossref → Crossref Deposits

Para depósito em lote, disparar via código ou Celery beat:

task_batch_deposit_doi_to_crossref.apply_async(
    kwargs=dict(user_id=1, username="admin", journal_id=42)
)

Algum cenário de contexto que queira dar?

O packtools (versão 4.14.4) já possui packtools.sps.formats.crossref.pipeline_crossref() que gera o XML completo a partir da árvore XML do artigo. O depósito usa a API legada do Crossref (/servlet/deposit) com autenticação por login_id/login_passwd.

As credenciais são armazenadas no modelo CrossrefConfiguration — em produção, recomenda-se uso de campos criptografados ou variáveis de ambiente injetadas.

Screenshots

N/A — funcionalidade de backend com interface Wagtail admin padrão.

Quais são tickets relevantes?

Criar funcionalidade de realizar o depósito de DOI no Crossref.

Referências

Original prompt

This section details on the original issue you should resolve

<issue_title>Criar funcionalidade de realizar o depósito de DOI no Crossref</issue_title>
<issue_description>### Descrição da tarefa
Criar funcionalidade de realizar o depósito de DOI no Crossref. Considere usar os componentes do packtools para gerar o XML do Crossref.

Subtarefas

  • Se não existir, criar formulário para gerenciar por journal os dados: crossmark_policy (link e doi), depositor, registrant Crossref
  • Criar tarefa para realizar o depósito para 1 documento a partir da seleção do documento ou clique no botão do documento
  • Criar tarefa para realizar depósito para loges de documentos</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 5, 2026 18:54
Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Copilot AI changed the title [WIP] Add DOI deposit functionality for Crossref Criar funcionalidade de depósito de DOI no Crossref Mar 5, 2026
@robertatakenaka robertatakenaka marked this pull request as ready for review March 6, 2026 10:30
Copilot AI review requested due to automatic review settings March 6, 2026 10:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implementa o fluxo de depósito de DOI no Crossref (configuração por periódico, geração de XML via packtools, envio ao endpoint legado do Crossref, registro/histórico de depósitos) e expõe controles no Wagtail admin para disparo e acompanhamento.

Changes:

  • Adiciona modelos CrossrefConfiguration e CrossrefDeposit + migration para persistir configuração/credenciais e histórico de depósitos.
  • Implementa controller e tasks Celery para gerar XML Crossref e realizar depósitos (unitário e em lote).
  • Integra com Wagtail admin (menu/snippets) e adiciona botões/histórico na tela de inspeção do artigo, além de uma view/URL para enfileirar depósitos.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
doi/controller.py Geração do XML Crossref e envio HTTP ao Crossref; orquestra o depósito por artigo.
doi/forms.py Adiciona form base para edição de CrossrefConfiguration no admin.
doi/migrations/0003_crossref_configuration_and_deposit.py Cria as tabelas de configuração e depósitos no banco.
doi/models.py Define os modelos e helpers (create_or_update, create, mark_*) para Crossref.
doi/tasks.py Define tasks Celery para depósito unitário e em lote.
doi/tests.py Adiciona testes para modelos e controller do depósito.
doi/urls.py Expõe endpoint HTTP para enfileirar depósito de artigo.
doi/views.py View que enfileira task de depósito via HTTP e mostra mensagens ao usuário.
doi/wagtail_hooks.py Registra snippets/viewsets no menu do Wagtail para gerenciar configuração e depósitos.
config/urls.py Inclui rotas do app doi no projeto.
article/views.py Injeta no inspect do artigo dados de depósitos recentes e flag de config Crossref.
article/templates/modeladmin/article/article/inspect.html Adiciona seção e botões para disparar depósito/redepósito e visualizar depósitos recentes.
.gitignore Ignora test.db.

Comment on lines +9 to +14
from lxml import etree
from packtools.sps.formats import crossref as crossref_format
from packtools.sps.pid_provider.xml_sps_lib import XMLWithPre

from tracker.models import UnexpectedEvent

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

There are multiple unused imports in this module (etree, XMLWithPre, UnexpectedEvent). Please remove them to avoid lint issues and keep dependencies minimal.

Suggested change
from lxml import etree
from packtools.sps.formats import crossref as crossref_format
from packtools.sps.pid_provider.xml_sps_lib import XMLWithPre
from tracker.models import UnexpectedEvent
from packtools.sps.formats import crossref as crossref_format

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +109
login_id = models.CharField(
_("Crossref Login ID"),
max_length=256,
null=True,
blank=True,
help_text=_("Crossref member account username for API deposit"),
)
login_password = models.CharField(
_("Crossref Login Password"),
max_length=256,
null=True,
blank=True,
help_text=_("Crossref member account password for API deposit"),
)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

login_password is stored as a plain CharField, which means Crossref credentials will be persisted in cleartext in the database and may be exposed via backups/admin access. Consider using an encrypted field (or storing credentials in a secrets backend / environment injection) and limit who can view/edit this field in the admin UI.

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +257

article_no_config = Article.objects.create(
pid_v3="S9999-99992024000100001",
creator=self.user,
)

with self.assertRaises(CrossrefDepositError):
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The "without config" controller test is not actually exercising the missing-configuration path: the created article has no journal, so deposit_article_doi fails earlier with "no associated journal". Also, the assertion expects CrossrefDepositError, but the controller raises CrossrefConfigurationNotFoundError when the journal exists but has no Crossref configuration. Update the test to create an article with a journal that lacks CrossrefConfiguration (and with an sps_pkg), and assert the correct exception type.

Suggested change
article_no_config = Article.objects.create(
pid_v3="S9999-99992024000100001",
creator=self.user,
)
with self.assertRaises(CrossrefDepositError):
from journal.models import Journal, OfficialJournal
from package.models import SPSPkg
# Create a journal without a CrossrefConfiguration
official_journal_no_config = OfficialJournal.objects.create(
title="No Config Journal",
creator=self.user,
)
journal_no_config = Journal.objects.create(
official_journal=official_journal_no_config,
title="No Config Journal",
journal_acron="noconfig",
creator=self.user,
)
# Create an article associated with that journal and an SPS package
article_no_config = Article.objects.create(
pid_v3="S9999-99992024000100001",
creator=self.user,
journal=journal_no_config,
)
sps_pkg = SPSPkg.objects.create(
pid_v3="S9999-99992024000100001",
sps_pkg_name="test-pkg-no-config",
creator=self.user,
)
article_no_config.sps_pkg = sps_pkg
article_no_config.save()
with self.assertRaises(CrossrefConfigurationNotFoundError):

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +90
<a href="{% url 'doi:deposit_article_doi' %}?article_id={{ article_id }}"
class="button bicolor button--icon">
<span class="icon-wrapper">
<svg class="icon icon-upload icon" aria-hidden="true" focusable="false">
<use href="#icon-upload"></use>
</svg>
</span>
{% trans "Deposit DOI to Crossref" %}
</a>
<a href="{% url 'doi:deposit_article_doi' %}?article_id={{ article_id }}&force=true"
class="button button-secondary bicolor button--icon">
<span class="icon-wrapper">
<svg class="icon icon-upload icon" aria-hidden="true" focusable="false">
<use href="#icon-upload"></use>
</svg>
</span>
{% trans "Re-deposit DOI to Crossref" %}
</a>
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The new admin buttons trigger deposits via a plain GET link. Once the view is changed to require POST + CSRF (recommended), update this UI to use a <form method="post"> with {% csrf_token %} (and include force as a hidden field for re-deposit) so the action remains usable from the inspect page.

Suggested change
<a href="{% url 'doi:deposit_article_doi' %}?article_id={{ article_id }}"
class="button bicolor button--icon">
<span class="icon-wrapper">
<svg class="icon icon-upload icon" aria-hidden="true" focusable="false">
<use href="#icon-upload"></use>
</svg>
</span>
{% trans "Deposit DOI to Crossref" %}
</a>
<a href="{% url 'doi:deposit_article_doi' %}?article_id={{ article_id }}&force=true"
class="button button-secondary bicolor button--icon">
<span class="icon-wrapper">
<svg class="icon icon-upload icon" aria-hidden="true" focusable="false">
<use href="#icon-upload"></use>
</svg>
</span>
{% trans "Re-deposit DOI to Crossref" %}
</a>
<form method="post" action="{% url 'doi:deposit_article_doi' %}" style="display: inline;">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ article_id }}">
<button type="submit" class="button bicolor button--icon">
<span class="icon-wrapper">
<svg class="icon icon-upload icon" aria-hidden="true" focusable="false">
<use href="#icon-upload"></use>
</svg>
</span>
{% trans "Deposit DOI to Crossref" %}
</button>
</form>
<form method="post" action="{% url 'doi:deposit_article_doi' %}" style="display: inline;">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ article_id }}">
<input type="hidden" name="force" value="true">
<button type="submit" class="button button-secondary bicolor button--icon">
<span class="icon-wrapper">
<svg class="icon icon-upload icon" aria-hidden="true" focusable="false">
<use href="#icon-upload"></use>
</svg>
</span>
{% trans "Re-deposit DOI to Crossref" %}
</button>
</form>

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +9
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from wagtail import hooks
from wagtail.snippets.models import register_snippet
from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

reverse and hooks are imported but not used in this file. Please remove unused imports to avoid lint warnings.

Suggested change
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from wagtail import hooks
from wagtail.snippets.models import register_snippet
from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup
from django.utils.translation import gettext_lazy as _
from wagtail.snippets.models import register_snippet
from wagtail.snippets.views.snippets import SnippetViewSetGroup

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +176
if login_id:
obj.login_id = login_id
if login_password:
obj.login_password = login_password
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

CrossrefConfiguration.create_or_update only updates login_id/login_password when a truthy value is provided, which makes it impossible to clear credentials (e.g., set them back to empty) via this API or via forms that submit blank values. Assign these fields unconditionally (or explicitly handle empty strings) so updates can remove credentials when needed.

Suggested change
if login_id:
obj.login_id = login_id
if login_password:
obj.login_password = login_password
obj.login_id = login_id
obj.login_password = login_password

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +19
import sys
import logging

from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.db import IntegrityError, models
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel
from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface

from core.models import CommonControlField

from .forms import DOIWithLangForm
from .forms import DOIWithLangForm, CrossrefConfigurationForm
from collection.models import Language


User = get_user_model()

logger = logging.getLogger(__name__)

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This file now imports sys, IntegrityError, get_user_model()/User, and defines logger, but none of these are used anywhere in the module. Please remove the unused imports/variables to avoid lint failures.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +42
article_id = request.GET.get("article_id")
next_url = request.GET.get("next") or request.META.get("HTTP_REFERER") or "/"

if not article_id:
messages.error(request, _("Article ID is required."))
return HttpResponseRedirect(next_url)

article = get_object_or_404(Article, pk=article_id)

task_deposit_doi_to_crossref.apply_async(
kwargs=dict(
user_id=request.user.id,
username=request.user.username,
article_id=article.id,
force=request.GET.get("force", "false").lower() == "true",
)
)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The view triggers a state-changing operation via GET parameters (article_id, force). This is CSRF-prone and can be triggered unintentionally (e.g., prefetching/crawlers). Require POST for enqueuing the task and add CSRF protection; keep GET only for rendering a confirmation page if needed.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +32
article_id = request.GET.get("article_id")
next_url = request.GET.get("next") or request.META.get("HTTP_REFERER") or "/"

if not article_id:
messages.error(request, _("Article ID is required."))
return HttpResponseRedirect(next_url)

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

next_url is taken directly from the next query param and then used in HttpResponseRedirect, which enables open redirects to external domains. Use url_has_allowed_host_and_scheme (or django.shortcuts.resolve_url + allowed hosts) to validate next_url and fall back to a safe internal URL.

Copilot uses AI. Check for mistakes.
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

reverse is imported but never used in this module. Please remove the unused import to keep the file clean and avoid lint failures.

Suggested change
from django.urls import reverse

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Criar funcionalidade de realizar o depósito de DOI no Crossref

3 participants