Skip to content

Sincroniza o app pid_provider (compartilhado entre core e upload)#1448

Open
robertatakenaka wants to merge 20 commits into
scieloorg:mainfrom
robertatakenaka:aplica_refatoracao_pid_provider_upload_pr_1023
Open

Sincroniza o app pid_provider (compartilhado entre core e upload)#1448
robertatakenaka wants to merge 20 commits into
scieloorg:mainfrom
robertatakenaka:aplica_refatoracao_pid_provider_upload_pr_1023

Conversation

@robertatakenaka

@robertatakenaka robertatakenaka commented Jul 15, 2026

Copy link
Copy Markdown
Member

O que esse PR faz?

Sincroniza o app pid_provider (compartilhado entre core e upload) trazendo para o core os avanços feitos no repositório do upload, fechando a divergência acumulada desde a última sincronização entre os dois projetos.

Problemas que resolve:

  • Falta de auditoria em falhas de registro: quando PidProviderXML.register() falha antes de existir um registro (unmatched, conflito, bad_request, erro inesperado), não havia rastreabilidade padronizada — só UnexpectedEvent genérico. Novo modelo PidProviderXMLRegistration grava sempre o resultado do fluxo, com FK nullable para PidProviderXML (casos de erro podem não ter registro associado).
  • Matching de XML frágil: a comparação por soma de pontos fixos (best_matches/match/title_similarity) foi substituída por score percentual comparável entre estratégias de busca (select_records/select_record/get_best_match + compare() em query_params.py).
  • Bugs de produção corrigidos:
    • fix_duplicated_pkg_name acessava other_pid.current_version, campo inexistente em OtherPid (correto é version) — lançaria AttributeError sempre que houvesse deduplicação com other_pid do tipo pid_v3.
    • mark_items_as_invalid calculava o resultado mas nunca persistia — o método não tinha efeito nenhum no banco. Agora usa bulk_update.
    • is_registered_xml_uri retornava None silenciosamente quando XMLWithPre.create() não produzia nenhum item; agora retorna um dict de erro explícito (EmptyXMLWithPreError).
    • is_registered_xml_zip usava return dentro de um generator no bloco except, o que interrompia a iteração sem propagar o erro ao chamador; corrigido para yield.
  • Rastreio pobre de falhas por URL: XMLURL guardava só uma string de exceção truncada manualmente. Agora tem status com choices, campo detail (JSON), campo is_public (inferido de document_item, novo parâmetro em provide_pid_for_xml_uri) e método record() centralizando a gravação — substituindo três handlers duplicados (_handle_xml_fetch_failure, _handle_pid_provider_failure, _handle_unexpected_error) em base_pid_provider.py.

Feature adicionada:

  • PidProviderXMLManager aplica select_related("current_version") por padrão em PidProviderXML.objects, eliminando repetição manual em vários classmethods (get_xml_with_pre, public_items, get_by_pid_v3, mark_items_as_invalid, etc.).
  • Novos ViewSets de admin (XMLURLViewSet, PidProviderXMLRegistrationViewSet) para dar visibilidade a essa auditoria no Wagtail.
  • Suíte de testes automatizados nova para o app (pid_provider/tests/), cobrindo BasePidProvider (uri/zip, casos de sucesso e falha), PidProviderXML.register/select_record/select_records/get_best_match, XMLURL (create/update/record/data) e QueryBuilderPidProviderXML/compare().
  • Bump de packtools de 4.15.0 para 4.16.8 (dependência usada por get_complete_publication_date, get_article_data, deprecated_sps_pkg_name_list, entre outras APIs novas consumidas neste PR).

Onde a revisão poderia começar?

pid_provider/models.py, pelo método PidProviderXML.register() — é onde a mudança de comportamento central acontece (uso de select_records/select_record, SkipSavePidProviderXML, gravação em PidProviderXMLRegistration). Em seguida:

  1. pid_provider/exceptions.py — nova exceção SkipSavePidProviderXML.
  2. pid_provider/query_params.pycompare()/get_best_match(), a base do novo matching, e a simplificação de QueryBuilderPidProviderXML (menos cached_property, novo validate_input_data()).
  3. pid_provider/models.pyPidProviderXMLRegistration, get_best_match(), correções de bug (fix_duplicated_pkg_name, mark_items_as_invalid), XMLURL.record(), PidProviderXMLManager.
  4. pid_provider/base_pid_provider.py — como XMLURL.record() substituiu os handlers antigos, o novo parâmetro document_item, e as correções em is_registered_xml_uri/is_registered_xml_zip.
  5. pid_provider/wagtail_hooks.py — ViewSets novos (revisão mais rápida, é só exposição no admin).
  6. pid_provider/tests/ — conferir se os cenários cobertos batem com o comportamento esperado descrito nos itens acima (os docstrings dos testes documentam bem as mudanças de contrato).
  7. requirements/base.txt — checar changelog do packtools entre 4.15.0 e 4.16.8 quanto a breaking changes adicionais não usados aqui.

