Browse Source

Merge branch '3.1.x' into 3828_refatorar_mesa_diretora

3828_refatorar_mesa_diretora
joaohortsenado 2 days ago
committed by GitHub
parent
commit
95df206635
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      sapl/audiencia/forms.py
  2. 4
      sapl/materia/forms.py
  3. 6
      sapl/relatorios/forms.py
  4. 53
      sapl/relatorios/tests.py
  5. 13
      sapl/relatorios/views.py
  6. 20
      sapl/sessao/models.py
  7. 90
      sapl/sessao/tests/test_sessao_view.py
  8. 21
      sapl/sessao/views.py
  9. 14
      sapl/templates/relatorios/RelatorioMateriasPorAutor_filter.html
  10. 8
      sapl/templates/relatorios/RelatorioNormasPorAutor_filter.html
  11. 2
      sapl/templates/relatorios/blocos_sessao_plenaria/ocorrencias_da_sessao.html
  12. 8
      sapl/utils.py

2
sapl/audiencia/forms.py

@ -115,7 +115,7 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
numero=materia,
ano=ano_materia,
tipo=tipo_materia)
except ObjectDoesNotExist:
except (ObjectDoesNotExist, ValueError):
msg = _('A matéria %s%s/%s não existe no cadastro'
' de matérias legislativas.' % (tipo_materia, materia, ano_materia))
self.logger.warning(

4
sapl/materia/forms.py

@ -1944,10 +1944,6 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
fields = [
to_column((Fieldset(
TipoProposicao._meta.verbose_name, Field('tipo')), 12)),
to_column(
(Alert('teste',
css_class="ementa_materia hidden alert-info",
dismiss=False), 12)),
to_column(('descricao', 12)),
to_column(('observacao', 12)),

6
sapl/relatorios/forms.py

@ -633,6 +633,8 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet):
@property
def qs(self):
parent = super().qs
if not self.data.get('autoria__autor'):
return parent.none()
return parent.distinct().order_by('-ano', '-numero', 'tipo', 'autoria__autor', '-autoria__primeiro_autor')
class Meta(FilterOverridesMetaMixin):
@ -744,7 +746,9 @@ class RelatorioNormasPorAutorFilterSet(django_filters.FilterSet):
@property
def qs(self):
parent = super().qs
return parent.distinct().filter(autorianorma__primeiro_autor=True) \
if not self.data.get('autorianorma__autor'):
return parent.none()
return parent.distinct() \
.order_by('autorianorma__autor', '-autorianorma__primeiro_autor', 'tipo', '-ano', '-numero')
class Meta(FilterOverridesMetaMixin):

53
sapl/relatorios/tests.py

@ -1,3 +1,52 @@
# from django.test import TestCase
import pytest
from model_bakery import baker
# Create your tests here.
from sapl.base.models import CasaLegislativa
from sapl.relatorios.views import get_sessao_plenaria
from sapl.sessao.models import ExpedienteSessao, SessaoPlenaria, TipoExpediente
from sapl.sessao.views import get_expedientes
def cria_sessao_com_expedientes():
"""Cria uma sessão cujos expedientes têm `ordenacao` inversa à ordem
alfabética dos nomes -- o caso que expõe o OSTicket #125461."""
sessao = baker.make(SessaoPlenaria)
tipo_leitura = baker.make(
TipoExpediente, nome='Leitura e Aprovação da Ata', ordenacao=1)
tipo_grande = baker.make(
TipoExpediente, nome='Grande Expediente', ordenacao=2)
for tipo in (tipo_grande, tipo_leitura):
baker.make(ExpedienteSessao, sessao_plenaria=sessao, tipo=tipo,
conteudo='<p>Conteúdo de {}.</p>'.format(tipo.nome))
return sessao
@pytest.mark.django_db(transaction=False)
def test_relatorio_sessao_respeita_ordenacao_do_tipo_expediente():
"""O PDF da Sessão Plenária deve seguir o campo `ordenacao` de
TipoExpediente, e não a ordem alfabética do nome."""
sessao = cria_sessao_com_expedientes()
casa = baker.make(CasaLegislativa)
user = baker.make('auth.User')
lst_expedientes = get_sessao_plenaria(sessao, casa, user)[6]
nomes = [e['nom_expediente'] for e in lst_expedientes]
assert nomes == ['Leitura e Aprovação da Ata', 'Grande Expediente']
@pytest.mark.django_db(transaction=False)
def test_relatorio_sessao_tem_mesma_ordem_de_expedientes_do_resumo():
"""O PDF e o Resumo exibido em tela não podem divergir: foi essa
divergência que originou o OSTicket #125461."""
sessao = cria_sessao_com_expedientes()
casa = baker.make(CasaLegislativa)
user = baker.make('auth.User')
do_pdf = [e['nom_expediente']
for e in get_sessao_plenaria(sessao, casa, user)[6]]
da_tela = [e['tipo'].nome for e in get_expedientes(sessao)['expedientes']]
assert do_pdf == da_tela

13
sapl/relatorios/views.py

@ -609,8 +609,10 @@ def get_sessao_plenaria(sessao, casa, user):
# Exibe os Expedientes
lst_expedientes = []
# A ordenação deve ser a mesma de sapl.sessao.views.get_expedientes, para
# que o PDF confira com o Resumo exibido em tela. OSTicket #125461
expedientes = ExpedienteSessao.objects.filter(
sessao_plenaria=sessao).order_by('tipo__nome')
sessao_plenaria=sessao).order_by('tipo__ordenacao', 'tipo__nome')
for e in expedientes:
conteudo = e.conteudo
if not is_empty(conteudo):
@ -2660,7 +2662,6 @@ class RelatorioMateriasPorAutorView(RelatorioMixin, FilterView):
return context
qs = context['object_list']
context['materias_resultado'] = list(collections.OrderedDict.fromkeys(qs))
context['qtdes'] = num_materias_por_tipo(qs)
qr = self.request.GET.copy()
@ -2675,9 +2676,9 @@ class RelatorioMateriasPorAutorView(RelatorioMixin, FilterView):
context['tipo'] = ''
if self.request.GET['autoria__autor']:
autor = int(self.request.GET['autoria__autor'])
context['autor'] = (str(Autor.objects.get(id=autor)))
context['autor'] = Autor.objects.get(id=autor)
else:
context['autor'] = ''
context['autor'] = None
context['periodo'] = (
self.request.GET['data_apresentacao_0'] +
' - ' + self.request.GET['data_apresentacao_1'])
@ -2921,9 +2922,9 @@ class RelatorioNormasPorAutorView(RelatorioMixin, FilterView):
if self.request.GET['autorianorma__autor']:
autor = int(self.request.GET['autorianorma__autor'])
context['autor'] = (str(Autor.objects.get(id=autor)))
context['autor'] = Autor.objects.get(id=autor)
else:
context['autor'] = ''
context['autor'] = None
context['periodo'] = (
self.request.GET['data_0'] +
' - ' + self.request.GET['data_1'])

