Skip to content
27 changes: 19 additions & 8 deletions article/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
DATA_AVAILABILITY_STATUS_UNINFORMED = "uninformed"
DATA_AVAILABILITY_STATUS_ABSENT = "absent"
DATA_AVAILABILITY_STATUS_NOT_PROCESSED = "not-processed"
DATA_AVAILABILITY_STATUS_INVALID = "invalid"

# Data availability status choices tuple
DATA_AVAILABILITY_STATUS = (
Expand All @@ -54,8 +55,18 @@
(DATA_AVAILABILITY_STATUS_UNINFORMED, _("Uso de dados não informado; nenhum dado de pesquisa gerado ou utilizado.")),
(DATA_AVAILABILITY_STATUS_ABSENT, _("Informação ausente no XML")),
(DATA_AVAILABILITY_STATUS_NOT_PROCESSED, _("XML não processado")),
(DATA_AVAILABILITY_STATUS_INVALID, _("Valor inválido recebido do XML")),
)

# Lista com valores válidos para validação
DATA_AVAILABILITY_STATUS_VALID_VALUES = [
DATA_AVAILABILITY_STATUS_AVAILABLE,
DATA_AVAILABILITY_STATUS_UPON_REQUEST,
DATA_AVAILABILITY_STATUS_IN_ARTICLE,
DATA_AVAILABILITY_STATUS_NOT_AVAILABLE,
DATA_AVAILABILITY_STATUS_UNINFORMED,
]

# Constantes para cada tipo de relacionamento
RELATED_TYPE_CORRECTED_ARTICLE = 'corrected-article'
RELATED_TYPE_CORRECTION_FORWARD = 'correction-forward'
Expand Down Expand Up @@ -85,38 +96,38 @@
# Erratas e correções
(RELATED_TYPE_CORRECTED_ARTICLE, _('Errata')),
(RELATED_TYPE_CORRECTION_FORWARD, _('Documento corrigido pela errata')),

# Retrações
(RELATED_TYPE_RETRACTED_ARTICLE, _('Retratação total')),
(RELATED_TYPE_RETRACTION_FORWARD, _('Documento retratado totalmente')),
(RELATED_TYPE_PARTIAL_RETRACTION, _('Retratação parcial')),
(RELATED_TYPE_PARTIAL_RETRACTION_FORWARD, _('Documento retratado parcialmente')),

# Adendos
(RELATED_TYPE_ADDENDED_ARTICLE, _('Adendo')),
(RELATED_TYPE_ADDENDUM, _('Documento objeto do adendo')),

# Manifestações de preocupação
(RELATED_TYPE_EXPRESSION_OF_CONCERN, _('Manifestação de preocupação')),
(RELATED_TYPE_OBJECT_OF_CONCERN, _('Documento objeto de manifestação de preocupação')),

# Comentários e respostas
(RELATED_TYPE_COMMENTARY_ARTICLE, _('Comentário')),
(RELATED_TYPE_COMMENTARY, _('Documento comentado')),
(RELATED_TYPE_REPLY_TO_COMMENTARY, _('Resposta para um comentário')),
(RELATED_TYPE_COMMENTARY_REPLY_OBJECT, _('Comentário objeto da resposta')),

# Cartas e respostas
(RELATED_TYPE_LETTER, _('Carta')),
(RELATED_TYPE_LETTER_OBJECT, _('Documento a que se refere a carta')),
(RELATED_TYPE_REPLY_TO_LETTER, _('Resposta para uma carta')),
(RELATED_TYPE_LETTER_REPLY_OBJECT, _('Carta objeto da resposta')),

# Pareceres
(RELATED_TYPE_REVIEWED_ARTICLE, _('Parecer (revisão por pares)')),
(RELATED_TYPE_REVIEWER_REPORT, _('Documento com parecer (revisão por pares)')),

# Preprints
(RELATED_TYPE_PREPRINT, _('Manuscrito disponibilizado em acesso aberto em servidor de preprints')),
(RELATED_TYPE_PUBLISHED_ARTICLE, _('Artigo publicado baseado no preprint')),
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Generated by Django 5.2.7 on 2026-01-05 19:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("article", "0044_alter_article_accepted_dateiso_and_more"),
]

