Skip to content

Issue#3828 Refatorar Mesa Diretora - #3829

Open
LeandroJatai wants to merge 28 commits into
3.1.xfrom
3828_refatorar_mesa_diretora
Open

Issue#3828 Refatorar Mesa Diretora#3829
LeandroJatai wants to merge 28 commits into
3.1.xfrom
3828_refatorar_mesa_diretora

Conversation

@LeandroJatai

@LeandroJatai LeandroJatai commented Apr 14, 2026

Copy link
Copy Markdown
Member

Refatora Mesa Diretora para utilizar o Crud de forma simplifica da criação e edição de mesas e composições

Descrição

  • Foi criado dois CRUDs para MesaDiretora e ComposicaoMesa, além dos forms correspondentes para criação e edição com regras de negócio ligadas aos dois modelos.
  • A MesaDiretora foi desligada de SessãoLegislativa e ligada à Legislatura. Os migrations cuidam dessa reassociação.
  • A tela de acesso público agora seleciona apenas legislatura, TABs são montados com quantas mesas tiver na legislatura selecionada. A navegação dos TABs é transformada em dropdown em telas menores
  • Após o merge do PR um commit deve ser feito após a execução de npm run build

Issue Relacionada

#3828
Nesta issue estão os requisitos funcionais e não funcionais, além da citação de várias outras issues que envolvem o tema

Motivação e Contexto

  • Mesas Diretoras nunca tiveram ligação conceitual com sessão legislativa. Sessão Legislativa possui uma definição e existência específica: inicia-se em meados de fevereiro e termina-se em meados de dezembro e diz sobre o intervalo onde haverá sessões plenárias.

  • Mesa diretora não: Mesa diretora, salvo raríssimas exceções, iniciam-se em 1º de janeiro e conclui-se com um ou dois anos. Dentro das exceções, estão a finalização inesperada de uma mesa e inicio de outra, seja por cassação de algum parlamentar, ou outro motivo qualquer que interrompa uma mesa e inicia-se outra.

  • Regras de não interseção entre mesas; de contenção em legislatura; de cargo único (que já existia); de só parlamentares da legislatura; de não duplicidade de parlamentar foram colocadas nos forms.

Como Isso Foi Testado?

foi criado o sapl/parlamentares/tests/test_mesadiretora.py que testa os forms

Capturas de Tela (se apropriado):

Tela Pública com 3 Mesas na mesma Legislatura. Nota-se os TABs com as três mesas, onde o Biênio de 23/24 possui duas mesas com datas de inicio e encerramento.
image

Tela de listagem do CRUDs tradicional para usuário que possui permissão de edição de mesa:
image
image

Tela de listagem das composições de uma mesa
image

Tipos de Mudanças

  • Bug fix (alteração que corrige uma issue e não altera funcionalidades já existentes)
  • Nova feature (alteração que adiciona uma funcionalidade e não altera funcionalidades já existentes)
  • Alteração disruptiva (Breaking change) (Correção ou funcionalidade que causa alteração nas funcionalidades existentes)

Checklist:

  • Eu li o documento de Contribuição (CONTRIBUTING).
  • Meu código segue o estilo de código deste projeto.
  • Minha alteração requer uma alteração na documentação. (Apostila das Oficinas)
  • Eu atualizei a documentação de acordo.
  • Eu adicionei testes para cobrir minhas mudanças.
  • Todos os testes novos e existentes passaram.

@edwardoliveira edwardoliveira left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A motivação conceitual do PR é correta (Mesa Diretora pertence à Legislatura, não à
SessaoLegislativa). Isso é uma das muitas "heranças malditas" da modelagem de dados (mal feita!) do SAPL 2.5 e anteriores. A simplificação do AJAX/jQuery para CRUDs padrão é bem vinda. Os problemas abaixo precisam ser resolvidos antes do merge.