20
sapl/sessao/models.py

@ -153,6 +153,26 @@ def anexo_upload_path(instance, filename):
# return get_sessao_media_path(instance, 'anexo', filename)
def restringe_sessoes_visiveis(qs, user):
"""Restringe o queryset às sessões visíveis na consulta pública.
Usuário autenticado enxerga todas. Para o público, ficam ocultas as
sessões cadastradas com antecedência, ou seja, as marcadas como não
iniciadas e sem pauta publicada.
`iniciada` nulo conta como visível: o default do campo surgiu na
migração 0027, que não preencheu as linhas existentes, então bases
antigas têm sessões realizadas com o campo em NULL.
"""
if user.is_authenticated:
return qs
return qs.filter(
Q(publicar_pauta=True) |
Q(iniciada=True) |
Q(iniciada__isnull=True))
class SessaoPlenaria(models.Model):
# TODO trash??? Seems to have been a FK in the past. Would be:
# andamento_sessao = models.ForeignKey(

90
sapl/sessao/tests/test_sessao_view.py

@ -1,4 +1,7 @@
import pytest
from datetime import date
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from model_bakery import baker
@ -8,7 +11,8 @@ from sapl.sessao.models import (SessaoPlenaria, TipoSessaoPlenaria,
IntegranteMesa, SessaoPlenariaPresenca,
JustificativaAusencia, ExpedienteSessao,
TipoExpediente, ExpedienteMateria,
Orador, OcorrenciaSessao)
Orador, OcorrenciaSessao,
restringe_sessoes_visiveis)
from sapl.parlamentares.models import Parlamentar, CargoMesa, Filiacao
@ -147,3 +151,87 @@ class TestResumoView():
resultado_get_ocorrencia = get_ocorrencias_da_sessao(self.sessao_plenaria)
assert resultado_get_ocorrencia['ocorrencias_da_sessao'][0] == ocorrencia
@pytest.mark.django_db(transaction=False)
def test_visiveis_para_oculta_do_anonimo_apenas_a_sessao_previa():
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
com_pauta = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=True)
iniciada = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False)
# Sessões anteriores à migração 0027 ficaram com `iniciada` em NULL.
legada = baker.make(SessaoPlenaria, iniciada=None, publicar_pauta=False)
visiveis = restringe_sessoes_visiveis(
SessaoPlenaria.objects.all(), AnonymousUser())
assert previa not in visiveis
assert com_pauta in visiveis
assert iniciada in visiveis
assert legada in visiveis
@pytest.mark.django_db(transaction=False)
def test_visiveis_para_nao_oculta_nada_de_usuario_autenticado():
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
operador = baker.make(get_user_model())
assert previa in restringe_sessoes_visiveis(
SessaoPlenaria.objects.all(), operador)
@pytest.mark.django_db(transaction=False)
def test_pesquisar_sessao_nao_lista_sessao_previa_para_anonimo(client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False,
data_inicio=date(2025, 11, 5))
iniciada = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False,
data_inicio=date(2025, 11, 5))
response = client.get(reverse('sapl.sessao:pesquisar_sessao'),
{'data_inicio__year': '2025'})
assert response.status_code == 200
assert previa not in response.context['object_list']
assert iniciada in response.context['object_list']
@pytest.mark.django_db(transaction=False)
def test_pesquisar_sessao_lista_sessao_previa_para_autenticado(admin_client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False,
data_inicio=date(2025, 11, 5))
response = admin_client.get(reverse('sapl.sessao:pesquisar_sessao'),
{'data_inicio__year': '2025'})
assert response.status_code == 200
assert previa in response.context['object_list']
@pytest.mark.django_db(transaction=False)
def test_detail_sessao_previa_indisponivel_para_anonimo(client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
response = client.get(reverse('sapl.sessao:sessaoplenaria_detail',
kwargs={'pk': previa.pk}))
assert response.status_code == 404
@pytest.mark.django_db(transaction=False)
def test_resumo_de_sessao_previa_indisponivel_para_anonimo(client):
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False)
response = client.get(reverse('sapl.sessao:resumo',
kwargs={'pk': previa.pk}))
assert response.status_code == 404
@pytest.mark.django_db(transaction=False)
def test_pauta_nao_publicada_indisponivel_para_anonimo(client):
sem_pauta = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False)
response = client.get(reverse('sapl.sessao:pauta_sessao_detail',
kwargs={'pk': sem_pauta.pk}))
assert response.status_code == 404

