Skip to content

Aprimora fluxo de processamento de artigos e faz correcoes journal issue etc#1402

Merged
robertatakenaka merged 27 commits intoscieloorg:mainfrom
robertatakenaka:aprimora_fluxo_de_processamento_de_artigos_e_faz_correcoes_journal_issue_etc
Mar 12, 2026
Merged

Aprimora fluxo de processamento de artigos e faz correcoes journal issue etc#1402
robertatakenaka merged 27 commits intoscieloorg:mainfrom
robertatakenaka:aprimora_fluxo_de_processamento_de_artigos_e_faz_correcoes_journal_issue_etc

Conversation

@robertatakenaka
Copy link
Member

Descrição

Refatoração do pipeline de processamento de artigos, consolidando múltiplas tasks Celery em duas tasks centrais (task_dispatch_articles e task_process_article_pipeline), introduzindo o modelo XMLURL para rastreamento de falhas de coleta, e corrigindo uma série de problemas menores em modelos, índices de busca e agendamento de tarefas.


Motivação

O pipeline anterior era composto por dezenas de tasks Celery com responsabilidades sobrepostas (task_select_articles_to_complete_data, task_load_articles, task_load_journal_articles, task_select_articles_to_load_from_api, task_select_articles_to_load_from_collection_endpoint, task_load_article_from_xml_url, task_load_article_from_pp_xml, entre outras), dificultando manutenção, rastreabilidade de erros e reprocessamento seletivo. Além disso, falhas de rede ao coletar XMLs ficavam registradas apenas em UnexpectedEvent, sem estrutura para reprocessamento ou análise de padrões.


Principais mudanças

article/controller.pyArticleIteratorBuilder

Nova classe que encadeia até quatro iteradores de seleção de artigos:

  • _iter_from_pid_provider: filtra PidProviderXML por ISSN, ano e proc_status_list
  • _iter_from_article: filtra Article por data_status; tenta recuperar pp_xml via get_by_pid_v3; emite None para artigos sem pp_xml (sinaliza skip)
  • _iter_from_harvest: coleta via OPACHarvester (scl) ou AMHarvester (demais coleções)
  • _iter_from_article_source: filtra ArticleSource por status e datas

article/tasks.py — consolidação de tasks

Tasks removidas:

  • task_select_articles_to_complete_data
  • task_select_articles_to_load_from_api
  • task_select_articles_to_load_from_collection_endpoint
  • task_load_article_from_xml_url
  • task_load_article_from_pp_xml
  • task_select_articles_to_load_from_article_source
  • task_load_articles
  • task_load_journal_articles
  • task_fix_journal_articles_status (lógica absorvida por task_fix_article_status)

Tasks novas:

  • task_dispatch_articles: orquestradora que instancia ArticleIteratorBuilder e dispara task_process_article_pipeline.delay para cada item
  • task_process_article_pipeline: pipeline com três pontos de entrada:
    • Fluxo A: xml_url + collection_acron + pidAMArticleArticleSourceadd_pid_providerload_article
    • Fluxo B: article_source_idadd_pid_providerload_article
    • Fluxo C: pp_xml_id direto → load_article

article/models.pyArticleSource

  • Renomeia complete_dataadd_pid_provider com pipeline em duas etapas (request_xml + request_pid), com skip inteligente quando arquivo/pid já existem e force_update=False
  • Renomeia get_or_create_pid_v3request_pid; usa get_by_pid_v3; lança UnableToRegisterPIDError
  • Novos StatusChoices: URL_ERROR e XML_ERROR
  • Novas exceções tipadas: RequestXMLException, XMLException, UnableToRegisterPIDError
  • Novo cached_property xml_with_pre
  • is_completed: logging detalhado em cada condição de retorno False
  • ArticleAffiliation: adiciona autocomplete_label e autocomplete_custom_queryset_filter

pid_provider/models.pyXMLURL e refatorações

Novo modelo XMLURL:
Rastreia todas as URLs processadas pelo PidProvider com status, pid, zipfile (ZIP comprimido em memória) e exceptions (máx. 255 chars).

PidProviderXML:

  • Novo get_by_pid_v3(pid_v3, partial_pid_v2, pid_v2): busca flexível; em MultipleObjectsReturned retorna o mais recente
  • find_duplicated_pkg_names: retorna list(set(...)) em vez de values_list flat
  • Remove find_duplicated_v2 (não utilizado)
  • Novo mark_items_as_duplicated(issns): bulk update
  • deduplicate_items: simplificado, delega para fix_duplicated_pkg_name
  • Renomeia fix_duplicated_itemsfix_duplicated_pkg_name; remove lógica por v2
  • Novo método de instância fix_pkg_name(pkg_name)

XMLVersion:

  • get_or_create: trata AttributeError/TypeError/ValueError ao verificar arquivo em disco; usa pid_provider_xml.v3 como nome de arquivo

pid_provider/base_pid_provider.py

Refatora provide_pid_for_xml_uri com três categorias de erro:

  • a) Falha ao obter XML → registra XMLURL com xml_fetch_failed
  • b) XML obtido, registro falhou → registra XMLURL com pid_provider_xml_failed + salva zipfile
  • c) Erro inesperado → registra em UnexpectedEvent

collection/models.py

Adiciona cached_property base_url: retorna domain com https:// prefixado quando ausente.

article/search_indexes.py

ArticleIndex — novos campos: issn, license, aff_country, aff_institution, open_access, indexed_at, crossmark_active

ArticleOAIIndex — novos campos: issn, publisher, orcid, format_; prepare_date simplificado para retornar obj.pub_date

Correção de URLs: substitui 'http://%s' % collection.domain por '%s' % collection.base_url em todos os campos de URL dos dois índices.