Comment thread sapl/parlamentares/migrations/0048_auto_20260413_1049.py Outdated
Comment thread sapl/parlamentares/migrations/0048_auto_20260413_1049.py Outdated
Comment thread sapl/parlamentares/migrations/0048_auto_20260413_1049.py Outdated
Comment thread sapl/parlamentares/forms.py Outdated
Comment thread sapl/parlamentares/models.py Outdated
Comment thread sapl/templates/parlamentares/mesadiretora_filter.html Outdated
Comment thread sapl/templates/crud/list.html Outdated
Comment thread sapl/parlamentares/views.py Outdated
Comment thread sapl/parlamentares/forms.py Outdated
Comment thread sapl/templates/parlamentares/mesadiretora_filter.html
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 17, 2026
LeandroJatai added a commit that referenced this pull request Apr 18, 2026
LeandroJatai added a commit that referenced this pull request Apr 18, 2026
LeandroJatai added a commit that referenced this pull request Apr 18, 2026
@LeandroJatai
LeandroJatai requested review from edwardoliveira and removed request for joaohortsenado April 18, 2026 01:32
Comment thread sapl/parlamentares/urls.py
Comment thread sapl/parlamentares/migrations/0049_auto_20260417_1917.py
def __init__(self, *args, **kwargs):
super(ComposicaoMesaForm, self).__init__(*args, **kwargs)
self.instance.mesa_diretora = self.initial.get('mesa_diretora')
self.fields['parlamentar'].queryset = self.fields['parlamentar'].queryset.filter(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ComposicaoMesaForm.__init__ quebra silenciosamente sem initial

Se o form for instanciado sem initial['mesa_diretora'], self.initial.get(...) retorna None e a próxima linha lança AttributeError: 'NoneType' object has no attribute 'legislatura'.

Hoje as views CRUD sempre passam initial, então não dispara em produção, mas qualquer caller futuro (admin, comando, outra view) quebra silenciosamente. Sugestão de guarda:

mesa = self.initial.get('mesa_diretora')
if mesa is not None:
    self.instance.mesa_diretora = mesa
    self.fields['parlamentar'].queryset = (
        self.fields['parlamentar'].queryset
        .filter(mandato__legislatura=mesa.legislatura)
    )

ultima_filiacao = self.filiacao_set.order_by('-data').first()
# este método conta com a ordenação default do model Filiacao para trazer a última filiação primeiro
# se order_by for adicionado aqui, o prefetch_related que inclui filiacao_set não irá pré-carregar como esperado
ultima_filiacao = self.filiacao_set.first()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

filiacao_atual agora depende da ordenação default — vale um teste regressivo

A troca por self.filiacao_set.first() é correta porque Filiacao.Meta.ordering = ('parlamentar', '-data', '-data_desfiliacao'), e o prefetch_related adicionado na ListView depende disso para não disparar query extra. O comentário deixa o motivo claro — ótimo.

Sugestão: adicionar um teste curto que verifique o invariante (criar duas filiações em datas diferentes e checar parlamentar.filiacao_atual). Se um dia alguém mexer no Meta.ordering, o teste pega.


def clean(self):
if self.data_inicio and self.data_fim:
if self.data_inicio >= self.data_fim:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Validação >= rejeita mesas de um único dia

Há cenários reais de mesa que dura um único dia (dissolução e recriação no mesmo dia, por cassação ou eventos extraordinários — exatamente o tipo de exceção citado na descrição do PR).

Considerar > em vez de >=, esse requisito precisa ser refinado... o que acha @edwardoliveira


def get_filterset_kwargs(self, filterset_class):
fk = super().get_filterset_kwargs(filterset_class)
if 'legislatura' not in self.request.GET and not 'mesa' in self.request.GET:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: not 'mesa' in self.request.GET'mesa' not in self.request.GET (PEP 8). Mesma observação na linha de baixo.

return _('Mesa da %(sessao)s sessao da %(legislatura)s Legislatura') % {
'sessao': self.sessao_legislativa, 'legislatura': self.sessao_legislativa.legislatura
}
return self.titulo or _('%(legislatura)s - %(data_inicio)s a %(data_fim)s') % {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datas em __str__ saem em ISO (2021-01-01) em vez do formato pt-br. Como titulo agora é o caminho principal, é raro cair no fallback, mas vale usar django.utils.formats.date_format(d, "SHORT_DATE_FORMAT") para consistência com o resto da aplicação.

{% endblock actions_search %}

{% block container_table_list %}
{% if perms.parlamentares.add_mesadiretora %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

o template usa add_mesadiretora para alternar entre listagem CRUD (admin) e visualização em abas (público). Funcionalmente está consistente com o resto do SAPL. Só ponderando se change_mesadiretora não seria semanticamente mais correto para representar "tem painel de gestão".

{{composicao.parlamentar.nome_parlamentar}}</a>
</div>
</td>
<td>{{composicao.parlamentar.filiacao_atual}}</td>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Partido pode divergir do partido da época

A view antiga (partido_parlamentar_sessao_legislativa) calculava a filiação na data da sessão. Esta agora mostra sempre a filiação mais recente. Para mesas atuais é a mesma coisa; para mesas históricas pode mostrar partido errado (ex.: parlamentar que mudou de partido depois).

É bom validar esse requisito também, @edwardoliveira . Se não for o caso, vale reintroduzir a lógica por data (ex.: método filiacao_em(data) no model).

'cargo': new_cargo.id,
})

composicao.refresh_from_db()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a cobertura dos forms está boa. mas acho que tem alguns pontos faltantes:

  1. MesaDiretoraFilterSet: testar que ?mesa=<id> resolve para a legislatura correta e que sem GET cai na legislatura atual; testar 404 para mesa inexistente.
  2. View pública (mesadiretora_filter.html): renderizar a página sem permissão de admin e validar as abas.
  3. test_composicaomesa_form_view_create/update: não fazem assert de status HTTP — adicionar pelo menos assert response.status_code in (200, 302) para detectar erros de form silenciosos.
  4. Variáveis mandato/mandato1/mandato2 criadas mas nunca lidas — substituir por _ = baker.make(...) para deixar a intenção clara.

NIT: faltou newline final no arquivo. (conferir isso nos demais arquivos)

@LeandroJatai
LeandroJatai force-pushed the 3828_refatorar_mesa_diretora branch from 1b144e4 to 5af98ee Compare May 30, 2026 17:30

@joaohortsenado joaohortsenado left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Testando com uma base real de produção, encontrei mais alguns pontos que precisam de atenção, @LeandroJatai @edwardoliveira

Comment on lines +530 to +534
if MesaDiretora.objects.filter(
legislatura=self.legislatura,
data_inicio__lte=self.data_fim,
data_fim__gte=self.data_inicio
).exclude(pk=self.pk).exists():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A checagem de interseção usa intervalo fechado e rejeita mesas que apenas encostam

No QA contra uma base real de produção, rodei full_clean() nas 64 mesas migradas. Duas delas caem aqui:

[id=36] Mesa Diretora Biênio 1971/1972 (1971-02-03 a 1972-01-31, leg=4)
    -> As datas da mesa diretora se sobrepõem com outra mesa diretora existente.
[id=37] Mesa Diretora Biênio 1972/1973 (1972-01-31 a 1973-01-30, leg=4)
    -> As datas da mesa diretora se sobrepõem com outra mesa diretora existente.

O único dia em comum é 1972-01-31: é o data_fim de uma e o data_inicio da seguinte. Como o filtro usa data_inicio__lte / data_fim__gte, esse encosto conta como sobreposição.

Isso não é um caso raro da base — é o padrão de casas cujas sessões legislativas iam de 31 de janeiro a 30 de janeiro do ano seguinte, e a 0046 herda essas datas fielmente (correto).

Consequência prática: essas mesas ficam gravadas num estado que o model rejeita. Quem abrir uma delas no CRUD e salvar — mesmo mudando só a descrição — leva erro de validação e não consegue gravar.

Conceitualmente, uma mesa que termina no dia X e outra que começa no dia X não se sobrepõem. Sugiro intervalo semiaberto:

if MesaDiretora.objects.filter(
    legislatura=self.legislatura,
    data_inicio__lt=self.data_fim,
    data_fim__gt=self.data_inicio
).exclude(pk=self.pk).exists():

Isso resolve os dois registros acima sozinho. Vale notar que essa mudança conversa com a pergunta que deixei em r3182291982 sobre mesa de um único dia — as duas regras tratam da mesma ambiguidade de borda, e provavelmente deveriam ser decididas juntas.

Comment on lines +8 to +18
schema_editor.execute("""
UPDATE parlamentares_mesadiretora md
SET
legislatura_id = sl.legislatura_id,
data_inicio = sl.data_inicio,
data_fim = sl.data_fim
FROM
parlamentares_sessaolegislativa sl
WHERE
sl.id = md.sessao_legislativa_id
""")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A migração pode gravar mesas fora do período da própria legislatura

O SQL está correto e rápido (as 59 migrations rodaram em 12,8s numa base real — o ponto do @edwardoliveira em r3093425814 ficou bem resolvido). O problema é que a SessaoLegislativa de origem nem sempre respeita o intervalo da Legislatura, e a nova regra de contenção introduzida no clean() passa a rejeitar o resultado.

Testando contra um dump de produção real, 2 das 64 mesas ficaram assim:

[id=10] Mesa Diretora 1959/1959  mesa: 1959-06-06 a 1959-12-31 | legislatura: 1959-06-06 a 1959-12-30
[id=21] Mesa Diretora 1977/1977  mesa: 1977-01-01 a 1977-12-31 | legislatura: 1977-01-31 a 1983-01-30

Somadas às 2 de borda coincidente que comentei em models.py, são 4 de 64 mesas (6%) inválidas logo após o upgrade. A migração em si não quebra — o efeito aparece depois, quando alguém tenta editar.

Não acho que a migration deva "consertar" datas silenciosamente: essa informação foi perdida pela modelagem anterior e qualquer chute pode piorar. Duas alternativas que me parecem melhores:

  1. Um management command de diagnóstico (algo como ./manage.py verifica_mesas_diretoras) que lista os registros que não passam no full_clean(), para a casa ajustar manualmente — junto de uma nota na documentação de upgrade.
  2. Ou, no mínimo, uma RunPython adicional que apenas registra em log quais mesas ficaram inconsistentes, para não ser uma descoberta silenciosa em produção.

(Registrando também que a preocupação que eu tinha levantado sobre IntegrityError na 0049 não se confirmou: sessao_legislativa era FK NOT NULL e as datas da sessão também, então o UPDATE sempre popula. Já resolvi aquela thread.)

schema_editor.execute("""
UPDATE parlamentares_mesadiretora
SET titulo = 'Mesa Diretora' ||
CASE WHEN EXTRACT(YEAR FROM data_fim)::integer - EXTRACT(YEAR FROM data_inicio)::integer = 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rótulo "Biênio" aplicado a mesas de aproximadamente um ano

A heurística compara os anos das datas, então marca como biênio qualquer mesa que apenas atravesse o Réveillon. Numa base real de produção, 12 dos 64 títulos gerados saíram assim:

Mesa Diretora Biênio 1973/1974 -> 1973-01-31 a 1974-01-30 (364 dias)
Mesa Diretora Biênio 1971/1972 -> 1971-02-03 a 1972-01-31 (362 dias)
Mesa Diretora Biênio 1969/1970 -> 1969-01-31 a 1970-01-30 (364 dias)
Mesa Diretora Biênio 1982/1983 -> 1982-01-01 a 1983-01-30 (394 dias)

Como o titulo agora é o rótulo principal na tela pública, é texto factualmente errado visível ao cidadão — e, uma vez gravado, só sai com edição manual mesa a mesa.

Comparar a duração real resolve:

SET titulo = 'Mesa Diretora' ||
    CASE WHEN data_fim - data_inicio > 400
         THEN ' Biênio'
         ELSE ''
    END || ' ' || ...

(400 dias em vez de 365 para dar folga a mesas de ~13 meses, que pelo visto são comuns em bases antigas.)

<script>
(function () {
// Suporte nativo a :has() — o CSS cuida de tudo, JS não é necessário
if (typeof CSS !== 'undefined' && CSS.supports && CSS.supports('selector(:has(*))')) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Este early-return torna o comportamento mobile 100% dependente do build

E acho que ainda precisa compilar a parada...

@joaohortsenado

joaohortsenado commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@LeandroJatai, obrigado pelo trabalho de fechar as threads do @edwardoliveira — conferi uma a uma no código e todas foram de fato atendidas. 👏

Acabei de subir uma review separada com os achados de um QA contra base real de produção — aquilo é assunto novo. Este comentário aqui é sobre outra coisa: ficaram 9 pontos da minha review de 04/05 sem retorno. Reconfirmei todos agora no HEAD atual (95df2066) e todos ainda se aplicam.

Agrupei por tipo de decisão para facilitar:


🔴 Precisam de decisão de requisito (acho que travam o resto)

Essas duas são perguntas de regra de negócio, não de código — vale o @edwardoliveira opinar:

1. Mesa de um único dia é cenário válido? (r3182291982)

models.py:521 usa if self.data_inicio >= self.data_fim, então uma mesa que começa e termina no mesmo dia é rejeitada:

>>> MesaDiretoraForm(data={'data_inicio': '15/06/2021', 'data_fim': '15/06/2021', ...}).errors
{'__all__': ['A data de início deve ser anterior à data de fim.']}

A própria descrição do PR cita dissolução por cassação como exceção real. Se mesa de um dia existe, o certo é >; se não existe, tudo bem manter — só queria confirmar que é decisão consciente.

Complemento: no QA contra base real, essa mesma família de regra gerou 4 mesas migradas que o clean() rejeita, duas delas por bordas coincidentes (fim de uma == início da seguinte). Detalhei em r3715887423 — pode ser que a resposta aqui resolva as duas coisas de uma vez, se a regra passar a usar intervalo semiaberto.

2. Partido exibido deve ser o da época da mesa ou o atual? (r3182292015)

mesadiretora_filter.html:63 usa {{ composicao.parlamentar.filiacao_atual }}. A view antiga (partido_parlamentar_sessao_legislativa) calculava a filiação na data da sessão. Para mesas atuais dá no mesmo; para mesas históricas pode mostrar partido errado — parlamentar que trocou de partido depois aparece com a sigla nova numa mesa de 1995.

Se o requisito for mostrar o partido da época, precisaria de algo como um filiacao_em(data) no model.


🟠 Correções de código

3. ComposicaoMesaForm.__init__ quebra sem initial (r3182291971)

Esse é o único que considero bug de fato. Reproduzido agora:

>>> ComposicaoMesaForm()
AttributeError: 'NoneType' object has no attribute 'legislatura'

Hoje não dispara em produção porque as views CRUD sempre passam initial, mas qualquer caller futuro (admin, management command, outra view, serializer) quebra. A guarda que sugeri na thread resolve em 3 linhas.

4. Import não usado (r3182291993)

forms.py:4flake8 acusa:

sapl/parlamentares/forms.py:4:1: F401 'crispy_forms.layout.Field' imported but unused

5. PEP 8 em views.py:1033 (r3182291998)

if 'legislatura' not in self.request.GET and not 'mesa' in self.request.GET:

'mesa' not in self.request.GET. (A linha de baixo já está na forma correta.)

6. Datas em ISO no __str__ (r3182292004)

>>> str(MesaDiretora(titulo='', data_inicio=date(2021,1,1), data_fim=date(2021,12,31), legislatura=leg))
'16ª (2021 - 2024)  - 2021-01-01 a 2021-12-31'

Como o titulo agora é o caminho principal, é raro cair no fallback — mas quando cai sai fora do padrão pt-br do resto da aplicação. django.utils.formats.date_format(d, "SHORT_DATE_FORMAT") resolve.


🟡 Semântica de permissão

7. add_mesadiretora como chave do modo admin (r3182292011)

mesadiretora_filter.html:13 usa add_mesadiretora para alternar entre listagem CRUD e visualização em abas. Funcionalmente está consistente com o resto do SAPL e não estou pedindo mudança — só queria seu parecer se change_mesadiretora não representaria melhor "tem painel de gestão", já que o que a condição decide é ver o painel, não criar.


🟢 Cobertura de testes

8. Teste do invariante de filiacao_atual (r3182291977)

A troca por self.filiacao_set.first() está correta e o comentário no código explica bem o motivo. Validei contra base real: 40 parlamentares com mais de uma filiação, zero divergências. Mas o comportamento agora depende de Filiacao.Meta.ordering — um teste curto (duas filiações em datas diferentes, checar filiacao_atual) protegeria contra alguém mexer no Meta depois.

9. Lacunas em test_mesadiretora.py (r3182292018)

A cobertura dos forms está boa. Faltam:

  • MesaDiretoraFilterSet: que ?mesa=<id> resolve para a legislatura correta, que sem GET cai na legislatura atual, e 404 para mesa inexistente. (Validei os três manualmente e funcionam — só não há teste.)
  • View pública: renderizar sem permissão de admin e conferir as abas.
  • test_composicaomesa_form_view_create / _update: sem assert de status HTTP, então erro silencioso de form passaria batido.
  • Variáveis criadas e nunca lidas + newline final. flake8 no arquivo:
test_mesadiretora.py:283:5: F841 local variable 'mandato' is assigned to but never used
test_mesadiretora.py:303:5: F841 local variable 'mandato' is assigned to but never used
test_mesadiretora.py:335:5: F841 local variable 'mandato1' is assigned to but never used
test_mesadiretora.py:336:5: F841 local variable 'mandato2' is assigned to but never used
test_mesadiretora.py:365:5: F841 local variable 'mandato' is assigned to but never used
test_mesadiretora.py:370:5: F841 local variable 'response' is assigned to but never used
test_mesadiretora.py:382:5: F841 local variable 'mandato' is assigned to but never used
test_mesadiretora.py:395:5: F841 local variable 'response' is assigned to but never used
test_mesadiretora.py:401:41: W292 no newline at end of file

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.

3 participants