Browse Source

Merge 1b144e48b0 into c0f7448561

pull/3829/merge
LeandroJataí 4 days ago
committed by GitHub
parent
commit
0ae4e8133b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      frontend/src/__global/scss/layouts/_globals.scss
  2. 89
      frontend/src/__global/scss/libs/bootstrap/_nav_tabs.scss
  3. 1
      frontend/src/__global/scss/libs/libs.scss
  4. 54
      sapl/parlamentares/forms.py
  5. 34
      sapl/parlamentares/migrations/0046_mesadiretora_legislatura.py
  6. 23
      sapl/parlamentares/migrations/0047_auto_20260412_2256.py
  7. 42
      sapl/parlamentares/migrations/0048_auto_20260413_1049.py
  8. 39
      sapl/parlamentares/migrations/0049_auto_20260417_1917.py
  9. 19
      sapl/parlamentares/migrations/0050_auto_20260417_2235.py
  10. 72
      sapl/parlamentares/models.py
  11. 401
      sapl/parlamentares/tests/test_mesadiretora.py
  12. 27
      sapl/parlamentares/urls.py
  13. 449
      sapl/parlamentares/views.py
  14. 13
      sapl/templates/crud/list.html
  15. 2
      sapl/templates/index.html
  16. 2
      sapl/templates/navbar.yaml
  17. 312
      sapl/templates/parlamentares/composicaomesa_form.html
  18. 11
      sapl/templates/parlamentares/layouts.yaml
  19. 156
      sapl/templates/parlamentares/mesadiretora_filter.html
  20. 146
      sapl/templates/parlamentares/public_composicaomesa_form.html
  21. 5
      sapl/templates/parlamentares/subnav_mesa.yaml
  22. 6
      sapl/utils.py

1
frontend/src/__global/scss/layouts/_globals.scss

@ -183,6 +183,7 @@ small {
hyphens: auto;*/
}
@media print {
a[href]:after {
content: none !important;

89
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;
}
}

1
frontend/src/__global/scss/libs/libs.scss

@ -1,2 +1,3 @@
@import "./bootstrap/nav_navbar";
@import "./bootstrap/nav_tabs";
@import "./bootstrap/table";

54
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)

34
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),
]

23
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',
),
]

42
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),
]

39
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'),
),
]

19
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'),
),
]

