Aprimora fluxo de processamento de artigos e faz correcoes journal issue etc#1402
Conversation
…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.
There was a problem hiding this comment.
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_pipelinee introduzArticleIteratorBuilderpara seleção de itens de múltiplas fontes. - Adiciona o modelo
pid_provider.XMLURLe integra seu uso noBasePidProviderpara 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_meta → load_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." |
There was a problem hiding this comment.
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).
| "article_source_id, pp_xml_id or pid_v3." | |
| "article_source_id, or pp_xml_id." |
| 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() | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
| 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 |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
_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.
| error_msg = response.get("error_message", "Unknown error") | |
| error_msg = response.get("error_message") or response.get("error_msg") or "Unknown error" |
|
|
||
| UPDATE_POLICY_TYPE = [ | ||
| ("correction", _("Correction")), | ||
| ("retraction", _("Retraction")), | ||
| ("partial-retraction", _("Partial Retraction")), | ||
| ("withdrawal", _("Withdrawal")), | ||
| ("expression-of-concern", _("Expression of Concern")), | ||
| ("other", _("Other")), | ||
| ] |
There was a problem hiding this comment.
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.
| UPDATE_POLICY_TYPE = [ | |
| ("correction", _("Correction")), | |
| ("retraction", _("Retraction")), | |
| ("partial-retraction", _("Partial Retraction")), | |
| ("withdrawal", _("Withdrawal")), | |
| ("expression-of-concern", _("Expression of Concern")), | |
| ("other", _("Other")), | |
| ] |
| 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() | ||
|
|
There was a problem hiding this comment.
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).
| 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() |
| 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 | ||
|
|
There was a problem hiding this comment.
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.
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()
There was a problem hiding this comment.
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"/> | ||
|
|
There was a problem hiding this comment.
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.
| <!-- 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"/> |
| } | ||
| ) | ||
| journals_processed += 1 | ||
| logging.info(f"Dispatching article with kwargs: {item_kwargs}") |
There was a problem hiding this comment.
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.
| logging.info(f"Dispatching article with kwargs: {item_kwargs}") | |
| logging.debug(f"Dispatching article with kwargs: {item_kwargs}") |
| @@ -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 | |||
There was a problem hiding this comment.
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.
| ("url", models.URLField(max_length=500, verbose_name="URL")), | ||
| ( | ||
| "status", | ||
| models.CharField( | ||
| blank=True, max_length=50, null=True, verbose_name="Status" | ||
| ), | ||
| ), |
There was a problem hiding this comment.
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.
| 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 | ||
| ) |
There was a problem hiding this comment.
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).
| timeout=self.timeout, | ||
| ) | ||
| if collection_acron == "scl": | ||
| return OPACHarvester(self.opac_url or "www.scielo.br", collection_acron, **kwargs) |
There was a problem hiding this comment.
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.
| 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) |
| # Afiliações (países e instituições para filtro geográfico/institucional) | ||
| aff_country = indexes.MultiValueField(null=True) | ||
| aff_institution = indexes.MultiValueField(null=True) | ||
|
|
There was a problem hiding this comment.
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.
| 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: |
There was a problem hiding this comment.
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.
|
|
||
|
|
||
| # Handle response based on success or failure | ||
| if response.get("error_type") or response.get("error_message"): |
There was a problem hiding this comment.
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).
| if response.get("error_type") or response.get("error_message"): | |
| if response.get("error_type") or response.get("error_msg"): |
| 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(): |
There was a problem hiding this comment.
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.
Descrição
Refatoração do pipeline de processamento de artigos, consolidando múltiplas tasks Celery em duas tasks centrais (
task_dispatch_articlesetask_process_article_pipeline), introduzindo o modeloXMLURLpara 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 emUnexpectedEvent, sem estrutura para reprocessamento ou análise de padrões.Principais mudanças
article/controller.py—ArticleIteratorBuilderNova classe que encadeia até quatro iteradores de seleção de artigos:
_iter_from_pid_provider: filtraPidProviderXMLpor ISSN, ano eproc_status_list_iter_from_article: filtraArticlepordata_status; tenta recuperarpp_xmlviaget_by_pid_v3; emiteNonepara artigos sem pp_xml (sinaliza skip)_iter_from_harvest: coleta viaOPACHarvester(scl) ouAMHarvester(demais coleções)_iter_from_article_source: filtraArticleSourcepor status e datasarticle/tasks.py— consolidação de tasksTasks removidas:
task_select_articles_to_complete_datatask_select_articles_to_load_from_apitask_select_articles_to_load_from_collection_endpointtask_load_article_from_xml_urltask_load_article_from_pp_xmltask_select_articles_to_load_from_article_sourcetask_load_articlestask_load_journal_articlestask_fix_journal_articles_status(lógica absorvida portask_fix_article_status)Tasks novas:
task_dispatch_articles: orquestradora que instanciaArticleIteratorBuildere disparatask_process_article_pipeline.delaypara cada itemtask_process_article_pipeline: pipeline com três pontos de entrada:xml_url+collection_acron+pid→AMArticle→ArticleSource→add_pid_provider→load_articlearticle_source_id→add_pid_provider→load_articlepp_xml_iddireto →load_articlearticle/models.py—ArticleSourcecomplete_data→add_pid_providercom pipeline em duas etapas (request_xml + request_pid), com skip inteligente quando arquivo/pid já existem eforce_update=Falseget_or_create_pid_v3→request_pid; usaget_by_pid_v3; lançaUnableToRegisterPIDErrorStatusChoices:URL_ERROReXML_ERRORRequestXMLException,XMLException,UnableToRegisterPIDErrorcached_property xml_with_preis_completed: logging detalhado em cada condição de retornoFalseArticleAffiliation: adicionaautocomplete_labeleautocomplete_custom_queryset_filterpid_provider/models.py—XMLURLe refatoraçõesNovo modelo
XMLURL:Rastreia todas as URLs processadas pelo PidProvider com
status,pid,zipfile(ZIP comprimido em memória) eexceptions(máx. 255 chars).PidProviderXML:get_by_pid_v3(pid_v3, partial_pid_v2, pid_v2): busca flexível; emMultipleObjectsReturnedretorna o mais recentefind_duplicated_pkg_names: retornalist(set(...))em vez devalues_listflatfind_duplicated_v2(não utilizado)mark_items_as_duplicated(issns): bulk updatededuplicate_items: simplificado, delega parafix_duplicated_pkg_namefix_duplicated_items→fix_duplicated_pkg_name; remove lógica por v2fix_pkg_name(pkg_name)XMLVersion:get_or_create: trataAttributeError/TypeError/ValueErrorao verificar arquivo em disco; usapid_provider_xml.v3como nome de arquivopid_provider/base_pid_provider.pyRefatora
provide_pid_for_xml_uricom três categorias de erro:XMLURLcomxml_fetch_failedXMLURLcompid_provider_xml_failed+ salva zipfileUnexpectedEventcollection/models.pyAdiciona
cached_property base_url: retornadomaincomhttps://prefixado quando ausente.article/search_indexes.pyArticleIndex— novos campos:issn,license,aff_country,aff_institution,open_access,indexed_at,crossmark_activeArticleOAIIndex— novos campos:issn,publisher,orcid,format_;prepare_datesimplificado para retornarobj.pub_dateCorreção de URLs: substitui
'http://%s' % collection.domainpor'%s' % collection.base_urlem todos os campos de URL dos dois índices.Outras mudanças
core/views.py: absorveCommonControlFieldViewSet(antes emcore/viewsets.py, arquivo removido)issue/tasks.py: renomeiaload_issue_from_article_meta→load_issue_from_articlemetajournal/choices.py: remove definição duplicada deUPDATE_POLICY_TYPEjournal/models.py: removeclassname='collapsed'de todos osInlinePaneljournal/wagtail_hooks.py: adicionaupdatedaolist_displayde 7 adminsbigbang/tasks_scheduler.py: registra todas as tasks obsoletas para limpeza; agendatask_dispatch_articlesno lugar das tasks removidaslocale/es:Publisher→Entidad Editorarequirements/base.txt: packtools4.15.0→4.16.1Migrações
article0048_alter_articlesource_statusurl_errorexml_errorao campostatuspid_provider0015_alter_xmlversion_file_xmlurlmax_lengthdeXMLVersion.file; cria modeloXMLURLTestes
Adicionados em
pid_provider/test_models.py:XMLURLTest(8 casos): criação, recuperação, atualização,save_filecom string/bytes/filename padrão,__str__BasePidProviderXMLURITest(3 casos): falha de fetch, sucesso, falha de registropython manage.py test pid_provider.test_modelsChecklist
tasks_schedulerXMLURLeBasePidProvider