Outras mudanças

  • core/views.py: absorve CommonControlFieldViewSet (antes em core/viewsets.py, arquivo removido)
  • issue/tasks.py: renomeia load_issue_from_article_metaload_issue_from_articlemeta
  • journal/choices.py: remove definição duplicada de UPDATE_POLICY_TYPE
  • journal/models.py: remove classname='collapsed' de todos os InlinePanel
  • journal/wagtail_hooks.py: adiciona updated ao list_display de 7 admins
  • bigbang/tasks_scheduler.py: registra todas as tasks obsoletas para limpeza; agenda task_dispatch_articles no lugar das tasks removidas
  • locale/es: PublisherEntidad Editora
  • requirements/base.txt: packtools 4.15.04.16.1

Migrações

App Migração Descrição
article 0048_alter_articlesource_status Adiciona url_error e xml_error ao campo status
pid_provider 0015_alter_xmlversion_file_xmlurl Amplia max_length de XMLVersion.file; cria modelo XMLURL

Testes

Adicionados em pid_provider/test_models.py:

  • XMLURLTest (8 casos): criação, recuperação, atualização, save_file com string/bytes/filename padrão, __str__
  • BasePidProviderXMLURITest (3 casos): falha de fetch, sucesso, falha de registro
python manage.py test pid_provider.test_models

Checklist

  • Migrações criadas e revisadas
  • Tasks obsoletas adicionadas à lista de limpeza do tasks_scheduler
  • Testes adicionados para XMLURL e BasePidProvider
  • Docstrings adicionadas/atualizadas nas tasks
  • Imports não utilizados removidos
  • Validar em ambiente de homologação com coleção real (scl)
  • Confirmar que tasks obsoletas não estão agendadas no Celery Beat antes do deploy

…s.py

Remove a primeira definição duplicada de UPDATE_POLICY_TYPE, mantendo
apenas a versão final com todos os tipos de política de atualização:
correction, retraction, partial-retraction, withdrawal,
expression-of-concern e other.
Introduz a propriedade base_url que retorna o domínio da coleção
pronto para compor URLs, adicionando o prefixo 'https://' quando o
campo domain não contém protocolo. Usado para substituir o uso direto
de collection.domain com concatenação manual de 'http://' nos índices
de busca e demais consumidores.
Consolida CommonControlFieldViewSet no módulo views.py ao lado de
CommonControlFieldCreateView, eliminando o arquivo viewsets.py que
passa a ser removido. O comportamento do ViewSet base (delegação para
form.save_all quando disponível) permanece idêntico. Atualiza o import
em pid_provider/wagtail_hooks.py para apontar para core.views.
…sion/PidProviderXML

XMLVersion:
- max_length=300 em file para caminhos longos.
- get_or_create: trata AttributeError/TypeError/ValueError ao verificar
  existência do arquivo em disco; usa pid_provider_xml.v3 como nome
  de arquivo em vez de sps_pkg_name.

PidProviderXML:
- Novo classmethod get_by_pid_v3(pid_v3, partial_pid_v2, pid_v2):
  busca flexível por v3/v2/v2__contains; em caso de múltiplos
  resultados retorna o mais recente.
- fix_pid_v2: passa a usar get_by_pid_v3 no lugar de objects.get(v3=).
- find_duplicated_pkg_names: retorna list(set(...)) em vez de
  values_list flat, evitando duplicatas no resultado.
- Remove find_duplicated_v2 (não mais utilizado).
- Novo classmethod mark_items_as_duplicated(issns): marca em bulk todos
  os PidProviderXML com pkg_name duplicado como PPXML_STATUS_DUPLICATED.
- deduplicate_items: simplificado — delega apenas para
  fix_duplicated_pkg_name, removendo lógica de v2.
- Renomeia fix_duplicated_items → fix_duplicated_pkg_name e remove
  o filtro por v2; adiciona tratamento de exceção com UnexpectedEvent.
- Novo método de instância fix_pkg_name(pkg_name): atualiza pkg_name
  quando difere do valor atual.

XMLURL (novo modelo):
- Rastreia URLs processadas pelo PidProvider com status, pid,
  zipfile comprimido e exceptions (máx. 255 chars).
- Métodos: get, create, create_or_update, save_file (gera ZIP em memória
  com zipfile.ZipFile e salva via ContentFile).
- xml_url_zipfile_path: função de upload path baseada em hash da URL.
- Função auxiliar _truncate_traceback em base_pid_provider.

Alterações na migração:
pid_provider: migration 0015 — amplia max_length de XMLVersion.file e cria modelo XMLURL
- XMLVersion.file: aumenta max_length de padrão para 300 para suportar
  caminhos de arquivo mais longos.
- Cria o novo modelo XMLURL com os campos: url (URLField 500), status,
  pid, zipfile (FileField 300, upload via xml_url_zipfile_path),
  exceptions (CharField 255) e chaves estrangeiras de auditoria
  (creator / updated_by). Índices criados em url, status e pid.
…s em três categorias

Divide o tratamento de exceções em três casos distintos:

a) Falha ao obter o XML (XMLWithPre.create lança exceção):
   registra XMLURL com status 'xml_fetch_failed', sem pid nem zipfile.

b) XML obtido mas criação do PidProviderXML falhou (resposta contém
   error_type ou error_message): registra XMLURL com status
   'pid_provider_xml_failed', salva zipfile comprimido do XML obtido.

c) Erro inesperado no bloco de registro: registra em UnexpectedEvent
   (comportamento anterior preservado).

Cada caso é extraído em método privado dedicado:
_handle_xml_fetch_failure, _handle_pid_provider_failure,
_register_success e _handle_unexpected_error.