72
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):
'''

401
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

27
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'),
]

449
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

13
sapl/templates/crud/list.html

@ -4,11 +4,14 @@
{% block base_content %}
<div class="d-flex context-actions justify-content-between align-items-start">
<div class="actions search flex-grow-1 pr-3">
{% if form %}
{% crispy form %}
{% endif %}
</div>
{% block actions_search %}
<div class="actions search flex-grow-1 pr-3">
{% if form %}
{% crispy form %}
{% endif %}
</div>
{% endblock actions_search %}
{% block actions %}
<div class="actions btn-group btn-group-lg" role="group">
{% if view.create_url %}

2
sapl/templates/index.html

@ -26,7 +26,7 @@
a dois anos.
</p>
</div>
<a href="{% url 'sapl.parlamentares:mesa_diretora' %}"></a>
<a href="{% url 'sapl.parlamentares:mesadiretora_list' %}"></a>
</div>
<div class="homeBlock">

2
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' %}

312
sapl/templates/parlamentares/composicaomesa_form.html

@ -1,312 +0,0 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% block actions %} {% endblock %}
{% block detail_content %}
{% if sessoes|length == 0 %}
<div class="alert alert-danger alert-dismissible" role="alert">
<b>{{legislatura_selecionada}}</b> não possui nenhuma Sessão Legislativa cadastrada.<br />
Clique <a href="{% url 'sapl.parlamentares:sessaolegislativa_create' %}">aqui</a> para cadastrar uma nova.
</div>
{% else %}
<div class="alert alert-danger alert-dismissible" id="div-error" role="alert" style="display: none">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<b><span id="error-message"></span></b>
</div>
<div class="alert alert-success alert-dismissible" id="div-success" style="display: none" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<b><span id="success-message"></span></b>
</div>
<fieldset class="form-group">
<legend>Escolha da Legislatura e da Sessão Legislativa</legend>
<div class="row">
<div class="col-md-6">
<label>Escolha uma Legislatura</label>
<select name="legislatura" id="id_legislatura" class="form-control">
{% for l in legislaturas %}
<option value="{{l.id}}" {% if l == legislatura_selecionada %} selected {% endif %}>
{{l}}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label>Escolha uma Sessão Legislativa</label>
<select name="sessao" id="id_sessao_legislativa" class="form-control">
{% for s in sessoes %}
<option value="{{s.id}}" {% if s == sessao_selecionada %} selected {% endif %}>
{{s}}
</option>
{% endfor %}
</select>
</div>
</div>
</fieldset>
<br />
<fieldset class="form-group">
<legend>Escolha da Composição da Mesa Diretora</legend>
<div class="row">
<div class="col-md-4">
<label>Composição da Mesa Diretora</label>
<select multiple size="5" class="form-control" id="id_composicao_mesa" name="composicao_mesa">
{% for p in composicao_mesa %}
<option value="{{p.id}}">
{{p.parlamentar}} || {{p.cargo}}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4" align="center">
<br /><br />
{% if perms.parlamentares.add_cargomesa %}
<input type="submit" style="display: none" name="Incluir" id="id_incluir" Value="Incluir" class="btn btn-primary" />
{% endif %}
<br />
<br />
{% if perms.parlamentares.add_composicaomesa %}
<input type="submit" style="display: none" name="Excluir" id="id_excluir" Value="Excluir" class="btn btn-danger" />
{% endif %}
</div>
<div class="col-md-4">
<label id='parlamentar-cargo-title' style="display: none">Parlamentar | Cargo</label>
<select class="form-control" name="parlamentar" id="id_parlamentar" style="display: none">
{% for p in parlamentares %}
<option value="{{p.id}}">{{p.nome_parlamentar}}</option>
{% endfor %}
</select>
<br />
<select class="form-control" name="cargo" id="id_cargo" style="display: none">
{% for c in cargos_vagos %}
<option value="{{c.id}}">{{c}}</option>
{% endfor %}
</select>
</div>
</div>
</fieldset>
{% endif %}
{% endblock detail_content %}
{% block extra_js %}
<script language="Javascript">
//##############################################################
// Inicialização da View #
//##############################################################
// Se a composicao estiver completa, deve-se esconder o botão e os
// campos de inserção
$(document).ready(function(){
if($("#id_parlamentar").val() == null || $("#id_cargo").val() == null){
$("#id_parlamentar").hide();
$("#id_cargo").hide();
$("#parlamentar-cargo-title").hide();
$('#id_incluir').hide();
}
else{
$("#id_parlamentar").show();
$("#id_cargo").show();
$("#parlamentar-cargo-title").show();
$('#id_incluir').show();
}
// Se a composição estiver vazia, deve-se esconder
// de Excluir
if (!$("#id_composicao_mesa option").val()){
$("#id_excluir").hide()
}
else{
$("#id_excluir").show()
}
});
//##############################################################
//# EVENT HANDLERS #
//##############################################################
function errors_handler(msg){
if (msg == null){
$("#div-success").hide()
$("#div-error").hide()
$("#success-message").html('')
$("#error-message").html('')
}
else{
if (msg[1] == 0){
$("#div-error").show()
$("#error-message").html(msg[0])
}
else{
$("#div-success").show()
$("#success-message").html(msg[0])
}
}
}
// Atualiza os campos após alguma operação de mudança da Legislatura/Sessao ou
// Inserção/Remoção
function altera_field(id_legislatura, id_sessao=null, msg=null){
// Pega o novo valor dos campos modificados
var sessao_value = id_sessao
var legislatura_value = id_legislatura
// Limpa os campos que serão atualizados
$("#id_sessao_legislativa option").remove();
$("#id_composicao_mesa option").remove();
$("#id_parlamentar option").remove();
$("#id_cargo option").remove();
$.get("{% url 'sapl.parlamentares:altera_field_mesa' %}",
{legislatura: legislatura_value, sessao: sessao_value},
function(data) {
// Caso não venha nenhum dado da requisição, retorna null
if ($.isEmptyObject(data)){
return null
}
lista_sessoes = data['lista_sessoes'];
lista_composicao = data['lista_composicao'];
lista_parlamentares = data['lista_parlamentares'];
lista_cargos = data['lista_cargos'];
// Atualiza a listagem dos campos
if (lista_sessoes != undefined) {
for (i = 0; i < lista_sessoes.length; i++) {
$('#id_sessao_legislativa').append('<option value="' + lista_sessoes[i][0] + '">' + lista_sessoes[i][1] + '</option>');
}
}
if (lista_composicao != null) {
for (i = 0; i < lista_composicao.length; i++) {
$('#id_composicao_mesa').append('<option value="' + lista_composicao[i][0] + '">' + lista_composicao[i][1] + ' || ' + lista_composicao[i][2] + '</option>');
}
}
// Caso ainda tenha parlamentares/cargos a serem inseridos, preenche a listagem com
// os disponíveis e garante que as ferramentas de inserção estejam disponíveis
if (lista_parlamentares != null && lista_parlamentares.length != 0 && lista_cargos.length != 0){
$('#id_incluir').show()
$('#id_cargo').show();
$('#id_parlamentar').show();
$("#parlamentar-cargo-title").show();
for (i = 0; i < lista_parlamentares.length; i++) {
$('#id_parlamentar').append('<option value="' + lista_parlamentares[i][0] + '">' + lista_parlamentares[i][1] + '</option>');
}
for (i = 0; i < lista_cargos.length; i++) {
$('#id_cargo').append('<option value="' + lista_cargos[i][0] + '">' + lista_cargos[i][1] + '</option>');
}
}
// Caso contrário, esconde do usuário essas opções
else{
$('#id_incluir').hide();
$('#id_cargo').hide();
$('#id_parlamentar').hide();
$("#parlamentar-cargo-title").hide();
}
// Garante que o botão de remoção aparecerá, caso tenha
// algum parlamentar na composição
if (lista_composicao != null && lista_composicao.length != 0){
$("#id_excluir").show()
}
else{
$("#id_excluir").hide()
}
// Garante que a Sessão atual será a selecionada previamente e, no caso em que
// o campo modificado seja o de Legislatura, que a Sessão seja a última daquela
// legislatura
$("#id_sessao_legislativa").val(data['sessao_selecionada'])
// Caso haja algum erro que venha após a alteração da legislatura/sessao
// Essa mensagem de erro é prioridade
if (data['msg'][1] == 0){
msg = data['msg']
}
errors_handler(msg)
});
}
//#############################################################
//# EVENTS CATCH #
//#############################################################
$("#id_legislatura").change(function(){
legislatura = $("#id_legislatura").val();
altera_field(legislatura);
});
$("#id_sessao_legislativa").change(function(){
legislatura = $("#id_legislatura").val();
sessao = $("#id_sessao_legislativa").val();
altera_field(legislatura, sessao);
});
$('#id_incluir').click(function(){
$.ajax({
data: {sessao: $("#id_sessao_legislativa").val(),
parlamentar: $("#id_parlamentar").val(),
cargo: $("#id_cargo").val()},
type: 'POST',
url: "{% url 'sapl.parlamentares:insere_parlamentar_composicao' %}",
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
success: function(data){
var msg = data['msg'];
legislatura = $("#id_legislatura").val();
sessao = $("#id_sessao_legislativa").val();
setTimeout(function(data){
// Atualiza os campos após a inserção
altera_field(legislatura, sessao, msg)
}, 500)
},
});
})
$('#id_excluir').click(function(){
// Pega o id do parlamentar que foi selecionado
selecionado = $("#id_composicao_mesa option:selected").val()
if (selecionado){
$.ajax({
data: {composicao_mesa: selecionado},
type: 'POST',
url: "{% url 'sapl.parlamentares:remove_parlamentar_composicao' %}",
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
success: function(data){
var msg = data['msg'];
legislatura = $("#id_legislatura").val();
sessao = $("#id_sessao_legislativa").val();
setTimeout(function(data){
// Atualiza os campos após a remoção
altera_field(legislatura, sessao, msg)
}, 500)
},
});
}
});
</script>
{% endblock %}

11
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

156
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 %}
<div class="actions search flex-grow-1 pr-3">
{% if filter.form %}
{% crispy filter.form %}
{% endif %}
</div>
{% endblock actions_search %}
{% block container_table_list %}
{% if perms.parlamentares.add_mesadiretora %}
{{ block.super }}
{% else %}
<div class="row">
<div class="col-12">
<ul class="nav nav-tabs" role="tablist">
{% for md in object_list %}
{% if request.GET.mesa == md.id|stringformat:"s" or not request.GET.mesa and forloop.first %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-mesa-{{ md.id }}-tab" data-toggle="tab" data-target="#tab-mesa-{{ md.id }}" type="button" role="tab" aria-controls="tab-mesa-{{ md.id }}" aria-selected="true">{{ md }}</button>
</li>
{% else %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-mesa-{{ md.id }}-tab" data-toggle="tab" data-target="#tab-mesa-{{ md.id }}" type="button" role="tab" aria-controls="tab-mesa-{{ md.id }}" aria-selected="false">{{ md }}</button>
</li>
{% endif %}
{% endfor %}
</ul>
<div class="tab-content">
{% for md in object_list %}
{% if request.GET.mesa == md.id|stringformat:"s" or not request.GET.mesa and forloop.first %}
<div class="lista-parlamentares tab-pane fade show active" id="tab-mesa-{{ md.id }}" role="tabpanel" aria-labelledby="tab-mesa-{{ md.id }}-tab">
{% else %}
<div class="lista-parlamentares tab-pane fade" id="tab-mesa-{{ md.id }}" role="tabpanel" aria-labelledby="tab-mesa-{{ md.id }}-tab">
{% endif %}
<i class="text-muted p-2 d-inline-block">(De {{md.data_inicio }} a {{ md.data_fim }})</i>
<a href="{% url 'sapl.parlamentares:mesadiretora_list' %}?mesa={{ md.id }}" class="btn btn-sm btn-link float-right mb-2">
<i class="fas fa-external-link-alt"></i>
</a>
<table class="table table-striped table-hover table-link-ordering m-0 p-0">
<thead>
<tr>
<th>Nome do Parlamentar</th>
<th>Partido</th>
<th>Cargo</th>
</tr>
</thead>
<tbody>
{% for composicao in md.composicaomesa_set.all %}
<tr>
<td>
<div id="d-flex">
{% if composicao.parlamentar.fotografia %}
<img class="img-fluid img-thumbnail" src="{% cropped_thumbnail composicao.parlamentar "cropping"%}">
{% endif %}
<a class="pl-2" href="{% url 'sapl.parlamentares:parlamentar_detail' composicao.parlamentar.pk %}">
{{composicao.parlamentar.nome_parlamentar}}</a>
</div>
</td>
<td>{{composicao.parlamentar.filiacao_atual}}</td>
<td>{{composicao.cargo}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endblock container_table_list %}
{% block extra_js %}
<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;
var mq = window.matchMedia('(max-width: 767px)');
function collapse(tabs) {
tabs.classList.remove('nav-tabs--open');
[].forEach.call(tabs.querySelectorAll('.nav-item'), function (item) {
item.style.display = item.querySelector('.nav-link.active') ? 'block' : 'none';
});
}
function expand(tabs) {
[].forEach.call(tabs.querySelectorAll('.nav-item'), function (item) {
item.style.display = 'block';
});
tabs.classList.add('nav-tabs--open');
}
function init() {
[].forEach.call(document.querySelectorAll('.nav-tabs'), function (tabs) {
if (tabs._dropdownBound) return;
tabs._dropdownBound = true;
// Expande ao clicar no container (apenas quando fechado)
tabs.addEventListener('click', function () {
if (!mq.matches || tabs.classList.contains('nav-tabs--open')) return;
expand(tabs);
setTimeout(function () {
document.addEventListener('click', function onOutside(ev) {
if (!tabs.contains(ev.target)) {
collapse(tabs);
document.removeEventListener('click', onOutside);
}
});
}, 0);
});
// Colapsa ao selecionar uma aba (apenas quando aberto)
[].forEach.call(tabs.querySelectorAll('.nav-link'), function (link) {
link.addEventListener('click', function () {
if (!tabs.classList.contains('nav-tabs--open')) return;
// Aguarda Bootstrap atualizar a classe .active
setTimeout(function () { collapse(tabs); }, 50);
});
});
});
// Estado inicial e ao redimensionar
function applyState(matches) {
[].forEach.call(document.querySelectorAll('.nav-tabs'), function (tabs) {
tabs.classList.remove('nav-tabs--open');
[].forEach.call(tabs.querySelectorAll('.nav-item'), function (item) {
item.style.display = '';
});
if (matches) collapse(tabs);
});
}
applyState(mq.matches);
if (mq.addEventListener) {
mq.addEventListener('change', function (e) { applyState(e.matches); });
} else {
mq.addListener(function (e) { applyState(e.matches); });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}());
</script>
{% endblock extra_js %}

146
sapl/templates/parlamentares/public_composicaomesa_form.html

@ -1,146 +0,0 @@
{% extends "crud/detail.html" %}
{% load i18n cropping%}
{% block actions %} {% endblock %}
{% block detail_content %}
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend>Escolha da Legislatura e da Sessão Legislativa</legend>
<div class="row">
<div class="col-md-6">
<label>Escolha uma Legislatura</label>
<select name="legislatura" id="id_legislatura" class="form-control">
{% for l in legislaturas %}
<option value="{{l.id}}" {% if l == legislatura_selecionada %} selected {% endif %}>
{{l}}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label>Escolha uma Sessão Legislativa</label>
<select name="sessao" id="id_sessao_legislativa" class="form-control">
{% for s in sessoes %}
<option value="{{s.id}}" {% if s == sessao_selecionada %} selected {% endif %}>
{{s}}
</option>
{% endfor %}
</select>
</div>
</div>
</fieldset>
<br />
<fieldset class="form-group lista-parlamentares">
<legend>Composição da Mesa Diretora</legend>
<table id="tabela-composicao" class="table table-striped table-hover table-link-ordering">
<thead>
<tr>
<th></th>
<th>Nome do Parlamentar</th>
<th>Partido</th>
<th>Cargo</th>
</tr>
</thead>
<tbody>
{% for p in composicao_mesa %}
<tr>
{% if p.parlamentar.fotografia %}
<td>
<div id="w-100">
<img class="img-fluid img-thumbnail" src="{% cropped_thumbnail p.parlamentar "cropping"%}">
</div>
{% else %}
<td></td>
{% endif %}
<td><a href="{% url 'sapl.parlamentares:parlamentar_detail' p.parlamentar.pk %}">{{p.parlamentar.nome_parlamentar}}</a></td>
<td>{{p.parlamentar.filiacao_atual}}</td>
<td>{{p.cargo}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
</form>
{% endblock detail_content %}
{% block extra_js %}
<script language="Javascript">
//##############################################################
//# USEFUL FUNCTIONS #
//##############################################################
function retorna_imagem(url){
if (url != null){
image_component = '<img class="img-fluid img-thumbnail" src="' + url + '">'
return image_component
}
else{
return ''
}
}
//##############################################################
//# EVENT HANDLERS #
//##############################################################
function altera_field(id_legislatura, id_sessao=null){
// Pega o novo valor do campo que foi modificado (Sessao/Legislatura)
var sessao_value = id_sessao
var legislatura_value = id_legislatura
// Limpa o campo que será atualizado
$("#id_sessao_legislativa option").remove();
$("#tabela-composicao tbody").empty();
$.get("/mesa-diretora/altera-field-mesa-public-view",
{legislatura: legislatura_value, sessao: sessao_value},
function(data) {
// Caso não venha nenhum dado da requisição, retorna null
if ($.isEmptyObject(data)){
return null
}
lista_sessoes = data['lista_sessoes'];
lista_parlamentares = data['lista_parlamentares'];
lista_cargos = data['lista_cargos'];
lista_fotos = data['lista_fotos']
lista_partidos = data['lista_partidos']
// Atualiza a listagem de sessões
for (i = 0; i < lista_sessoes.length; i++) {
$('#id_sessao_legislativa').append('<option value="' + lista_sessoes[i][0] + '">' + lista_sessoes[i][1] + '</option>');
}
// Atualiza a tabela de composição
for (i = 0; i < lista_parlamentares.length; i++) {
$("#tabela-composicao tbody").append('<tr> <td> <div class="w-100">' + retorna_imagem(lista_fotos[i]) +' </div> </td> <td><a href="/parlamentar/' + lista_parlamentares[i][0] + '">' + lista_parlamentares[i][1] + '</a></td> <td>' + lista_partidos[i] + '</td> <td>'+ lista_cargos[i][1] + '</td> </tr>');
}
// Garante que a Sessão atual será a selecionada previamente e, no caso em que
// o campo modificado seja o de Legislatura, que a Sessão seja a última daquela
// legislatura
$("#id_sessao_legislativa").val(data['sessao_selecionada'])
});
}
//#############################################################
//# EVENTS CATCH #
//#############################################################
$("#id_legislatura").change(function(){
legislatura = $("#id_legislatura").val();
altera_field(legislatura);
});
$("#id_sessao_legislativa").change(function(){
legislatura = $("#id_legislatura").val();
sessao = $("#id_sessao_legislativa").val();
altera_field(legislatura, sessao);
});
</script>
{% endblock %}

5
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

6
sapl/utils.py

@ -262,6 +262,12 @@ def montar_helper_autor(self):
' class="btn btn-dark">Cancelar</a>')]))
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__(

Loading…
Cancel
Save