diff --git a/frontend/src/__global/scss/layouts/_globals.scss b/frontend/src/__global/scss/layouts/_globals.scss index 3cb9c6f82..86515b3be 100644 --- a/frontend/src/__global/scss/layouts/_globals.scss +++ b/frontend/src/__global/scss/layouts/_globals.scss @@ -183,6 +183,7 @@ small { hyphens: auto;*/ } + @media print { a[href]:after { content: none !important; diff --git a/frontend/src/__global/scss/libs/bootstrap/_nav_tabs.scss b/frontend/src/__global/scss/libs/bootstrap/_nav_tabs.scss new file mode 100644 index 000000000..fc0823637 --- /dev/null +++ b/frontend/src/__global/scss/libs/bootstrap/_nav_tabs.scss @@ -0,0 +1,89 @@ +@import "~bootstrap/scss/variables"; + +// Estilização do tab-content conectado ao nav-tabs (substitui inline style) +.nav-tabs + .tab-content { + border: 1px solid $nav-tabs-border-color; + border-top: 0; + border-radius: 0 0 $border-radius $border-radius; +} + +@media (max-width: 992px) { + .nav-tabs { + position: relative; + flex-direction: column; + border: 1px solid $nav-tabs-border-color; + border-radius: $border-radius; // Totalmente arredondado — parece um botão/select + background-color: $white; + overflow: hidden; // Recorta filhos nas bordas arredondadas + + // Seta indicando dropdown + &::after { + content: "▾"; + position: absolute; + right: 0.75rem; + top: 0.6rem; + font-size: 1rem; + color: $secondary; + pointer-events: none; + transition: transform 0.2s ease; + } + + // Oculta todos os itens por padrão + .nav-item { + display: none; + width: 100%; + + .nav-link { + border: none; + border-bottom: 1px solid $nav-tabs-border-color; + border-radius: 0; + width: 100%; + text-align: left; + padding-right: 2rem; + margin-bottom: 0; + + &.active { + background-color: $nav-tabs-link-active-bg; + color: $nav-tabs-link-active-color; + border-color: transparent; + } + + &:hover:not(.active) { + background-color: $light; + } + } + + &:last-child .nav-link { + border-bottom: none; + } + } + + // CSS nativo: exibe somente o item ativo (browsers com suporte a :has) + .nav-item:has(.nav-link.active) { + display: block; + } + + // Estado expandido: via :focus-within (nativo) ou .nav-tabs--open (fallback JS) + &:focus-within, + &.nav-tabs--open { + border-radius: $border-radius $border-radius 0 0; // Arredonda apenas topo quando aberto + overflow: visible; + z-index: $zindex-dropdown; + + &::after { + transform: rotate(180deg); + } + + .nav-item { + display: block; + } + } + } + + // Em mobile, tab-content é visualmente independente do nav-tabs (que vira um select) + .nav-tabs + .tab-content { + border-top: 1px solid $nav-tabs-border-color; + border-radius: $border-radius; // Rounding completo — desconectado do nav-tabs + margin-top: 0.5rem; + } +} diff --git a/frontend/src/__global/scss/libs/libs.scss b/frontend/src/__global/scss/libs/libs.scss index 76a4e7c73..4f3233661 100644 --- a/frontend/src/__global/scss/libs/libs.scss +++ b/frontend/src/__global/scss/libs/libs.scss @@ -1,2 +1,3 @@ @import "./bootstrap/nav_navbar"; +@import "./bootstrap/nav_tabs"; @import "./bootstrap/table"; diff --git a/sapl/parlamentares/forms.py b/sapl/parlamentares/forms.py index 89ab424f5..7acb4a984 100755 --- a/sapl/parlamentares/forms.py +++ b/sapl/parlamentares/forms.py @@ -1,7 +1,7 @@ from datetime import timedelta import logging -from crispy_forms.layout import Fieldset, Layout +from crispy_forms.layout import Fieldset, Layout, Field from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, User @@ -19,10 +19,10 @@ from sapl.base.models import Autor, TipoAutor from sapl.crispy_layout_mixin import SaplFormHelper from sapl.crispy_layout_mixin import form_actions, to_row from sapl.rules import SAPL_GROUP_VOTANTE -from sapl.utils import FileFieldCheckMixin +from sapl.utils import FileFieldCheckMixin, SelectSubmitChangeWidget -from .models import (Coligacao, ComposicaoColigacao, Filiacao, Frente, Legislatura, - Mandato, Parlamentar, Partido, Votante, Bloco, FrenteParlamentar, BlocoMembro) +from .models import (Coligacao, ComposicaoColigacao, ComposicaoMesa, Filiacao, Frente, Legislatura, + Mandato, MesaDiretora, Parlamentar, Partido, Votante, Bloco, FrenteParlamentar, BlocoMembro) class CustomImageCropWidget(ImageCropWidget): @@ -752,3 +752,49 @@ class BlocoMembroForm(ModelForm): _("Parlamentar já é membro do bloco parlamentar.")) return cd + +class MesaDiretoraFilterSet(django_filters.FilterSet): + + legislatura = django_filters.ModelChoiceFilter( + label='', + queryset=Legislatura.objects.all(), + widget=SelectSubmitChangeWidget) + + class Meta: + model = MesaDiretora + fields = ['legislatura'] + + def __init__(self, *args, **kwargs): + super(MesaDiretoraFilterSet, self).__init__(*args, **kwargs) + + row0 = to_row([('legislatura', 5)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Escolha da Legislatura'), + row0,) + ) + + +class MesaDiretoraForm(ModelForm): + + class Meta: + model = MesaDiretora + fields = '__all__' + + +class ComposicaoMesaForm(ModelForm): + + class Meta: + model = ComposicaoMesa + fields = ( + 'parlamentar', + 'cargo' + ) + + 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( + mandato__legislatura=self.initial.get('mesa_diretora').legislatura) diff --git a/sapl/parlamentares/migrations/0046_mesadiretora_legislatura.py b/sapl/parlamentares/migrations/0046_mesadiretora_legislatura.py new file mode 100644 index 000000000..d7e6d59d1 --- /dev/null +++ b/sapl/parlamentares/migrations/0046_mesadiretora_legislatura.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.28 on 2026-04-13 01:48 + +from django.db import migrations, models +import django.db.models.deletion +from datetime import date + +def add_legislatura_to_mesa_diretora(apps, schema_editor): + 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 + """) + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0045_auto_20251201_1531'), + ] + + operations = [ + migrations.AddField( + model_name='mesadiretora', + name='legislatura', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='parlamentares.Legislatura', verbose_name='Legislatura'), + ), + migrations.RunPython(add_legislatura_to_mesa_diretora), + ] diff --git a/sapl/parlamentares/migrations/0047_auto_20260412_2256.py b/sapl/parlamentares/migrations/0047_auto_20260412_2256.py new file mode 100644 index 000000000..66eae56c1 --- /dev/null +++ b/sapl/parlamentares/migrations/0047_auto_20260412_2256.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.28 on 2026-04-13 01:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0046_mesadiretora_legislatura'), + ] + + operations = [ + migrations.AlterField( + model_name='mesadiretora', + name='legislatura', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parlamentares.Legislatura', verbose_name='Legislatura'), + ), + migrations.RemoveField( + model_name='mesadiretora', + name='sessao_legislativa', + ), + ] diff --git a/sapl/parlamentares/migrations/0048_auto_20260413_1049.py b/sapl/parlamentares/migrations/0048_auto_20260413_1049.py new file mode 100644 index 000000000..91910292c --- /dev/null +++ b/sapl/parlamentares/migrations/0048_auto_20260413_1049.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.28 on 2026-04-13 13:49 + +from django.db import migrations, models +import django.db.models.deletion + +def preencher_titulo_mesa_diretora(apps, schema_editor): + 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 + THEN ' Biênio' + ELSE '' + END || ' ' || + EXTRACT(YEAR FROM data_inicio)::integer::text || '/' || + EXTRACT(YEAR FROM data_fim)::integer::text + WHERE data_inicio IS NOT NULL AND data_fim IS NOT NULL + """) + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0047_auto_20260412_2256'), + ] + + operations = [ + migrations.AlterModelOptions( + name='mesadiretora', + options={'ordering': ('-legislatura', '-data_inicio'), 'verbose_name': 'Mesa Diretora', 'verbose_name_plural': 'Mesas Diretoras'}, + ), + migrations.AddField( + model_name='mesadiretora', + name='titulo', + field=models.CharField(default='', max_length=100, verbose_name='Título da Mesa Diretora'), + ), + migrations.AlterField( + model_name='composicaomesa', + name='mesa_diretora', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='composicaomesa_set', to='parlamentares.MesaDiretora'), + ), + migrations.RunPython(preencher_titulo_mesa_diretora), + ] diff --git a/sapl/parlamentares/migrations/0049_auto_20260417_1917.py b/sapl/parlamentares/migrations/0049_auto_20260417_1917.py new file mode 100644 index 000000000..401e07433 --- /dev/null +++ b/sapl/parlamentares/migrations/0049_auto_20260417_1917.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.28 on 2026-04-17 22:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0048_auto_20260413_1049'), + ] + + operations = [ + migrations.AlterField( + model_name='composicaomesa', + name='cargo', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parlamentares.CargoMesa', verbose_name='Cargo'), + ), + migrations.AlterField( + model_name='composicaomesa', + name='mesa_diretora', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='composicaomesa_set', to='parlamentares.MesaDiretora', verbose_name='Mesa Diretora'), + ), + migrations.AlterField( + model_name='composicaomesa', + name='parlamentar', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parlamentares.Parlamentar', verbose_name='Parlamentar'), + ), + migrations.AlterField( + model_name='mesadiretora', + name='data_fim', + field=models.DateField(verbose_name='Data Fim'), + ), + migrations.AlterField( + model_name='mesadiretora', + name='data_inicio', + field=models.DateField(verbose_name='Data Início'), + ), + ] diff --git a/sapl/parlamentares/migrations/0050_auto_20260417_2235.py b/sapl/parlamentares/migrations/0050_auto_20260417_2235.py new file mode 100644 index 000000000..99ba7bfc9 --- /dev/null +++ b/sapl/parlamentares/migrations/0050_auto_20260417_2235.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.28 on 2026-04-18 01:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0049_auto_20260417_1917'), + ] + + operations = [ + migrations.AlterField( + model_name='mesadiretora', + name='legislatura', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='mesadiretora_set', to='parlamentares.Legislatura', verbose_name='Legislatura'), + ), + ] diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index eecc2f314..0ca2c8356 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -1,4 +1,5 @@ +from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -298,7 +299,9 @@ class Parlamentar(models.Model): @property def filiacao_atual(self): - 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() if ultima_filiacao and not ultima_filiacao.data_desfiliacao: return ultima_filiacao.partido.sigla else: @@ -491,29 +494,54 @@ class CargoMesa(models.Model): class MesaDiretora(models.Model): - data_inicio = models.DateField(verbose_name=_('Data Início'), null=True) - data_fim = models.DateField(verbose_name=_('Data Fim'), null=True) - sessao_legislativa = models.ForeignKey(SessaoLegislativa, - on_delete=models.PROTECT) + titulo = models.CharField(max_length=100, default='', verbose_name=_('Título da Mesa Diretora')) + data_inicio = models.DateField(verbose_name=_('Data Início')) + data_fim = models.DateField(verbose_name=_('Data Fim')) + legislatura = models.ForeignKey(Legislatura, + on_delete=models.PROTECT, + verbose_name=_('Legislatura'), + related_name='mesadiretora_set' + ) descricao = models.TextField(verbose_name=_('Descrição'), blank=True) class Meta: verbose_name = _('Mesa Diretora') verbose_name_plural = _('Mesas Diretoras') - ordering = ('-data_inicio', '-sessao_legislativa') + ordering = ('-legislatura', '-data_inicio') def __str__(self): - 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') % { + 'legislatura': self.legislatura, + 'data_inicio': self.data_inicio, + 'data_fim': self.data_fim + } + + def clean(self): + if self.data_inicio and self.data_fim: + if self.data_inicio >= self.data_fim: + raise ValidationError( + _('A data de início deve ser anterior à data de fim.')) + + if self.legislatura_id: + if self.data_inicio < self.legislatura.data_inicio or self.data_fim > self.legislatura.data_fim: + raise ValidationError( + _('As datas da mesa diretora devem estar dentro do período da legislatura.')) + + if MesaDiretora.objects.filter( + legislatura=self.legislatura, + data_inicio__lte=self.data_fim, + data_fim__gte=self.data_inicio + ).exclude(pk=self.pk).exists(): + raise ValidationError( + _('As datas da mesa diretora se sobrepõem com outra mesa diretora existente.')) class ComposicaoMesa(models.Model): - # TODO M2M ???? Ternary????? - parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT) - cargo = models.ForeignKey(CargoMesa, on_delete=models.PROTECT) + parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT, verbose_name=_('Parlamentar')) + cargo = models.ForeignKey(CargoMesa, on_delete=models.PROTECT, verbose_name=_('Cargo')) mesa_diretora = models.ForeignKey( - MesaDiretora, on_delete=models.PROTECT, null=True) + MesaDiretora, on_delete=models.PROTECT, + related_name='composicaomesa_set', verbose_name=_('Mesa Diretora')) class Meta: verbose_name = _('Ocupação de cargo na Mesa') @@ -525,6 +553,24 @@ class ComposicaoMesa(models.Model): 'parlamentar': self.parlamentar, 'cargo': self.cargo } + def clean(self): + if self.parlamentar_id and self.mesa_diretora_id: + if ComposicaoMesa.objects.filter( + mesa_diretora=self.mesa_diretora, + parlamentar=self.parlamentar, + ).exclude(pk=self.pk).exists(): + raise ValidationError( + _('Parlamentar já ocupa um cargo nesta mesa diretora.')) + + if self.cargo_id and self.mesa_diretora_id: + if self.cargo.unico: + if ComposicaoMesa.objects.filter( + mesa_diretora=self.mesa_diretora, + cargo=self.cargo + ).exclude(pk=self.pk).exists(): + raise ValidationError( + _('Cargo único já ocupado por outro parlamentar.')) + class Frente(models.Model): ''' diff --git a/sapl/parlamentares/tests/test_mesadiretora.py b/sapl/parlamentares/tests/test_mesadiretora.py new file mode 100644 index 000000000..053cef26c --- /dev/null +++ b/sapl/parlamentares/tests/test_mesadiretora.py @@ -0,0 +1,401 @@ +import pytest +from datetime import date + +from django.core.exceptions import ValidationError +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ +from model_bakery import baker + +from sapl.parlamentares.forms import ComposicaoMesaForm, MesaDiretoraForm +from sapl.parlamentares.models import ComposicaoMesa, MesaDiretora + + +# ===================================================================== +# Testes de validação a nível de Model — MesaDiretora +# ===================================================================== + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_model_clean_data_inicio_maior_que_data_fim(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio=date(2021, 1, 1), + data_fim=date(2024, 12, 31) + ) + mesa = MesaDiretora( + titulo='Mesa', + data_inicio=date(2022, 1, 1), + data_fim=date(2021, 12, 31), + legislatura=legislatura + ) + with pytest.raises(ValidationError, match='A data de início deve ser anterior à data de fim.'): + mesa.clean() + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_model_clean_data_fora_da_legislatura(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio=date(2021, 1, 1), + data_fim=date(2024, 12, 31) + ) + mesa = MesaDiretora( + titulo='Mesa', + data_inicio=date(2020, 1, 1), + data_fim=date(2021, 12, 31), + legislatura=legislatura + ) + with pytest.raises(ValidationError, match='As datas da mesa diretora devem estar dentro do período da legislatura.'): + mesa.clean() + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_model_clean_intersecao(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio=date(2021, 1, 1), + data_fim=date(2024, 12, 31) + ) + baker.make( + 'parlamentares.MesaDiretora', + legislatura=legislatura, + titulo='Mesa Existente', + data_inicio=date(2021, 1, 1), + data_fim=date(2022, 12, 31) + ) + mesa = MesaDiretora( + titulo='Nova Mesa', + data_inicio=date(2022, 1, 1), + data_fim=date(2023, 12, 31), + legislatura=legislatura + ) + with pytest.raises(ValidationError, match='As datas da mesa diretora se sobrepõem com outra mesa diretora existente.'): + mesa.clean() + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_model_clean_valido(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio=date(2021, 1, 1), + data_fim=date(2024, 12, 31) + ) + mesa = MesaDiretora( + titulo='Mesa', + data_inicio=date(2021, 1, 1), + data_fim=date(2022, 12, 31), + legislatura=legislatura + ) + mesa.clean() # não deve lançar exceção + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_model_full_clean_sem_data_inicio(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio=date(2021, 1, 1), + data_fim=date(2024, 12, 31) + ) + mesa = MesaDiretora( + titulo='Mesa', + data_fim=date(2022, 12, 31), + legislatura=legislatura + ) + with pytest.raises(ValidationError) as exc_info: + mesa.full_clean() + + assert 'data_inicio' in exc_info.value.message_dict + assert exc_info.value.message_dict['data_inicio'] == [_('Este campo não pode ser nulo.')] + + +# ===================================================================== +# Testes de validação a nível de Model — ComposicaoMesa +# ===================================================================== + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_model_clean_parlamentar_duplicado(): + parlamentar = baker.make('parlamentares.Parlamentar') + cargo1 = baker.make('parlamentares.CargoMesa') + cargo2 = baker.make('parlamentares.CargoMesa') + mesa_diretora = baker.make('parlamentares.MesaDiretora') + + ComposicaoMesa.objects.create( + parlamentar=parlamentar, cargo=cargo1, mesa_diretora=mesa_diretora + ) + + composicao = ComposicaoMesa( + parlamentar=parlamentar, cargo=cargo2, mesa_diretora=mesa_diretora + ) + with pytest.raises(ValidationError, match='Parlamentar já ocupa um cargo nesta mesa diretora.'): + composicao.clean() + + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_model_clean_cargo_unico(): + parlamentar1 = baker.make('parlamentares.Parlamentar') + parlamentar2 = baker.make('parlamentares.Parlamentar') + cargo = baker.make('parlamentares.CargoMesa', unico=True) + mesa_diretora = baker.make('parlamentares.MesaDiretora') + + ComposicaoMesa.objects.create( + parlamentar=parlamentar1, cargo=cargo, mesa_diretora=mesa_diretora + ) + + composicao = ComposicaoMesa( + parlamentar=parlamentar2, cargo=cargo, mesa_diretora=mesa_diretora + ) + with pytest.raises(ValidationError, match='Cargo único já ocupado por outro parlamentar.'): + composicao.clean() + + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_model_clean_valido(): + parlamentar = baker.make('parlamentares.Parlamentar') + cargo = baker.make('parlamentares.CargoMesa') + mesa_diretora = baker.make('parlamentares.MesaDiretora') + + composicao = ComposicaoMesa( + parlamentar=parlamentar, cargo=cargo, mesa_diretora=mesa_diretora + ) + composicao.clean() # não deve lançar exceção + + +# ===================================================================== +# Testes de validação via Form — MesaDiretora +# ===================================================================== + +def test_mesadiretora_form_invalido(): + form = MesaDiretoraForm(data={}) + + assert not form.is_valid() + + errors = form.errors + + assert errors['titulo'] == [_('Este campo é obrigatório.')] + assert errors['data_inicio'] == [_('Este campo é obrigatório.')] + assert errors['data_fim'] == [_('Este campo é obrigatório.')] + assert errors['legislatura'] == [_('Este campo é obrigatório.')] + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_form_data_inicio_maior_que_data_fim(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio='2021-01-01', + data_fim='2024-12-31' + ) + + form = MesaDiretoraForm(data={ + 'titulo': 'Mesa Diretora 2021-2022', + 'data_inicio': '2022-01-01', + 'data_fim': '2021-12-31', + 'legislatura': legislatura.id, + }) + + assert not form.is_valid() + errors = form.errors + assert errors['__all__'] == [_('A data de início deve ser anterior à data de fim.')] + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_form_valido(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio='2021-01-01', + data_fim='2024-12-31' + ) + + form = MesaDiretoraForm(data={ + 'titulo': 'Mesa Diretora 2021-2022', + 'data_inicio': '2021-01-01', + 'data_fim': '2022-12-31', + 'legislatura': legislatura.id, + }) + + assert form.is_valid() + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_form_intersecao(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio='2021-01-01', + data_fim='2024-12-31' + ) + baker.make( + 'parlamentares.MesaDiretora', + legislatura=legislatura, + titulo='Mesa Diretora 2021-2022', + data_inicio='2021-01-01', + data_fim='2022-12-31') + + form = MesaDiretoraForm(data={ + 'titulo': 'Mesa Diretora 2022-2023', + 'data_inicio': '2022-01-01', + 'data_fim': '2023-12-31', + 'legislatura': legislatura.id, + }) + + assert not form.is_valid() + errors = form.errors + assert errors['__all__'] == [_('As datas da mesa diretora se sobrepõem com outra mesa diretora existente.')] + + +@pytest.mark.django_db(transaction=False) +def test_mesadiretora_form_data_fora_da_legislatura(): + legislatura = baker.make( + 'parlamentares.Legislatura', + data_inicio='2021-01-01', + data_fim='2024-12-31') + + form = MesaDiretoraForm(data={ + 'titulo': 'Mesa Diretora 2020-2021', + 'data_inicio': '2020-01-01', + 'data_fim': '2021-12-31', + 'legislatura': legislatura.id, + }) + + assert not form.is_valid() + errors = form.errors + assert errors['__all__'] == [_('As datas da mesa diretora devem estar dentro do período da legislatura.')] + + +# ===================================================================== +# Testes de validação via Form — ComposicaoMesa +# ===================================================================== + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_form_invalido(): + mesa_diretora = baker.make('parlamentares.MesaDiretora') + form = ComposicaoMesaForm(data={}, initial={'mesa_diretora': mesa_diretora}) + + assert not form.is_valid() + + errors = form.errors + + assert errors['parlamentar'] == [_('Este campo é obrigatório.')] + assert errors['cargo'] == [_('Este campo é obrigatório.')] + + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_form_valido(): + parlamentar = baker.make('parlamentares.Parlamentar') + cargo = baker.make('parlamentares.CargoMesa') + mesa_diretora = baker.make('parlamentares.MesaDiretora') + mandato = baker.make( + 'parlamentares.Mandato', + parlamentar=parlamentar, + legislatura=mesa_diretora.legislatura) + + form = ComposicaoMesaForm(data={ + 'parlamentar': parlamentar.id, + 'cargo': cargo.id, + }, initial={ + 'mesa_diretora': mesa_diretora, + }) + + assert form.is_valid() + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_form_parlamentar_ocupando_cargo_na_mesma_mesa(): + parlamentar = baker.make('parlamentares.Parlamentar') + cargo1 = baker.make('parlamentares.CargoMesa') + cargo2 = baker.make('parlamentares.CargoMesa') + mesa_diretora = baker.make('parlamentares.MesaDiretora') + mandato = baker.make( + 'parlamentares.Mandato', + parlamentar=parlamentar, + legislatura=mesa_diretora.legislatura) + + ComposicaoMesa.objects.create( + parlamentar=parlamentar, + cargo=cargo1, + mesa_diretora=mesa_diretora + ) + + form = ComposicaoMesaForm(data={ + 'parlamentar': parlamentar.id, + 'cargo': cargo2.id, + }, initial={ + 'mesa_diretora': mesa_diretora, + }) + + assert not form.is_valid() + errors = form.errors + assert errors['__all__'] == [_('Parlamentar já ocupa um cargo nesta mesa diretora.')] + + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_form_parlamentar_cargo_unico_mesma_mesa(): + parlamentar1 = baker.make('parlamentares.Parlamentar') + parlamentar2 = baker.make('parlamentares.Parlamentar') + + cargo = baker.make('parlamentares.CargoMesa', unico=True) + + mesa_diretora = baker.make('parlamentares.MesaDiretora') + + mandato1 = baker.make('parlamentares.Mandato', parlamentar=parlamentar1, legislatura=mesa_diretora.legislatura) + mandato2 = baker.make('parlamentares.Mandato', parlamentar=parlamentar2, legislatura=mesa_diretora.legislatura) + + ComposicaoMesa.objects.create( + parlamentar=parlamentar1, + cargo=cargo, + mesa_diretora=mesa_diretora + ) + + form = ComposicaoMesaForm(data={ + 'parlamentar': parlamentar2.id, + 'cargo': cargo.id, + }, initial={ + 'mesa_diretora': mesa_diretora, + }) + + assert not form.is_valid() + errors = form.errors + assert errors['__all__'] == [_('Cargo único já ocupado por outro parlamentar.')] + + +# ===================================================================== +# Testes de integração via View — ComposicaoMesa +# ===================================================================== + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_form_view_create(admin_client): + parlamentar = baker.make('parlamentares.Parlamentar') + cargo = baker.make('parlamentares.CargoMesa') + mesa_diretora = baker.make('parlamentares.MesaDiretora') + mandato = baker.make( + 'parlamentares.Mandato', + parlamentar=parlamentar, + legislatura=mesa_diretora.legislatura) + + response = admin_client.post(reverse('sapl.parlamentares:composicaomesa_create', kwargs={'pk': mesa_diretora.id}), data={ + 'parlamentar': parlamentar.id, + 'cargo': cargo.id, + }) + + assert ComposicaoMesa.objects.filter(parlamentar=parlamentar, cargo=cargo, mesa_diretora=mesa_diretora).exists() + +@pytest.mark.django_db(transaction=False) +def test_composicaomesa_form_view_update(admin_client): + parlamentar = baker.make('parlamentares.Parlamentar') + cargo = baker.make('parlamentares.CargoMesa') + mesa_diretora = baker.make('parlamentares.MesaDiretora') + mandato = baker.make( + 'parlamentares.Mandato', + parlamentar=parlamentar, + legislatura=mesa_diretora.legislatura) + + composicao = ComposicaoMesa.objects.create( + parlamentar=parlamentar, + cargo=cargo, + mesa_diretora=mesa_diretora + ) + + new_cargo = baker.make('parlamentares.CargoMesa') + + response = admin_client.post(reverse('sapl.parlamentares:composicaomesa_update', kwargs={'pk': composicao.id}), data={ + 'parlamentar': parlamentar.id, + 'cargo': new_cargo.id, + }) + + composicao.refresh_from_db() + assert composicao.cargo == new_cargo \ No newline at end of file diff --git a/sapl/parlamentares/urls.py b/sapl/parlamentares/urls.py index d67a1f6b1..cdbc522a3 100644 --- a/sapl/parlamentares/urls.py +++ b/sapl/parlamentares/urls.py @@ -1,11 +1,11 @@ from django.conf.urls import include, url -from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud, +from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud, ComposicaoMesaCrud, MesaDiretoraCrud, coligacao_legislatura, ComposicaoColigacaoCrud, DependenteCrud, FiliacaoCrud, FrenteCrud, FrenteList, LegislaturaCrud, MandatoCrud, - MesaDiretoraView, NivelInstrucaoCrud, + NivelInstrucaoCrud, ParlamentarCrud, ParlamentarMateriasView, ParlamentarNormasView, ParticipacaoParlamentarCrud, PartidoCrud, ProposicaoParlamentarCrud, @@ -13,12 +13,8 @@ from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud, SessaoLegislativaCrud, TipoAfastamentoCrud, TipoDependenteCrud, TipoMilitarCrud, VotanteView, - altera_field_mesa, - altera_field_mesa_public_view, frente_atualiza_lista_parlamentares, - insere_parlamentar_composicao, parlamentares_frente_selected, - remove_parlamentar_composicao, parlamentares_filiados, BlocoCrud, PesquisarParlamentarView, VincularParlamentarView, get_sessoes_legislatura, FrenteCargoCrud, FrenteParlamentarCrud, @@ -104,22 +100,11 @@ urlpatterns = [ url(r'^sistema/mesa-diretora/cargo-mesa/', include(CargoMesaCrud.get_urls())), - url(r'^mesa-diretora/$', - MesaDiretoraView.as_view(), name='mesa_diretora'), - - url(r'^mesa-diretora/altera-field-mesa/$', - altera_field_mesa, name='altera_field_mesa'), - - url(r'^mesa-diretora/altera-field-mesa-public-view/$', - altera_field_mesa_public_view, name='altera_field_mesa_public_view'), - - url(r'^mesa-diretora/insere-parlamentar-composicao/$', - insere_parlamentar_composicao, name='insere_parlamentar_composicao'), - - url(r'^mesa-diretora/remove-parlamentar-composicao/$', - remove_parlamentar_composicao, name='remove_parlamentar_composicao'), + url(r'^mesa-diretora/', include( + MesaDiretoraCrud.get_urls() + + ComposicaoMesaCrud.get_urls() + )), url(r'^parlamentar/get-sessoes-legislatura/$', get_sessoes_legislatura, name='get_sessoes_legislatura'), - ] diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index a46ce2b84..ac9ef2620 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db.models import F, Q from django.db.models.aggregates import Count -from django.http import JsonResponse +from django.http import Http404, JsonResponse from django.http.response import HttpResponseRedirect from django.shortcuts import render from django.templatetags.static import static @@ -35,7 +35,7 @@ from sapl.parlamentares.apps import AppConfig from sapl.rules import SAPL_GROUP_VOTANTE from sapl.utils import (parlamentares_ativos, show_results_filter_set, ratelimit_ip) -from .forms import (ColigacaoFilterSet, FiliacaoForm, FrenteForm, LegislaturaForm, MandatoForm, +from .forms import (ColigacaoFilterSet, ComposicaoMesaForm, FiliacaoForm, FrenteForm, LegislaturaForm, MandatoForm, MesaDiretoraFilterSet, MesaDiretoraForm, ParlamentarCreateForm, ParlamentarForm, VotanteForm, ParlamentarFilterSet, PartidoFilterSet, VincularParlamentarForm, BlocoForm, FrenteParlamentarForm, BlocoMembroForm) @@ -1007,280 +1007,93 @@ def parlamentares_filiados(request, pk): return render(request, template_name, {'partido': partido, 'parlamentares': parlamentares_filiados}) -class MesaDiretoraView(FormView): - template_name = 'parlamentares/composicaomesa_form.html' - success_url = reverse_lazy('sapl.parlamentares:mesa_diretora') - logger = logging.getLogger(__name__) - - def get_template_names(self): - if self.request.user.has_perm('parlamentares.change_composicaomesa'): - if 'iframe' not in self.request.GET: - if not self.request.session.get('iframe'): - return 'parlamentares/composicaomesa_form.html' - elif self.request.GET['iframe'] == '0': - return 'parlamentares/composicaomesa_form.html' - - return 'parlamentares/public_composicaomesa_form.html' +class MesaDiretoraCrud(Crud): + model = MesaDiretora + help_topic = 'mesa_diretora' + public = [RP_DETAIL, RP_LIST] - # Essa função avisa quando se pode compor uma Mesa Legislativa - def validation(self, request): - username = request.user.username - self.logger.info('user=' + username + '. Não há nenhuma Sessão Legislativa cadastrada. ' + - 'Só é possível compor uma Mesa Diretora quando ' + - 'há uma Sessão Legislativa cadastrada.') - mensagem = _('Não há nenhuma Sessão Legislativa cadastrada. ' + - 'Só é possível compor uma Mesa Diretora quando ' + - 'há uma Sessão Legislativa cadastrada.') - messages.add_message(request, messages.INFO, mensagem) - - return self.render_to_response( - {'legislaturas': Legislatura.objects.all( - ).order_by('-numero'), - 'legislatura_selecionada': Legislatura.objects.last(), - 'cargos_vagos': CargoMesa.objects.all()}) + class BaseMixin(Crud.BaseMixin): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if 'subnav_template_name' not in context: + context['subnav_template_name'] = 'parlamentares/subnav_mesa.yaml' + return context - @xframe_options_exempt - def get(self, request, *args, **kwargs): + class ListView(FilterView, Crud.ListView): + filterset_class = MesaDiretoraFilterSet + paginate_by = None - if (not Legislatura.objects.exists() or - not SessaoLegislativa.objects.exists()): - return self.validation(request) - - legislatura = Legislatura.objects.first() - sessoes = SessaoLegislativa.objects.filter( - legislatura=legislatura).order_by("data_inicio") - - year = timezone.now().year - - sessao_atual = sessoes.filter(data_inicio__year__lte=year).exclude( - data_inicio__gt=timezone.now()).order_by('-data_inicio').first() - - mesa_diretora = sessao_atual.mesadiretora_set.order_by( - '-data_inicio').first() if sessao_atual else None - - composicao_mesa = ComposicaoMesa.objects.select_related('cargo', 'parlamentar').filter( - mesa_diretora=mesa_diretora).order_by('cargo__id_ordenacao', 'cargo_id') - - cargos_ocupados = [m.cargo for m in composicao_mesa] - cargos = CargoMesa.objects.all() - cargos_vagos = list(set(cargos) - set(cargos_ocupados)) - - parlamentares = legislatura.mandato_set.all() - parlamentares_ocupados = [m.parlamentar for m in composicao_mesa] - parlamentares_vagos = list( - set( - [p.parlamentar for p in parlamentares if p.parlamentar.ativo]) - set( - parlamentares_ocupados)) - parlamentares_vagos.sort(key=lambda x: x.nome_parlamentar) - # Se todos os cargos estiverem ocupados, a listagem de parlamentares - # deve ser renderizada vazia - if not cargos_vagos: - parlamentares_vagos = [] - - return self.render_to_response( - {'legislaturas': Legislatura.objects.all( - ).order_by('-numero'), - 'legislatura_selecionada': legislatura, - 'sessoes': sessoes, - 'sessao_selecionada': sessao_atual, - 'composicao_mesa': composicao_mesa, - 'parlamentares': parlamentares_vagos, - 'cargos_vagos': cargos_vagos - }) - - -def altera_field_mesa(request): - """ - Essa função lida com qualquer alteração nos campos - da Mesa Diretora, após qualquer - operação (Legislatura/Sessão/Inclusão/Remoção), - atualizando os campos após cada alteração - """ - # TODO: Adicionar opção de selecionar mesa diretora no CRUD + def get_id_legislatura_atual(self): + return Legislatura.objects.filter( + data_inicio__lte=timezone.now() + ).order_by('-data_inicio').values_list('id', flat=True).first() + + 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: + fk['data'] = {'legislatura': self.get_id_legislatura_atual()} + elif 'legislatura' not in self.request.GET and 'mesa' in self.request.GET: + legislatura_da_mesa = Legislatura.objects.filter( + mesadiretora_set__id=self.request.GET['mesa'] + ).values_list('id', flat=True).first() + if not legislatura_da_mesa: + raise Http404("MesaDiretora {} não encontrada.".format(self.request.GET['mesa'])) + fk['data'] = {'legislatura': legislatura_da_mesa} + return fk - logger = logging.getLogger(__name__) - legislatura = request.GET['legislatura'] - sessoes = SessaoLegislativa.objects.filter( - legislatura=legislatura).order_by('-data_inicio') - username = request.user.username + def get_queryset(self): + return super().get_queryset().prefetch_related( + 'composicaomesa_set__parlamentar__filiacao_set__partido', + 'composicaomesa_set__cargo' + ) - if not sessoes: - return JsonResponse({'msg': ('Nenhuma sessão encontrada!', 0)}) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subnav_template_name'] = '' + context['title'] = ' ' + return context - # Verifica se já tem uma sessão selecionada. Ocorre quando - # é alterado o campo de sessão ou feita alguma operação - # de inclusão/remoção. - if request.GET['sessao']: - sessao_selecionada = SessaoLegislativa.objects.get( - id=request.GET['sessao']) + def get(self, request, *args, **kwargs): + return FilterView.get(self, request, *args, **kwargs) - # Caso a mudança tenha sido no campo legislatura, a sessão - # atual deve ser a primeira daquela legislatura - else: - year = timezone.now().year - logger.debug( - "user={}. Tentando obter id de sessoes com data_inicio.ano={}.".format(username, year)) - sessao_selecionada = sessoes.filter(data_inicio__year=year).first() - if not sessao_selecionada: - logger.error("user=" + username + ". Id de sessoes com data_inicio.ano={} não encontrado. " - "Selecionado o ID da primeira sessão.".format(year)) - sessao_selecionada = sessoes.first() - - mesa_diretora = request.GET.get('mesa_diretora') - - # Mesa nao deve ser informada ainda - if not mesa_diretora: - # Cria nova mesa diretora ou retorna a primeira - mesa_diretora, _ = MesaDiretora.objects.get_or_create( - sessao_legislativa=sessao_selecionada) - - # TODO: quando a mesa for criada explicitamente em tabelas auxiliares, - # deve-se somente tentar recuperar a mesa, e caso nao exista - # retornar o erro abaixo - # return JsonResponse({'msg': ('Nenhuma mesa encontrada na sessão!')}) - else: - try: - mesa_diretora = MesaDiretora.objects.get( - id=mesa_diretora, sessao_legislativa=sessao_selecionada) - except ObjectDoesNotExist: - mesa_diretora = MesaDiretora.objects.filter( - sessao_legislativa=sessao_selecionada).first() - - # Atualiza os componentes da view após a mudança - composicao_mesa = ComposicaoMesa.objects.select_related('cargo', 'parlamentar').filter( - mesa_diretora=mesa_diretora).order_by('cargo_id') - - cargos_ocupados = [m.cargo for m in composicao_mesa] - cargos = CargoMesa.objects.all() - cargos_vagos = list(set(cargos) - set(cargos_ocupados)) - - parlamentares = Legislatura.objects.get( - id=legislatura).mandato_set.all() - parlamentares_ocupados = [m.parlamentar for m in composicao_mesa] - parlamentares_vagos = list( - set( - [p.parlamentar for p in parlamentares]) - set( - parlamentares_ocupados)) - - parlamentares_vagos.sort(key=lambda x: x.nome_parlamentar) - lista_sessoes = [(s.id, s.__str__()) for s in sessoes] - lista_composicao = [(c.id, c.parlamentar.__str__(), - c.cargo.__str__()) for c in composicao_mesa] - lista_parlamentares = [( - p.id, p.__str__()) for p in parlamentares_vagos] - lista_cargos = [(c.id, c.__str__()) for c in cargos_vagos] - - return JsonResponse( - {'lista_sessoes': lista_sessoes, - 'lista_composicao': lista_composicao, - 'lista_parlamentares': lista_parlamentares, - 'lista_cargos': lista_cargos, - 'sessao_selecionada': sessao_selecionada.id, - 'msg': ('', 1)}) - - -def insere_parlamentar_composicao(request): - """ - Essa função lida com qualquer operação de inserção - na composição da Mesa Diretora - """ - logger = logging.getLogger(__name__) - username = request.user.username - if request.user.has_perm( - '%s.add_%s' % ( - AppConfig.label, ComposicaoMesa._meta.model_name)): - composicao = ComposicaoMesa() + class UpdateView(Crud.UpdateView): + form_class = MesaDiretoraForm - try: - # logger.debug( - # "user=" + username + ". Tentando obter SessaoLegislativa com id={}.".format(request.POST['sessao'])) - mesa_diretora, _ = MesaDiretora.objects.get_or_create( - sessao_legislativa_id=int(request.POST['sessao'])) - composicao.mesa_diretora = mesa_diretora - except MultiValueDictKeyError: - logger.error( - "user=" + username + ". 'MultiValueDictKeyError', nenhuma sessão foi inserida!") - return JsonResponse({'msg': ('Nenhuma sessão foi inserida!', 0)}) + class CreateView(Crud.CreateView): + form_class = MesaDiretoraForm - try: - logger.debug( - "user=" + username + ". Tentando obter Parlamentar com id={}.".format(request.POST['parlamentar'])) - composicao.parlamentar = Parlamentar.objects.get( - id=int(request.POST['parlamentar'])) - except MultiValueDictKeyError: - logger.error( - "user=" + username + ". 'MultiValueDictKeyError', nenhum parlamentar foi inserido!") - return JsonResponse({ - 'msg': ('Nenhum parlamentar foi inserido!', 0)}) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subnav_template_name'] = '' + return context - try: - logger.info("user=" + username + - ". Tentando obter CargoMesa com id={}.".format(request.POST['cargo'])) - composicao.cargo = CargoMesa.objects.get( - id=int(request.POST['cargo'])) - parlamentar_ja_inserido = ComposicaoMesa.objects.filter( - mesa_diretora=mesa_diretora, - cargo=composicao.cargo).exists() - - if parlamentar_ja_inserido: - return JsonResponse({'msg': ('Parlamentar já inserido!', 0)}) - composicao.save() - - except MultiValueDictKeyError: - logger.error("user=" + username + - ". 'MultiValueDictKeyError', nenhum cargo foi inserido!") - return JsonResponse({'msg': ('Nenhum cargo foi inserido!', 0)}) - - logger.info("user=" + username + ". Parlamentar inserido com sucesso!") - return JsonResponse({'msg': ('Parlamentar inserido com sucesso!', 1)}) +class ComposicaoMesaCrud(MasterDetailCrud): + model = ComposicaoMesa + parent_field = 'mesa_diretora' + help_topic = 'mesa_diretora' + public = [RP_LIST, RP_DETAIL] - else: - logger.error("user=" + username + - " não tem permissão para esta operação!") - return JsonResponse( - {'msg': ('Você não tem permissão para esta operação!', 0)}) + class BaseMixin(MasterDetailCrud.BaseMixin): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subnav_template_name'] = 'parlamentares/subnav_mesa.yaml' + return context + class UpdateView(MasterDetailCrud.UpdateView): + form_class = ComposicaoMesaForm -def remove_parlamentar_composicao(request): - """ - Essa função lida com qualquer operação de remoção - na composição da Mesa Diretora - """ - logger = logging.getLogger(__name__) - username = request.user.username - if request.POST and request.user.has_perm( - '%s.delete_%s' % ( - AppConfig.label, ComposicaoMesa._meta.model_name)): + def get_initial(self): + initial = super().get_initial() + initial['mesa_diretora'] = self.object.mesa_diretora + return initial - if 'composicao_mesa' in request.POST: - try: - logger.debug("user=" + username + ". Tentando obter ComposicaoMesa com id={}.".format( - request.POST['composicao_mesa'])) - composicao = ComposicaoMesa.objects.get( - id=request.POST['composicao_mesa']) - except ObjectDoesNotExist: - logger.error( - "user=" + username + - ". ComposicaoMesa com id={} não encontrada, portanto não pode ser removida." - .format(request.POST['composicao_mesa'])) - return JsonResponse( - {'msg': ( - 'Composição da Mesa não pôde ser removida!', 0)}) - - composicao.delete() - - logger.info("user=" + username + ". ComposicaoMesa com id={} excluido com sucesso!".format( - request.POST['composicao_mesa'])) - return JsonResponse( - {'msg': ( - 'Parlamentar excluido com sucesso!', 1)}) - else: - logger.info("user=" + username + - ". Nenhum parlamentar escolhido para ser excluído.") - return JsonResponse( - {'msg': ( - 'Selecione algum parlamentar para ser excluido!', 0)}) + class CreateView(MasterDetailCrud.CreateView): + form_class = ComposicaoMesaForm + def get_initial(self): + initial = super().get_initial() + initial['mesa_diretora'] = MesaDiretora.objects.get(pk=self.kwargs['pk']) + return initial def partido_parlamentar_sessao_legislativa(sessao, parlamentar): """ @@ -1328,120 +1141,6 @@ def partido_parlamentar_sessao_legislativa(sessao, parlamentar): return filiacao.partido.sigla -def altera_field_mesa_public_view(request): - """ - Essa função lida com qualquer alteração nos campos - da Mesa Diretora para usuários anônimos, - atualizando os campos após cada alteração - """ - - # TODO: Adicionar opção de selecionar mesa diretora no CRUD - - logger = logging.getLogger(__name__) - username = request.user.username - legislatura = request.GET['legislatura'] - if legislatura: - legislatura = Legislatura.objects.get(id=legislatura) - else: - legislatura = Legislatura.objects.order_by('-data_inicio').first() - - sessoes = legislatura.sessaolegislativa_set.filter( - tipo='O').order_by('-data_inicio') - - if not sessoes: - return JsonResponse({'msg': ('Nenhuma sessão encontrada!', 0)}) - - # Verifica se já tem uma sessão selecionada. Ocorre quando é alterado o - # campo de sessão - - sessao_selecionada = request.GET['sessao'] - if not sessao_selecionada: - year = timezone.now().year - logger.info( - f"user={username}. Tentando obter sessões com data_inicio.ano = {year}.") - sessao_selecionada = sessoes.filter(data_inicio__year=year).first() - if sessao_selecionada is None: - logger.error(f"user={username}. Sessões não encontradas com com data_inicio.ano = {year}. " - "Selecionado o id da primeira sessão.") - sessao_selecionada = sessoes.first() - else: - sessao_selecionada = SessaoLegislativa.objects.get( - id=sessao_selecionada) - - # Atualiza os componentes da view após a mudança - lista_sessoes = [(s.id, s.__str__()) for s in sessoes] - - # Pegar Mesas diretoras da sessao - mesa_diretora = request.GET.get('mesa_diretora') - - # Mesa nao deve ser informada ainda - if not mesa_diretora: - try: - mesa_diretora = sessao_selecionada.mesadiretora_set.first() - except ObjectDoesNotExist: - logger.error( - f"user={username}. Mesa não encontrada com sessão Nº {sessao_selecionada.id}. ") - else: - # Cria nova mesa diretora ou retorna a primeira - mesa_diretora, _ = MesaDiretora.objects.get_or_create( - sessao_legislativa=sessao_selecionada) - - # TODO: quando a mesa for criada explicitamente em tabelas auxiliares, - # deve-se somente tentar recuperar a mesa, e caso nao exista - # retornar o erro abaixo - # logger.error(f"user={username}. Mesa Nº {mesa_diretora} não encontrada na sessão Nº {sessao_selecionada.id}. " - # "Selecionada a mesa com o primeiro id na sessão") - - composicao_mesa = ComposicaoMesa.objects.select_related('cargo', 'parlamentar').filter( - mesa_diretora=mesa_diretora).order_by('cargo_id') - cargos_ocupados = list(composicao_mesa.values_list( - 'cargo__id', 'cargo__descricao')) - parlamentares_ocupados = list(composicao_mesa.values_list( - 'parlamentar__id', 'parlamentar__nome_parlamentar')) - - lista_fotos = [] - lista_partidos = [] - - sessao = SessaoLegislativa.objects.get(id=sessao_selecionada.id) - for p in parlamentares_ocupados: - parlamentar = Parlamentar.objects.get(id=p[0]) - lista_partidos.append( - partido_parlamentar_sessao_legislativa(sessao, parlamentar)) - if parlamentar.fotografia: - try: - logger.warning( - f"Iniciando cropping da imagem {parlamentar.fotografia}") - thumbnail_url = get_backend().get_thumbnail_url( - parlamentar.fotografia, - { - 'size': (128, 128), - 'box': parlamentar.cropping, - 'crop': True, - 'detail': True, - } - ) - logger.warning( - f"Cropping da imagem {parlamentar.fotografia} realizado com sucesso") - lista_fotos.append(thumbnail_url) - except Exception as e: - logger.error(e) - logger.error( - F'erro processando arquivo: {parlamentar.fotografia.path}') - else: - lista_fotos.append(None) - - return JsonResponse({ - 'lista_parlamentares': parlamentares_ocupados, - 'lista_partidos': lista_partidos, - 'lista_cargos': cargos_ocupados, - 'lista_sessoes': lista_sessoes, - 'lista_fotos': lista_fotos, - 'sessao_selecionada': sessao_selecionada.id, - 'mesa_diretora': mesa_diretora.id, - 'msg': ('', 1) - }) - - class VincularParlamentarView(PermissionRequiredMixin, FormView): logger = logging.getLogger(__name__) form_class = VincularParlamentarForm diff --git a/sapl/templates/crud/list.html b/sapl/templates/crud/list.html index 44b394337..02a943205 100644 --- a/sapl/templates/crud/list.html +++ b/sapl/templates/crud/list.html @@ -4,11 +4,14 @@ {% block base_content %}
- + {% block actions_search %} + + {% endblock actions_search %} + {% block actions %}
{% if view.create_url %} diff --git a/sapl/templates/index.html b/sapl/templates/index.html index 3cb59bbc6..57b73cbcd 100644 --- a/sapl/templates/index.html +++ b/sapl/templates/index.html @@ -26,7 +26,7 @@ a dois anos.

- +
diff --git a/sapl/templates/navbar.yaml b/sapl/templates/navbar.yaml index e4dc39c10..1a5cb415b 100644 --- a/sapl/templates/navbar.yaml +++ b/sapl/templates/navbar.yaml @@ -6,7 +6,7 @@ - title: {% trans 'Institucional' %} children: - title: {% trans 'Mesa Diretora' %} - url: sapl.parlamentares:mesa_diretora + url: sapl.parlamentares:mesadiretora_list - title: {% trans 'Bancadas Parlamentares' %} url: sapl.sessao:bancada_list - title: {% trans 'Blocos Parlamentares' %} diff --git a/sapl/templates/parlamentares/composicaomesa_form.html b/sapl/templates/parlamentares/composicaomesa_form.html deleted file mode 100644 index 36cbb8bfc..000000000 --- a/sapl/templates/parlamentares/composicaomesa_form.html +++ /dev/null @@ -1,312 +0,0 @@ -{% extends "crud/detail.html" %} -{% load i18n %} -{% block actions %} {% endblock %} - -{% block detail_content %} - {% if sessoes|length == 0 %} - - - {% else %} - - - - -
- Escolha da Legislatura e da Sessão Legislativa -
-
- - -
-
- - -
-
-
-
-
- Escolha da Composição da Mesa Diretora -
-
- - -
- -
-

- {% if perms.parlamentares.add_cargomesa %} - - {% endif %} -
-
- {% if perms.parlamentares.add_composicaomesa %} - - {% endif %} -
- -
- - -
- -
- -
-
- {% endif %} -{% endblock detail_content %} - - -{% block extra_js %} - - - -{% endblock %} diff --git a/sapl/templates/parlamentares/layouts.yaml b/sapl/templates/parlamentares/layouts.yaml index 9d2037477..45a0efe3a 100644 --- a/sapl/templates/parlamentares/layouts.yaml +++ b/sapl/templates/parlamentares/layouts.yaml @@ -166,3 +166,14 @@ BlocoMembroList: - id - cargo:4 parlamentar - data_entrada data_saida + +MesaDiretora: + {% trans 'Período e Legislatura' %}: + - titulo + - data_inicio data_fim legislatura:6 + {% trans 'Descrição' %}: + - descricao + +ComposicaoMesa: + {% trans 'Composição da Mesa' %}: + - cargo:4 parlamentar \ No newline at end of file diff --git a/sapl/templates/parlamentares/mesadiretora_filter.html b/sapl/templates/parlamentares/mesadiretora_filter.html new file mode 100644 index 000000000..a1ec31a73 --- /dev/null +++ b/sapl/templates/parlamentares/mesadiretora_filter.html @@ -0,0 +1,156 @@ +{% extends "crud/list.html" %} +{% load i18n common_tags crispy_forms_tags cropping %} + +{% block actions_search %} + +{% endblock actions_search %} + +{% block container_table_list %} + {% if perms.parlamentares.add_mesadiretora %} + {{ block.super }} + {% else %} +
+
+ + +
+ {% for md in object_list %} + {% if request.GET.mesa == md.id|stringformat:"s" or not request.GET.mesa and forloop.first %} +
+ {% else %} +
+ {% endif %} + (De {{md.data_inicio }} a {{ md.data_fim }}) + + + + + + + + + + + + + {% for composicao in md.composicaomesa_set.all %} + + + + + + {% endfor %} + + +
+ {% endfor %} +
+
+
+ {% endif %} + +{% endblock container_table_list %} + +{% block extra_js %} + +{% endblock extra_js %} diff --git a/sapl/templates/parlamentares/public_composicaomesa_form.html b/sapl/templates/parlamentares/public_composicaomesa_form.html deleted file mode 100644 index 5981adffc..000000000 --- a/sapl/templates/parlamentares/public_composicaomesa_form.html +++ /dev/null @@ -1,146 +0,0 @@ -{% extends "crud/detail.html" %} -{% load i18n cropping%} -{% block actions %} {% endblock %} - -{% block detail_content %} -
- {% csrf_token %} -
- Escolha da Legislatura e da Sessão Legislativa -
-
- - -
-
- - -
-
-
-
-
- Composição da Mesa Diretora - - - - - - - - - - - {% for p in composicao_mesa %} - - {% if p.parlamentar.fotografia %} - - {% endif %} - - - - - {% endfor %} - - -
-
-{% endblock detail_content %} - -{% block extra_js %} - - - -{% endblock %} diff --git a/sapl/templates/parlamentares/subnav_mesa.yaml b/sapl/templates/parlamentares/subnav_mesa.yaml new file mode 100644 index 000000000..5391023db --- /dev/null +++ b/sapl/templates/parlamentares/subnav_mesa.yaml @@ -0,0 +1,5 @@ +{% load i18n common_tags %} +- title: {% trans 'Mesa Diretora' %} + url: sapl.parlamentares:mesadiretora_detail +- title: {% trans 'Composição da Mesa' %} + url: sapl.parlamentares:composicaomesa_list \ No newline at end of file diff --git a/sapl/utils.py b/sapl/utils.py index ee97094aa..19690d0e9 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -262,6 +262,12 @@ def montar_helper_autor(self): ' class="btn btn-dark">Cancelar')])) +class SelectSubmitChangeWidget(forms.Select): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.attrs.update({'onchange': 'this.form.submit();'}) + + class SaplGenericForeignKey(GenericForeignKey): def __init__(