21
sapl/sessao/views.py

@ -64,7 +64,7 @@ from .models import (Bancada, CargoBancada, CargoMesa,
SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente,
TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar, TipoRetiradaPauta,
RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia,
ORDENACAO_RESUMO, RegistroLeitura)
ORDENACAO_RESUMO, RegistroLeitura, restringe_sessoes_visiveis)
TipoSessaoCrud = CrudAux.build(TipoSessaoPlenaria, 'tipo_sessao_plenaria')
TipoJustificativaCrud = CrudAux.build(TipoJustificativa, 'tipo_justificativa')
@ -1348,6 +1348,13 @@ class SessaoCrud(Crud):
class DetailView(Crud.DetailView):
def get(self, request, *args, **kwargs):
if not restringe_sessoes_visiveis(
SessaoPlenaria.objects.filter(pk=kwargs.get('pk')),
request.user).exists():
raise Http404()
return super().get(request, *args, **kwargs)
@property
def layout_key(self):
sessao = self.object
@ -2327,6 +2334,10 @@ class ResumoView(DetailView):
model = SessaoPlenaria
logger = logging.getLogger(__name__)
def get_queryset(self):
return restringe_sessoes_visiveis(
SessaoPlenaria.objects.all(), self.request.user)
def get_context(self, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
@ -3833,6 +3844,12 @@ class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
('situacao', 'Situação')
)
def get_queryset(self):
qs = SessaoPlenaria.objects.all()
if not self.request.user.is_authenticated:
qs = qs.filter(publicar_pauta=True)
return qs
def hook_autor(self, obj):
return ','.join(obj['autor'])
@ -4028,6 +4045,8 @@ class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView):
qs = self.get_queryset().select_related(
'tipo', 'sessao_legislativa', 'legislatura')
qs = restringe_sessoes_visiveis(qs, self.request.user)
qs = qs.distinct().order_by(
'-legislatura__numero', '-data_inicio', '-hora_inicio')