Adiciona função auxiliar _truncate_traceback(tb_str, max_length=255)
para garantir que tracebacks caibam no campo CharField(max_length=255)
do modelo XMLURL.
provide_pid_for_opac_and_am_xml passa a usar PidProviderXML.get_by_pid_v3
para busca por pid_v3 e pid_v2 de forma unificada, eliminando dois
blocos separados de objects.get. Adiciona retorno explícito None no
caminho de erro ao final da função.
…id_for_xml_uri

XMLURLTest:
- test_create_xmlurl: verifica criação com url, status, pid e creator.
- test_get_xmlurl: verifica recuperação pelo campo url.
- test_create_or_update_existing: verifica atualização de registro existente.
- test_create_or_update_new: verifica criação quando não existe.
- test_save_file_with_string_content: verifica geração de ZIP a partir de string.
- test_save_file_with_bytes_content: verifica geração de ZIP a partir de bytes.
- test_save_file_default_filename: verifica nome de arquivo padrão no ZIP.
- test_str_method: verifica representação string do modelo.

BasePidProviderXMLURITest:
- test_provide_pid_for_xml_uri_fetch_failure: simula falha de rede e
  verifica XMLURL com status 'xml_fetch_failed'.
- test_provide_pid_for_xml_uri_success: simula registro bem-sucedido e
  verifica XMLURL com status 'success' e pid preenchido.
- test_provide_pid_for_xml_uri_registration_failure: simula falha de
  registro e verifica XMLURL com status 'pid_provider_xml_failed'
  e zipfile salvo.
…Article/ArticleAffiliation

Exceções novas:
- RequestXMLException: erro não-recuperável ao requisitar XML (NonRetryableError).
- XMLException: erro genérico ao processar conteúdo XML.
- UnableToRegisterPIDError: falha ao registrar PID no PidProvider.

Article:
- Remove método complete_data (lógica internalizada no ArticleSource).
- is_pp_xml_valid: usa PidProviderXML.get_by_pid_v3 em vez de
  objects.get(v3=); trata DoesNotExist atribuindo None.

ArticleSource:
- Novos StatusChoices: URL_ERROR e XML_ERROR.
- Novo cached_property xml_with_pre: tenta obter XMLWithPre de
  pid_provider_xml, depois do arquivo local e por último da URL.
- cached_property sps_pkg_name: delega para xml_with_pre.sps_pkg_name.
- request_xml: remove parâmetro opcional detail/force_update; lança
  RequestXMLException para NonRetryableError e XMLException para demais.
- Renomeia complete_data → add_pid_provider: pipeline em duas etapas
  (request_xml + request_pid) com skip inteligente quando arquivo/pid
  já existem e force_update=False; tratamento separado para
  XMLException (→ XML_ERROR), RequestXMLException (→ URL_ERROR) e
  exceções genéricas (→ ERROR).
- Renomeia get_or_create_pid_v3 → request_pid: usa get_by_pid_v3;
  lança UnableToRegisterPIDError quando v3 não retornado.
- create / create_or_update: adicionam parâmetro auto_solve_pid_conflict
  e delegam para add_pid_provider; simplificam fluxo removendo chamadas
  diretas a request_xml e save().
- is_completed: adiciona logging detalhado em cada condição de retorno False.
- mark_as_url_error / mark_as_xml_error: novos métodos de marcação.
- Métodos mark_as_completed/error/reprocess: sem alteração de interface.

ArticleAffiliation:
- Adiciona autocomplete_label e autocomplete_custom_queryset_filter
  para suporte ao widget de autocompletar no admin.
…em xmlsps.py

load_article:
- Resolve pp_xml por v3 antes do bloco try usando get_by_pid_v3,
  eliminando o elif v3 dentro do try.
- Extrai xml_with_pre.sps_pkg_name em variável local sps_pkg_name para
  evitar acessos repetidos ao atributo nos logs e atribuições.
…collection.base_url

ArticleIndex — novos campos:
- issn (MultiValueField): ISSN eletrônico, impresso e ISSN-L do periódico.
- license (CharField): tipo de licença do artigo.
- aff_country / aff_institution (MultiValueField): países e instituições
  das afiliações para filtro geográfico/institucional.
- open_access (CharField): status OA do periódico.
- indexed_at (MultiValueField): bases de indexação do periódico.
- crossmark_active (BooleanField): indica se Crossmark está ativo.

ArticleOAIIndex — novos campos:
- issn: ISSN oficial via metadata.dc.relation.
- publisher: nomes dos editores via metadata.dc.publisher.
- orcid: ORCIDs dos autores via metadata.dc.contributor.orcid.
- format_: formato do documento via metadata.dc.format.
- prepare_date: simplificado para retornar obj.pub_date diretamente.

Correção de URLs (ArticleIndex e ArticleOAIIndex):
- Substitui 'http://%s' % collection.domain por
  '%s' % collection.base_url em fulltext_pdf_*, fulltext_html_*,
  urls e identifier, garantindo protocolo correto (https) e
  evitando duplicação de 'http://'.
…nificada de artigos

Introduz a classe ArticleIteratorBuilder que encadeia até quatro
iteradores independentes de seleção de artigos:

- _iter_from_pid_provider: itera PidProviderXML filtrados por ISSN,
  intervalo de ano de publicação, datas e proc_status_list; usa
  Journal.get_journal_issns para agrupar por periódico.

- _iter_from_article: itera Article por data_status; tenta recuperar
  pp_xml via get_by_pid_v3 quando ausente; yields None para artigos
  sem pp_xml recuperável (sinaliza skip no dispatcher).