Como este poderia ser testado manualmente?

  1. Rodar PidProviderXML.register() com:
    • Um XML novo (sem registro prévio) → conferir que PidProviderXMLRegistration.event_status == "created".
    • O mesmo XML sem alterações (force_update=False) → conferir event_status == "skipped" e que nenhuma nova versão foi salva.
    • Um XML com dados que batam com mais de um registro (unmatched) → conferir que PidProviderXMLRegistration grava event_status == "unmatched" com detail populado, mesmo sem pid_provider_xml associado.
    • Um XML com PID v3 já usado por outro documento diferente → conferir event_status == "conflict".
  2. Rodar PidProviderXML.deduplicate_items(user, issns) (ou fix_duplicated_pkg_name diretamente) em um cenário com documentos duplicados que tenham other_pid do tipo pid_v3 — confirmar que não lança mais AttributeError.
  3. Rodar PidProviderXML.mark_items_as_invalid(issns) com ao menos um XML inválido/corrompido no set — verificar no banco que proc_status foi de fato atualizado para PPXML_STATUS_INVALID (antes, nada era persistido).
  4. Simular falha de fetch e falha de registro em BasePidProvider.provide_pid_for_xml_uri(...), incluindo com document_item={"status": True/False} — conferir que XMLURL grava status, detail (com traceback/response), is_public corretamente inferido e, quando há xml_with_pre, o zipfile correspondente.
  5. Rodar BasePidProvider.is_registered_xml_uri(...) contra uma URI que não retorna nenhum XML — conferir que agora vem um dict de erro (EmptyXMLWithPreError) em vez de None.
  6. Rodar BasePidProvider.is_registered_xml_zip(...) com um zip que causa exceção durante a leitura — conferir que o erro chega via iteração do generator (antes era perdido).
  7. Rodar a suíte nova: python manage.py test pid_provider.tests.
  8. No admin do Wagtail, verificar os novos menus XML URLs e PID Registration Events — listagem, filtros (status/is_public e event_status/created) e busca funcionando.
  9. Após atualizar packtools, rodar a suíte completa do pid_provider (e idealmente de proc/migration) para pegar eventuais regressões de API do pacote.

Algum cenário de contexto que queira dar?

O app pid_provider vive em dois repositórios (core e upload) e evolui em paralelo nos dois — de tempos em tempos é preciso reconciliar manualmente os avanços de um lado para o outro, já que não há um pacote compartilhado único. Este PR é uma dessas reconciliações: o upload avançou nesses pontos (auditoria, matching, correções, testes) e agora trazemos isso para o core.

Um ponto de contexto importante: o core já tem em produção o modelo XMLEvent, usado para auditar falhas no consumo do XML (na geração de Article). O novo PidProviderXMLRegistration audita um momento diferente do ciclo de vida: o registro do PID em si, inclusive quando ele falha antes de qualquer Article existir. Avaliamos unificar os dois modelos e decidimos não fazer isso agora — a diferença de nullability/cascade entre eles (SET_NULL vs CASCADE) reflete uma diferença real de domínio, e XMLEvent já está em produção, então o custo de generalizar não compensa o ganho.

Outro ponto: o bump de packtools (4.15.0 → 4.16.8) é uma dependência direta das novas chamadas (get_complete_publication_date, get_article_data, deprecated_sps_pkg_name_list) e não é opcional — sem ele o código novo não roda.

Screenshots

Não aplicável — mudanças de modelo, lógica de matching e auditoria; sem alteração de UI além dos novos ViewSets padrão do Wagtail admin (listagem tabular padrão, sem customização visual).

Quais são os tickets relevantes?

#1447

PR no Upload: scieloorg/scms-upload#1023

Referências


Segurança da informação (NSI.04)

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Sim — descreva os controles de proteção aplicados (criptografia, mascaramento, anonimização, etc.):
  • Não

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Sim — descreva o que mudou e por quê:
  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Sim
  • Não

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Sim — link do job:
  • Não aplicável ainda

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Sim — confirme que há sanitização/parametrização (prepared statements, escaping, etc.):
  • Não — todas as consultas usam o ORM do Django (Q, Prefetch, filter, bulk_update), sem SQL bruto nem interpolação de entrada externa.

