From bb8ede027ff7a206187438348228ba8924be3861 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Mon, 5 Jan 2026 17:08:05 -0300 Subject: [PATCH 1/7] =?UTF-8?q?Adiciona=20valida=C3=A7=C3=A3o=20para=20sta?= =?UTF-8?q?tus=20de=20disponibilidade=20de=20dados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cria constante DATA_AVAILABILITY_STATUS_INVALID e lista DATA_AVAILABILITY_STATUS_VALID_VALUES para validar valores vindos do XML, preservando informações inválidas para análise posterior. --- article/choices.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/article/choices.py b/article/choices.py index c8d4afc4..e79af2b1 100644 --- a/article/choices.py +++ b/article/choices.py @@ -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 = ( @@ -54,8 +55,12 @@ (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 = [status[0] for status in DATA_AVAILABILITY_STATUS] + # Constantes para cada tipo de relacionamento RELATED_TYPE_CORRECTED_ARTICLE = 'corrected-article' RELATED_TYPE_CORRECTION_FORWARD = 'correction-forward' @@ -85,38 +90,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')), -] \ No newline at end of file +] From 88586191f5786afc6045dee58d3ea32a4f30dd3f Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Mon, 5 Jan 2026 17:31:26 -0300 Subject: [PATCH 2/7] =?UTF-8?q?Adiciona=20campo=20para=20armazenar=20valor?= =?UTF-8?q?es=20inv=C3=A1lidos=20de=20data=20availability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cria campo invalid_data_availability_status para preservar valores inválidos vindos do XML quando não estiverem na lista de choices. --- article/models.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/article/models.py b/article/models.py index 00b86544..811ee9c3 100755 --- a/article/models.py +++ b/article/models.py @@ -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, @@ -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 = {} @@ -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, @@ -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"), @@ -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 @@ -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), @@ -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), @@ -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")), @@ -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( From 1465fdd873a62606b0e930eb162a73664baf8ecb Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Mon, 5 Jan 2026 17:33:36 -0300 Subject: [PATCH 3/7] =?UTF-8?q?Implementa=20valida=C3=A7=C3=A3o=20de=20dat?= =?UTF-8?q?a=20availability=20status=20antes=20de=20salvar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Valida se o valor extraído do XML está na lista de choices válidos. Valores inválidos são preservados em invalid_data_availability_status e o status é marcado como "invalid". --- article/sources/xmlsps.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/article/sources/xmlsps.py b/article/sources/xmlsps.py index 7501501f..508b0075 100755 --- a/article/sources/xmlsps.py +++ b/article/sources/xmlsps.py @@ -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) @@ -351,10 +351,16 @@ 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 se o status está na lista de valores aceitos + if status and status not in choices.DATA_AVAILABILITY_STATUS_VALID_VALUES: + article.invalid_data_availability_status = status + article.data_availability_status = choices.DATA_AVAILABILITY_STATUS_INVALID + else: + article.invalid_data_availability_status = None + article.data_availability_status = status or choices.DATA_AVAILABILITY_STATUS_ABSENT # SAVE OBRIGATÓRIO ANTES DE ADICIONAR OS ITENS M2M article.save() - + for item in items: DataAvailabilityStatement.create_or_update( user=user, @@ -1007,17 +1013,17 @@ 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, @@ -1025,7 +1031,7 @@ def add_related_articles(xmltree, article, user, errors): ext_link_type=ext_link_type, related_type=related_type ) - + except Exception as e: add_error( errors, @@ -1033,6 +1039,6 @@ def add_related_articles(xmltree, article, user, errors): e, related_article_data=related_article_data ) - + except Exception as e: add_error(errors, "add_related_articles", e) From 8bf64587014bdde8e5611d1983a8e9153450a357 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Mon, 5 Jan 2026 17:36:20 -0300 Subject: [PATCH 4/7] =?UTF-8?q?Adiciona=20migra=C3=A7=C3=A3o=20para=20camp?= =?UTF-8?q?o=20invalid=5Fdata=5Favailability=5Fstatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cria campo no modelo Article para armazenar valores inválidos de data availability recebidos do XML. --- ...valid_data_availability_status_and_more.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 article/migrations/0045_article_invalid_data_availability_status_and_more.py diff --git a/article/migrations/0045_article_invalid_data_availability_status_and_more.py b/article/migrations/0045_article_invalid_data_availability_status_and_more.py new file mode 100644 index 00000000..51fc6b8d --- /dev/null +++ b/article/migrations/0045_article_invalid_data_availability_status_and_more.py @@ -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", + ), + ), + ] From 094a2f6337f538065df8299eeeb6769661925841 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Tue, 6 Jan 2026 16:12:40 -0300 Subject: [PATCH 5/7] =?UTF-8?q?Corrige=20valida=C3=A7=C3=A3o=20de=20data?= =?UTF-8?q?=20availability=20para=20valores=20SPS=20oficiais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- article/choices.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/article/choices.py b/article/choices.py index e79af2b1..53e49e80 100644 --- a/article/choices.py +++ b/article/choices.py @@ -59,7 +59,13 @@ ) # Lista com valores válidos para validação -DATA_AVAILABILITY_STATUS_VALID_VALUES = [status[0] for status in DATA_AVAILABILITY_STATUS] +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' From fa09b6adfe53c0d8a73b7d853b2ec4baf57bc475 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Tue, 6 Jan 2026 16:14:49 -0300 Subject: [PATCH 6/7] =?UTF-8?q?Corrige=20l=C3=B3gica=20de=20valida=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrige preservação de histórico em invalid_data_availability_status Corrige validação de data availability para valores SPS oficiais --- article/sources/xmlsps.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/article/sources/xmlsps.py b/article/sources/xmlsps.py index 508b0075..51245dcb 100755 --- a/article/sources/xmlsps.py +++ b/article/sources/xmlsps.py @@ -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 @@ -351,13 +358,20 @@ def add_data_availability_status(xmltree, errors, article, user): continue items.append({"language": lang, "text": text}) - # Valida se o status está na lista de valores aceitos - if status and status not in choices.DATA_AVAILABILITY_STATUS_VALID_VALUES: + # Valida o status extraído do XML + if status is None: + # Valor ausente no XML (orientação mais recente do SPS) + # Não altera invalid_data_availability_status (preserva histórico) + article.data_availability_status = choices.DATA_AVAILABILITY_STATUS_ABSENT + 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 - article.data_availability_status = status or choices.DATA_AVAILABILITY_STATUS_ABSENT + article.data_availability_status = status + # SAVE OBRIGATÓRIO ANTES DE ADICIONAR OS ITENS M2M article.save() From c8a43d8f87639fbeb98327583ab2cdee12d583f1 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Wed, 7 Jan 2026 11:23:30 -0300 Subject: [PATCH 7/7] Atribui 'None' para article.invalid_data_availability_status --- article/sources/xmlsps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/article/sources/xmlsps.py b/article/sources/xmlsps.py index 51245dcb..f5428eed 100755 --- a/article/sources/xmlsps.py +++ b/article/sources/xmlsps.py @@ -361,8 +361,8 @@ def add_data_availability_status(xmltree, errors, article, user): # Valida o status extraído do XML if status is None: # Valor ausente no XML (orientação mais recente do SPS) - # Não altera invalid_data_availability_status (preserva histórico) article.data_availability_status = choices.DATA_AVAILABILITY_STATUS_ABSENT + 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