- _iter_from_harvest: coleta documentos via OPACHarvester (Brasil/scl)
  ou AMHarvester (demais coleções); carrega coleções se banco vazio;
  yields dict com xml_url, collection_acron, pid e source_date.

- _iter_from_article_source: itera ArticleSource via
  get_queryset_to_complete_data com filtros de data, force_update e
  article_source_status_list.

Todos os iteradores são encadeados em __iter__ via yield from,
permitindo múltiplas fontes ativas simultaneamente na mesma instância.

Imports removidos: load_article, date_utils, SciELOJournal,
XMLVersionXmlWithPreError, PPXML_STATUS_DUPLICATED/DEDUPLICATED,
DATA_STATUS_DUPLICATED/DEDUPLICATED/PUBLIC, Q, datetime.
Imports adicionados: itertools (reserva), AMHarvester, OPACHarvester,
Collection, ArticleSource, choices (módulo).
…icle_pipeline

Tasks removidas (substituídas):
- task_select_articles_to_complete_data
- task_select_articles_to_load_from_api
- task_select_articles_to_load_from_collection_endpoint
- task_load_article_from_xml_url
- task_load_article_from_pp_xml
- task_select_articles_to_load_from_article_source
- task_load_articles
- task_load_journal_articles
- task_fix_journal_articles_status (lógica absorvida por task_fix_article_status)

Tasks novas:
- task_dispatch_articles: orquestradora que instancia ArticleIteratorBuilder
  com todos os filtros disponíveis (collection, journal, ano, data,
  proc_status_list, data_status_list, article_source_status_list, limit,
  timeout, opac_url) e dispara task_process_article_pipeline.delay para
  cada item; contabiliza dispatched/skipped; registra UnexpectedEvent
  em caso de erro.

- task_process_article_pipeline: pipeline com três pontos de entrada:
  Fluxo A (xml_url + collection_acron + pid → AMArticle → ArticleSource
    → add_pid_provider → pp_xml_id),
  Fluxo B (article_source_id → add_pid_provider → pp_xml_id),
  Fluxo C (pp_xml_id direto).
  Após obter pp_xml, chama load_article e atualiza pp_xml.collections.
  Se export_to_articlemeta=True, verifica disponibilidade e dispara
  task_export_article_to_articlemeta.

task_fix_article_status: absorve a lógica de task_fix_journal_articles_status,
iterando diretamente sobre journal_id_list (derivada de collection/journal
ou de journal_id direto) sem subtarefas; aceita journal_id direto como
atalho.

task_check_article_availability: sem alteração funcional; docstring
corrigida (removia docstring de outra task).

Docstrings adicionadas/corrigidas em: load_funding_data, load_preprint,
task_convert_xml_to_other_formats_for_articles, convert_xml_to_other_formats,
transfer_license_statements_fk_to_article_license,
get_researcher_identifier_unnormalized, normalize_stored_email,
task_export_articles_to_articlemeta, task_export_article_to_articlemeta.

Imports removidos: traceback, datetime/timedelta, group, transaction,
Count/F/Prefetch/Q/Subquery, choices, fetch_data, AMHarvester,
OPACHarvester, SciELOJournal, PPXML_STATUS_DONE/TODO/INVALID,
PidProvider.
…m_articlemeta

Ajusta nome da função Celery e as três referências a action= em
UnexpectedEvent.create para o novo nome sem underscore antes de 'meta'.
…urnal e SciELOJournal

Todos os InlinePanel definidos nos grupos de painéis de Journal
(other_titles, mission, history, focus, thematic_area, title_in_database,
owner_history, publisher_history, sponsor_history, copyright_holder_history,
related_journal_urls, open_science_form_files, open_access_text, open_data,
preprint, peer_review, open_science_compliance, notes) e de SciELOJournal
(journal_history) têm o atributo classname='collapsed' removido, exibindo
os painéis expandidos por padrão no Wagtail admin.
…IndexedAtAdmin,

AdditionalIndexedAtAdmin, WebOfKnowledgeAdmin, SubjectAdmin, WosAreaAdmin e StandardAdmin
…atch_articles

delete_outdated_tasks: adiciona ao inventário de limpeza todas as tasks
removidas neste ciclo de refatoração:
task_select_articles_to_complete_data, task_select_articles_to_load_from_api,
task_select_articles_to_load_from_collection_endpoint,
task_select_articles_to_load_from_article_source, task_load_articles,
task_load_journal_articles, task_load_article_from_xml_url,
task_create_article_source, task_create_pid_provider_xml,
task_fix_journal_articles_status, task_select_articles_to_export_to_articlemeta,
issue.tasks.load_issue_from_article_meta (legacy).

schedule_tasks: chama delete_outdated_tasks no início; substitui
schedule_task_select_articles_to_complete_data,
schedule_task_select_articles_to_load_from_api,
schedule_task_select_articles_to_load_from_article_source e
schedule_task_load_articles pela nova schedule_task_dispatch_articles
(horário 02:01); remove schedule_bigbang_delete_outdated_tasks (chamada
manual no início do agendamento); atualiza referência da task de issue
para load_issue_from_articlemeta com parâmetros simplificados.

schedule_task_dispatch_articles: agenda task_dispatch_articles com todos
os parâmetros disponíveis (collection, journal, datas, proc/data/
article_source status lists, limit, timeout, opac_url,
export_to_articlemeta, auto_solve_pid_conflict).

schedule_task_export_articles_to_articlemeta: atualiza nome da task de
task_select_articles_to_export_to_articlemeta para
task_export_articles_to_articlemeta.
Copilot AI review requested due to automatic review settings March 12, 2026 15:30
Copy link
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