Este PR expõe novos endpoints, telas ou serviços?

  • Sim — HTTPS obrigatório está garantido e o acesso segue o princípio de menor privilégio?
    • Novas telas são exclusivamente no Wagtail admin (XMLURLViewSet, PidProviderXMLRegistrationViewSet), não são endpoints públicos; seguem o mesmo controle de acesso do admin já existente (autenticação/permissões do Wagtail), sem novo endpoint de API externa.
  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado
  • Sim (bloquear merge e corrigir antes de prosseguir)

Propósito: permitir que is_updated() sinalize 'não precisa salvar'
via exceção em vez de retorno implícito, deixando o fluxo de
register() explícito sobre por que parou (equal / já atualizado).

Técnico: nova classe SkipSavePidProviderXML(Exception), usada em
models.py dentro de is_updated() e capturada em register().
Propósito: formalizar os valores possíveis de status do XMLURL, que
antes eram strings livres passadas soltas pelo código
(base_pid_provider.py), sem validação nem enumeração no admin.

Técnico: novas constantes XMLURL_STATUS_SUCCESS,
XMLURL_STATUS_XML_FETCH_FAILED, XMLURL_STATUS_PID_PROVIDER_XML_FAILED
e a tupla XMLURL_STATUS, usada como choices= no campo status de
XMLURL em models.py.
…score percentual

Propósito: substituir a comparação por soma de pontos fixos
(get_score) por um score percentual comparável entre estratégias de
busca (ids / journal+issue+artigo / journal+artigo), permitindo à
PidProviderXML.get_best_match decidir com um único limiar (0.6) em
vez de múltiplos pesos ad-hoc. Também centraliza a validação dos
dados mínimos de entrada (validate_input_data), antes espalhada nas
properties issn_query/article_data_query.

Técnico:
- Adiciona compare()/compare_items()/compare_lists() em substituição
  a get_score(); calcula percentual_score = total_score / nº de itens
  comparados.
- QueryBuilderPidProviderXML passa a centralizar os dados via
  adapter_data (xml_adapter.data), compare_data
  (get_data_to_compare()) e xml_with_pre_data
  (get_article_data(300)), eliminando as ~20 cached_property
  individuais que replicavam atributos do xml_adapter.
- Adiciona validate_input_data(), pkg_name_list e
  article_location_params como properties dedicadas.
- article_data_query e issue_params passam a ler de adapter_data em
  vez de cached_property próprias.
…ion) e corrige bugs de matching/persistência

Propósito: (a) auditar todo o fluxo de PidProviderXML.register(),
inclusive quando não há PidProviderXML resultante (unmatched/erro/
conflito), coisa que antes só ia para UnexpectedEvent sem
padronização por status; (b) alinhar o matching de XML ao novo
QueryBuilderPidProviderXML/compare(); (c) corrigir dois bugs de
persistência encontrados na revisão.

Técnico:
- Novo modelo PidProviderXMLRegistration: FK pid_provider_xml
  nullable (SET_NULL) pois unmatched/bad_request/error podem não ter
  registro; event_status fechado (created/updated/skipped/forbidden/
  conflict/unmatched/bad_request/error); LIGHTWEIGHT_STATUSES evita
  gravar detail pesado nos casos comuns (created/updated/skip_update).
- register() reescrito: usa select_records()/select_record() (novos,
  baseados em compare()) no lugar de get_records()/get_record();
  passa a chamar PidProviderXMLRegistration.record(...) no finally.
- is_updated() vira staticmethod e levanta SkipSavePidProviderXML em
  vez de retornar registered.data.
- get_best_match() substitui best_matches(): usa compare() com
  percentual_score > 0.6 como corte, no lugar da soma de pontos fixos
  de match()/title_similarity() (removidos).
- Novo campo readable_data (JSONField) e property data_to_compare,
  evitando reparsing do XML a cada comparação.
- add_collections() extraído de _save(), com fallback de issn_path
  via FieldError (compatibilidade entre app core e app upload, que
  usam relations diferentes: scielojournal vs journalproc).
- Novo PidProviderXMLManager com select_related('current_version')
  automático em PidProviderXML.objects.
- Correção de bug: fix_duplicated_pkg_name usava
  other_pid.current_version (campo inexistente em OtherPid,
  causaria AttributeError) — corrigido para other_pid.version.
- Correção de bug: mark_items_as_invalid calculava 'invalid' mas
  nunca persistia; agora acumula items_to_update e salva via
  bulk_update(['proc_status']).