14
sapl/templates/relatorios/RelatorioMateriasPorAutor_filter.html

@ -17,7 +17,7 @@
&emsp;Tipo de matéria: {{ tipo }}<br />
&emsp;Data de apresentação: {{ periodo }}<br /><br /><br/>
{% if materias_resultado %}
{% if object_list %}
<table class="table table-bordered table-hover">
<thead class="thead-default" >
<tr class="active">
@ -43,10 +43,10 @@
<tr class="active">
<th width="10%">Matéria</th>
<th>Ementa</th>
<th width="20%">Autor(es)</th>
<th width="20%">Coautor(es)</th>
</tr>
</thead>
{% for materia in materias_resultado %}
{% for materia in object_list %}
<tbody>
<tr>
<td>
@ -56,11 +56,9 @@
</td>
<td>{% autoescape off %}{{materia.ementa}}<br>{{materia.observacao}}{% endautoescape %}</td>
<td>
{% for autor in materia.autoria_set.all %}
{% if not autor.primeiro_autor %}
{{ autor.autor }}<br />
{% else %}
<u><b><i>{{ autor.autor }}</i></b></u><br />
{% for autoria in materia.autoria_set.all %}
{% if autoria.autor != autor %}
{{ autoria.autor }}<br />
{% endif %}
{% endfor %}
</td>

8
sapl/templates/relatorios/RelatorioNormasPorAutor_filter.html

@ -54,13 +54,11 @@
</a></td>
<td>{% autoescape off %}{{norma.ementa}}<br>{{norma.observacao}}{% endautoescape %}</td>
<td>
{% if norma.autorianorma_set.first != norma.autorianorma_set.last %}
{% for autor in norma.autorianorma_set.all %}
{% if not autor.primeiro_autor %}
{{ autor.autor }}<br />
{% for autoria in norma.autorianorma_set.all %}
{% if autoria.autor != autor %}
{{ autoria.autor }}<br />
{% endif %}
{% endfor %}
{% endif %}
</td>
</tr>
</tbody>

2
sapl/templates/relatorios/blocos_sessao_plenaria/ocorrencias_da_sessao.html

@ -1,4 +1,4 @@
<h2 class="gray-title">Ocorrências da Sessão</h2>
{% for o in lst_ocorrencias%}
<p>{{o|striptags|safe}}</p>
<p>{{o.conteudo|striptags|safe}}</p>
{% endfor %}

8
sapl/utils.py

@ -617,6 +617,9 @@ TIPOS_TEXTO_PERMITIDOS = (
'text/pdf',
'text/x-pdf',
'text/plain',
'text/csv',
'application/vnd.ms-excel',
'application/csv',
'application/txt',
'browser/internal',
'text/anytext',
@ -1401,8 +1404,9 @@ class MultiFormatOutputMixin:
return {fmt: fields for fmt in self.formats_impl}
def render_to_response(self, context, **response_kwargs):
format_result = getattr(self.request, self.request.method).get(
'format', None)
# HEAD shares query params with GET; Django has no request.HEAD attribute.
_method = 'GET' if self.request.method == 'HEAD' else self.request.method
format_result = getattr(self.request, _method).get('format', None)
if format_result:
if format_result not in self.formats_impl:

Loading…
Cancel
Save