Esta PR refatora o pipeline de processamento de artigos (reduzindo várias tasks antigas para duas tasks centrais) e adiciona rastreabilidade de falhas de coleta/registro via o novo modelo XMLURL, além de ajustes em admin, índices de busca e agendamentos.

Changes:

  • Consolida o processamento em task_dispatch_articles + task_process_article_pipeline e introduz ArticleIteratorBuilder para seleção de itens de múltiplas fontes.
  • Adiciona o modelo pid_provider.XMLURL e integra seu uso no BasePidProvider para registrar falhas/sucessos e persistir o XML compactado.
  • Atualiza índices de busca e pequenos ajustes em admin/migrações/traduções/scheduler.

Reviewed changes

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

Show a summary per file
File Description
pid_provider/wagtail_hooks.py Atualiza import do CommonControlFieldViewSet após mudança de localização.
pid_provider/test_models.py Adiciona testes para XMLURL e cenários de BasePidProvider.
pid_provider/sources/harvesting.py Usa PidProviderXML.get_by_pid_v3() para reaproveitar registros quando possível.
pid_provider/models.py Ajusta XMLVersion.file e adiciona o novo modelo XMLURL e helper get_by_pid_v3.
pid_provider/migrations/0015_alter_xmlversion_file_xmlurl.py Migração para XMLVersion.file(max_length=300) e criação do modelo XMLURL.
pid_provider/base_pid_provider.py Registra falhas/sucessos em XMLURL e mantém UnexpectedEvent para erros inesperados.
organization/migrations/0012_alter_organization_url.py Ajuste de URLField para permitir null/blank.
locale/es/LC_MESSAGES/django.po Ajuste de tradução de “Publisher”.
journal/wagtail_hooks.py Inclui updated no list_display em múltiplos admins.
journal/models.py Remove classname="collapsed" de InlinePanels.
journal/migrations/0059_alter_digitalpreservationagency_options.py Ajusta verbose_name/verbose_name_plural.
journal/choices.py (Re)declara UPDATE_POLICY_TYPE.
issue/tasks.py Renomeia task load_issue_from_article_metaload_issue_from_articlemeta.
institution/migrations/0008_alter_institution_url_alter_scimago_url.py Ajuste de URLField para permitir null/blank.
core/views.py Adiciona CommonControlFieldViewSet baseado em SnippetViewSet com save_all(user) quando disponível.
core/home/migrations/0015_alter_formpage_thank_you_text.py Ajuste de campo RichTextField (help_text/blank).
collection/models.py Adiciona base_url para normalizar domínio com protocolo.
bigbang/tasks_scheduler.py Limpa tasks obsoletas e agenda task_dispatch_articles no lugar das removidas.
article/tasks.py Implementa task_dispatch_articles e task_process_article_pipeline e remove/absorve tasks antigas.
article/sources/xmlsps.py Permite obter pp_xml via PidProviderXML.get_by_pid_v3 quando só há v3.
article/search_indexes.py Novos campos e correção de URLs via collection.base_url; simplifica prepare_dates.
article/models.py Refatora ArticleSource para add_pid_provider (request_xml + request_pid) e novos status/erros.
article/migrations/0048_alter_articlesource_status.py Adiciona url_error e xml_error em ArticleSource.status.
article/controller.py Introduz ArticleIteratorBuilder para encadear fontes de seleção/dispatch.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if not pp_xml_id:
raise ValueError(
"No valid entry point provided. Please provide either xml_url, "
"article_source_id, pp_xml_id or pid_v3."
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The error message mentions pid_v3 as a valid entry point, but this task doesn’t accept a pid_v3 parameter. This is misleading for operators troubleshooting task failures; update the message to list only supported inputs (xml_url, article_source_id, pp_xml_id).

Suggested change
"article_source_id, pp_xml_id or pid_v3."
"article_source_id, or pp_xml_id."

Copilot uses AI. Check for mistakes.
Comment on lines +1274 to 1285
params = {}
if pid_v3:
params["v3"] = pid_v3
if pid_v2:
params["v2"] = pid_v2
if partial_pid_v2:
params["v2__contains"] = partial_pid_v2
try:
return cls.objects.get(**params)
except cls.MultipleObjectsReturned as e:
return cls.objects.filter(**params).order_by("-updated").first()

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

PidProviderXML.get_by_pid_v3 builds a params dict and passes it to objects.get(**params). When both pid_v3 and pid_v2 are provided this becomes an AND filter, which is not a “flexible” lookup and can fail even when either PID matches. Also, if all inputs are falsy, params is empty and get() can raise MultipleObjectsReturned (or return an arbitrary single row), which is unsafe. Consider guarding against empty input and using a Q-based OR lookup across v3/v2/partial v2, returning the most recent match deterministically.

Suggested change
params = {}
if pid_v3:
params["v3"] = pid_v3
if pid_v2:
params["v2"] = pid_v2
if partial_pid_v2:
params["v2__contains"] = partial_pid_v2
try:
return cls.objects.get(**params)
except cls.MultipleObjectsReturned as e:
return cls.objects.filter(**params).order_by("-updated").first()
"""
Retrieve a PidProviderXML instance by one or more PID values.
The lookup is flexible:
- Matches by v3 when ``pid_v3`` is provided.
- Matches by v2 when ``pid_v2`` is provided.
- Matches by partial v2 (substring) when ``partial_pid_v2`` is provided.
When multiple values are provided, results are combined with OR and
the most recently updated matching record is returned.
"""
# Guard against an empty lookup which would otherwise result in an
# unfiltered query that may return arbitrary or multiple rows.
if not (pid_v3 or pid_v2 or partial_pid_v2):
raise cls.DoesNotExist(
"PidProviderXML.get_by_pid_v3 called without any PID argument"
)
query_parts = []
if pid_v3:
query_parts.append(Q(v3=pid_v3))
if pid_v2:
query_parts.append(Q(v2=pid_v2))
if partial_pid_v2:
query_parts.append(Q(v2__contains=partial_pid_v2))
# Combine all query parts using OR logic.
combined_query = query_parts[0]
for extra_q in query_parts[1:]:
combined_query |= extra_q
# Deterministically return the most recently updated matching record.
obj = cls.objects.filter(combined_query).order_by("-updated").first()
if obj is None:
raise cls.DoesNotExist(
f"No PidProviderXML found for the provided PID arguments: "
f"pid_v3={pid_v3!r}, pid_v2={pid_v2!r}, partial_pid_v2={partial_pid_v2!r}"
)
return obj

Copilot uses AI. Check for mistakes.
Comment on lines +1366 to +1377
def deduplicate_items(cls, user, issns):
"""
Corrige todos os artigos marcados como DATA_STATUS_DUPLICATED com base nos ISSNs fornecidos.

Args:
issns: Lista de ISSNs para verificar duplicatas.
user: Usuário que está executando a operação.
"""
duplicated_v2 = cls.find_duplicated_v2(issns)
if duplicated_v2.exists():
if mark_as_duplicated:
cls.objects.filter(v2__in=duplicated_v2).exclude(
proc_status=choices.PPXML_STATUS_DUPLICATED
).update(
proc_status=choices.PPXML_STATUS_DUPLICATED,
)
if deduplicate:
for v2 in duplicated_v2:
cls.fix_duplicated_items(user, None, v2)

duplicated_pkg_names = cls.find_duplicated_pkg_names(issns)
if duplicated_pkg_names.exists():
if mark_as_duplicated:
cls.objects.filter(pkg_name__in=duplicated_pkg_names).exclude(
proc_status=choices.PPXML_STATUS_DUPLICATED
).update(
proc_status=choices.PPXML_STATUS_DUPLICATED,
)
if deduplicate:
for pkg_name in duplicated_pkg_names:
cls.fix_duplicated_items(user, pkg_name, None)
for pkg_name in duplicated_pkg_names:
cls.fix_duplicated_pkg_name(pkg_name, user)
return duplicated_pkg_names
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

PidProviderXML.deduplicate_items had its signature changed to accept only (user, issns), but there are still call sites passing mark_as_duplicated / deduplicate keyword args (e.g. pid_provider/tasks.py:285). This will raise TypeError at runtime. Either keep backwards-compatible kwargs or update all callers accordingly.

Copilot uses AI. Check for mistakes.
def _handle_pid_provider_failure(self, response, xml_with_pre, xml_uri, name, user, origin_date, force_update, is_published):
"""Handle exception type b) - XML obtained but PidProviderXML creation failed"""
# Format error information from response (not from an exception context)
error_msg = response.get("error_message", "Unknown error")
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

_handle_pid_provider_failure reads response.get("error_message"), but PidProviderXML.register() (and other codepaths) populate error_msg instead. This will often record "Unknown error" in XMLURL.exceptions even when an error message exists. Consider falling back to error_msg when error_message is absent.

Suggested change
error_msg = response.get("error_message", "Unknown error")
error_msg = response.get("error_message") or response.get("error_msg") or "Unknown error"

Copilot uses AI. Check for mistakes.
Comment on lines +176 to +184

UPDATE_POLICY_TYPE = [
("correction", _("Correction")),
("retraction", _("Retraction")),
("partial-retraction", _("Partial Retraction")),
("withdrawal", _("Withdrawal")),
("expression-of-concern", _("Expression of Concern")),
("other", _("Other")),
]
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

UPDATE_POLICY_TYPE is defined twice in this module, which can lead to confusion and accidental edits to the wrong copy (the latter definition silently overrides the former). Remove the duplicate and keep a single canonical definition.

Suggested change
UPDATE_POLICY_TYPE = [
("correction", _("Correction")),
("retraction", _("Retraction")),
("partial-retraction", _("Partial Retraction")),
("withdrawal", _("Withdrawal")),
("expression-of-concern", _("Expression of Concern")),
("other", _("Other")),
]

Copilot uses AI. Check for mistakes.
Comment on lines +467 to +471
yield from self._iter_from_harvest()
yield from self._iter_from_article_source()
yield from self._iter_from_pid_provider()
yield from self._iter_from_article()

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

ArticleIteratorBuilder.__iter__ unconditionally yields from all four iterators. This contradicts the class docstring (which says iterators are activated by “exclusive” args) and means every dispatch will also harvest all collections (and also iterate PidProviderXML + Article + ArticleSource), potentially duplicating work and massively increasing load. Make iterator selection conditional so only the requested sources run (and default to only _iter_from_pid_provider when no exclusive args are provided).

Suggested change
yield from self._iter_from_harvest()
yield from self._iter_from_article_source()
yield from self._iter_from_pid_provider()
yield from self._iter_from_article()
"""
Itera sobre artigos de acordo com os argumentos "exclusivos".
A seleção dos iteradores segue a documentação da classe:
- Se `opac_url` foi informado, itera a partir do harvest
(`_iter_from_harvest`).
- Se `article_source_status_list` foi informado, itera a partir de
`ArticleSource` (`_iter_from_article_source`).
- Se `data_status_list` foi informado, itera a partir de `Article`
(`_iter_from_article`).
- Se `proc_status_list` foi informado, itera a partir de
`PidProviderXML` (`_iter_from_pid_provider`).
Caso nenhum desses argumentos "exclusivos" seja informado, o
comportamento padrão é iterar apenas a partir de `PidProviderXML`.
"""
any_selected = False
# Harvest somente quando explicitamente solicitado (por exemplo, via opac_url)
if self.opac_url is not None:
any_selected = True
yield from self._iter_from_harvest()
# ArticleSource controlado por article_source_status_list
if self.article_source_status_list is not None:
any_selected = True
yield from self._iter_from_article_source()
# Article controlado por data_status_list
if self.data_status_list is not None:
any_selected = True
yield from self._iter_from_article()
# PidProviderXML controlado por proc_status_list
if self.proc_status_list is not None:
any_selected = True
yield from self._iter_from_pid_provider()
# Padrão: apenas PidProviderXML quando nenhum argumento exclusivo foi informado
if not any_selected:
yield from self._iter_from_pid_provider()

Copilot uses AI. Check for mistakes.
Comment on lines +929 to +938
article_source = ArticleSource.create_or_update(
user=user,
url=xml_url,
source_date=source_date,
force_update=force_update,
am_article=am_article,
auto_solve_pid_conflict=auto_solve_pid_conflict,
)
return {
"status": "success",
"message": "Processing all articles without journal filters",
"filters": {
"from_pub_year": from_pub_year,
"until_pub_year": until_pub_year,
"from_updated_date": from_updated_date,
"until_updated_date": until_updated_date,
"proc_status_list": proc_status_list,
},
}
pp_xml_id = article_source.pid_provider_xml.id

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

In the xml_url flow, ArticleSource.create_or_update() triggers add_pid_provider(), which catches errors internally and may leave article_source.pid_provider_xml unset. Accessing article_source.pid_provider_xml.id will then raise AttributeError and turn expected pipeline failures into UnexpectedEvent noise. Consider checking article_source.status/pid_provider_xml and returning early (or raising a clearer error) when PID registration fails.

Copilot uses AI. Check for mistakes.
robertatakenaka and others added 7 commits March 12, 2026 13:32
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Adiciona campo string multiValuado 'indexed_at' ao schema do Solr
para rastrear a data de indexação por documento.
OPACHarvester:
- Aceita URL completa (https://...) no parâmetro domain, removendo
  o prefixo 'https://' hardcoded na construção das URLs
- Define limit=100 e timeout=5 quando não informados

AMHarvester:
- Aceita limit=None e resolve internamente para 1000, permitindo
  que chamadores distingam 'não informado' de valor explícito
…rutor de URL

Adiciona parâmetro force_update em check_availability() para ignorar
o retorno antecipado de is_available() quando uma reavaliação forçada
é necessária.
…torBuilder

- Inicializa contadores em __init__ para cada iterador
  (_iter_from_harvest_count, _article_source_count, _pid_provider_count, _article_count)
- Incrementa contadores em cada método iterador
- Registra totais por iterador ao fim de cada um
- Registra resumo consolidado em __iter__ após todos os iteradores serem esgotados
…le_pipeline

- Move check_availability para fora do bloco export_to_articlemeta,
  executando sempre que force_update=True
- Passa flag combinada: force_update=export_to_articlemeta or force_update
- Adiciona log de despacho antes de task_process_article_pipeline.delay()
Copy link
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

Copilot reviewed 26 out of 26 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<!-- Field to journal CNPQ subject areas -->
<field name="subject_areas" type="string" indexed="true" stored="true" multiValued="true"/>
<field name="indexed_at" type="string" indexed="true" stored="true" multiValued="true"/>

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The schema adds indexed_at, but the updated ArticleIndex now sends additional fields (e.g., issn, aff_country, aff_institution, crossmark_active). For multi-valued fields, Solr needs multiValued="true" definitions; otherwise they’ll fall back to the * dynamicField (multiValued="false") and indexing will fail when Haystack submits lists/booleans. Add explicit <field ...> entries (with correct types/multiValued) for each new indexed field introduced in article/search_indexes.py.

Suggested change
<!-- Additional fields indexed by ArticleIndex -->
<field name="issn" type="string" indexed="true" stored="true" multiValued="true"/>
<field name="aff_country" type="string" indexed="true" stored="true" multiValued="true"/>
<field name="aff_institution" type="string" indexed="true" stored="true" multiValued="true"/>
<field name="crossmark_active" type="boolean" indexed="true" stored="true" multiValued="false"/>

Copilot uses AI. Check for mistakes.
}
)
journals_processed += 1
logging.info(f"Dispatching article with kwargs: {item_kwargs}")
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