- XMLURL ganha campos detail (JSONField) e is_public, status com
  choices=XMLURL_STATUS, e o método record() centralizando a
  gravação de sucesso/falha (fetch e pid_provider_xml_failed).
Propósito: eliminar a duplicação de lógica de log entre
_handle_xml_fetch_failure, _handle_pid_provider_failure e
_register_success, delegando tudo para o novo XMLURL.record(), que
já resolve status, pid, detail e salvamento do zipfile num só lugar.

Técnico:
- Remove _handle_xml_fetch_failure, _handle_pid_provider_failure,
  _register_success e o helper _truncate_traceback (não é mais
  necessário truncar manualmente; detail agora é JSONField).
- provide_pid_for_xml_uri passa a chamar XMLURL.record(...)
  diretamente nos três casos (fetch falhou / pid_provider falhou /
  sucesso), repassando document_item (novo parâmetro).
- _handle_unexpected_error passa document_item adiante para o
  self.provide_pid_for_xml_uri(...) do detail de retry.
…n no admin

Propósito: dar visibilidade no Wagtail admin aos dois novos pontos de
auditoria (falhas de fetch/registro por URL, e eventos de registro
de PID por documento), que antes só existiam como dados sem tela.

Técnico:
- Novo XMLURLViewSet: list_display (url, status, pid), list_filter
  por status e is_public, search_fields (url, status, pid).
- Novo PidProviderXMLRegistrationViewSet: list_display (pkg_name,
  event_status, pid_provider_xml, created), list_filter
  (event_status, created), search_fields incluindo
  pid_provider_xml__v2/v3, list_per_page=50 e select_related
  ('pid_provider_xml') em get_queryset (volume alto esperado).
- Ambos adicionados a PidProviderViewSetGroup.items.
Propósito: Atualizar a versão do pacote packtools para incorporar
correções e o novo recurso de nomeação de pacotes (sps_pkg_name)
utilizado no registro de XMLURL.

Solução técnica: Atualizado o pin de packtools de 4.15.0 para 4.16.8
em requirements/base.txt.
Propósito: Permitir distinguir falhas inesperadas de falhas de
obtenção do XML e de falhas do pid provider, dando mais precisão
ao diagnóstico de registros de XMLURL.

Solução técnica: Adicionada a constante XMLURL_STATUS_UNEXPECTED_FAILURE
e sua entrada correspondente na tupla XMLURL_STATUS.
Propósito: Simplificar e tornar mais robusto o registro de tentativas
de obtenção/registro de XML, removendo duplicação entre o campo
'exceptions' e o campo 'detail', e centralizando a decisão de status.

Solução técnica:
- Removido o campo 'exceptions' do modelo (informação passa a ficar
  em 'detail').
- Reescrito o método 'record' para receber traceback_msg, is_public
  e params explicitamente.
- Adicionado o método estático 'get_status', que determina o status
  (xml_fetch_failed, unexpected_failure, pid_provider_xml_failed ou
  success) a partir de xml_with_pre e response.
- Adicionada a property 'data', que expõe de forma unificada o
  resultado (response ou exception + traceback).
- Ajustado o nome do arquivo salvo para usar xml_with_pre.sps_pkg_name.
Propósito: Garantir que toda tentativa de obtenção/registro de XML
(sucesso, falha de obtenção ou erro inesperado) passe por um único
ponto de registro, e corrigir perda de erro em generator.

Solução técnica:
- Unificado o fluxo de try/except em provide_pid_for_xml_uri,
  removendo o método auxiliar _handle_unexpected_error e chamando
  XMLURL.record tanto no caminho de sucesso quanto no de exceção.
- Tratado o caso em que XMLWithPre.create não retorna nenhum item,
  retornando dict de erro explícito (EmptyXMLWithPreError).
- Corrigido 'return' para 'yield' no bloco de exceção de verificação
  de XML em ZIP, evitando que o erro fosse descartado dentro do
  generator.
- Adicionados imports de traceback e do módulo choices.
Propósito: Aplicar no banco de dados as mudanças de modelo introduzidas
em XMLURL: remoção do campo 'exceptions' (substituído por 'detail') e
inclusão da nova opção 'unexpected_failure' no campo 'status'.

Solução técnica: Gerada migration 0018 com as operações RemoveField
(exceptions) e AlterField (status, incluindo XMLURL_STATUS_UNEXPECTED_FAILURE
nas choices).
Propósito: Criar o pacote pid_provider/tests como ponto de entrada
para a suíte de testes unitários do app pid_provider.