operations = [
migrations.AddField(
model_name="article",
name="invalid_data_availability_status",
field=models.CharField(
blank=True,
help_text="Armazena valores inválidos recebidos do XML",
max_length=255,
null=True,
verbose_name="Invalid data availability status from XML",
),
),
migrations.AlterField(
model_name="article",
name="data_availability_status",
field=models.CharField(
blank=True,
choices=[
(
"data-available",
"Os dados de pesquisa estão disponíveis em repositório.",
),
(
"data-available-upon-request",
"Os dados de pesquisa só estão disponíveis mediante solicitação.",
),
(
"data-in-article",
"Os dados de pesquisa estão disponíveis no corpo do documento.",
),
(
"data-not-available",
"Os dados de pesquisa não estão disponíveis.",
),
(
"uninformed",
"Uso de dados não informado; nenhum dado de pesquisa gerado ou utilizado.",
),
("absent", "Informação ausente no XML"),
("not-processed", "XML não processado"),
("invalid", "Valor inválido recebido do XML"),
],
default="not-processed",
max_length=30,
null=True,
verbose_name="Data Availability Status",
),
),
]
34 changes: 21 additions & 13 deletions article/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ class Article(
default=choices.DATA_AVAILABILITY_STATUS_NOT_PROCESSED,
)

invalid_data_availability_status = models.CharField(
_("Invalid data availability status from XML"),
max_length=255,
null=True,
blank=True,
help_text=_("Armazena valores inválidos recebidos do XML")
)