task_dispatch_articles logs every dispatched item at INFO (Dispatching article with kwargs: ...). For large backfills this can generate very high log volume and impact observability/ingestion costs. Consider downgrading to DEBUG or logging periodic summaries/counters instead.

Suggested change
logging.info(f"Dispatching article with kwargs: {item_kwargs}")
logging.debug(f"Dispatching article with kwargs: {item_kwargs}")

Copilot uses AI. Check for mistakes.
Comment on lines 142 to +167
@@ -163,8 +163,8 @@ def __init__(
self.collection_acron = collection_acron
self.from_date = from_date or "2000-01-01"
self.until_date = until_date or datetime.utcnow().isoformat()[:10]
self.limit = limit
self.timeout = timeout
self.limit = limit or 100
self.timeout = timeout or 5
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

OPACHarvester now expects domain to include the scheme (default https://...) and concatenates URLs with f"{self.domain}/...". The docstring still suggests passing just www.scielo.br, and existing callers still do so. To prevent invalid URLs, normalize domain in __init__ (prepend https:// when missing) or revert to building URLs with an explicit scheme.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +60
("url", models.URLField(max_length=500, verbose_name="URL")),
(
"status",
models.CharField(
blank=True, max_length=50, null=True, verbose_name="Status"
),
),
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

XMLURL.url is created without unique=True/UniqueConstraint, but application code uses XMLURL.get(url=...)/create_or_update() assuming a single row per URL. Consider enforcing uniqueness at the DB level here (or updating code to handle multiple rows) to prevent MultipleObjectsReturned in production.

Copilot uses AI. Check for mistakes.
Comment on lines +1615 to +1623
url = models.URLField(
_("URL"), max_length=500, null=False, blank=False
)
status = models.CharField(
_("Status"), max_length=50, null=True, blank=True
)
pid = models.CharField(
_("Article PID"), max_length=23, null=True, blank=True
)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

XMLURL.create()/create_or_update() assume url is unique (they call get(url=...)), but the model/migration do not enforce uniqueness. This can lead to MultipleObjectsReturned during error handling (e.g., in BasePidProvider._handle_xml_fetch_failure) and make retries unreliable. Either add a DB-level unique constraint on url (and update the migration) or change get()/update logic to tolerate multiple rows (e.g., update the latest record).

Copilot uses AI. Check for mistakes.
timeout=self.timeout,
)
if collection_acron == "scl":
return OPACHarvester(self.opac_url or "www.scielo.br", collection_acron, **kwargs)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

ArticleIteratorBuilder._build_harvester() passes "www.scielo.br" to OPACHarvester, but OPACHarvester now expects domain to already include the scheme (it concatenates with f"{self.domain}/..."). This will generate invalid URLs like www.scielo.br/api/.... Pass a fully qualified base URL (e.g., https://www.scielo.br) or normalize inside OPACHarvester.

Suggested change
return OPACHarvester(self.opac_url or "www.scielo.br", collection_acron, **kwargs)
domain = self.opac_url or "https://www.scielo.br"
if not domain.startswith(("http://", "https://")):
domain = f"https://{domain}"
return OPACHarvester(domain, collection_acron, **kwargs)

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +57
# Afiliações (países e instituições para filtro geográfico/institucional)
aff_country = indexes.MultiValueField(null=True)
aff_institution = indexes.MultiValueField(null=True)

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

aff_country and aff_institution fields were added to ArticleIndex, but there are no corresponding prepare_aff_country / prepare_aff_institution methods. As a result these fields will not be populated (defeating their purpose for filtering). Implement the prepare methods (e.g., aggregating countries/institutions from affiliations) or remove the fields until implemented.

Copilot uses AI. Check for mistakes.
Comment on lines +1273 to +1283
def get_by_pid_v3(cls, pid_v3, partial_pid_v2=None, pid_v2=None):
params = {}
if pid_v3:
params["v3"] = pid_v3
if pid_v2:
params["v2"] = pid_v2
if partial_pid_v2:
params["v2__contains"] = partial_pid_v2
try:
return cls.objects.get(**params)
except cls.MultipleObjectsReturned as e:
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

PidProviderXML.get_by_pid_v3() builds params dynamically, but if callers pass no PID inputs (all params None/empty), it will call objects.get(**{}), which can raise MultipleObjectsReturned unexpectedly. Consider validating that at least one of pid_v3, pid_v2, or partial_pid_v2 was provided and returning None or raising ValueError otherwise.

Copilot uses AI. Check for mistakes.


# Handle response based on success or failure
if response.get("error_type") or response.get("error_message"):
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

provide_pid_for_xml_uri is checking error_message, but PidProviderXML.register() populates error_msg on failures. As-is, failures returned as {error_type, error_msg} will be treated as success and recorded in XMLURL with status success (and _handle_pid_provider_failure will also lose the real message). Update the condition and _handle_pid_provider_failure to consistently use the actual error keys returned by register() (and/or normalize the response in register() to one key).

Suggested change
if response.get("error_type") or response.get("error_message"):
if response.get("error_type") or response.get("error_msg"):

Copilot uses AI. Check for mistakes.
Comment on lines +501 to +537
proc_status_list=self.proc_status_list or [PPXML_STATUS_TODO, PPXML_STATUS_INVALID],
)
self._iter_from_pid_provider_count += qs.count()
for item in qs.iterator():
yield {"pp_xml_id": item.id}
logging.info(f"_iter_from_pid_provider: yielded {self._iter_from_pid_provider_count} items")

def _iter_from_article(self):
"""
Itera Articles filtrados por data_status.
Yields None para artigos sem pp_xml recuperável (sinaliza skip).
"""
filters = {
"data_status__in": self.data_status_list or [
choices.DATA_STATUS_PENDING,
choices.DATA_STATUS_UNDEF,
choices.DATA_STATUS_INVALID,
]
}
journal_id_list = Journal.get_ids(
collection_acron_list=self.collection_acron_list,
journal_acron_list=self.journal_acron_list,
)
if journal_id_list:
filters["journal__in"] = journal_id_list
if self.from_pub_year:
filters["pub_year__gte"] = self.from_pub_year
if self.until_pub_year:
filters["pub_year__lte"] = self.until_pub_year
if self.from_date:
filters["updated__gte"] = self.from_date
if self.until_date:
filters["updated__lte"] = self.until_date

articles = Article.objects.filter(**filters)
self._iter_from_article_count += articles.count()
for article in articles.iterator():
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Both _iter_from_pid_provider() and _iter_from_article() call .count() on potentially large querysets (and then iterate them), causing extra full COUNT queries that can be expensive in production. Consider removing these .count() calls (or using a cheap counter while iterating) and logging only the iterated count.

Copilot uses AI. Check for mistakes.
@robertatakenaka robertatakenaka merged commit 2a877d6 into scieloorg:main Mar 12, 2026
7 of 9 checks passed
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.

2 participants