Solução técnica: Adicionado arquivo __init__.py vazio.
Propósito: Cobrir o comportamento de XMLURL após a refatoração de
record()/get_status()/data, incluindo os três cenários documentados
(falha ao obter XML, sucesso completo, erro inesperado) e a
idempotência via create_or_update.

Solução técnica: Adicionados testes para get()/create()/create_or_update(),
save_file() (incluindo caminho de exceção), get_status() (staticmethod,
os 5 ramos de status) e a property data (resposta, exceção+traceback,
e caso vazio).
Propósito: Cobrir a construção de queries (identifier_queries, issn_query,
issue_params, article_location_params, article_data_query) e as funções
de similaridade (compare, compare_items, compare_lists, get_score,
zero_to_none) usadas na busca por candidatos de PID.

Solução técnica: Adicionados testes unitários com mocks de xml_adapter
e patch de how_similar, cobrindo validação de dados de entrada
(validate_input_data), montagem de pkg_name_list e agregação de scores
em compare().
Propósito: Cobrir a seleção do melhor candidato dentre múltiplos
registros aprovados (score > 0.6), incluindo os casos de 0, 1, 2 e 3+
candidatos aprovados e o desempate por data de atualização.

Solução técnica: Adicionados testes com mock de compare, verificando
que 'matched' só aparece com 2+ aprovados, que 'registered' recebe
sempre o de maior score, e que matched exclui o item já promovido
a registered (matched[1:]).
Propósito: Cobrir a lógica de decisão de select_record (singular),
que consome o generator select_records e resolve o melhor match por
label, incluindo os bugs corrigidos de fatiamento duplicado de
matched_items e de auto-referência em unmatched_items.

Solução técnica: Adicionados testes com get_best_match mockado,
cobrindo listas vazias (puladas via 'if not results'), retorno
antecipado no primeiro label com 'registered', acumulação de
unmatched_items reais entre labels, e chamada correta de
get_best_match com candidatos e data_to_compare.
Propósito: Cobrir o novo comportamento de select_records como generator
lazy que apenas monta e materializa (list()) os candidatos por
estratégia de busca (ids, journal-issue-article, journal-article),
sem mais orquestrar best_matches ou levantar DoesNotExist.

Solução técnica: Adicionados testes com banco real (TestCase) e
QueryBuilderPidProviderXML mockado, verificando a ordem dos labels
yield-ados, que cada branch retorna list (não QuerySet) e a
laziness do generator (nada executa até o primeiro next()).
Propósito: Cobrir a nova orquestração de register(), incluindo a
mudança de is_updated (agora levanta exceções em vez de retornar
dict), a correção do tratamento de ForbiddenPidProviderXMLRegistrationError
(antes tratado incorretamente como bad_request) e a regra de quando
a auditoria (PidProviderXMLRegistration.record) deve ou não ser gravada.

Solução técnica: Adicionados testes cobrindo os caminhos created,
updated, skipped 'limpos' (sem auditoria), os mesmos caminhos com
matched_items (com auditoria), conflict/forbidden/unmatched/bad_request
(sempre com auditoria), o fallback event_status='error' e um conjunto
de testes de invariante garantindo que record() nunca é chamado mais
de uma vez por execução.
Propósito: Cobrir provide_pid_for_xml_with_pre, provide_pid_for_xml_zip,
provide_pid_for_xml_uri, is_registered_xml_with_pre/uri/zip, incluindo
os bugs corrigidos de generator que retornava None silenciosamente
(EmptyXMLWithPreError) e de 'return' vs 'yield' em is_registered_xml_zip.

Solução técnica: Adicionados testes unitários com mocks de
PidProviderXML/XMLURL/XMLWithPre/UnexpectedEvent, cobrindo o fluxo
unificado try/except de provide_pid_for_xml_uri (obtenção do XML e
registro no mesmo bloco), o repasse correto de argumentos em zip, a
preservação de resultados parciais quando um generator falha no meio,
e a confirmação de que 'except Exception as exception:' faz o binding
correto da exceção capturada e a repassa para XMLURL.record.
Propósito: Corrigir incompatibilidade de assinatura do método get_queryset, que não recebia o parâmetro request exigido pela API do Wagtail (ModelViewSet), causando potencial erro em tempo de execução ao construir a listagem.

Solução técnica: Adiciona o parâmetro request à assinatura de get_queryset e repassa-o para super().get_queryset(request) (CommonControlFieldViewSet). Inclui fallback defensivo para self.model._default_manager.all() caso a superclasse retorne None, evitando AttributeError ao encadear select_related('pid_provider_xml').
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