peer_review_stats = models.JSONField(
_("Peer review statistics"),
default=dict,
Expand Down Expand Up @@ -840,7 +848,7 @@ def classic_available(self, collection_acron_list=None):
return self.get_availability(
fmt="html", collection_acron_list=collection_acron_list, params=params
)

def get_text_langs(self, collection_acron_list=None, fmt=None):
by_collection = {}
params = {}
Expand All @@ -862,12 +870,12 @@ def get_text_langs(self, collection_acron_list=None, fmt=None):

def add_event(self, user, name):
return ArticleEvent.create(user, self, name)
def add_related_article(self, user, href, ext_link_type, related_type, related_article=None):

def add_related_article(self, user, href, ext_link_type, related_type, related_article=None):
return RelatedArticle.create_or_update(
user,
self,
href,
href,
ext_link_type,
related_type,
related_article=related_article,
Expand Down Expand Up @@ -2314,14 +2322,14 @@ class ArticleExporter(BaseExporter):

class RelatedArticle(CommonControlField):
"""Relacionamento entre artigos via DOI."""

article = ParentalKey(
Article,
on_delete=models.CASCADE,
related_name="related_articles",
verbose_name=_("Article"),
)

href = models.CharField(
max_length=255,
verbose_name=_("DOI"),
Expand Down Expand Up @@ -2399,7 +2407,7 @@ def create(cls, user, article, href, ext_link_type, related_type, related_articl
raise ValueError("External link type is required")
if not related_type:
raise ValueError("Related type is required")

try:
obj = cls()
obj.article = article
Expand Down Expand Up @@ -2434,12 +2442,12 @@ class ArticlePeerReviewStats(Article):
"""
Proxy model para análise de peer review com campos relacionados pré-carregados.
"""

class Meta:
proxy = True
verbose_name = _("Peer Review Stats")
verbose_name_plural = _("Peer Review Stats")

# Informações básicas do artigo
panels_basic_info = [
FieldPanel("sps_pkg_name", read_only=True),
Expand All @@ -2448,14 +2456,14 @@ class Meta:
FieldPanel("article_type", read_only=True),
FieldPanel("data_availability_status", read_only=True),
]

# Datas do processo de peer review
panels_dates = [
FieldPanel("preprint_dateiso", read_only=True),
FieldPanel("received_dateiso", read_only=True),
FieldPanel("accepted_dateiso", read_only=True),
]

# Intervalos entre as datas (em dias)
panels_intervals = [
FieldPanel("days_preprint_to_received", read_only=True),
Expand All @@ -2474,7 +2482,7 @@ class Meta:
panels_statistics = [
FieldPanel("peer_review_stats", read_only=True),
]

edit_handler = TabbedInterface(
[
ObjectList(panels_basic_info, heading=_("Basic Information")),
Expand All @@ -2483,7 +2491,7 @@ class Meta:
ObjectList(panels_statistics, heading=_("Complete Statistics")),
]
)

def get_queryset(self, request):
"""QuerySet otimizado com select_related e prefetch_related"""
return self.objects.select_related(
Expand Down
48 changes: 34 additions & 14 deletions article/sources/xmlsps.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,32 +299,32 @@ def add_peer_review_dates(xmltree, article, errors):
"""
try:
dates = ArticleDates(xmltree=xmltree)

# Obter estatísticas completas de peer review
peer_review_stats = dates.get_peer_reviewed_stats(serialize_dates=True)

# Armazenar estatísticas completas em JSON
article.peer_review_stats = peer_review_stats

# Extrair datas individuais em formato ISO
article.preprint_dateiso = peer_review_stats.get("preprint_date")
article.received_dateiso = peer_review_stats.get("received_date")
article.received_dateiso = peer_review_stats.get("received_date")
article.accepted_dateiso = peer_review_stats.get("accepted_date")

# Extrair intervalos em dias
article.days_preprint_to_received = peer_review_stats.get("days_from_preprint_to_received")
article.days_received_to_accepted = peer_review_stats.get("days_from_received_to_accepted")
article.days_accepted_to_published = peer_review_stats.get("days_from_accepted_to_published")
article.days_preprint_to_published = peer_review_stats.get("days_from_preprint_to_published")
article.days_receive_to_published = peer_review_stats.get("days_from_received_to_published")

# Extrair flags de estimativa
article.days_preprint_to_received_estimated = peer_review_stats.get("estimated_days_from_preprint_to_received")
article.days_received_to_accepted_estimated = peer_review_stats.get("estimated_days_from_received_to_accepted")
article.days_accepted_to_published_estimated = peer_review_stats.get("estimated_days_from_accepted_to_published")
article.days_preprint_to_published_estimated = peer_review_stats.get("estimated_days_from_preprint_to_published")
article.days_receive_to_published_estimated = peer_review_stats.get("estimated_days_from_received_to_published")

except Exception as e:
add_error(errors, "add_peer_review_dates", e)

Expand All @@ -333,9 +333,16 @@ def add_data_availability_status(xmltree, errors, article, user):
"""
Extrai a declaração de disponibilidade de dados do XML.

Lógica de validação:
- Valor inválido: preserva em invalid_data_availability_status e marca como "invalid"
- Valor válido explícito: limpa invalid_data_availability_status
- Valor ausente: mantém invalid_data_availability_status inalterado (preserva histórico)

Args:
xmltree: Árvore XML do artigo
errors: Lista para coletar erros
article: Instância do modelo Article
user: Usuário responsável pela operação
"""
try:
status = None
Expand All @@ -351,10 +358,23 @@ def add_data_availability_status(xmltree, errors, article, user):
continue
items.append({"language": lang, "text": text})

article.data_availability_status = status or choices.DATA_AVAILABILITY_STATUS_ABSENT
# Valida o status extraído do XML
if status is None:
# Valor ausente no XML (orientação mais recente do SPS)
article.data_availability_status = choices.DATA_AVAILABILITY_STATUS_ABSENT
Copy link
Member

Choose a reason for hiding this comment

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

@Rossi-Luciano acho que é melhor adicionar article.invalid_data_availability_status = None

article.invalid_data_availability_status = None
elif status not in choices.DATA_AVAILABILITY_STATUS_VALID_VALUES:
# Valor inválido encontrado no XML
article.invalid_data_availability_status = status
article.data_availability_status = choices.DATA_AVAILABILITY_STATUS_INVALID
else:
# Valor válido explícito presente
article.invalid_data_availability_status = None
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The validation logic has a subtle issue: when the status from XML is None (absent), it correctly sets the status to DATA_AVAILABILITY_STATUS_ABSENT, but it also sets invalid_data_availability_status to None. However, when a valid status is provided from the XML, invalid_data_availability_status is also set to None. This means that if an article previously had an invalid status and is then updated with a valid status or no status, the original invalid value will be lost. Consider whether the invalid_data_availability_status field should only be cleared when explicitly appropriate, or if historical tracking of invalid values is needed.

Suggested change
article.invalid_data_availability_status = None
# Não sobrescreve `invalid_data_availability_status` quando o status é válido ou ausente,
# preservando qualquer valor inválido previamente armazenado.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK

article.data_availability_status = status

# SAVE OBRIGATÓRIO ANTES DE ADICIONAR OS ITENS M2M
article.save()

for item in items:
DataAvailabilityStatement.create_or_update(
user=user,
Expand Down Expand Up @@ -1007,32 +1027,32 @@ def add_related_articles(xmltree, article, user, errors):
"""
try:
related_articles = RelatedArticles(xmltree)

for related_article_data in related_articles.related_articles():
try:
# Extrair dados do artigo relacionado
href = related_article_data.get("href")
if not href:
continue

ext_link_type = related_article_data.get("ext-link-type")
related_type = related_article_data.get("related-article-type")

# Adicionar relacionamento ao artigo
article.add_related_article(
user=user,
href=href,
ext_link_type=ext_link_type,
related_type=related_type
)

except Exception as e:
add_error(
errors,
"add_related_articles.process_item",
e,
related_article_data=related_article_data
)

except Exception as e:
add_error(errors, "add_related_articles", e)
Loading