diff --git a/article/choices.py b/article/choices.py index c8d4afc4..53e49e80 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,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' @@ -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')), -] \ No newline at end of file +] 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", + ), + ), + ] 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( diff --git a/article/sources/xmlsps.py b/article/sources/xmlsps.py index 7501501f..f5428eed 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) @@ -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,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 + 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 + 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, @@ -1007,17 +1027,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 +1045,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 +1053,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)