Browse Source

resolvendo conflitos

pull/3001/head
ricardocanela 6 years ago
parent
commit
390ca90a1d
  1. 10
      .dockerignore
  2. 5
      Dockerfile
  3. 6
      requirements/requirements.txt
  4. 34
      sapl/api/views.py
  5. 31
      sapl/audiencia/forms.py
  6. 32
      sapl/audiencia/migrations/0013_auto_20191023_1522.py
  7. 21
      sapl/audiencia/migrations/0014_auto_20191023_1538.py
  8. 8
      sapl/audiencia/models.py
  9. 31
      sapl/base/admin.py
  10. 21
      sapl/base/forms.py
  11. 33
      sapl/base/migrations/0038_auditlog.py
  12. 16
      sapl/base/migrations/0040_merge_20191030_1655.py
  13. 20
      sapl/base/migrations/0041_auto_20191204_1130.py
  14. 42
      sapl/base/models.py
  15. 42
      sapl/base/receivers.py
  16. 3
      sapl/base/signals.py
  17. 8
      sapl/base/tests/test_login.py
  18. 2
      sapl/base/views.py
  19. 43
      sapl/comissoes/forms.py
  20. 42
      sapl/comissoes/migrations/0022_auto_20191120_1440.py
  21. 10
      sapl/comissoes/models.py
  22. 58
      sapl/compilacao/forms.py
  23. 27
      sapl/compilacao/migrations/0013_auto_20190924_0830.py
  24. 29
      sapl/compilacao/models.py
  25. 3
      sapl/compilacao/templatetags/compilacao_filters.py
  26. 406
      sapl/compilacao/views.py
  27. 7
      sapl/crispy_layout_mixin.py
  28. 65
      sapl/crud/base.py
  29. 30
      sapl/materia/forms.py
  30. 2
      sapl/materia/migrations/0058_auto_20191001_1450.py
  31. 16
      sapl/materia/migrations/0059_merge_20191003_0854.py
  32. 4
      sapl/materia/migrations/0060_auto_20190930_1136.py
  33. 37
      sapl/materia/migrations/0061_auto_20191120_1440.py
  34. 2
      sapl/materia/migrations/0062_auto_20190905_1134.py
  35. 2
      sapl/materia/migrations/0063_auto_20190905_1135.py
  36. 2
      sapl/materia/migrations/0064_remove_tramitacao_turno.py
  37. 8
      sapl/materia/models.py
  38. 25
      sapl/materia/views.py
  39. 43
      sapl/norma/forms.py
  40. 22
      sapl/norma/migrations/0028_auto_20191024_1330.py
  41. 22
      sapl/norma/migrations/0029_auto_20191024_1344.py
  42. 16
      sapl/norma/migrations/0030_merge_20191204_1129.py
  43. 18
      sapl/norma/models.py
  44. 24
      sapl/norma/views.py
  45. 9
      sapl/painel/urls.py
  46. 93
      sapl/painel/views.py
  47. 10
      sapl/parlamentares/forms.py
  48. 24
      sapl/parlamentares/tests/test_parlamentares.py
  49. 39
      sapl/parlamentares/views.py
  50. 47
      sapl/protocoloadm/forms.py
  51. 31
      sapl/protocoloadm/migrations/0026_auto_20191120_1440.py
  52. 6
      sapl/protocoloadm/models.py
  53. 53
      sapl/protocoloadm/views.py
  54. 5
      sapl/rules/map_rules.py
  55. 128
      sapl/sessao/forms.py
  56. 3
      sapl/sessao/migrations/0047_auto_20190829_1253.py
  57. 2
      sapl/sessao/migrations/0047_merge_20191009_1535.py
  58. 32
      sapl/sessao/migrations/0048_auto_20191029_1418.py
  59. 32
      sapl/sessao/migrations/0049_auto_20191029_1434.py
  60. 22
      sapl/sessao/migrations/0050_auto_20191029_1441.py
  61. 13
      sapl/sessao/migrations/0051_merge_20191209_0910.py
  62. 91
      sapl/sessao/migrations/0052_auto_20191209_0828.py
  63. 77
      sapl/sessao/models.py
  64. 28
      sapl/sessao/urls.py
  65. 136
      sapl/sessao/views.py
  66. 4
      sapl/settings.py
  67. 0
      sapl/static/sapl/frontend/css/chunk-vendors.c41142f2.css
  68. 0
      sapl/static/sapl/frontend/css/chunk-vendors.c41142f2.css.gz
  69. 1
      sapl/static/sapl/frontend/css/compilacao.3d691cd9.css
  70. BIN
      sapl/static/sapl/frontend/css/compilacao.3d691cd9.css.gz
  71. 1
      sapl/static/sapl/frontend/css/compilacao.eff62463.css
  72. BIN
      sapl/static/sapl/frontend/css/compilacao.eff62463.css.gz
  73. BIN
      sapl/static/sapl/frontend/css/painel.5d957a9b.css.gz
  74. 2
      sapl/static/sapl/frontend/css/painel.fd494bea.css
  75. BIN
      sapl/static/sapl/frontend/css/painel.fd494bea.css.gz
  76. 1
      sapl/static/sapl/frontend/css/sessao.559521f7.css
  77. 0
      sapl/static/sapl/frontend/js/chunk-09995afe.98f3367d.js
  78. 0
      sapl/static/sapl/frontend/js/chunk-09995afe.98f3367d.js.gz
  79. 0
      sapl/static/sapl/frontend/js/chunk-2d0c4a82.624674a7.js
  80. 0
      sapl/static/sapl/frontend/js/chunk-2d0c4a82.624674a7.js.gz
  81. 0
      sapl/static/sapl/frontend/js/chunk-2d0e8be2.67905a03.js
  82. 0
      sapl/static/sapl/frontend/js/chunk-2d0e8be2.67905a03.js.gz
  83. 0
      sapl/static/sapl/frontend/js/chunk-31d76f93.968a637a.js
  84. 0
      sapl/static/sapl/frontend/js/chunk-31d76f93.968a637a.js.gz
  85. 0
      sapl/static/sapl/frontend/js/chunk-681dd124.08a628b0.js
  86. 0
      sapl/static/sapl/frontend/js/chunk-681dd124.08a628b0.js.gz
  87. BIN
      sapl/static/sapl/frontend/js/chunk-vendors.3ef59900.js.gz
  88. 73
      sapl/static/sapl/frontend/js/chunk-vendors.dfcd310b.js
  89. BIN
      sapl/static/sapl/frontend/js/chunk-vendors.dfcd310b.js.gz
  90. 1
      sapl/static/sapl/frontend/js/compilacao.0684a9a9.js
  91. BIN
      sapl/static/sapl/frontend/js/compilacao.0684a9a9.js.gz
  92. 1
      sapl/static/sapl/frontend/js/compilacao.6b8172da.js
  93. BIN
      sapl/static/sapl/frontend/js/compilacao.6b8172da.js.gz
  94. 7
      sapl/static/sapl/frontend/js/global.3df943f5.js
  95. BIN
      sapl/static/sapl/frontend/js/global.3df943f5.js.gz
  96. 7
      sapl/static/sapl/frontend/js/global.ce91d2f8.js
  97. BIN
      sapl/static/sapl/frontend/js/global.ce91d2f8.js.gz
  98. 2
      sapl/static/sapl/frontend/js/online.0f0e1e97.js
  99. BIN
      sapl/static/sapl/frontend/js/online.0f0e1e97.js.gz
  100. BIN
      sapl/static/sapl/frontend/js/online.487f3d5a.js.gz

10
.dockerignore

@ -1,4 +1,14 @@
media media
collected_static collected_static
.git .git
.gitignore
whoosh whoosh
bower
*.log
~*
*.pyc
.cache
.project
.travis.yml
.env
.idea

5
Dockerfile

@ -1,9 +1,9 @@
FROM alpine:3.8 FROM alpine:3.10
ENV BUILD_PACKAGES postgresql-dev graphviz-dev graphviz build-base git pkgconfig \ ENV BUILD_PACKAGES postgresql-dev graphviz-dev graphviz build-base git pkgconfig \
python3-dev libxml2-dev jpeg-dev libressl-dev libffi-dev libxslt-dev \ python3-dev libxml2-dev jpeg-dev libressl-dev libffi-dev libxslt-dev \
nodejs py3-lxml py3-magic postgresql-client poppler-utils antiword \ nodejs py3-lxml py3-magic postgresql-client poppler-utils antiword \
curl jq openssh-client vim bash curl jq openssh-client vim bash postgresql-client cairo-dev
RUN apk update --update-cache && apk upgrade RUN apk update --update-cache && apk upgrade
@ -13,6 +13,7 @@ RUN apk add --no-cache python3 nginx tzdata && \
python3 -m ensurepip && \ python3 -m ensurepip && \
rm -r /usr/lib/python*/ensurepip && \ rm -r /usr/lib/python*/ensurepip && \
pip3 install --upgrade pip setuptools && \ pip3 install --upgrade pip setuptools && \
pip3 install wheel && \
rm -r /root/.cache && \ rm -r /root/.cache && \
rm -f /etc/nginx/conf.d/* rm -f /etc/nginx/conf.d/*

6
requirements/requirements.txt

@ -20,12 +20,12 @@ easy-thumbnails==2.5
python-decouple==3.1 python-decouple==3.1
psycopg2-binary==2.7.6.1 psycopg2-binary==2.7.6.1
pyyaml==4.2b1 pyyaml==4.2b1
pytz==2018.9 pytz==2019.3
rtyaml==0.0.5 rtyaml==0.0.5
python-magic==0.4.15 python-magic==0.4.15
unipath==1.1 unipath==1.1
WeasyPrint==44 WeasyPrint==50
Pillow==5.1.0 Pillow==6.2.0
gunicorn==19.9.0 gunicorn==19.9.0
kombu==4.2.0 kombu==4.2.0
billiard==3.5.0.2 billiard==3.5.0.2

34
sapl/api/views.py

@ -3,6 +3,7 @@ import logging
from django import apps from django import apps
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.db.models import Q from django.db.models import Q
from django.db.models.fields.files import FileField from django.db.models.fields.files import FileField
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
@ -26,10 +27,11 @@ from sapl.base.models import Autor, AppConfig, DOC_ADM_OSTENSIVO
from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\ from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\
MateriaLegislativa, Tramitacao MateriaLegislativa, Tramitacao
from sapl.norma.models import NormaJuridica from sapl.norma.models import NormaJuridica
from sapl.painel.models import Cronometro
from sapl.parlamentares.models import Parlamentar from sapl.parlamentares.models import Parlamentar
from sapl.protocoloadm.models import DocumentoAdministrativo,\ from sapl.protocoloadm.models import DocumentoAdministrativo,\
DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado
from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao, SessaoPlenariaPresenca
from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria
@ -514,7 +516,6 @@ class _SessaoPlenariaViewSet:
@action(detail=False) @action(detail=False)
def years(self, request, *args, **kwargs): def years(self, request, *args, **kwargs):
years = choice_anos_com_sessaoplenaria() years = choice_anos_com_sessaoplenaria()
serializer = ChoiceSerializer(years, many=True) serializer = ChoiceSerializer(years, many=True)
return Response(serializer.data) return Response(serializer.data)
@ -533,6 +534,21 @@ class _SessaoPlenariaViewSet:
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['GET'])
def parlamentares_presentes(self, request, *args, **kwargs):
sessao = self.get_object()
parlamentares = Parlamentar.objects.filter(sessaoplenariapresenca__sessao_plenaria=sessao)
page = self.paginate_queryset(parlamentares)
if page is not None:
serializer = SaplApiViewSetConstrutor.get_class_for_model(
Parlamentar).serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(page, many=True)
return Response(serializer.data)
@customize(NormaJuridica) @customize(NormaJuridica)
class _NormaJuridicaViewset: class _NormaJuridicaViewset:
@ -540,3 +556,17 @@ class _NormaJuridicaViewset:
def destaques(self, request, *args, **kwargs): def destaques(self, request, *args, **kwargs):
self.queryset = self.get_queryset().filter(norma_de_destaque=True) self.queryset = self.get_queryset().filter(norma_de_destaque=True)
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
@customize(Cronometro)
class _CronometroViewSet:
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset()
try:
filter_condition = {k:v[0] for (k,v) in self.request.GET.items()}
qs = qs.filter(**filter_condition)
except FieldError as e:
pass
return qs

31
sapl/audiencia/forms.py

@ -1,17 +1,16 @@
import logging import logging
from django import forms from django import forms
from sapl.settings import MAX_DOC_UPLOAD_SIZE
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import transaction from django.db import transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica, AnexoAudienciaPublica
from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout
from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import Button, Column, Fieldset, HTML, Layout
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica, AnexoAudienciaPublica
from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, SaplFormLayout, to_row
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.utils import timezone, FileFieldCheckMixin from sapl.utils import timezone, FileFieldCheckMixin, validar_arquivo
class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -119,17 +118,14 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
upload_ata = self.cleaned_data.get('upload_ata', False) upload_ata = self.cleaned_data.get('upload_ata', False)
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_pauta and upload_pauta.size > MAX_DOC_UPLOAD_SIZE: if upload_pauta:
raise ValidationError("O arquivo Pauta da Audiência Pública deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_pauta, "Pauta da Audiência Pública")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_pauta.size/1024)/1024))
if upload_ata and upload_ata.size > MAX_DOC_UPLOAD_SIZE: if upload_ata:
raise ValidationError("O arquivo Ata da Audiência Pública deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_ata, "Ata da Audiência Pública")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_ata.size/1024)/1024))
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE: if upload_anexo:
raise ValidationError("O arquivo Anexo da Audiência Pública deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_anexo, "Anexo da Audiência Pública")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
return cleaned_data return cleaned_data
@ -164,8 +160,7 @@ class AnexoAudienciaPublicaForm(forms.ModelForm):
arquivo = self.cleaned_data.get('arquivo', False) arquivo = self.cleaned_data.get('arquivo', False)
if arquivo and arquivo.size > MAX_DOC_UPLOAD_SIZE: if arquivo:
raise ValidationError("O arquivo deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(arquivo, "Arquivo")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (arquivo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data

32
sapl/audiencia/migrations/0013_auto_20191023_1522.py

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-23 18:22
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.audiencia.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0012_auto_20191001_1115'),
]
operations = [
migrations.AlterField(
model_name='audienciapublica',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.audiencia.models.anexo_upload_path, verbose_name='Anexo da Audiência Pública'),
),
migrations.AlterField(
model_name='audienciapublica',
name='upload_ata',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.audiencia.models.ata_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Ata da Audiência Pública'),
),
migrations.AlterField(
model_name='audienciapublica',
name='upload_pauta',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.audiencia.models.pauta_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Pauta da Audiência Pública'),
),
]

21
sapl/audiencia/migrations/0014_auto_20191023_1538.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-23 18:38
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0013_auto_20191023_1522'),
]
operations = [
migrations.AlterField(
model_name='anexoaudienciapublica',
name='arquivo',
field=models.FileField(max_length=300, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.utils.texto_upload_path, verbose_name='Arquivo'),
),
]

8
sapl/audiencia/models.py

@ -87,7 +87,7 @@ class AudienciaPublica(models.Model):
max_length=150, blank=True, max_length=150, blank=True,
verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)')) verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)'))
upload_pauta = models.FileField( upload_pauta = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=pauta_upload_path, upload_to=pauta_upload_path,
@ -95,7 +95,7 @@ class AudienciaPublica(models.Model):
verbose_name=_('Pauta da Audiência Pública'), verbose_name=_('Pauta da Audiência Pública'),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_ata = models.FileField( upload_ata = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=ata_upload_path, upload_to=ata_upload_path,
@ -103,7 +103,7 @@ class AudienciaPublica(models.Model):
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_anexo = models.FileField( upload_anexo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=anexo_upload_path, upload_to=anexo_upload_path,
@ -167,7 +167,7 @@ class AnexoAudienciaPublica(models.Model):
audiencia = models.ForeignKey(AudienciaPublica, audiencia = models.ForeignKey(AudienciaPublica,
on_delete=models.PROTECT) on_delete=models.PROTECT)
arquivo = models.FileField( arquivo = models.FileField(
max_length=200, max_length=300,
upload_to=texto_upload_path, upload_to=texto_upload_path,
storage=OverwriteStorage(), storage=OverwriteStorage(),
verbose_name=_('Arquivo')) verbose_name=_('Arquivo'))

31
sapl/base/admin.py

@ -1,8 +1,9 @@
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from reversion.models import Revision from reversion.models import Revision
from sapl.base.models import AuditLog
from sapl.utils import register_all_models_in_admin from sapl.utils import register_all_models_in_admin
register_all_models_in_admin(__name__) register_all_models_in_admin(__name__)
@ -20,5 +21,31 @@ class RevisionAdmin(admin.ModelAdmin):
self.message_user(request, _('You cannot change history.')) self.message_user(request, _('You cannot change history.'))
return redirect('admin:reversion_revision_changelist') return redirect('admin:reversion_revision_changelist')
admin.site.register(Revision, RevisionAdmin) admin.site.register(Revision, RevisionAdmin)
class AuditLogAdmin(admin.ModelAdmin):
pass
def has_add_permission(self, request):
return False
# def has_change_permission(self, request, obj=None):
# return False
#
def has_delete_permission(self, request, obj=None):
return False
def save_model(self, request, obj, form, change):
pass
def delete_model(self, request, obj):
pass
def save_related(self, request, form, formsets, change):
pass
# Na linha acima register_all_models_in_admin registrou AuditLog
admin.site.unregister(AuditLog)
admin.site.register(AuditLog, AuditLogAdmin)

21
sapl/base/forms.py

@ -844,15 +844,17 @@ class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet):
self.filters['data_inicio'].label = 'Período (Inicial - Final)' self.filters['data_inicio'].label = 'Período (Inicial - Final)'
self.form.fields['legislatura'].required = True
tipo_sessao_ordinaria = self.filters['tipo'].queryset.filter(nome='Ordinária') tipo_sessao_ordinaria = self.filters['tipo'].queryset.filter(nome='Ordinária')
if tipo_sessao_ordinaria: if tipo_sessao_ordinaria:
self.form.initial['tipo'] = tipo_sessao_ordinaria.first() self.form.initial['tipo'] = tipo_sessao_ordinaria.first()
row1 = to_row([('data_inicio', 12)]) row1 = to_row([('legislatura', 4),
row2 = to_row([('legislatura', 4),
('sessao_legislativa', 4), ('sessao_legislativa', 4),
('tipo', 4)]) ('tipo', 4)])
row3 = to_row([('exibir_ordem_dia', 12)]) row2 = to_row([('exibir_ordem_dia', 12)])
row3 = to_row([('data_inicio', 12)])
buttons = FormActions( buttons = FormActions(
*[ *[
@ -1326,9 +1328,14 @@ class ConfiguracoesAppForm(ModelForm):
'tramitacao_materia', 'tramitacao_materia',
'tramitacao_documento'] 'tramitacao_documento']
def clean_mostrar_brasao_painel(self):
mostrar_brasao_painel = self.cleaned_data.get( def clean(self):
'mostrar_brasao_painel', False) cleaned_data = super().clean()
if not self.is_valid():
return cleaned_data
mostrar_brasao_painel = self.cleaned_data.get('mostrar_brasao_painel', False)
casa = CasaLegislativa.objects.first() casa = CasaLegislativa.objects.first()
if not casa: if not casa:
@ -1341,7 +1348,7 @@ class ConfiguracoesAppForm(ModelForm):
raise ValidationError("Não há logitipo configurado para esta " raise ValidationError("Não há logitipo configurado para esta "
"Casa legislativa.") "Casa legislativa.")
return mostrar_brasao_painel return cleaned_data
class RecuperarSenhaForm(PasswordResetForm): class RecuperarSenhaForm(PasswordResetForm):

33
sapl/base/migrations/0038_auditlog.py

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-15 13:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0037_auto_20190527_0901'),
]
operations = [
migrations.CreateModel(
name='AuditLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(blank=True, db_index=True, max_length=100, verbose_name='username')),
('operation', models.CharField(db_index=True, max_length=1, verbose_name='operation')),
('timestamp', models.DateTimeField(db_index=True, verbose_name='timestamp')),
('object', models.CharField(blank=True, max_length=4096, verbose_name='object')),
('object_id', models.PositiveIntegerField(db_index=True, verbose_name='object_id')),
('model_name', models.CharField(db_index=True, max_length=100, verbose_name='model')),
('app_name', models.CharField(db_index=True, max_length=100, verbose_name='app')),
],
options={
'verbose_name': 'AuditLog',
'verbose_name_plural': 'AuditLogs',
'ordering': ('-id',),
},
),
]

16
sapl/base/migrations/0040_merge_20191030_1655.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-30 19:55
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0038_auditlog'),
('base', '0039_auto_20190913_1228'),
]
operations = [
]

20
sapl/base/migrations/0041_auto_20191204_1130.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-12-04 14:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0040_merge_20191030_1655'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='assinatura_ata',
field=models.CharField(choices=[('M', 'Mesa Diretora da Sessão'), ('P', 'Apenas o Presidente da Sessão'), ('T', 'Todos os Parlamentares Presentes na Sessão')], default='T', max_length=1, verbose_name='Quem deve assinar a ata'),
),
]

42
sapl/base/models.py

@ -136,7 +136,7 @@ class AppConfig(models.Model):
max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O') max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O')
assinatura_ata = models.CharField( assinatura_ata = models.CharField(
verbose_name=_('Quem deve assina a ata'), verbose_name=_('Quem deve assinar a ata'),
max_length=1, choices=ASSINATURA_ATA_CHOICES, default='T') max_length=1, choices=ASSINATURA_ATA_CHOICES, default='T')
mostrar_brasao_painel = models.BooleanField( mostrar_brasao_painel = models.BooleanField(
@ -265,3 +265,43 @@ class AutorUser(models.Model):
def __str__(self): def __str__(self):
return "%s - %s" % (self.autor, self.user) return "%s - %s" % (self.autor, self.user)
class AuditLog(models.Model):
operation = ('C', 'D', 'U')
MAX_DATA_LENGTH = 4096 # 4KB de texto
username = models.CharField(max_length=100,
verbose_name=_('username'),
blank=True,
db_index=True)
operation = models.CharField(max_length=1,
verbose_name=_('operation'),
db_index=True)
timestamp = models.DateTimeField(verbose_name=_('timestamp'),
db_index=True)
object = models.CharField(max_length=MAX_DATA_LENGTH,
blank=True,
verbose_name=_('object'))
object_id = models.PositiveIntegerField(verbose_name=_('object_id'),
db_index=True)
model_name = models.CharField(max_length=100, verbose_name=_('model'),
db_index=True)
app_name = models.CharField(max_length=100,
verbose_name=_('app'),
db_index=True)
class Meta:
verbose_name = _('AuditLog')
verbose_name_plural = _('AuditLogs')
ordering = ('-id',)
def __str__(self):
return "[%s] %s %s.%s %s" % (self.timestamp,
self.operation,
self.app_name,
self.model_name,
self.username,
)

42
sapl/base/receivers.py

@ -1,7 +1,13 @@
import logging
from django.core import serializers
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from sapl.base.signals import tramitacao_signal from sapl.base.email_utils import do_envia_email_tramitacao
from sapl.base.models import AuditLog
from sapl.base.signals import tramitacao_signal, post_delete_signal, post_save_signal
from sapl.materia.models import Tramitacao from sapl.materia.models import Tramitacao
from sapl.protocoloadm.models import TramitacaoAdministrativo from sapl.protocoloadm.models import TramitacaoAdministrativo
from sapl.utils import get_base_url from sapl.utils import get_base_url
@ -39,3 +45,37 @@ def status_tramitacao_materia(sender, instance, **kwargs):
documento = instance.documento documento = instance.documento
documento.tramitacao = True documento.tramitacao = True
documento.save() documento.save()
@receiver(post_delete_signal)
@receiver(post_save_signal)
def audit_log(sender, **kwargs):
logger = logging.getLogger(__name__)
instance = kwargs.get('instance')
operation = kwargs.get('operation')
user = kwargs.get('request').user
model_name = instance.__class__.__name__
app_name = instance._meta.app_label
object_id = instance.id
data = serializers.serialize('json', [instance])
if len(data) > AuditLog.MAX_DATA_LENGTH:
data = data[:AuditLog.MAX_DATA_LENGTH]
if user:
username = user.username
else:
username = ''
try:
AuditLog.objects.create(username=username,
operation=operation,
model_name=model_name,
app_name=app_name,
timestamp=timezone.now(),
object_id=object_id,
object=data)
except Exception as e:
logger.error('Error saving auditing log object')
logger.error(e)

3
sapl/base/signals.py

@ -46,3 +46,6 @@ def cria_models_tipo_autor(app_config=None, verbosity=2, interactive=True,
post_migrate.connect(receiver=cria_models_tipo_autor) post_migrate.connect(receiver=cria_models_tipo_autor)
post_delete_signal = django.dispatch.Signal(providing_args=['instance', 'request'])
post_save_signal = django.dispatch.Signal(providing_args=['instance', 'operation', 'request'])

8
sapl/base/tests/test_login.py

@ -12,9 +12,13 @@ def user():
def test_login_aparece_na_barra_para_usuario_nao_logado(client): def test_login_aparece_na_barra_para_usuario_nao_logado(client):
import re
response = client.get('/') response = client.get('/')
assert '<a class="nav-link" href="/login/"><img src="/static/sapl/frontend/img/user.png"></a>' in str( content = str(response.content)
response.content) query = '<a class="nav-link" href="/login/"><img src=".*/user.png"></a>'
result = re.findall(query, content)
assert len(result) > 0
def test_username_do_usuario_logado_aparece_na_barra(client, user): def test_username_do_usuario_logado_aparece_na_barra(client, user):

2
sapl/base/views.py

@ -2053,8 +2053,6 @@ class AppConfigCrud(CrudAux):
reverse('sapl.base:appconfig_update', reverse('sapl.base:appconfig_update',
kwargs={'pk': app_config.pk})) kwargs={'pk': app_config.pk}))
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
class ListView(CrudAux.ListView): class ListView(CrudAux.ListView):

43
sapl/comissoes/forms.py

@ -12,13 +12,16 @@ from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sapl.base.models import Autor, TipoAutor from sapl.base.models import Autor, TipoAutor
from sapl.comissoes.models import (Comissao, Composicao, DocumentoAcessorio, from sapl.comissoes.models import (Comissao, Composicao,
Participacao, Periodo, Reuniao) DocumentoAcessorio, Participacao,
from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, to_row Periodo, Reuniao)
from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
to_row)
from sapl.materia.models import MateriaEmTramitacao, PautaReuniao from sapl.materia.models import MateriaEmTramitacao, PautaReuniao
from sapl.parlamentares.models import Legislatura, Mandato, Parlamentar from sapl.parlamentares.models import (Legislatura, Mandato,
from sapl.settings import MAX_DOC_UPLOAD_SIZE Parlamentar)
from sapl.utils import FileFieldCheckMixin, FilterOverridesMetaMixin from sapl.utils import (FileFieldCheckMixin, FilterOverridesMetaMixin,
validar_arquivo)
class ComposicaoForm(forms.ModelForm): class ComposicaoForm(forms.ModelForm):
@ -405,17 +408,14 @@ class ReuniaoForm(ModelForm):
upload_ata = self.cleaned_data.get('upload_ata', False) upload_ata = self.cleaned_data.get('upload_ata', False)
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_pauta and upload_pauta.size > MAX_DOC_UPLOAD_SIZE: if upload_pauta:
raise ValidationError("O arquivo Pauta da Reunião deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_pauta, "Pauta da Reunião")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_pauta.size/1024)/1024))
if upload_ata and upload_ata.size > MAX_DOC_UPLOAD_SIZE: if upload_ata:
raise ValidationError("O arquivo Ata da Reunião deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_ata, "Ata da Reunião")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_ata.size/1024)/1024))
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE: if upload_anexo:
raise ValidationError("O arquivo Anexo da Reunião deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_anexo, "Anexo da Reunião")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -431,9 +431,10 @@ class PautaReuniaoFilterSet(django_filters.FilterSet):
self.filters['materia__tipo'].label = "Tipo da Matéria" self.filters['materia__tipo'].label = "Tipo da Matéria"
self.filters['materia__ano'].label = "Ano da Matéria" self.filters['materia__ano'].label = "Ano da Matéria"
self.filters['materia__numero'].label = "Número da Matéria"
self.filters['materia__data_apresentacao'].label = "Data (Inicial - Final)" self.filters['materia__data_apresentacao'].label = "Data (Inicial - Final)"
row1 = to_row([('materia__numero', 4), ('materia__tipo', 4), ('materia__ano', 4)]) row1 = to_row([('materia__tipo', 4), ('materia__ano', 4), ('materia__numero', 4)])
row2 = to_row([('materia__data_apresentacao', 12)]) row2 = to_row([('materia__data_apresentacao', 12)])
self.form.helper = SaplFormHelper() self.form.helper = SaplFormHelper()
@ -482,9 +483,8 @@ class DocumentoAcessorioCreateForm(FileFieldCheckMixin, forms.ModelForm):
arquivo = self.cleaned_data.get('arquivo', False) arquivo = self.cleaned_data.get('arquivo', False)
if arquivo and arquivo.size > MAX_DOC_UPLOAD_SIZE: if arquivo:
raise ValidationError("O arquivo Texto Integral deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(arquivo, "Texto Integral")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (arquivo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -509,8 +509,7 @@ class DocumentoAcessorioEditForm(FileFieldCheckMixin, forms.ModelForm):
arquivo = self.cleaned_data.get('arquivo', False) arquivo = self.cleaned_data.get('arquivo', False)
if arquivo and arquivo.size > MAX_DOC_UPLOAD_SIZE: if arquivo:
raise ValidationError("O arquivo Texto Integral deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(arquivo, "Texto Integral")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (arquivo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data

42
sapl/comissoes/migrations/0022_auto_20191120_1440.py

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-11-20 17:40
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.comissoes.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('comissoes', '0021_auto_20191001_1115'),
]
operations = [
migrations.AlterField(
model_name='documentoacessorio',
name='arquivo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.anexo_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'),
),
migrations.AlterField(
model_name='documentoacessorio',
name='autor',
field=models.CharField(max_length=200, verbose_name='Autor'),
),
migrations.AlterField(
model_name='reuniao',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.anexo_upload_path, verbose_name='Anexo da Reunião'),
),
migrations.AlterField(
model_name='reuniao',
name='upload_ata',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.ata_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Ata da Reunião'),
),
migrations.AlterField(
model_name='reuniao',
name='upload_pauta',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.pauta_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Pauta da Reunião'),
),
]

10
sapl/comissoes/models.py

@ -236,21 +236,21 @@ class Reuniao(models.Model):
max_length=150, blank=True, max_length=150, blank=True,
verbose_name=_('URL do Arquivo de Vídeo (Formatos MP4 / FLV / WebM)')) verbose_name=_('URL do Arquivo de Vídeo (Formatos MP4 / FLV / WebM)'))
upload_pauta = models.FileField( upload_pauta = models.FileField(
max_length=200, max_length=300,
blank=True, null=True, blank=True, null=True,
upload_to=pauta_upload_path, upload_to=pauta_upload_path,
verbose_name=_('Pauta da Reunião'), verbose_name=_('Pauta da Reunião'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_ata = models.FileField( upload_ata = models.FileField(
max_length=200, max_length=300,
blank=True, null=True, blank=True, null=True,
upload_to=ata_upload_path, upload_to=ata_upload_path,
verbose_name=_('Ata da Reunião'), verbose_name=_('Ata da Reunião'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_anexo = models.FileField( upload_anexo = models.FileField(
max_length=200, max_length=300,
blank=True, null=True, blank=True, null=True,
upload_to=anexo_upload_path, upload_to=anexo_upload_path,
storage=OverwriteStorage(), storage=OverwriteStorage(),
@ -317,11 +317,11 @@ class DocumentoAcessorio(models.Model):
data = models.DateField(blank=True, null=True, data = models.DateField(blank=True, null=True,
default=None, verbose_name=_('Data')) default=None, verbose_name=_('Data'))
autor = models.CharField( autor = models.CharField(
max_length=100, verbose_name=_('Autor')) max_length=200, verbose_name=_('Autor'))
ementa = models.TextField(blank=True, verbose_name=_('Ementa')) ementa = models.TextField(blank=True, verbose_name=_('Ementa'))
indexacao = models.TextField(blank=True) indexacao = models.TextField(blank=True)
arquivo = models.FileField( arquivo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=anexo_upload_path, upload_to=anexo_upload_path,

58
sapl/compilacao/forms.py

@ -13,6 +13,7 @@ from django.forms.forms import Form
from django.forms.models import ModelForm from django.forms.models import ModelForm
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils.choices import Choices
from sapl import utils from sapl import utils
from sapl.compilacao.models import (NOTAS_PUBLICIDADE_CHOICES, from sapl.compilacao.models import (NOTAS_PUBLICIDADE_CHOICES,
@ -115,7 +116,7 @@ class TaForm(ModelForm):
queryset=TipoTextoArticulado.objects.all(), queryset=TipoTextoArticulado.objects.all(),
required=True, required=True,
empty_label=None) empty_label=None)
numero = forms.IntegerField( numero = forms.CharField(
label=TextoArticulado._meta.get_field( label=TextoArticulado._meta.get_field(
'numero').verbose_name, 'numero').verbose_name,
required=True) required=True)
@ -744,6 +745,12 @@ class DispositivoEdicaoBasicaForm(ModelForm):
class DispositivoSearchModalForm(Form): class DispositivoSearchModalForm(Form):
TIPO_RESULTADO_CHOICES = Choices(
('C', 'coincidentes', _('Apenas Coincidentes')),
('I', 'internos', _('Incluir Internos')),
('S', 'coin_sequentes', _('Coincidentes e seus sequentes')),
)
tipo_ta = forms.ModelChoiceField( tipo_ta = forms.ModelChoiceField(
label=_('Tipo do Texto Articulado'), label=_('Tipo do Texto Articulado'),
queryset=TipoTextoArticulado.objects.all(), queryset=TipoTextoArticulado.objects.all(),
@ -758,9 +765,9 @@ class DispositivoSearchModalForm(Form):
ano_ta = forms.IntegerField( ano_ta = forms.IntegerField(
label=_('Ano do Documento'), required=False) label=_('Ano do Documento'), required=False)
dispositivos_internos = forms.ChoiceField( tipo_resultado = forms.ChoiceField(
label=_('Dispositivos Internos?'), label=_('Tipo do Resultado?'),
choices=utils.YES_NO_CHOICES, choices=TIPO_RESULTADO_CHOICES,
widget=forms.RadioSelect(), widget=forms.RadioSelect(),
required=False) required=False)
@ -769,7 +776,7 @@ class DispositivoSearchModalForm(Form):
choices=[(10, _('Dez Dispositivos')), choices=[(10, _('Dez Dispositivos')),
(30, _('Trinta Dispositivos')), (30, _('Trinta Dispositivos')),
(50, _('Cinquenta Dispositivos')), (50, _('Cinquenta Dispositivos')),
(0, _('Tudo que atender aos Critérios da Busca'))], (100, _('Cem Dispositivos'))],
widget=forms.Select(), widget=forms.Select(),
required=False) required=False)
@ -789,22 +796,33 @@ class DispositivoSearchModalForm(Form):
to_column(('num_ta', 4)), to_column(('num_ta', 4)),
to_column(('ano_ta', 4)), to_column(('ano_ta', 4)),
to_column(('max_results', 4))), to_column(('max_results', 4))),
Row( Row(
to_column(('tipo_ta', 6)), to_column(('tipo_resultado', 3)),
to_column(('tipo_model', 6))), to_column(
Row(to_column((InlineRadios('dispositivos_internos'), 3)), (
to_column(('rotulo_dispositivo', 2)), Div(
to_column((FieldWithButtons( Row(
Field( to_column(('tipo_ta', 6)),
'texto_dispositivo', to_column(('tipo_model', 6))),
placeholder=_('Digite palavras, letras, ' Row(
'números ou algo' to_column(('rotulo_dispositivo', 4)),
' que estejam no texto.')), to_column(
StrictButton( (
_('Buscar'), FieldWithButtons(
css_class='btn-busca btn-primary')), 7)) Field(
'texto_dispositivo',
placeholder=_('Digite palavras, letras, '
'números ou algo'
' que estejam no texto.')),
StrictButton(
_('Buscar'),
css_class='btn-busca btn-primary')), 8))
)
), 9
)
) )
) ))
self.helper = SaplFormHelper() self.helper = SaplFormHelper()
self.helper.layout = Layout( self.helper.layout = Layout(
@ -821,7 +839,7 @@ class DispositivoSearchModalForm(Form):
choice = ch(kwargs['instance'].ta.tipo_ta_id) choice = ch(kwargs['instance'].ta.tipo_ta_id)
self.base_fields['tipo_model'].choices = choice self.base_fields['tipo_model'].choices = choice
kwargs['initial'].update({'dispositivos_internos': False}) kwargs['initial'].update({'tipo_resultado': 'C'})
super(DispositivoSearchModalForm, self).__init__(*args, **kwargs) super(DispositivoSearchModalForm, self).__init__(*args, **kwargs)

27
sapl/compilacao/migrations/0013_auto_20190924_0830.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-09-24 11:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compilacao', '0012_bug_auto_inserido'),
]
operations = [
migrations.AlterField(
model_name='textoarticulado',
name='editable_only_by_owners',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=True,
verbose_name='Editável apenas pelos donos do Texto Articulado?'),
),
migrations.AlterField(
model_name='textoarticulado',
name='editing_locked',
field=models.BooleanField(choices=[(
True, 'Sim'), (False, 'Não')], default=True, verbose_name='Texto Articulado em Edição?'),
),
]

29
sapl/compilacao/models.py

@ -221,12 +221,12 @@ class TextoArticulado(TimestampedMixin):
editable_only_by_owners = models.BooleanField( editable_only_by_owners = models.BooleanField(
choices=YES_NO_CHOICES, choices=YES_NO_CHOICES,
default=True, default=True,
verbose_name=_('Editável apenas pelos donos do Texto Articulado')) verbose_name=_('Editável apenas pelos donos do Texto Articulado?'))
editing_locked = models.BooleanField( editing_locked = models.BooleanField(
choices=YES_NO_CHOICES, choices=YES_NO_CHOICES,
default=True, default=True,
verbose_name=_('Texto Articulado em Edição')) verbose_name=_('Texto Articulado em Edição?'))
privacidade = models.IntegerField( privacidade = models.IntegerField(
_('Privacidade'), _('Privacidade'),
@ -416,8 +416,8 @@ class TextoArticulado(TimestampedMixin):
def clone_for(self, obj): def clone_for(self, obj):
# O clone gera um texto válido original dada a base self, # O clone gera um texto válido original dada a base self,
# mesmo sendo esta base um texto compilado. # mesmo sendo esta base um Texto Articulado.
# Os dispositivos a clonar será com base no texto compilado # Os dispositivos a clonar será com base no Texto Articulado
assert self.tipo_ta and self.tipo_ta.content_type, _( assert self.tipo_ta and self.tipo_ta.content_type, _(
'Não é permitido chamar o método clone_for ' 'Não é permitido chamar o método clone_for '
@ -1072,6 +1072,10 @@ class Dispositivo(BaseModel, TimestampedMixin):
'Permissão alteração global do dispositivo de vigência')), 'Permissão alteração global do dispositivo de vigência')),
) )
def ws_sync(self):
return self.ta and self.ta.privacidade in (
STATUS_TA_IMMUTABLE_PUBLIC, STATUS_TA_PUBLIC)
def clean(self): def clean(self):
""" """
Check for instances with null values in unique_together fields. Check for instances with null values in unique_together fields.
@ -1113,14 +1117,15 @@ class Dispositivo(BaseModel, TimestampedMixin):
self.contagem_continua = self.tipo_dispositivo.contagem_continua self.contagem_continua = self.tipo_dispositivo.contagem_continua
try: """try:
if self.texto: if self.texto:
self.texto = self.texto.replace('\xa0', '')
self.texto = str(BeautifulSoup(self.texto, "html.parser")) self.texto = str(BeautifulSoup(self.texto, "html.parser"))
if self.texto_atualizador: if self.texto_atualizador:
self.texto_atualizador = str(BeautifulSoup( self.texto_atualizador = str(BeautifulSoup(
self.texto_atualizador, "html.parser")) self.texto_atualizador, "html.parser"))
except: except:
pass pass"""
return super().save( return super().save(
force_insert=force_insert, force_update=force_update, using=using, force_insert=force_insert, force_update=force_update, using=using,
@ -1624,7 +1629,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
yield ultimo yield ultimo
@staticmethod @staticmethod
def new_instance_based_on(dispositivo_base, tipo_base): def new_instance_based_on(dispositivo_base, tipo_base, base_alteracao=None):
dp = Dispositivo() dp = Dispositivo()
dp.tipo_dispositivo = tipo_base dp.tipo_dispositivo = tipo_base
@ -1639,6 +1644,16 @@ class Dispositivo(BaseModel, TimestampedMixin):
dp.dispositivo_pai = dispositivo_base.dispositivo_pai dp.dispositivo_pai = dispositivo_base.dispositivo_pai
dp.publicacao = dispositivo_base.publicacao dp.publicacao = dispositivo_base.publicacao
b = base_alteracao if base_alteracao else dispositivo_base
# teste de criação inversa de itens alterados por mesmo bloco
dp.ta_publicado = b.ta_publicado
dp.dispositivo_atualizador = b.dispositivo_atualizador
if dp.ta_publicado:
dp.ordem_bloco_atualizador = b.ordem_bloco_atualizador + \
Dispositivo.INTERVALO_ORDEM
dp.dispositivo_vigencia = dispositivo_base.dispositivo_vigencia dp.dispositivo_vigencia = dispositivo_base.dispositivo_vigencia
if dp.dispositivo_vigencia: if dp.dispositivo_vigencia:
dp.inicio_eficacia = dp.dispositivo_vigencia.inicio_eficacia dp.inicio_eficacia = dp.dispositivo_vigencia.inicio_eficacia

3
sapl/compilacao/templatetags/compilacao_filters.py

@ -65,6 +65,8 @@ def get_bloco_atualizador(pk_atualizador):
@register.simple_tag @register.simple_tag
def dispositivo_desativado(dispositivo, inicio_vigencia, fim_vigencia): def dispositivo_desativado(dispositivo, inicio_vigencia, fim_vigencia):
if dispositivo.dispositivo_de_revogacao:
return 'revogado'
if inicio_vigencia and fim_vigencia: if inicio_vigencia and fim_vigencia:
if dispositivo.fim_vigencia is None: if dispositivo.fim_vigencia is None:
return '' return ''
@ -292,7 +294,6 @@ def nomenclatura_heranca(d, ignore_ultimo=0, ignore_primeiro=0):
return result return result
@register.filter @register.filter
def list(obj): def list(obj):
return [obj, ] return [obj, ]

406
sapl/compilacao/views.py

@ -10,7 +10,7 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError, PermissionDenied
from django.core.signing import Signer from django.core.signing import Signer
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.db import transaction from django.db import transaction
@ -494,6 +494,18 @@ class TaListView(CompMixin, ListView):
~Q(owners=self.request.user.id), ~Q(owners=self.request.user.id),
privacidade=STATUS_TA_PRIVATE) privacidade=STATUS_TA_PRIVATE)
if 'check' in self.request.GET:
qs = qs.filter(
temp_check_migrations=False,
privacidade=0,
).exclude(dispositivos_set__tipo_dispositivo_id=3)
if 'check_dvt' in self.request.GET:
qs = qs.filter(
).filter(
dispositivos_set__isnull=False,
dispositivos_set__dispositivo_vigencia__isnull=True).distinct()
return qs return qs
@ -565,13 +577,29 @@ class TaDeleteView(CompMixin, DeleteView):
template_name = "crud/confirm_delete.html" template_name = "crud/confirm_delete.html"
permission_required = 'compilacao.delete_textoarticulado' permission_required = 'compilacao.delete_textoarticulado'
def post(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied
return DeleteView.post(self, request, *args, **kwargs)
@property @property
def detail_url(self): def detail_url(self):
return reverse_lazy('sapl.compilacao:ta_detail', return reverse_lazy('sapl.compilacao:ta_detail',
kwargs={'pk': self.kwargs['pk']}) kwargs={'pk': self.kwargs['pk']})
def get_success_url(self): def get_success_url(self):
return reverse_lazy('sapl.compilacao:ta_list') messages.info(self.request, 'Texto Articulado excluido com sucesso!')
reverse_url = '%s:%s_detail' % (
self.object.content_object._meta.app_config.name,
self.object.content_object._meta.model_name)
return reverse_lazy(reverse_url,
kwargs={'pk': self.object.content_object.pk})
@property
def title(self):
return '<b>Texto Articulado:</b> %s' % self.object
class DispositivoSuccessUrlMixin(CompMixin): class DispositivoSuccessUrlMixin(CompMixin):
@ -849,6 +877,10 @@ class TextView(CompMixin, ListView):
fim_vigencia = None fim_vigencia = None
ta_vigencia = None ta_vigencia = None
@property
def title(self):
return '<b>Texto Articulado:</b> %s' % self.object
def has_permission(self): def has_permission(self):
self.object = self.ta self.object = self.ta
return self.object.has_view_permission(self.request) return self.object.has_view_permission(self.request)
@ -1078,7 +1110,7 @@ class TextEditView(CompMixin, TemplateView):
self.object.content_object.save() self.object.content_object.save()
else: else:
if 'lock' in request.GET: if 'lock' in request.GET or 'check' in request.GET:
# TODO - implementar logging de ação de usuário # TODO - implementar logging de ação de usuário
notificacoes = self.get_notificacoes( notificacoes = self.get_notificacoes(
@ -1097,11 +1129,17 @@ class TextEditView(CompMixin, TemplateView):
'sapl.compilacao:ta_text_notificacoes', kwargs={ 'sapl.compilacao:ta_text_notificacoes', kwargs={
'ta_id': self.object.id})) 'ta_id': self.object.id}))
self.object.editing_locked = True if 'lock' in request.GET:
self.object.privacidade = STATUS_TA_PUBLIC self.object.editing_locked = True
self.object.save() self.object.privacidade = STATUS_TA_PUBLIC
messages.success(request, _( self.object.save()
'Texto Articulado bloqueado com sucesso.')) messages.success(request, _(
'Texto Articulado publicado com sucesso.'))
else:
self.object.temp_check_migrations = True
self.object.save()
messages.success(request, _(
'Texto Articulado Checado...'))
if self.object.content_object: if self.object.content_object:
self.object.content_object.save() self.object.content_object.save()
@ -2083,6 +2121,10 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
if len(result) > 2: if len(result) > 2:
result.pop() result.pop()
result[0]['itens'] = result[1]['itens'] + result[0]['itens']
result[0]['tipo_insert'] = 'Inserção'
result[1]['itens'] = []
return result return result
except Exception as e: except Exception as e:
@ -2097,7 +2139,6 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
dvt = Dispositivo.objects.get(pk=self.kwargs['dispositivo_id']) dvt = Dispositivo.objects.get(pk=self.kwargs['dispositivo_id'])
if dvt.auto_inserido: if dvt.auto_inserido:
dvt = dvt.dispositivo_pai dvt = dvt.dispositivo_pai
try: try:
Dispositivo.objects.filter( Dispositivo.objects.filter(
ta=dvt.ta, ta_publicado__isnull=True ta=dvt.ta, ta_publicado__isnull=True
@ -2109,7 +2150,7 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
Dispositivo.objects.filter(ta_publicado=dvt.ta Dispositivo.objects.filter(ta_publicado=dvt.ta
).update( ).update(
dispositivo_vigencia=dvt, dispositivo_vigencia=dvt,
inicio_vigencia=dvt.inicio_eficacia, inicio_vigencia=dvt.inicio_vigencia,
inicio_eficacia=dvt.inicio_eficacia) inicio_eficacia=dvt.inicio_eficacia)
dps = Dispositivo.objects.filter(dispositivo_vigencia=dvt) dps = Dispositivo.objects.filter(dispositivo_vigencia=dvt)
@ -2162,6 +2203,10 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
dp_auto_insert = None dp_auto_insert = None
base = Dispositivo.objects.get(pk=self.kwargs['dispositivo_id']) base = Dispositivo.objects.get(pk=self.kwargs['dispositivo_id'])
if base.dispositivo_atualizador:
registro_inclusao = True
tipo = TipoDispositivo.objects.get(pk=context['tipo_pk']) tipo = TipoDispositivo.objects.get(pk=context['tipo_pk'])
pub_last = Publicacao.objects.order_by( pub_last = Publicacao.objects.order_by(
'data', 'hora').filter(ta=base.ta).last() 'data', 'hora').filter(ta=base.ta).last()
@ -2197,11 +2242,13 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
dp_pai = dp dp_pai = dp
if dp_irmao is not None: if dp_irmao is not None:
dp = Dispositivo.new_instance_based_on(dp_irmao, tipo) dp = Dispositivo.new_instance_based_on(
dp_irmao, tipo, base_alteracao=base)
dp.transform_in_next(variacao) dp.transform_in_next(variacao)
else: else:
# Inserção sem precedente # Inserção sem precedente
dp = Dispositivo.new_instance_based_on(dp_pai, tipo) dp = Dispositivo.new_instance_based_on(
dp_pai, tipo, base_alteracao=base)
dp.dispositivo_pai = dp_pai dp.dispositivo_pai = dp_pai
dp.nivel += 1 dp.nivel += 1
@ -2224,6 +2271,9 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
else: else:
dp.set_numero_completo([1, 0, 0, 0, 0, 0, ]) dp.set_numero_completo([1, 0, 0, 0, 0, 0, ])
if dp.dispositivo_atualizador:
registro_inclusao = True
# verificar se existe restrição de quantidade de itens # verificar se existe restrição de quantidade de itens
if dp.dispositivo_pai: if dp.dispositivo_pai:
for perfil in perfil_parents: for perfil in perfil_parents:
@ -2262,7 +2312,8 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
dp.incrementar_irmaos(variacao, [local_add, ], force=False) dp.incrementar_irmaos(variacao, [local_add, ], force=False)
dp.publicacao = pub_last dp.publicacao = pub_last
dp.save()
dp.save(clean=not registro_inclusao)
count_auto_insert = 0 count_auto_insert = 0
if create_auto_inserts: if create_auto_inserts:
@ -2314,6 +2365,10 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
ordem += Dispositivo.INTERVALO_ORDEM ordem += Dispositivo.INTERVALO_ORDEM
dp = Dispositivo.objects.get(pk=dp_pk) dp = Dispositivo.objects.get(pk=dp_pk)
dp.ta_publicado = None
dp.dispositivo_atualizador = None
dp.ordem_bloco_atualizador = 0
dp.save(clean=False)
''' Reenquadrar todos os dispositivos que possuem pai ''' Reenquadrar todos os dispositivos que possuem pai
antes da inserção atual e que são inferiores a dp, antes da inserção atual e que são inferiores a dp,
@ -2624,8 +2679,12 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
ds = d ds = d
while ds.dispositivo_subsequente: while ds.dispositivo_subsequente:
ds = ds.dispositivo_subsequente ds = ds.dispositivo_subsequente
dsps_ids.add(ds.pk) dsps_ids.add(ds.pk)
if revogacao and ds.dispositivo_de_revogacao:
dsps_ids.remove(ds.pk)
if em_bloco: if em_bloco:
proximo_bloco = Dispositivo.objects.filter( proximo_bloco = Dispositivo.objects.filter(
ordem__gt=ds.ordem, ordem__gt=ds.ordem,
@ -2636,9 +2695,18 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
'ta_id': ds.ta_id, 'ta_id': ds.ta_id,
'nivel__gte': ds.nivel, 'nivel__gte': ds.nivel,
'ordem__gte': ds.ordem, 'ordem__gte': ds.ordem,
'dispositivo_subsequente__isnull': True 'dispositivo_subsequente__isnull': True,
} }
if revogacao:
params.update(
{
'dispositivo_de_revogacao': False,
'tipo_dispositivo__dispositivo_de_articulacao': False
}
)
if proximo_bloco: if proximo_bloco:
params['ordem__lt'] = proximo_bloco.ordem params['ordem__lt'] = proximo_bloco.ordem
@ -2655,9 +2723,9 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
dsps_ids = Dispositivo.objects.filter( dsps_ids = Dispositivo.objects.filter(
id__in=dsps_ids id__in=dsps_ids
).values_list('id', flat="True") ).values_list('id', flat="True").order_by('ordem')
for dsp in dsps_ids: with transaction.atomic():
with transaction.atomic(): for dsp in dsps_ids:
data.update( data.update(
self.registra_alteracao( self.registra_alteracao(
bloco_alteracao, bloco_alteracao,
@ -2713,10 +2781,10 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
if ndp.dispositivo_vigencia: if ndp.dispositivo_vigencia:
ndp.inicio_eficacia = ndp.dispositivo_vigencia.inicio_eficacia ndp.inicio_eficacia = ndp.dispositivo_vigencia.inicio_eficacia
ndp.inicio_vigencia = ndp.dispositivo_vigencia.inicio_eficacia ndp.inicio_vigencia = ndp.dispositivo_vigencia.inicio_vigencia
else: else:
ndp.inicio_eficacia = bloco_alteracao.inicio_eficacia ndp.inicio_eficacia = bloco_alteracao.inicio_eficacia
ndp.inicio_vigencia = bloco_alteracao.inicio_eficacia ndp.inicio_vigencia = bloco_alteracao.inicio_vigencia
try: try:
ordem = dsp_a_alterar.criar_espaco( ordem = dsp_a_alterar.criar_espaco(
@ -2748,6 +2816,7 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
).ordem_bloco_atualizador + Dispositivo.INTERVALO_ORDEM ).ordem_bloco_atualizador + Dispositivo.INTERVALO_ORDEM
else: else:
ndp.ordem_bloco_atualizador = Dispositivo.INTERVALO_ORDEM ndp.ordem_bloco_atualizador = Dispositivo.INTERVALO_ORDEM
ndp.save() ndp.save()
p.dispositivo_subsequente = ndp p.dispositivo_subsequente = ndp
@ -2765,10 +2834,10 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
filhos_diretos = dsp_a_alterar.dispositivos_filhos_set filhos_diretos = dsp_a_alterar.dispositivos_filhos_set
for d in filhos_diretos.all(): for d in filhos_diretos.all():
d.dispositivo_pai = ndp d.dispositivo_pai = ndp
d.save() d.save(clean=False)
ndp.ta.reordenar_dispositivos() # ndp.ta.reordenar_dispositivos()
bloco_alteracao.ordenar_bloco_alteracao() # bloco_alteracao.ordenar_bloco_alteracao()
if not revogacao: if not revogacao:
if 'message' not in data: if 'message' not in data:
@ -2980,201 +3049,174 @@ class DispositivoSearchFragmentFormView(ListView):
itens.append(item) itens.append(item)
return JsonResponse(itens, safe=False) return JsonResponse(itens, safe=False)
response = ListView.get(self, request, *args, **kwargs) return ListView.get(self, request, *args, **kwargs)
if not self.object_list or \ def get_queryset(self):
not isinstance(self.object_list, list) and \ result = []
not self.object_list.exists():
messages.info(
request, _('Não foram encontrados resultados '
'com seus critérios de busca!'))
username = self.request.user.username
self.logger.error("user=" + username + ". Não foram encontrados "
"resultados com esses critérios de busca. "
"id_tipo_ta=".format(request.GET['tipo_ta']))
try: try:
r = response.render() tipo_model = self.request.GET.get('tipo_model', '')
return response limit = int(self.request.GET.get('max_results', 100))
except Exception as e: tipo_ta = self.request.GET.get('tipo_ta', '')
messages.error(request, "Erro - %s" % str(e)) num_ta = self.request.GET.get('num_ta', '')
context = {} ano_ta = self.request.GET.get('ano_ta', '')
self.template_name = 'compilacao/messages.html' rotulo = self.request.GET.get('rotulo', '')
username = self.request.user.username str_texto = self.request.GET.get('texto', '')
self.logger.error("user=" + username + ". " + str(e)) texto = str_texto.split(' ')
return self.render_to_response(context)
def get_queryset(self): tipo_resultado = self.request.GET.get('tipo_resultado', '')
try: tipo_resultado = '' if tipo_resultado == 'False' else tipo_resultado
n = 10
if 'max_results' in self.request.GET:
n = int(self.request.GET['max_results'])
q = Q() model_class = None
if 'initial_ref' in self.request.GET:
initial_ref = self.request.GET['initial_ref']
if initial_ref:
q = q & Q(pk=initial_ref)
result = Dispositivo.objects.filter(q).select_related( if tipo_ta:
'ta').exclude( tipo_ta = TipoTextoArticulado.objects.get(pk=tipo_ta)
tipo_dispositivo__dispositivo_de_alteracao=True)
if tipo_ta and tipo_model:
integrations_view_names = get_integrations_view_names()
for item in integrations_view_names:
if hasattr(item, 'model_type_foreignkey') and\
hasattr(item, 'model'):
if (tipo_ta.content_type.model ==
item.model.__name__.lower() and
tipo_ta.content_type.app_label ==
item.model._meta.app_label):
model_class = item.model
model_type_class = item.model_type_foreignkey
tipo_model = item.model_type_foreignkey.objects.get(
pk=tipo_model)
break
return result[:n] column_field = ''
if model_class:
for field in model_class._meta.fields:
if field.related_model == model_type_class:
column_field = field.column
break
str_texto = '' dts = self.request.GET.get('data_type_selection', '')
texto = '' df = self.request.GET.get('data_function', '')
rotulo = ''
num_ta = ''
ano_ta = ''
if 'texto' in self.request.GET: AND_CONTROLS = ''
str_texto = self.request.GET['texto'] if dts == 'checkbox':
AND_CONTROLS = 'AND td.dispositivo_de_alteracao = false'
else:
if df == 'alterador':
AND_CONTROLS = '''AND td.dispositivo_de_alteracao = true
AND td.dispositivo_de_articulacao = true'''
texto = list(map("d.texto ~* '{}'".format, texto))
AND_TEXTO_ROTULO = ''
if str_texto and rotulo:
AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* '{BUSCA_ROTULO}') OR
({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* '{BUSCA_ROTULO}')
)'''.format(
BUSCA_TEXTO=' AND '.join(texto),
BUSCA_ROTULO=rotulo
)
elif str_texto:
AND_TEXTO_ROTULO = ' AND %s' % ' AND '.join(texto)
elif rotulo:
AND_TEXTO_ROTULO = "AND d.rotulo ~* '{BUSCA_ROTULO}'".format(
BUSCA_ROTULO=rotulo)
else:
AND_TEXTO_ROTULO = ''
jtms = '' # JOIN_TYPE_MODEL_SELECTED
atms = '' # AND_TYPE_MODEL_SELECTED
if tipo_model:
jtms = 'JOIN {gfk_table} gfkt on (gfkt.id = ta.object_id)'.format(
gfk_table=model_class._meta.db_table)
atms = 'AND gfkt.{gfk_field_type} = {gfk_field_type_id}'.format(
gfk_field_type=column_field,
gfk_field_type_id=tipo_model.id,
)
texto = str_texto.split(' ') sql = '''
SELECT d.* FROM compilacao_dispositivo d
JOIN compilacao_dispositivo dp on (d.dispositivo_pai_id = dp.id)
JOIN compilacao_tipodispositivo td on (d.tipo_dispositivo_id = td.id)
JOIN compilacao_textoarticulado ta on (d.ta_id = ta.id)
if 'rotulo' in self.request.GET: {JOIN_TYPE_MODEL_SELECTED}
rotulo = self.request.GET['rotulo']
if rotulo:
q = q & Q(rotulo__icontains=rotulo)
for item in texto: where d.nivel > 0
if not item:
continue
if q:
q = q & (Q(texto__icontains=item) |
Q(texto_atualizador__icontains=item))
else:
q = (Q(texto__icontains=item) |
Q(texto_atualizador__icontains=item))
if 'tipo_ta' in self.request.GET:
tipo_ta = self.request.GET['tipo_ta']
if tipo_ta:
q = q & Q(ta__tipo_ta_id=tipo_ta)
if 'num_ta' in self.request.GET:
num_ta = self.request.GET['num_ta']
if num_ta:
q = q & Q(ta__numero=num_ta)
if 'ano_ta' in self.request.GET:
ano_ta = self.request.GET['ano_ta']
if ano_ta:
q = q & Q(ta__ano=ano_ta)
if not q.children and not n:
n = 10
q = q & Q(nivel__gt=0)
result = Dispositivo.objects.order_by(
'-ta__data',
'-ta__ano',
'-ta__numero',
'ta',
'ordem').filter(q).select_related('ta')
if 'data_type_selection' in self.request.GET and\
self.request.GET['data_type_selection'] == 'checkbox':
result = result.exclude(
tipo_dispositivo__dispositivo_de_alteracao=True)
else:
if 'data_function' in self.request.GET and\
self.request.GET['data_function'] == 'alterador':
result = result.exclude(
tipo_dispositivo__dispositivo_de_alteracao=False,
)
result = result.exclude(
tipo_dispositivo__dispositivo_de_articulacao=False,
)
print(str(result.query))
def resultados(r): {AND_TYPE_MODEL_SELECTED}
if n:
return r[:n]
else:
return r
"""if num_ta and ano_ta and not rotulo and not str_texto and\ {AND_TEXTO_ROTULO}
'data_type_selection' in self.request.GET and\ {AND1_NUMERO}
self.request.GET['data_type_selection'] == 'checkbox': {AND2_ANO}
return r {AND3_TIPO_TA}
else: {AND_CONTROLS}
return r[:n]"""
if 'tipo_model' not in self.request.GET: order by ta.data desc,
return resultados(result) ta.numero desc,
ta.id desc,
d.ordem
{limit};
'''.format(
tipo_model = self.request.GET['tipo_model'] limit='limit {}'.format(limit) if limit else '',
if not tipo_model:
return resultados(result)
integrations_view_names = get_integrations_view_names() JOIN_TYPE_MODEL_SELECTED=jtms,
AND_TYPE_MODEL_SELECTED=atms,
tipo_ta = TipoTextoArticulado.objects.get(pk=tipo_ta) AND3_TIPO_TA="AND ta.tipo_ta_id = {}".format(
tipo_ta.id) if tipo_ta else '',
model_class = None AND2_ANO="AND ta.ano = {}".format(
for item in integrations_view_names: ano_ta) if ano_ta else '',
if hasattr(item, 'model_type_foreignkey') and\
hasattr(item, 'model'):
if (tipo_ta.content_type.model ==
item.model.__name__.lower() and
tipo_ta.content_type.app_label ==
item.model._meta.app_label):
model_class = item.model
model_type_class = item.model_type_foreignkey
tipo_model = item.model_type_foreignkey.objects.get(
pk=tipo_model)
break
if not model_class: AND1_NUMERO="AND ta.numero ~* '{}'".format(
return resultados(result) num_ta) if num_ta else '',
column_field = '' AND_TEXTO_ROTULO=AND_TEXTO_ROTULO if AND_TEXTO_ROTULO else '',
for field in model_class._meta.fields: AND_CONTROLS=AND_CONTROLS if AND_CONTROLS else ''
if field.related_model == model_type_class: )
column_field = field.column
break
if not column_field: result = Dispositivo.objects.raw(sql)
return resultados(result)
r = [] r = []
ids = set()
""" def proc_dispositivos(ds):
ao integrar um model ao app de compilação, se este model possuir
texto_articulado = GenericRelation( for d in ds:
TextoArticulado, related_query_name='texto_articulado')
será uma integração mais eficiente para as buscas de Dispositivos if d.id not in ids:
""" r.append(d)
if hasattr(model_class, 'texto_articulado'): ids.add(d.id)
q = q & Q(**{
'ta__texto_articulado__' + column_field: tipo_model.pk
})
if n:
result = result.filter(q)[:n]
else:
result = result.filter(q)
for d in result: if tipo_resultado == 'I':
if not d.ta.content_object or\ if ds != result:
not hasattr(d.ta.content_object, column_field): d.I = True
continue proc_dispositivos(d.dispositivos_filhos_set.filter(
tipo_dispositivo__dispositivo_de_alteracao=False
))
elif tipo_resultado == 'S' and ds == result:
if tipo_model.pk == getattr(d.ta.content_object, column_field): seq = Dispositivo.objects.filter(
r.append(d) ta=d.ta,
ordem__gt=d.ordem,
nivel__gt=0,
tipo_dispositivo__dispositivo_de_alteracao=False
)
proc_dispositivos(seq[:limit])
elif tipo_resultado == 'S':
d.S = True
proc_dispositivos(result)
if (len(r) == n and (not num_ta or
not ano_ta or rotulo or str_texto)):
break
return r return r
except Exception as e: except Exception as e:
username = self.request.user.username username = self.request.user.username
self.logger.error("user=" + username + ". " + str(e)) self.logger.error("user=" + username + ". " + str(e))
return []
pass
class DispositivoSearchModalView(FormView): class DispositivoSearchModalView(FormView):

7
sapl/crispy_layout_mixin.py

@ -258,7 +258,12 @@ class CrispyLayoutFormMixin:
if func: if func:
verbose_name, text = getattr(self, func)(obj, fieldname) verbose_name, text = getattr(self, func)(obj, fieldname)
else: else:
verbose_name, text = get_field_display(obj, fieldname) hook_fieldname = 'hook_%s' % fieldname
if hasattr(self, hook_fieldname):
verbose_name, text = getattr(
self, hook_fieldname)(obj)
else:
verbose_name, text = get_field_display(obj, fieldname)
return { return {
'id': fieldname, 'id': fieldname,

65
sapl/crud/base.py

@ -16,7 +16,6 @@ from django.http.response import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import cached_property
from django.utils.translation import string_concat from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DeleteView, DetailView, ListView, from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
@ -24,16 +23,15 @@ from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
from django.views.generic.base import ContextMixin from django.views.generic.base import ContextMixin
from django.views.generic.list import MultipleObjectMixin from django.views.generic.list import MultipleObjectMixin
from sapl.base.signals import post_delete_signal, post_save_signal
from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display
from sapl.crispy_layout_mixin import SaplFormHelper from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL,
RP_LIST) RP_LIST)
from sapl.utils import normalize from sapl.utils import normalize
logger = logging.getLogger(settings.BASE_DIR.name) logger = logging.getLogger(settings.BASE_DIR.name)
ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \
'list', 'create', 'detail', 'update', 'delete' 'list', 'create', 'detail', 'update', 'delete'
@ -82,7 +80,6 @@ def make_pagination(index, num_pages):
head = from_to(1, PAGINATION_LENGTH - len(tail) - 1) head = from_to(1, PAGINATION_LENGTH - len(tail) - 1)
return head + [None] + tail return head + [None] + tail
""" """
variáveis do crud: variáveis do crud:
help_topic help_topic
@ -386,12 +383,13 @@ class CrudBaseMixin(CrispyLayoutFormMixin):
class CrudListView(PermissionRequiredContainerCrudMixin, ListView): class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
permission_required = (RP_LIST, ) permission_required = (RP_LIST,)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@classmethod @classmethod
def get_url_regex(cls): def get_url_regex(cls):
return r'^$' return r'^$'
paginate_by = 10 paginate_by = 10
no_entries_msg = _('Nenhum registro encontrado.') no_entries_msg = _('Nenhum registro encontrado.')
@ -423,7 +421,13 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
if hasattr(f, 'related_model') and f.related_model: if hasattr(f, 'related_model') and f.related_model:
m = f.related_model m = f.related_model
if f: if f:
s.append(force_text(f.verbose_name)) hook = 'hook_header_{}'.format(''.join(fn))
if hasattr(self, hook):
header = getattr(self, hook)()
s.append(header)
else:
s.append(force_text(f.verbose_name))
s = ' / '.join(s) s = ' / '.join(s)
r.append(s) r.append(s)
return r return r
@ -598,7 +602,7 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
model_ordering = (model_ordering,) model_ordering = (model_ordering,)
for mo in model_ordering: for mo in model_ordering:
if mo not in ordering: if mo not in ordering:
ordering = ordering + (mo, ) ordering = ordering + (mo,)
queryset = queryset.order_by(*ordering) queryset = queryset.order_by(*ordering)
# print(ordering) # print(ordering)
@ -618,9 +622,38 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
return queryset return queryset
class AuditLogMixin(object):
def delete(self, request, *args, **kwargs):
# Classe deve implementar um get_object(), i.e., deve ser uma View
deleted_object = self.get_object()
try:
return super(AuditLogMixin, self).delete(request, args, kwargs)
finally:
post_delete_signal.send(sender=None,
instance=deleted_object,
operation='D',
request=self.request)
# SAVE/UPDATE method
def form_valid(self, form):
try:
if not form.instance.pk:
operation = 'C'
else:
operation = 'U'
return super(AuditLogMixin, self).form_valid(form)
finally:
post_save_signal.send(sender=None,
instance=form.instance,
operation=operation,
request=self.request
)
class CrudCreateView(PermissionRequiredContainerCrudMixin, class CrudCreateView(PermissionRequiredContainerCrudMixin,
FormMessagesMixin, CreateView): FormMessagesMixin, AuditLogMixin, CreateView):
permission_required = (RP_ADD, ) permission_required = (RP_ADD,)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@classmethod @classmethod
@ -692,7 +725,7 @@ class CrudCreateView(PermissionRequiredContainerCrudMixin,
class CrudDetailView(PermissionRequiredContainerCrudMixin, class CrudDetailView(PermissionRequiredContainerCrudMixin,
DetailView, MultipleObjectMixin): DetailView, MultipleObjectMixin):
permission_required = (RP_DETAIL, ) permission_required = (RP_DETAIL,)
no_entries_msg = _('Nenhum registro Associado.') no_entries_msg = _('Nenhum registro Associado.')
paginate_by = 10 paginate_by = 10
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -837,8 +870,8 @@ class CrudDetailView(PermissionRequiredContainerCrudMixin,
class CrudUpdateView(PermissionRequiredContainerCrudMixin, class CrudUpdateView(PermissionRequiredContainerCrudMixin,
FormMessagesMixin, UpdateView): FormMessagesMixin, AuditLogMixin, UpdateView):
permission_required = (RP_CHANGE, ) permission_required = (RP_CHANGE,)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def form_valid(self, form): def form_valid(self, form):
@ -868,8 +901,8 @@ class CrudUpdateView(PermissionRequiredContainerCrudMixin,
class CrudDeleteView(PermissionRequiredContainerCrudMixin, class CrudDeleteView(PermissionRequiredContainerCrudMixin,
FormMessagesMixin, DeleteView): FormMessagesMixin, AuditLogMixin, DeleteView):
permission_required = (RP_DELETE, ) permission_required = (RP_DELETE,)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@classmethod @classmethod
@ -929,10 +962,12 @@ class Crud:
def _add_base(view): def _add_base(view):
if view: if view:
class CrudViewWithBase(cls.BaseMixin, view): class CrudViewWithBase(cls.BaseMixin, view):
model = cls.model model = cls.model
help_topic = cls.help_topic help_topic = cls.help_topic
crud = cls crud = cls
CrudViewWithBase.__name__ = view.__name__ CrudViewWithBase.__name__ = view.__name__
return CrudViewWithBase return CrudViewWithBase
@ -966,11 +1001,13 @@ class Crud:
def build(cls, _model, _help_topic, _model_set=None, list_field_names=[]): def build(cls, _model, _help_topic, _model_set=None, list_field_names=[]):
def create_class(_list_field_names): def create_class(_list_field_names):
class ModelCrud(cls): class ModelCrud(cls):
model = _model model = _model
model_set = _model_set model_set = _model_set
help_topic = _help_topic help_topic = _help_topic
list_field_names = _list_field_names list_field_names = _list_field_names
return ModelCrud return ModelCrud
ModelCrud = create_class(list_field_names) ModelCrud = create_class(list_field_names)

30
sapl/materia/forms.py

@ -4,8 +4,8 @@ import os
import sapl import sapl
from crispy_forms.bootstrap import Alert, InlineRadios from crispy_forms.bootstrap import Alert, InlineRadios
from crispy_forms.layout import (HTML, Button, Field, Fieldset, from crispy_forms.layout import (Button, Field, Fieldset, HTML, Layout, Row)
Layout, Row)
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
@ -24,11 +24,12 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sapl.base.models import AppConfig, Autor, TipoAutor from sapl.base.models import AppConfig, Autor, TipoAutor
from sapl.base.signals import post_save_signal
from sapl.comissoes.models import Comissao, Composicao, Participacao from sapl.comissoes.models import Comissao, Composicao, Participacao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PRIVATE) STATUS_TA_PRIVATE)
from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
to_column, to_row, SaplFormHelper) SaplFormLayout, to_column, to_row)
from sapl.materia.models import (AssuntoMateria, Autoria, MateriaAssunto, from sapl.materia.models import (AssuntoMateria, Autoria, MateriaAssunto,
MateriaLegislativa, Orgao, MateriaLegislativa, Orgao,
RegimeTramitacao, StatusTramitacao, RegimeTramitacao, StatusTramitacao,
@ -46,7 +47,7 @@ from sapl.utils import (ChoiceWithoutValidationField, choice_anos_com_materias,
autor_label, autor_modal, gerar_hash_arquivo, proposicao_modal, autor_label, autor_modal, gerar_hash_arquivo, proposicao_modal,
models_with_gr_for_model, qs_override_django_filter, models_with_gr_for_model, qs_override_django_filter,
RangeWidgetOverride, SEPARADOR_HASH_PROPOSICAO, RangeWidgetOverride, SEPARADOR_HASH_PROPOSICAO,
YES_NO_CHOICES) validar_arquivo, YES_NO_CHOICES)
from .models import (AcompanhamentoMateria, Anexada, Autoria, from .models import (AcompanhamentoMateria, Anexada, Autoria,
DespachoInicial, DocumentoAcessorio, Numeracao, DespachoInicial, DocumentoAcessorio, Numeracao,
@ -116,7 +117,7 @@ class ReceberProposicaoForm(Form):
row1 = to_row([('cod_hash', 12)]) row1 = to_row([('cod_hash', 12)])
row2 = to_row( row2 = to_row(
[(Button('pesquisar-cod-proposicao', [(Button('pesquisar-cod-proposicao',
'Pesquisar Código de Proposicoes', 'Pesquisar Código de Proposições por Data',
css_class='btn btn-primary btn-sm'), 4) css_class='btn btn-primary btn-sm'), 4)
]) ])
self.helper = SaplFormHelper() self.helper = SaplFormHelper()
@ -258,9 +259,8 @@ class MateriaLegislativaForm(FileFieldCheckMixin, ModelForm):
texto_original = self.cleaned_data.get('texto_original', False) texto_original = self.cleaned_data.get('texto_original', False)
if texto_original and texto_original.size > MAX_DOC_UPLOAD_SIZE: if texto_original:
raise ValidationError("O arquivo Texto Original deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" validar_arquivo(texto_original, "Texto Original")
.format((MAX_DOC_UPLOAD_SIZE / 1024) / 1024, (texto_original.size / 1024) / 1024))
return cleaned_data return cleaned_data
@ -361,9 +361,8 @@ class DocumentoAcessorioForm(FileFieldCheckMixin, ModelForm):
arquivo = self.cleaned_data.get('arquivo', False) arquivo = self.cleaned_data.get('arquivo', False)
if arquivo and arquivo.size > MAX_DOC_UPLOAD_SIZE: if arquivo:
raise ValidationError("O arquivo Texto Integral deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" validar_arquivo(arquivo, "Texto Integral")
.format((MAX_DOC_UPLOAD_SIZE / 1024) / 1024, (arquivo.size / 1024) / 1024))
return self.cleaned_data return self.cleaned_data
@ -1938,9 +1937,8 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
def clean_texto_original(self): def clean_texto_original(self):
texto_original = self.cleaned_data.get('texto_original', False) texto_original = self.cleaned_data.get('texto_original', False)
if texto_original and texto_original.size > MAX_DOC_UPLOAD_SIZE: if texto_original:
raise ValidationError("O arquivo Texto Original deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" validar_arquivo(texto_original, "Texto Original")
.format((MAX_DOC_UPLOAD_SIZE / 1024) / 1024, (texto_original.size / 1024) / 1024))
return texto_original return texto_original
@ -2469,7 +2467,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
# dados básicos # dados básicos
doc = DocumentoAcessorio() doc = DocumentoAcessorio()
doc.materia = proposicao.materia_de_vinculo doc.materia = proposicao.materia_de_vinculo
doc.autor = str(proposicao.autor) doc.autor = str(proposicao.autor)[:200]
doc.tipo = proposicao.tipo.tipo_conteudo_related doc.tipo = proposicao.tipo.tipo_conteudo_related
doc.ementa = proposicao.descricao doc.ementa = proposicao.descricao

2
sapl/materia/migrations/0059_auto_20191001_1450.py → sapl/materia/migrations/0058_auto_20191001_1450.py

@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('materia', '0058_auto_20191001_1115'), ('materia', '0057_materiaemtramitacao'),
] ]
operations = [ operations = [

16
sapl/materia/migrations/0059_merge_20191003_0854.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-03 11:54
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0058_auto_20191001_1450'),
('materia', '0058_auto_20191001_1115'),
]
operations = [
]

4
sapl/materia/migrations/0063_auto_20191010_1228.py → sapl/materia/migrations/0060_auto_20190930_1136.py

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-10 15:28 # Generated by Django 1.11.20 on 2019-09-30 14:36
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('materia', '0062_remove_tramitacao_turno'), ('materia', '0059_merge_20191003_0854'),
] ]
operations = [ operations = [

37
sapl/materia/migrations/0061_auto_20191120_1440.py

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-11-20 17:40
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.materia.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('materia', '0060_auto_20190930_1136'),
]
operations = [
migrations.AlterField(
model_name='documentoacessorio',
name='arquivo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.anexo_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'),
),
migrations.AlterField(
model_name='documentoacessorio',
name='autor',
field=models.CharField(blank=True, max_length=200, verbose_name='Autor'),
),
migrations.AlterField(
model_name='materialegislativa',
name='texto_original',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.materia_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'),
),
migrations.AlterField(
model_name='proposicao',
name='texto_original',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.materia_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'),
),
]

2
sapl/materia/migrations/0060_auto_20190905_1134.py → sapl/materia/migrations/0062_auto_20190905_1134.py

@ -9,7 +9,7 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('materia', '0059_auto_20191001_1450'), ('materia', '0061_auto_20191120_1440'),
] ]
operations = [ operations = [

2
sapl/materia/migrations/0061_auto_20190905_1135.py → sapl/materia/migrations/0063_auto_20190905_1135.py

@ -37,7 +37,7 @@ def migra_tipos_turnos_tramitacao(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('materia', '0060_auto_20190905_1134'), ('materia', '0062_auto_20190905_1134')
] ]
operations = [ operations = [

2
sapl/materia/migrations/0062_remove_tramitacao_turno.py → sapl/materia/migrations/0064_remove_tramitacao_turno.py

@ -8,7 +8,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('materia', '0061_auto_20190905_1135'), ('materia', '0063_auto_20190905_1135')
] ]
operations = [ operations = [

8
sapl/materia/models.py

@ -257,7 +257,7 @@ class MateriaLegislativa(models.Model):
'materia_principal', 'materia_principal',
'materia_anexada')) 'materia_anexada'))
texto_original = models.FileField( texto_original = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=materia_upload_path, upload_to=materia_upload_path,
@ -535,13 +535,13 @@ class DocumentoAcessorio(models.Model):
data = models.DateField(blank=True, null=True, data = models.DateField(blank=True, null=True,
default=None, verbose_name=_('Data')) default=None, verbose_name=_('Data'))
autor = models.CharField( autor = models.CharField(
max_length=50, blank=True, verbose_name=_('Autor')) max_length=200, blank=True, verbose_name=_('Autor'))
ementa = models.TextField(blank=True, verbose_name=_('Ementa')) ementa = models.TextField(blank=True, verbose_name=_('Ementa'))
indexacao = models.TextField(blank=True) indexacao = models.TextField(blank=True)
arquivo = models.FileField( arquivo = models.FileField(
blank=True, blank=True,
null=True, null=True,
max_length=255, max_length=300,
upload_to=anexo_upload_path, upload_to=anexo_upload_path,
verbose_name=_('Texto Integral'), verbose_name=_('Texto Integral'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
@ -807,7 +807,7 @@ class Proposicao(models.Model):
('I', 'Incorporada')), ('I', 'Incorporada')),
verbose_name=_('Status Proposição')) verbose_name=_('Status Proposição'))
texto_original = models.FileField( texto_original = models.FileField(
max_length=200, max_length=300,
upload_to=materia_upload_path, upload_to=materia_upload_path,
blank=True, blank=True,
null=True, null=True,

25
sapl/materia/views.py

@ -33,13 +33,13 @@ from django_filters.views import FilterView
from sapl.base.email_utils import do_envia_email_confirmacao from sapl.base.email_utils import do_envia_email_confirmacao
from sapl.base.models import Autor, CasaLegislativa, AppConfig as BaseAppConfig from sapl.base.models import Autor, CasaLegislativa, AppConfig as BaseAppConfig
from sapl.base.signals import tramitacao_signal from sapl.base.signals import tramitacao_signal, post_delete_signal, post_save_signal
from sapl.comissoes.models import Comissao, Participacao, Composicao from sapl.comissoes.models import Comissao, Participacao, Composicao
from sapl.compilacao.models import STATUS_TA_IMMUTABLE_RESTRICT, STATUS_TA_PRIVATE from sapl.compilacao.models import STATUS_TA_IMMUTABLE_RESTRICT, STATUS_TA_PRIVATE
from sapl.compilacao.views import IntegracaoTaView from sapl.compilacao.views import IntegracaoTaView
from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, SaplFormLayout from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, SaplFormLayout
from sapl.crud.base import (Crud, CrudAux, make_pagination, MasterDetailCrud, from sapl.crud.base import (Crud, CrudAux, make_pagination, MasterDetailCrud,
PermissionRequiredForAppCrudMixin, RP_DETAIL, RP_LIST) PermissionRequiredForAppCrudMixin, RP_DETAIL, RP_LIST,)
from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm, from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm,
ConfirmarProposicaoForm, DevolverProposicaoForm, ConfirmarProposicaoForm, DevolverProposicaoForm,
DespachoInicialCreateForm, LegislacaoCitadaForm, DespachoInicialCreateForm, LegislacaoCitadaForm,
@ -324,7 +324,7 @@ def recuperar_proposicao(request):
data = request.GET.get('data') data = request.GET.get('data')
proposicoes = Proposicao.objects.filter( proposicoes = Proposicao.objects.filter(
data_envio__date=datetime.strptime(data, '%d/%m/%Y').date(), data_envio__date=datetime.strptime(data, '%Y-%m-%d').date(),
data_envio__isnull=False, data_envio__isnull=False,
data_recebimento__isnull=True, data_recebimento__isnull=True,
data_devolucao__isnull=True data_devolucao__isnull=True
@ -807,7 +807,7 @@ class ProposicaoCrud(Crud):
p = Proposicao.objects.get(id=kwargs['pk']) p = Proposicao.objects.get(id=kwargs['pk'])
msg_error = '' msg_error = ''
if p and p.autor.user == user: if p and p.autor == user.autoruser.autor:
if action == 'send': if action == 'send':
if p.data_envio and p.data_recebimento: if p.data_envio and p.data_recebimento:
msg_error = _('Proposição já foi enviada e recebida.') msg_error = _('Proposição já foi enviada e recebida.')
@ -1408,7 +1408,7 @@ class TramitacaoCrud(MasterDetailCrud):
messages.add_message(request, messages.ERROR, msg) messages.add_message(request, messages.ERROR, msg)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
tramitacoes_deletar = [tramitacao.id] tramitacoes_deletar = [tramitacao]
if materia.tramitacao_set.count() == 0: if materia.tramitacao_set.count() == 0:
materia.em_tramitacao = False materia.em_tramitacao = False
materia.save() materia.save()
@ -1419,11 +1419,18 @@ class TramitacaoCrud(MasterDetailCrud):
for ma in mat_anexadas: for ma in mat_anexadas:
tram_anexada = ma.tramitacao_set.last() tram_anexada = ma.tramitacao_set.last()
if compara_tramitacoes_mat(tram_anexada, tramitacao): if compara_tramitacoes_mat(tram_anexada, tramitacao):
tramitacoes_deletar.append(tram_anexada.id) tramitacoes_deletar.append(tram_anexada)
if ma.tramitacao_set.count() == 0: if ma.tramitacao_set.count() == 0:
ma.em_tramitacao = False ma.em_tramitacao = False
ma.save() ma.save()
Tramitacao.objects.filter(id__in=tramitacoes_deletar).delete() Tramitacao.objects.filter(id__in=[t.id for t in tramitacoes_deletar]).delete()
# TODO: otimizar para passar a lista de matérias
for tramitacao in tramitacoes_deletar:
post_delete_signal.send(sender=None,
instance=tramitacao,
operation='C',
request=self.request)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -2678,8 +2685,8 @@ class MateriaPesquisaSimplesView(PermissionRequiredMixin, FormView):
kwargs.update({'tipo': form.cleaned_data['tipo_materia']}) kwargs.update({'tipo': form.cleaned_data['tipo_materia']})
if form.cleaned_data.get('data_inicial'): if form.cleaned_data.get('data_inicial'):
kwargs.update({'data__gte': form.cleaned_data['data_inicial'], kwargs.update({'data_apresentacao__gte': form.cleaned_data['data_inicial'],
'data__lte': form.cleaned_data['data_final']}) 'data_apresentacao__lte': form.cleaned_data['data_final']})
materias = MateriaLegislativa.objects.filter( materias = MateriaLegislativa.objects.filter(
**kwargs).order_by('-numero', 'ano') **kwargs).order_by('-numero', 'ano')

43
sapl/norma/forms.py

@ -1,27 +1,28 @@
import django_filters
import logging import logging
from sapl.crispy_layout_mixin import SaplFormHelper
from crispy_forms.layout import Fieldset, Layout from crispy_forms.layout import Fieldset, Layout
from django import forms from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.forms import ModelForm, widgets, ModelChoiceField from django.forms import ModelChoiceField, ModelForm, widgets
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor, TipoAutor from sapl.base.models import Autor, TipoAutor
from sapl.crispy_layout_mixin import form_actions, to_row from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, to_row
from sapl.materia.forms import choice_anos_com_materias from sapl.materia.forms import choice_anos_com_materias
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.materia.models import (MateriaLegislativa,
from sapl.settings import MAX_DOC_UPLOAD_SIZE TipoMateriaLegislativa)
from sapl.utils import NormaPesquisaOrderingFilter, RangeWidgetOverride, \ from sapl.utils import (ANO_CHOICES, choice_anos_com_normas,
choice_anos_com_normas, FilterOverridesMetaMixin, FileFieldCheckMixin, ANO_CHOICES FileFieldCheckMixin, FilterOverridesMetaMixin,
NormaPesquisaOrderingFilter, RangeWidgetOverride,
validar_arquivo)
from .models import (AnexoNormaJuridica, AssuntoNorma, NormaJuridica, NormaRelacionada, from .models import (AnexoNormaJuridica, AssuntoNorma, AutoriaNorma,
TipoNormaJuridica, AutoriaNorma) NormaJuridica, NormaRelacionada, TipoNormaJuridica)
def get_esferas(): def get_esferas():
@ -217,9 +218,8 @@ class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
texto_integral = self.cleaned_data.get('texto_integral', False) texto_integral = self.cleaned_data.get('texto_integral', False)
if texto_integral and texto_integral.size > MAX_DOC_UPLOAD_SIZE: if texto_integral:
raise ValidationError("O arquivo Texto Integral deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(texto_integral, "Texto Integral")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (texto_integral.size/1024)/1024))
return texto_integral return texto_integral
@ -283,6 +283,14 @@ class AutoriaNormaForm(ModelForm):
class AnexoNormaJuridicaForm(FileFieldCheckMixin, ModelForm): class AnexoNormaJuridicaForm(FileFieldCheckMixin, ModelForm):
logger = logging.getLogger(__name__)
anexo_arquivo = forms.FileField(
required=True,
label="Arquivo Anexo"
)
class Meta: class Meta:
model = AnexoNormaJuridica model = AnexoNormaJuridica
fields = ['norma', 'anexo_arquivo', 'assunto_anexo'] fields = ['norma', 'anexo_arquivo', 'assunto_anexo']
@ -290,8 +298,6 @@ class AnexoNormaJuridicaForm(FileFieldCheckMixin, ModelForm):
'norma': forms.HiddenInput(), 'norma': forms.HiddenInput(),
} }
logger = logging.getLogger(__name__)
def clean(self): def clean(self):
cleaned_data = super(AnexoNormaJuridicaForm, self).clean() cleaned_data = super(AnexoNormaJuridicaForm, self).clean()
@ -300,9 +306,8 @@ class AnexoNormaJuridicaForm(FileFieldCheckMixin, ModelForm):
anexo_arquivo = self.cleaned_data.get('anexo_arquivo', False) anexo_arquivo = self.cleaned_data.get('anexo_arquivo', False)
if anexo_arquivo and anexo_arquivo.size > MAX_DOC_UPLOAD_SIZE: if anexo_arquivo:
raise ValidationError("O Arquivo Anexo deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(anexo_arquivo, "Arquivo Anexo")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (anexo_arquivo.size/1024)/1024))
return cleaned_data return cleaned_data

22
sapl/norma/migrations/0028_auto_20191024_1330.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-24 16:30
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.norma.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('norma', '0027_auto_20191001_1115'),
]
operations = [
migrations.AlterField(
model_name='normajuridica',
name='texto_integral',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.norma.models.norma_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'),
),
]

22
sapl/norma/migrations/0029_auto_20191024_1344.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-24 16:44
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.norma.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('norma', '0028_auto_20191024_1330'),
]
operations = [
migrations.AlterField(
model_name='anexonormajuridica',
name='anexo_arquivo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.norma.models.norma_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Arquivo Anexo'),
),
]

16
sapl/norma/migrations/0030_merge_20191204_1129.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-12-04 14:29
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('norma', '0029_auto_20191024_1344'),
('norma', '0028_merge_20191003_0909'),
]
operations = [
]

18
sapl/norma/models.py

@ -79,7 +79,7 @@ class NormaJuridica(models.Model):
) )
texto_integral = models.FileField( texto_integral = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=norma_upload_path, upload_to=norma_upload_path,
@ -184,10 +184,14 @@ class NormaJuridica(models.Model):
return anexos return anexos
def __str__(self): def __str__(self):
return _('%(tipo)s%(numero)s de %(data)s') % { numero_norma = self.numero
if numero_norma.isnumeric():
numero_norma = '{0:,}'.format(int(self.numero)).replace(',', '.')
return _('%(tipo)s%(numero)s, de %(data)s') % {
'tipo': self.tipo, 'tipo': self.tipo,
'numero': self.numero, 'numero': numero_norma,
'data': defaultfilters.date(self.data, "d \d\e F \d\e Y")} 'data': defaultfilters.date(self.data, "d \d\e F \d\e Y").lower()}
@property @property
def epigrafe(self): def epigrafe(self):
@ -346,8 +350,8 @@ class NormaRelacionada(models.Model):
def __str__(self): def __str__(self):
return _('Principal: %(norma_principal)s' return _('Principal: %(norma_principal)s'
' - Relacionada: %(norma_relacionada)s') % { ' - Relacionada: %(norma_relacionada)s') % {
'norma_principal': self.norma_principal, 'norma_principal': str(self.norma_principal),
'norma_relacionada': self.norma_relacionada} 'norma_relacionada': str(self.norma_relacionada)}
@reversion.register() @reversion.register()
@ -364,7 +368,7 @@ class AnexoNormaJuridica(models.Model):
max_length=250 max_length=250
) )
anexo_arquivo = models.FileField( anexo_arquivo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=norma_upload_path, upload_to=norma_upload_path,

24
sapl/norma/views.py

@ -78,7 +78,7 @@ class NormaDestaquesView(ListView):
class NormaPesquisaView(FilterView): class NormaPesquisaView(FilterView):
model = NormaJuridica model = NormaJuridica
filterset_class = NormaFilterSet filterset_class = NormaFilterSet
paginate_by = 10 paginate_by = 50
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
@ -274,7 +274,8 @@ class NormaCrud(Crud):
pk=self.kwargs['pk'] pk=self.kwargs['pk']
) )
# Feito desta forma para que sejam materializados os assuntos antigos # Feito desta forma para que sejam materializados os assuntos
# antigos
assuntos_antigos = set(norma_antiga.assuntos.all()) assuntos_antigos = set(norma_antiga.assuntos.all())
dict_objeto_antigo = norma_antiga.__dict__ dict_objeto_antigo = norma_antiga.__dict__
@ -282,11 +283,11 @@ class NormaCrud(Crud):
dict_objeto_novo = self.object.__dict__ dict_objeto_novo = self.object.__dict__
atributos = ['tipo_id', 'numero', 'ano', 'data', 'esfera_federacao', atributos = ['tipo_id', 'numero', 'ano', 'data', 'esfera_federacao',
'complemento', 'materia_id', 'numero', 'complemento', 'materia_id', 'numero',
'data_publicacao', 'data_vigencia', 'data_publicacao', 'data_vigencia',
'veiculo_publicacao', 'pagina_inicio_publicacao', 'veiculo_publicacao', 'pagina_inicio_publicacao',
'pagina_fim_publicacao', 'ementa', 'indexacao', 'pagina_fim_publicacao', 'ementa', 'indexacao',
'observacao', 'texto_integral'] 'observacao', 'texto_integral']
for atributo in atributos: for atributo in atributos:
if dict_objeto_antigo[atributo] != dict_objeto_novo[atributo]: if dict_objeto_antigo[atributo] != dict_objeto_novo[atributo]:
@ -295,7 +296,8 @@ class NormaCrud(Crud):
self.object.save() self.object.save()
break break
# Campo Assuntos não veio no __dict__, então é comparado separadamente # Campo Assuntos não veio no __dict__, então é comparado
# separadamente
assuntos_novos = set(self.object.assuntos.all()) assuntos_novos = set(self.object.assuntos.all())
if assuntos_antigos != assuntos_novos: if assuntos_antigos != assuntos_novos:
self.object.user = self.request.user self.object.user = self.request.user
@ -391,7 +393,8 @@ class ImpressosView(PermissionRequiredMixin, TemplateView):
def gerar_pdf_impressos(request, context, template_name): def gerar_pdf_impressos(request, context, template_name):
template = loader.get_template(template_name) template = loader.get_template(template_name)
html = template.render(context, request) html = template.render(context, request)
pdf = weasyprint.HTML(string=html, base_url=request.build_absolute_uri()).write_pdf() pdf = weasyprint.HTML(
string=html, base_url=request.build_absolute_uri()).write_pdf()
response = HttpResponse(pdf, content_type='application/pdf') response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'inline; filename="relatorio_impressos.pdf"' response['Content-Disposition'] = 'inline; filename="relatorio_impressos.pdf"'
@ -418,7 +421,8 @@ class NormaPesquisaSimplesView(PermissionRequiredMixin, FormView):
kwargs.update({'data__gte': form.cleaned_data['data_inicial'], kwargs.update({'data__gte': form.cleaned_data['data_inicial'],
'data__lte': form.cleaned_data['data_final']}) 'data__lte': form.cleaned_data['data_final']})
normas = NormaJuridica.objects.filter(**kwargs).order_by('-numero', 'ano') normas = NormaJuridica.objects.filter(
**kwargs).order_by('-numero', 'ano')
quantidade_normas = normas.count() quantidade_normas = normas.count()
normas = normas[:2000] if quantidade_normas > 2000 else normas normas = normas[:2000] if quantidade_normas > 2000 else normas

9
sapl/painel/urls.py

@ -4,7 +4,8 @@ from .apps import AppConfig
from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view, from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view,
painel_parlamentar_view, painel_view, painel_votacao_view, painel_parlamentar_view, painel_view, painel_votacao_view,
switch_painel, verifica_painel, votante_view, CronometroPainelCrud, switch_painel, verifica_painel, votante_view, CronometroPainelCrud,
PainelConfigCrud,ordena_cronometro, painel_parcial_view) PainelConfigCrud, ordena_cronometro, painel_parcial_view, painel_discurso_view,
get_dados_painel_discurso)
app_name = AppConfig.name app_name = AppConfig.name
@ -12,6 +13,7 @@ urlpatterns = [
url(r'^painel-principal/(?P<pk>\d+)$', painel_view, url(r'^painel-principal/(?P<pk>\d+)$', painel_view,
name="painel_principal"), name="painel_principal"),
url(r'^painel/(?P<pk>\d+)/dados$', get_dados_painel, name='dados_painel'), url(r'^painel/(?P<pk>\d+)/dados$', get_dados_painel, name='dados_painel'),
url(r'^painel/mensagem$', painel_mensagem_view, name="painel_mensagem"), url(r'^painel/mensagem$', painel_mensagem_view, name="painel_mensagem"),
url(r'^painel/parlamentar$', painel_parlamentar_view, url(r'^painel/parlamentar$', painel_parlamentar_view,
name='painel_parlamentar'), name='painel_parlamentar'),
@ -31,4 +33,9 @@ urlpatterns = [
url(r'^painel-parcial/(?P<pk>\d+)/(?P<opcoes>\d+)/$', painel_parcial_view, url(r'^painel-parcial/(?P<pk>\d+)/(?P<opcoes>\d+)/$', painel_parcial_view,
name="painel_parcial"), name="painel_parcial"),
url(r'^painel-discurso/(?P<sessao_pk>\d+)/(?P<lista_pk>\d+)/$', painel_discurso_view,
name="painel_discurso"),
url(r'^painel/(?P<pk>\d+)/(?P<lista_pk>\d+)/dados-discurso$', get_dados_painel_discurso,
name='dados_painel_discurso'),
] ]

93
sapl/painel/views.py

@ -1,6 +1,7 @@
import html import html
import json import json
import logging import logging
import os
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
@ -21,7 +22,8 @@ from sapl.parlamentares.models import Legislatura, Parlamentar, Votante
from sapl.sessao.models import (ExpedienteMateria, OradorExpediente, OrdemDia, from sapl.sessao.models import (ExpedienteMateria, OradorExpediente, OrdemDia,
PresencaOrdemDia, RegistroVotacao, PresencaOrdemDia, RegistroVotacao,
SessaoPlenaria, SessaoPlenariaPresenca, SessaoPlenaria, SessaoPlenariaPresenca,
VotoParlamentar, RegistroLeitura) VotoParlamentar, CronometroLista, ListaDiscurso,
ParlamentarLista, RegistroLeitura)
from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave
from .forms import CronometroForm, ConfiguracoesPainelForm from .forms import CronometroForm, ConfiguracoesPainelForm
@ -353,12 +355,15 @@ def painel_view(request, pk):
'resultado': True, 'resultado': True,
'materia': True 'materia': True
} }
now = timezone.localtime(timezone.now())
utc_offset = now.utcoffset().total_seconds() / 60
context = {'head_title': str(_('Painel Plenário')), context = {'head_title': str(_('Painel Plenário')),
'sessao_id': pk, 'sessao_id': pk,
'cronometros': Cronometro.objects.filter(ativo=True).order_by('ordenacao'), 'cronometros': Cronometro.objects.filter(ativo=True).order_by('ordenacao'),
'painel_config': PainelConfig.objects.first(), 'painel_config': PainelConfig.objects.first(),
'casa': CasaLegislativa.objects.last(), 'casa': CasaLegislativa.objects.last(),
'exibicao': exibicao 'exibicao': exibicao,
'utc_offset': utc_offset,
} }
return render(request, 'painel/index.html', context) return render(request, 'painel/index.html', context)
@ -375,12 +380,15 @@ def painel_parcial_view(request, pk, opcoes):
'resultado': bit_is_set(opcoes, 3), 'resultado': bit_is_set(opcoes, 3),
'materia': bit_is_set(opcoes, 4) 'materia': bit_is_set(opcoes, 4)
} }
now = timezone.localtime(timezone.now())
utc_offset = now.utcoffset().total_seconds() / 60
context = {'head_title': str(_('Painel Plenário')), context = {'head_title': str(_('Painel Plenário')),
'sessao_id': pk, 'sessao_id': pk,
'cronometros': Cronometro.objects.filter(ativo=True).order_by('ordenacao'), 'cronometros': Cronometro.objects.filter(ativo=True).order_by('ordenacao'),
'painel_config': PainelConfig.objects.first(), 'painel_config': PainelConfig.objects.first(),
'casa': CasaLegislativa.objects.last(), 'casa': CasaLegislativa.objects.last(),
'exibicao': exibicao 'exibicao': exibicao,
'utc_offset': utc_offset,
} }
return render(request, 'painel/index.html', context) return render(request, 'painel/index.html', context)
@ -727,16 +735,21 @@ def get_dados_painel(request, pk):
expediente__sessao_plenaria=sessao).order_by('data_hora').last() expediente__sessao_plenaria=sessao).order_by('data_hora').last()
# Obtém última matéria que foi votada, através do timestamp mais recente # Obtém última matéria que foi votada, através do timestamp mais recente
ordem_expediente = None
ultimo_timestamp = None
if last_ordem_voto: if last_ordem_voto:
ordem_expediente = last_ordem_voto.ordem ordem_expediente = last_ordem_voto.ordem
ultimo_timestamp = last_ordem_voto.data_hora ultimo_timestamp = last_ordem_voto.data_hora
if last_expediente_voto and last_expediente_voto.data_hora > ultimo_timestamp: if (last_expediente_voto and ultimo_timestamp and last_expediente_voto.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_expediente_voto):
ordem_expediente = last_expediente_voto.expediente ordem_expediente = last_expediente_voto.expediente
ultimo_timestamp = last_expediente_voto.data_hora ultimo_timestamp = last_expediente_voto.data_hora
if last_ordem_leitura and last_ordem_leitura.data_hora > ultimo_timestamp: if (last_ordem_leitura and ultimo_timestamp and last_ordem_leitura.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_ordem_leitura):
ordem_expediente = last_ordem_leitura.ordem ordem_expediente = last_ordem_leitura.ordem
ultimo_timestamp = last_ordem_leitura.data_hora ultimo_timestamp = last_ordem_leitura.data_hora
if last_expediente_leitura and last_expediente_leitura.data_hora > ultimo_timestamp: if (last_expediente_leitura and ultimo_timestamp and last_expediente_leitura.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_expediente_leitura):
ordem_expediente = last_expediente_leitura.expediente ordem_expediente = last_expediente_leitura.expediente
ultimo_timestamp = last_expediente_leitura.data_hora ultimo_timestamp = last_expediente_leitura.data_hora
@ -748,3 +761,71 @@ def get_dados_painel(request, pk):
# Retorna que não há nenhuma matéria já votada ou aberta # Retorna que não há nenhuma matéria já votada ou aberta
return response_nenhuma_materia(get_presentes(pk, response, None)) return response_nenhuma_materia(get_presentes(pk, response, None))
@user_passes_test(check_permission)
def painel_discurso_view(request, sessao_pk, lista_pk):
cronometros_ids = CronometroLista.objects.filter(tipo_lista_id=lista_pk).values_list('cronometro', flat=True)
cronometros = Cronometro.objects.filter(id__in=cronometros_ids)
lista = ListaDiscurso.objects.get(tipo_id=lista_pk, sessao_plenaria_id=sessao_pk)
context = {
'head_title': str(_('Painel de Discurso')),
'sessao_id': sessao_pk,
'lista': lista,
'cronometros': cronometros,
'casa': CasaLegislativa.objects.last(),
'painel_config': PainelConfig.objects.first(),
}
return render(request, 'painel/painel_discurso.html', context)
@user_passes_test(check_permission)
def get_dados_painel_discurso(request, pk, lista_pk):
sessao = SessaoPlenaria.objects.get(id=pk)
casa = CasaLegislativa.objects.first()
app_config = ConfiguracoesAplicacao.objects.first()
brasao = None
if casa and app_config and (bool(casa.logotipo)):
brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None
CRONOMETRO_STATUS = {
'I': 'start',
'R': 'reset',
'S': 'stop',
'C': 'increment'
}
cronometros = Cronometro.objects.filter(cronometrolista__tipo_lista_id=lista_pk)
dict_status_cronometros = dict(cronometros.order_by('ordenacao').values_list('id', 'status'))
for key, value in dict_status_cronometros.items():
dict_status_cronometros[key] = CRONOMETRO_STATUS[dict_status_cronometros[key]]
dict_duracao_cronometros = dict(cronometros.values_list('id', 'duracao_cronometro'))
for key, value in dict_duracao_cronometros.items():
dict_duracao_cronometros[key] = value.seconds
lista = ListaDiscurso.objects.get(tipo_id=lista_pk, sessao_plenaria_id=pk)
orador = lista.orador_atual
oradores = ParlamentarLista.objects.filter(lista=lista).order_by('ordenacao').values_list('parlamentar__nome_parlamentar', flat=True)
response = {
'sessao_plenaria': str(sessao),
'sessao_plenaria_data': sessao.data_inicio.strftime('%d/%m/%Y'),
'sessao_plenaria_hora_inicio': sessao.hora_inicio,
'cronometros': dict_status_cronometros,
'duracao_cronometros': dict_duracao_cronometros,
'sessao_finalizada': sessao.finalizada,
'brasao': brasao,
'orador': orador.nome_parlamentar if orador else '',
'orador_img': orador.fotografia.url if orador and os.path.isfile(orador.fotografia.path) else None,
'oradores': list(oradores)
}
return JsonResponse(response)

10
sapl/parlamentares/forms.py

@ -79,6 +79,8 @@ def validar_datas_legislatura(eleicao, inicio, fim, pk=None):
class MandatoForm(ModelForm): class MandatoForm(ModelForm):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
data_fim_mandato = forms.DateField(label=_('Fim do Mandato'))
class Meta: class Meta:
model = Mandato model = Mandato
fields = ['legislatura', 'coligacao', 'votos_recebidos', fields = ['legislatura', 'coligacao', 'votos_recebidos',
@ -145,8 +147,12 @@ class MandatoForm(ModelForm):
existe_mandato = Mandato.objects.filter( existe_mandato = Mandato.objects.filter(
parlamentar=data['parlamentar'], parlamentar=data['parlamentar'],
legislatura=data['legislatura']).exists() legislatura=data['legislatura'])
if existe_mandato and data['titular']:
if self.instance.pk:
existe_mandato = existe_mandato.exclude(id=self.instance.pk)
if existe_mandato.exists() and data['titular']:
self.logger.error("Mandato nesta legislatura (parlamentar={}, legislatura={}) já existe." self.logger.error("Mandato nesta legislatura (parlamentar={}, legislatura={}) já existe."
.format(data['parlamentar'], data['legislatura'])) .format(data['parlamentar'], data['legislatura']))
raise ValidationError(_('Mandato nesta legislatura já existe.')) raise ValidationError(_('Mandato nesta legislatura já existe.'))

24
sapl/parlamentares/tests/test_parlamentares.py

@ -121,14 +121,18 @@ def test_mandato_submit(admin_client):
admin_client.post(reverse('sapl.parlamentares:mandato_create', admin_client.post(reverse('sapl.parlamentares:mandato_create',
kwargs={'pk': 14}), kwargs={'pk': 14}),
{'parlamentar': 14, # hidden field {
'legislatura': 5, 'parlamentar': 14, # hidden field
'data_inicio_mandato': \ 'legislatura': 5,
Legislatura.objects.get(id=5).data_inicio, 'data_inicio_mandato': \
'data_expedicao_diploma': '2016-03-22', Legislatura.objects.get(id=5).data_inicio,
'observacao': 'Observação do mandato', 'data_fim_mandato': \
'salvar': 'salvar'}, Legislatura.objects.get(id=5).data_fim,
follow=True) 'data_expedicao_diploma': '2016-03-22',
'observacao': 'Observação do mandato',
'salvar': 'salvar'
},
follow=True)
mandato = Mandato.objects.first() mandato = Mandato.objects.first()
assert str(_('Observação do mandato')) == str(_(mandato.observacao)) assert str(_('Observação do mandato')) == str(_(mandato.observacao))
@ -166,13 +170,15 @@ def test_mandato_form_duplicado():
Mandato.objects.create(parlamentar=parlamentar, Mandato.objects.create(parlamentar=parlamentar,
legislatura=legislatura, legislatura=legislatura,
data_expedicao_diploma='2017-07-25', data_expedicao_diploma='2017-07-25',
data_inicio_mandato=legislatura.data_inicio,) data_inicio_mandato=legislatura.data_inicio,
data_fim_mandato=legislatura.data_fim)
form = MandatoForm(data={ form = MandatoForm(data={
'parlamentar': str(parlamentar.pk), 'parlamentar': str(parlamentar.pk),
'legislatura': str(legislatura.pk), 'legislatura': str(legislatura.pk),
'data_expedicao_diploma': '01/07/2015', 'data_expedicao_diploma': '01/07/2015',
'data_inicio_mandato': legislatura.data_inicio, 'data_inicio_mandato': legislatura.data_inicio,
'data_fim_mandato': legislatura.data_fim,
'titular':True, 'titular':True,
}) })

39
sapl/parlamentares/views.py

@ -447,6 +447,9 @@ class MandatoCrud(MasterDetailCrud):
return {'parlamentar': Parlamentar.objects.get( return {'parlamentar': Parlamentar.objects.get(
pk=self.kwargs['pk'])} pk=self.kwargs['pk'])}
class UpdateView(MasterDetailCrud.UpdateView):
form_class = MandatoForm
class ComposicaoColigacaoCrud(MasterDetailCrud): class ComposicaoColigacaoCrud(MasterDetailCrud):
model = ComposicaoColigacao model = ComposicaoColigacao
@ -534,8 +537,7 @@ class ParlamentarCrud(Crud):
list_field_names = [ list_field_names = [
'nome_parlamentar', 'nome_parlamentar',
'filiacao_atual', 'filiacao_atual',
'ativo', 'ativo']
'mandato_titular']
class DetailView(Crud.DetailView): class DetailView(Crud.DetailView):
@ -611,8 +613,7 @@ class ParlamentarCrud(Crud):
username = self.request.user.username username = self.request.user.username
if legislatura_id >= 0: if legislatura_id >= 0:
return queryset.filter( return queryset.filter(
mandato__legislatura_id=legislatura_id).annotate( mandato__legislatura_id=legislatura_id).distinct()
mandato_titular=F('mandato__titular')).distinct()
else: else:
try: try:
self.logger.debug( self.logger.debug(
@ -628,8 +629,7 @@ class ParlamentarCrud(Crud):
". Objeto encontrado com sucesso.") ". Objeto encontrado com sucesso.")
if l is None: if l is None:
return Legislatura.objects.all() return Legislatura.objects.all()
return queryset.filter(mandato__legislatura_id=l).annotate( return queryset.filter(mandato__legislatura_id=l)
mandato_titular=F('mandato__titular'))
def get_headers(self): def get_headers(self):
return [_('Parlamentar'), _('Partido'), return [_('Parlamentar'), _('Partido'),
@ -644,19 +644,38 @@ class ParlamentarCrud(Crud):
context['legislaturas'] = legislaturas context['legislaturas'] = legislaturas
context['legislatura_id'] = self.take_legislatura_id() context['legislatura_id'] = self.take_legislatura_id()
# Pega a Legislatura
legislatura = Legislatura.objects.get(
id=context['legislatura_id'])
for row in context['rows']: for row in context['rows']:
# Pega o Parlamentar por meio da pk # Pega o Parlamentar por meio da pk
parlamentar = Parlamentar.objects.get( parlamentar = Parlamentar.objects.get(
id=(row[0][1].split('/')[-1])) id=(row[0][1].split('/')[-1]))
# Conserta a issue do github https://github.com/interlegis/sapl/issues/3028
# Inicialmente a titularidade era conseguida através do código
# queryset.filter(mandato__legislatura_id=legislatura_id).annotate(
# mandato_titular=F('mandato__titular')).distinct()
# em get_queryset(), MAS não funciona se o parlamentar tem vários
# mandatos na mesma legislatura, sendo ao menos um titular e outro não,
# pois isso gera entradas repetidas. Este código corrige essa situação.
mandato = Mandato.objects.filter(
parlamentar=parlamentar,
data_inicio_mandato__gte=legislatura.data_inicio,
data_fim_mandato__lte=legislatura.data_fim
).order_by('-data_inicio_mandato').first()
if mandato:
titular = 'Sim' if mandato.titular else 'Não'
row.append((titular, None))
else:
row.append(('-', None))
for index, value in enumerate(row): for index, value in enumerate(row):
row[index] += (None if index else parlamentar,) row[index] += (None if index else parlamentar,)
# Pega a Legislatura
legislatura = Legislatura.objects.get(
id=context['legislatura_id'])
# Coloca a filiação atual ao invés da última # Coloca a filiação atual ao invés da última
# As condições para mostrar a filiação são: # As condições para mostrar a filiação são:
# A data de filiacao deve ser menor que a data de fim # A data de filiacao deve ser menor que a data de fim

47
sapl/protocoloadm/forms.py

@ -1,11 +1,11 @@
import django_filters
import logging import logging
from crispy_forms.bootstrap import InlineRadios, Alert, FormActions from crispy_forms.bootstrap import InlineRadios, Alert, FormActions
from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import (Button, Column, Div, Fieldset, HTML,
from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout, Div, Submit Layout, Submit)
from django import forms from django import forms
from sapl.settings import MAX_DOC_UPLOAD_SIZE
from django.core.exceptions import (MultipleObjectsReturned, from django.core.exceptions import (MultipleObjectsReturned,
ObjectDoesNotExist, ValidationError) ObjectDoesNotExist, ValidationError)
from django.db import models, transaction from django.db import models, transaction
@ -13,24 +13,28 @@ from django.db.models import Max
from django.forms import ModelForm from django.forms import ModelForm
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor, TipoAutor, AppConfig from sapl.base.models import Autor, TipoAutor, AppConfig
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.base.signals import post_save_signal
from sapl.materia.models import (MateriaLegislativa, TipoMateriaLegislativa, from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
SaplFormLayout, to_row)
from sapl.materia.models import (MateriaLegislativa,
TipoMateriaLegislativa,
UnidadeTramitacao) UnidadeTramitacao)
from sapl.protocoloadm.models import Protocolo from sapl.protocoloadm.models import Protocolo
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, AnoNumeroOrderingFilter, from sapl.utils import (AnoNumeroOrderingFilter, autor_label, autor_modal,
RangeWidgetOverride, autor_label, autor_modal,
choice_anos_com_protocolo, choice_force_optional,
choice_anos_com_documentoadministrativo, choice_anos_com_documentoadministrativo,
FilterOverridesMetaMixin, choice_anos_com_materias, choice_anos_com_materias,
FileFieldCheckMixin, lista_anexados) choice_anos_com_protocolo, choice_force_optional,
FileFieldCheckMixin, FilterOverridesMetaMixin,
lista_anexados, RangeWidgetOverride, RANGE_ANOS,
validar_arquivo, YES_NO_CHOICES)
from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, from .models import (Anexado, AcompanhamentoDocumento,
DocumentoAdministrativo, DocumentoAcessorioAdministrativo,
Protocolo, TipoDocumentoAdministrativo, DocumentoAdministrativo, Protocolo,
TramitacaoAdministrativo, Anexado) TipoDocumentoAdministrativo,
TramitacaoAdministrativo)
TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'), TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'),
@ -664,9 +668,8 @@ class DocumentoAcessorioAdministrativoForm(FileFieldCheckMixin, ModelForm):
arquivo = self.cleaned_data.get('arquivo', False) arquivo = self.cleaned_data.get('arquivo', False)
if arquivo and arquivo.size > MAX_DOC_UPLOAD_SIZE: if arquivo:
raise ValidationError("O arquivo deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(arquivo, "Arquivo")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (arquivo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -1157,9 +1160,8 @@ class DocumentoAdministrativoForm(FileFieldCheckMixin, ModelForm):
texto_integral = self.cleaned_data.get('texto_integral', False) texto_integral = self.cleaned_data.get('texto_integral', False)
if texto_integral and texto_integral.size > MAX_DOC_UPLOAD_SIZE: if texto_integral:
raise ValidationError("O arquivo Texto Integral deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(texto_integral, "Texto Integral")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (texto_integral.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -1563,7 +1565,6 @@ class TramitacaoEmLoteAdmForm(ModelForm):
) )
) )
def clean(self): def clean(self):
cleaned_data = super(TramitacaoEmLoteAdmForm, self).clean() cleaned_data = super(TramitacaoEmLoteAdmForm, self).clean()

31
sapl/protocoloadm/migrations/0026_auto_20191120_1440.py

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-11-20 17:40
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0025_auto_20191001_1115'),
]
operations = [
migrations.AlterField(
model_name='documentoacessorioadministrativo',
name='arquivo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.utils.texto_upload_path, verbose_name='Arquivo'),
),
migrations.AlterField(
model_name='documentoacessorioadministrativo',
name='autor',
field=models.CharField(blank=True, max_length=200, verbose_name='Autor'),
),
migrations.AlterField(
model_name='documentoadministrativo',
name='texto_integral',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.utils.texto_upload_path, verbose_name='Texto Integral'),
),
]

6
sapl/protocoloadm/models.py

@ -164,7 +164,7 @@ class DocumentoAdministrativo(models.Model):
observacao = models.TextField( observacao = models.TextField(
blank=True, verbose_name=_('Observação')) blank=True, verbose_name=_('Observação'))
texto_integral = models.FileField( texto_integral = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
storage=OverwriteStorage(), storage=OverwriteStorage(),
@ -232,7 +232,7 @@ class DocumentoAcessorioAdministrativo(models.Model):
verbose_name=_('Tipo')) verbose_name=_('Tipo'))
nome = models.CharField(max_length=30, verbose_name=_('Nome')) nome = models.CharField(max_length=30, verbose_name=_('Nome'))
arquivo = models.FileField( arquivo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=texto_upload_path, upload_to=texto_upload_path,
@ -240,7 +240,7 @@ class DocumentoAcessorioAdministrativo(models.Model):
verbose_name=_('Arquivo')) verbose_name=_('Arquivo'))
data = models.DateField(blank=True, null=True, verbose_name=_('Data')) data = models.DateField(blank=True, null=True, verbose_name=_('Data'))
autor = models.CharField( autor = models.CharField(
max_length=50, blank=True, verbose_name=_('Autor')) max_length=200, blank=True, verbose_name=_('Autor'))
assunto = models.TextField( assunto = models.TextField(
blank=True, verbose_name=_('Assunto')) blank=True, verbose_name=_('Assunto'))
indexacao = models.TextField(blank=True) indexacao = models.TextField(blank=True)

53
sapl/protocoloadm/views.py

@ -26,7 +26,7 @@ from django_filters.views import FilterView
import sapl import sapl
from sapl.base.email_utils import do_envia_email_confirmacao from sapl.base.email_utils import do_envia_email_confirmacao
from sapl.base.models import Autor, CasaLegislativa, AppConfig from sapl.base.models import Autor, CasaLegislativa, AppConfig
from sapl.base.signals import tramitacao_signal from sapl.base.signals import tramitacao_signal, post_delete_signal
from sapl.comissoes.models import Comissao from sapl.comissoes.models import Comissao
from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, make_pagination, from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, make_pagination,
RP_LIST, RP_DETAIL) RP_LIST, RP_DETAIL)
@ -37,7 +37,7 @@ from sapl.protocoloadm.models import Protocolo
from sapl.relatorios.views import relatorio_doc_administrativos from sapl.relatorios.views import relatorio_doc_administrativos
from sapl.utils import (create_barcode, get_base_url, get_client_ip, from sapl.utils import (create_barcode, get_base_url, get_client_ip,
get_mime_type_from_file_extension, lista_anexados, get_mime_type_from_file_extension, lista_anexados,
show_results_filter_set, mail_service_configured) show_results_filter_set, mail_service_configured, from_date_to_datetime_utc)
from .forms import (AcompanhamentoDocumentoForm, AnularProtocoloAdmForm, from .forms import (AcompanhamentoDocumentoForm, AnularProtocoloAdmForm,
DocumentoAcessorioAdministrativoForm, DocumentoAcessorioAdministrativoForm,
@ -564,12 +564,24 @@ class ProtocoloDocumentoView(PermissionRequiredMixin,
legislatura = Legislatura.objects.filter( legislatura = Legislatura.objects.filter(
data_inicio__year__lte=timezone.now().year, data_inicio__year__lte=timezone.now().year,
data_fim__year__gte=timezone.now().year).first() data_fim__year__gte=timezone.now().year).first()
data_inicio = legislatura.data_inicio data_inicio = legislatura.data_inicio
data_fim = legislatura.data_fim data_fim = legislatura.data_fim
data_inicio_utc = from_date_to_datetime_utc(data_inicio)
data_fim_utc = from_date_to_datetime_utc(data_fim)
numero = Protocolo.objects.filter( numero = Protocolo.objects.filter(
data__gte=data_inicio, Q(data__isnull=False,
data__lte=data_fim).aggregate( data__gte=data_inicio,
Max('numero')) data__lte=data_fim) |
Q(timestamp__isnull=False,
timestamp__gte=data_inicio_utc,
timestamp__lte=data_fim_utc) |
Q(timestamp_data_hora_manual__isnull=False,
timestamp_data_hora_manual__gte=data_inicio_utc,
timestamp_data_hora_manual__lte=data_fim_utc,)).\
aggregate(Max('numero'))
elif numeracao == 'U': elif numeracao == 'U':
numero = Protocolo.objects.all().aggregate(Max('numero')) numero = Protocolo.objects.all().aggregate(Max('numero'))
@ -760,10 +772,22 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
data_fim__year__gte=timezone.now().year).first() data_fim__year__gte=timezone.now().year).first()
data_inicio = legislatura.data_inicio data_inicio = legislatura.data_inicio
data_fim = legislatura.data_fim data_fim = legislatura.data_fim
data_inicio_utc = from_date_to_datetime_utc(data_inicio)
data_fim_utc = from_date_to_datetime_utc(data_fim)
numero = Protocolo.objects.filter( numero = Protocolo.objects.filter(
data__gte=data_inicio, Q(data__isnull=False,
data__lte=data_fim).aggregate( data__gte=data_inicio,
Max('numero')) data__lte=data_fim) |
Q(timestamp__isnull=False,
timestamp__gte=data_inicio_utc,
timestamp__lte=data_fim_utc) |
Q(timestamp_data_hora_manual__isnull=False,
timestamp_data_hora_manual__gte=data_inicio_utc,
timestamp_data_hora_manual__lte=data_fim_utc,)).\
aggregate(Max('numero'))
elif numeracao == 'U': elif numeracao == 'U':
numero = Protocolo.objects.all().aggregate(Max('numero')) numero = Protocolo.objects.all().aggregate(Max('numero'))
@ -1268,7 +1292,7 @@ class TramitacaoAdmCrud(MasterDetailCrud):
messages.add_message(request, messages.ERROR, msg) messages.add_message(request, messages.ERROR, msg)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
tramitacoes_deletar = [tramitacao.id] tramitacoes_deletar = [tramitacao]
if documento.tramitacaoadministrativo_set.count() == 0: if documento.tramitacaoadministrativo_set.count() == 0:
documento.tramitacao = False documento.tramitacao = False
documento.save() documento.save()
@ -1278,12 +1302,19 @@ class TramitacaoAdmCrud(MasterDetailCrud):
for da in docs_anexados: for da in docs_anexados:
tram_anexada = da.tramitacaoadministrativo_set.last() tram_anexada = da.tramitacaoadministrativo_set.last()
if compara_tramitacoes_doc(tram_anexada, tramitacao): if compara_tramitacoes_doc(tram_anexada, tramitacao):
tramitacoes_deletar.append(tram_anexada.id) tramitacoes_deletar.append(tram_anexada)
if da.tramitacaoadministrativo_set.count() == 0: if da.tramitacaoadministrativo_set.count() == 0:
da.tramitacao = False da.tramitacao = False
da.save() da.save()
TramitacaoAdministrativo.objects.filter( TramitacaoAdministrativo.objects.filter(
id__in=tramitacoes_deletar).delete() id__in=[t.id for t in tramitacoes_deletar]).delete()
# TODO: otimizar para passar a lista de matérias
for tramitacao in tramitacoes_deletar:
post_delete_signal.send(sender=None,
instance=tramitacao,
operation='C',
request=self.request)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

5
sapl/rules/map_rules.py

@ -188,6 +188,10 @@ rules_group_sessao = {
(sessao.JustificativaAusencia, __base__, __perms_publicas__), (sessao.JustificativaAusencia, __base__, __perms_publicas__),
(sessao.RetiradaPauta, __base__, __perms_publicas__), (sessao.RetiradaPauta, __base__, __perms_publicas__),
(sessao.RegistroLeitura, __base__, __perms_publicas__), (sessao.RegistroLeitura, __base__, __perms_publicas__),
(sessao.ListaDiscurso, __base__, __perms_publicas__),
(sessao.TipoListaDiscurso, __base__, __perms_publicas__),
(sessao.CronometroLista, __base__, __perms_publicas__),
(sessao.ParlamentarLista, __base__, __perms_publicas__),
] ]
} }
@ -236,6 +240,7 @@ rules_group_geral = {
(base.TipoAutor, __base__, __perms_publicas__), (base.TipoAutor, __base__, __perms_publicas__),
(base.Autor, __base__, __perms_publicas__), (base.Autor, __base__, __perms_publicas__),
(base.AutorUser, __base__, __perms_publicas__), (base.AutorUser, __base__, __perms_publicas__),
(base.AuditLog, __base__, set()),
(protocoloadm.StatusTramitacaoAdministrativo, __base__, set()), (protocoloadm.StatusTramitacaoAdministrativo, __base__, set()),
(protocoloadm.TipoDocumentoAdministrativo, __base__, set()), (protocoloadm.TipoDocumentoAdministrativo, __base__, set()),

128
sapl/sessao/forms.py

@ -1,35 +1,44 @@
import django_filters
from crispy_forms.layout import Button, Fieldset, HTML, Layout
from datetime import datetime from datetime import datetime
from crispy_forms.layout import HTML, Button, Fieldset, Layout
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.forms import ModelForm from django.forms import ModelForm, widgets
from django.forms.widgets import CheckboxSelectMultiple from django.forms.widgets import CheckboxSelectMultiple
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.settings import MAX_DOC_UPLOAD_SIZE
from sapl.base.models import Autor, TipoAutor 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.crispy_layout_mixin import form_actions, to_row, SaplFormLayout SaplFormHelper, SaplFormLayout)
from sapl.materia.forms import MateriaLegislativaFilterSet from sapl.materia.forms import MateriaLegislativaFilterSet
from sapl.materia.models import (MateriaLegislativa, StatusTramitacao, from sapl.materia.models import (MateriaLegislativa, StatusTramitacao,
TipoMateriaLegislativa) TipoMateriaLegislativa)
from sapl.painel.models import Cronometro
from sapl.parlamentares.models import Parlamentar, Mandato from sapl.parlamentares.models import Parlamentar, Mandato
from sapl.utils import (RANGE_DIAS_MES, RANGE_MESES, from sapl.utils import (RANGE_DIAS_MES, RANGE_MESES,
MateriaPesquisaOrderingFilter, autor_label, MateriaPesquisaOrderingFilter, autor_label,
autor_modal, timezone, choice_anos_com_sessaoplenaria, autor_modal, timezone, choice_anos_com_sessaoplenaria,
FileFieldCheckMixin, verifica_afastamento_parlamentar) FileFieldCheckMixin, verifica_afastamento_parlamentar)
from sapl.parlamentares.models import Mandato, Parlamentar
from .models import (ExpedienteMateria, JustificativaAusencia, from sapl.utils import (autor_label, autor_modal,
Orador, OradorExpediente, OrdemDia, PresencaOrdemDia, SessaoPlenaria, choice_anos_com_sessaoplenaria,
SessaoPlenariaPresenca, TipoResultadoVotacao, FileFieldCheckMixin,
OcorrenciaSessao, RetiradaPauta, TipoRetiradaPauta, OradorOrdemDia, ORDENACAO_RESUMO, MateriaPesquisaOrderingFilter,
ResumoOrdenacao, RegistroLeitura) RANGE_DIAS_MES, RANGE_MESES,
timezone, validar_arquivo)
from .models import (ExpedienteMateria,
JustificativaAusencia, OcorrenciaSessao, Orador,
OradorExpediente, OradorOrdemDia, OrdemDia,
ORDENACAO_RESUMO, PresencaOrdemDia,
RegistroLeitura, ResumoOrdenacao, RetiradaPauta,
SessaoPlenaria, SessaoPlenariaPresenca,
TipoResultadoVotacao, TipoRetiradaPauta,
CronometroLista)
MES_CHOICES = RANGE_MESES MES_CHOICES = RANGE_MESES
DIA_CHOICES = RANGE_DIAS_MES DIA_CHOICES = RANGE_DIAS_MES
@ -155,18 +164,14 @@ class SessaoPlenariaForm(FileFieldCheckMixin, ModelForm):
upload_ata = self.cleaned_data.get('upload_ata', False) upload_ata = self.cleaned_data.get('upload_ata', False)
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_pauta and upload_pauta.size > MAX_DOC_UPLOAD_SIZE: if upload_pauta:
raise ValidationError("O arquivo Pauta da Sessão deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_pauta, "Pauta da Sessão")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_pauta.size/1024)/1024))
if upload_ata and upload_ata.size > MAX_DOC_UPLOAD_SIZE: if upload_ata:
raise ValidationError("O arquivo Ata da Sessão deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_ata, "Ata da Sessão")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_ata.size/1024)/1024))
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE:
raise ValidationError("O arquivo Anexo da Sessão deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
if upload_anexo:
validar_arquivo(upload_anexo, "Anexo da Sessão")
return self.cleaned_data return self.cleaned_data
@ -526,7 +531,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
o = MateriaPesquisaOrderingFilter() o = MateriaPesquisaOrderingFilter()
tramitacao__status = django_filters.ModelChoiceFilter( tramitacao__status = django_filters.ModelChoiceFilter(
required=True, required=False,
queryset=StatusTramitacao.objects.all(), queryset=StatusTramitacao.objects.all(),
label=_('Status da Matéria')) label=_('Status da Matéria'))
@ -547,7 +552,11 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # Colocar super().__init__(*args, **kwargs) quebra a tela
# de adicionar várias matérias em expediente e ordem dia.
# pois herda da classe AdicionarVariasMateriasFilterSet em
# vez de MateriaLegislativaFilterSet
super(MateriaLegislativaFilterSet, self).__init__(*args, **kwargs)
self.filters['tipo'].label = 'Tipo de Matéria' self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['autoria__autor__tipo'].label = 'Tipo de Autor' self.filters['autoria__autor__tipo'].label = 'Tipo de Autor'
@ -632,9 +641,8 @@ class OradorForm(ModelForm):
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE: if upload_anexo:
raise ValidationError("O arquivo Anexo do Orador deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_anexo, "Anexo do Orador")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -677,9 +685,8 @@ class OradorExpedienteForm(ModelForm):
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE: if upload_anexo:
raise ValidationError("O arquivo Anexo do Orador deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_anexo, "Anexo do Orador")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -720,9 +727,8 @@ class OradorOrdemDiaForm(ModelForm):
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE: if upload_anexo:
raise ValidationError("O arquivo Anexo do Orador deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_anexo, "Anexo do Orador")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
return self.cleaned_data return self.cleaned_data
@ -824,9 +830,8 @@ class JustificativaAusenciaForm(ModelForm):
upload_anexo = self.cleaned_data.get('upload_anexo', False) upload_anexo = self.cleaned_data.get('upload_anexo', False)
if upload_anexo and upload_anexo.size > MAX_DOC_UPLOAD_SIZE: if upload_anexo:
raise ValidationError("O arquivo Anexo de Justificativa deve ser menor que {0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb" \ validar_arquivo(upload_anexo, "Anexo de Justificativa")
.format((MAX_DOC_UPLOAD_SIZE/1024)/1024, (upload_anexo.size/1024)/1024))
if not sessao_plenaria.finalizada or sessao_plenaria.finalizada is None: if not sessao_plenaria.finalizada or sessao_plenaria.finalizada is None:
raise ValidationError( raise ValidationError(
@ -890,3 +895,52 @@ class OrdemExpedienteLeituraForm(forms.ModelForm):
form_actions(more=actions), form_actions(more=actions),
) )
) )
class CronometroListaForm(ModelForm):
cronometro = forms.ModelChoiceField(
queryset=Cronometro.objects.all(),
label="Cronômetro"
)
nome_lista = forms.CharField(
label='Lista de Discussão',
widget=widgets.TextInput(attrs={'readonly': 'readonly'})
)
class Meta:
model = CronometroLista
exclude = []
widgets = {
'tipo_lista': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
row1 = to_row(
[('nome_lista', 6),('cronometro', 6),])
row2 = to_row(
[('tipo_lista', 6)]
)
actions = [HTML('<a href="{{ view.cancel_url }}"'
' class="btn btn-dark">Cancelar</a>')]
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Vincular Cronômetro à Lista de Discussão'),
row1, row2,
HTML("&nbsp;"),
form_actions(more=actions)
)
)
def save(self):
cd = self.cleaned_data
cronometro = cd['cronometro']
tipo_lista = cd['tipo_lista']
cronometro_lista = CronometroLista.objects.create(cronometro=cronometro, tipo_lista=tipo_lista)
return cronometro_lista

3
sapl/sessao/migrations/0048_auto_20190829_1253.py → sapl/sessao/migrations/0047_auto_20190829_1253.py

@ -8,7 +8,8 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('sessao', '0047_merge_20191009_1535'), ('sessao', '0046_auto_20190827_1228'),
('sessao', '0046_auto_20191001_1115'),
] ]
operations = [ operations = [

2
sapl/sessao/migrations/0047_merge_20191009_1535.py

@ -2,7 +2,7 @@
# Generated by Django 1.11.20 on 2019-10-09 18:35 # Generated by Django 1.11.20 on 2019-10-09 18:35
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

32
sapl/sessao/migrations/0048_auto_20191029_1418.py

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-29 17:18
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.sessao.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('sessao', '0047_auto_20190829_1253')
]
operations = [
migrations.AlterField(
model_name='sessaoplenaria',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo da Sessão'),
),
migrations.AlterField(
model_name='sessaoplenaria',
name='upload_ata',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.ata_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Ata da Sessão'),
),
migrations.AlterField(
model_name='sessaoplenaria',
name='upload_pauta',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.pauta_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Pauta da Sessão'),
),
]

32
sapl/sessao/migrations/0049_auto_20191029_1434.py

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-29 17:34
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.sessao.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('sessao', '0048_auto_20191029_1418'),
]
operations = [
migrations.AlterField(
model_name='orador',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador'),
),
migrations.AlterField(
model_name='oradorexpediente',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador'),
),
migrations.AlterField(
model_name='oradorordemdia',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador'),
),
]

22
sapl/sessao/migrations/0050_auto_20191029_1441.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-29 17:41
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.sessao.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('sessao', '0049_auto_20191029_1434'),
]
operations = [
migrations.AlterField(
model_name='justificativaausencia',
name='upload_anexo',
field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo de Justificativa'),
),
]

13
sapl/sessao/migrations/0051_merge_20191209_0910.py

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-10-09 18:35
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('sessao', '0047_merge_20191009_1535'),
('sessao', '0050_auto_20191029_1441')
]

91
sapl/sessao/migrations/0052_auto_20191209_0828.py

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-12-09 11:28
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0035_merge_20190802_0954'),
('painel', '0011_cronometro_last_stop_duration'),
('sessao', '0051_merge_20191209_0910'),
]
operations = [
migrations.CreateModel(
name='CronometroLista',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cronometro', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='painel.Cronometro')),
],
options={
'verbose_name': 'Tipo de Lista de Discurso - Cronômetro',
'verbose_name_plural': 'Tipo de Listas de Discurso - Cronômetros',
'ordering': ['tipo_lista', 'cronometro'],
},
),
migrations.CreateModel(
name='ListaDiscurso',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('orador_atual', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parlamentares.Parlamentar')),
('sessao_plenaria', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sessao.SessaoPlenaria', verbose_name='Sessão Plenária')),
],
options={
'verbose_name': 'Lista de Discurso',
'verbose_name_plural': 'Listas de Discurso',
'ordering': ['tipo', 'sessao_plenaria'],
},
),
migrations.CreateModel(
name='ParlamentarLista',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ordenacao', models.PositiveIntegerField(blank=True, null=True, verbose_name='Ordenação')),
('lista', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sessao.ListaDiscurso')),
('parlamentar', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parlamentares.Parlamentar')),
],
options={
'verbose_name': 'Lista de Discurso - Parlamentar',
'verbose_name_plural': 'Listas de Discurso - Parlamentares',
'ordering': ['lista', 'parlamentar', 'ordenacao'],
},
),
migrations.CreateModel(
name='TipoListaDiscurso',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=100, verbose_name='Tipo')),
],
options={
'verbose_name': 'Tipo de Lista de Discurso',
'verbose_name_plural': 'Tipos de Lista de Discurso',
'ordering': ['nome'],
},
),
migrations.AddField(
model_name='listadiscurso',
name='tipo',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sessao.TipoListaDiscurso', verbose_name='Tipo de Lista de Discurso'),
),
migrations.AddField(
model_name='cronometrolista',
name='tipo_lista',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sessao.TipoListaDiscurso'),
),
migrations.AlterUniqueTogether(
name='parlamentarlista',
unique_together=set([('lista', 'parlamentar')]),
),
migrations.AlterUniqueTogether(
name='listadiscurso',
unique_together=set([('tipo', 'sessao_plenaria')]),
),
migrations.AlterUniqueTogether(
name='cronometrolista',
unique_together=set([('tipo_lista', 'cronometro')]),
),
]

77
sapl/sessao/models.py

@ -10,6 +10,7 @@ import reversion
from sapl.base.models import Autor from sapl.base.models import Autor
from sapl.materia.models import MateriaLegislativa from sapl.materia.models import MateriaLegislativa
from sapl.painel.models import Cronometro
from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar,
Partido, SessaoLegislativa) Partido, SessaoLegislativa)
from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation, from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation,
@ -127,7 +128,7 @@ class SessaoPlenaria(models.Model):
max_length=150, blank=True, max_length=150, blank=True,
verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)')) verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)'))
upload_pauta = models.FileField( upload_pauta = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=pauta_upload_path, upload_to=pauta_upload_path,
@ -135,7 +136,7 @@ class SessaoPlenaria(models.Model):
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_ata = models.FileField( upload_ata = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
upload_to=ata_upload_path, upload_to=ata_upload_path,
@ -143,7 +144,7 @@ class SessaoPlenaria(models.Model):
verbose_name=_('Ata da Sessão'), verbose_name=_('Ata da Sessão'),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_anexo = models.FileField( upload_anexo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
storage=OverwriteStorage(), storage=OverwriteStorage(),
@ -381,7 +382,7 @@ class AbstractOrador(models.Model): # Oradores
observacao = models.CharField( observacao = models.CharField(
max_length=150, blank=True, verbose_name=_('Observação')) max_length=150, blank=True, verbose_name=_('Observação'))
upload_anexo = models.FileField( upload_anexo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
storage=OverwriteStorage(), storage=OverwriteStorage(),
@ -733,7 +734,7 @@ class JustificativaAusencia(models.Model):
OrdemDia, blank=True, verbose_name=_('Matérias do Ordem do Dia')) OrdemDia, blank=True, verbose_name=_('Matérias do Ordem do Dia'))
upload_anexo = models.FileField( upload_anexo = models.FileField(
max_length=200, max_length=300,
blank=True, blank=True,
null=True, null=True,
storage=OverwriteStorage(), storage=OverwriteStorage(),
@ -874,3 +875,69 @@ class RegistroLeitura(models.Model):
'RegistroLeitura deve ter exatamente um dos campos ' 'RegistroLeitura deve ter exatamente um dos campos '
'ordem ou expediente preenchido. Ambos estão preenchidos: ' 'ordem ou expediente preenchido. Ambos estão preenchidos: '
'{}, {}'. format(self.ordem, self.expediente)) '{}, {}'. format(self.ordem, self.expediente))
class TipoListaDiscurso(models.Model):
nome = models.CharField(max_length=100, verbose_name=_('Tipo'))
class Meta:
verbose_name = _('Tipo de Lista de Discurso')
verbose_name_plural = _('Tipos de Lista de Discurso')
ordering = ['nome']
def __str__(self):
return self.nome
@reversion.register()
class ListaDiscurso(models.Model):
tipo = models.ForeignKey(TipoListaDiscurso,
on_delete=models.PROTECT,
verbose_name=_('Tipo de Lista de Discurso'))
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.PROTECT,
verbose_name=_('Sessão Plenária'))
orador_atual = models.ForeignKey(Parlamentar,
on_delete=models.PROTECT,
blank=True,
null=True)
class Meta:
verbose_name = _('Lista de Discurso')
verbose_name_plural = _('Listas de Discurso')
ordering = ['tipo', 'sessao_plenaria']
unique_together = (('tipo', 'sessao_plenaria'),)
def __str__(self):
return str(self.tipo) + ' - ' + str(self.sessao_plenaria)
@reversion.register()
class CronometroLista(models.Model):
cronometro = models.ForeignKey(Cronometro, on_delete=models.PROTECT)
tipo_lista = models.ForeignKey(TipoListaDiscurso, on_delete=models.PROTECT)
class Meta:
verbose_name = _('Tipo de Lista de Discurso - Cronômetro')
verbose_name_plural = _('Tipo de Listas de Discurso - Cronômetros')
ordering = ['tipo_lista', 'cronometro']
unique_together = (('tipo_lista', 'cronometro'),)
def __str__(self):
return str(self.tipo_lista) + ' - ' + str(self.cronometro)
@reversion.register()
class ParlamentarLista(models.Model):
parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT)
lista = models.ForeignKey(ListaDiscurso, on_delete=models.PROTECT)
ordenacao = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name=_("Ordenação")
)
class Meta:
verbose_name = _('Lista de Discurso - Parlamentar')
verbose_name_plural = _('Listas de Discurso - Parlamentares')
ordering = ['lista', 'parlamentar', 'ordenacao']
unique_together = (('lista', 'parlamentar'),)
def __str__(self):
return str(self.lista) + ' - ' + str(self.parlamentar)

28
sapl/sessao/urls.py

@ -38,7 +38,13 @@ from sapl.sessao.views import (AdicionarVariasMateriasExpediente,
voto_nominal_parlamentar, voto_nominal_parlamentar,
ExpedienteLeituraView, ExpedienteLeituraView,
OrdemDiaLeituraView, OrdemDiaLeituraView,
retirar_leitura) retirar_leitura,
ListaDiscursoView,
TipoListaDiscursoCrud,
CronometroListaFormView,
salva_listadiscurso_parlamentares,
salva_orador_listadiscurso,
get_orador_listadiscurso)
from .apps import AppConfig from .apps import AppConfig
@ -102,6 +108,10 @@ urlpatterns = [
include(TipoJustificativaCrud.get_urls())), include(TipoJustificativaCrud.get_urls())),
url(r'^sistema/sessao-plenaria/tipo-retirada-pauta/', url(r'^sistema/sessao-plenaria/tipo-retirada-pauta/',
include(TipoRetiradaPautaCrud.get_urls())), include(TipoRetiradaPautaCrud.get_urls())),
url(r'^sistema/sessao-plenaria/tipo-lista-discurso/',
include(TipoListaDiscursoCrud.get_urls())),
url(r'^sistema/resumo-ordenacao/', url(r'^sistema/resumo-ordenacao/',
resumo_ordenacao, resumo_ordenacao,
name='resumo_ordenacao'), name='resumo_ordenacao'),
@ -202,4 +212,20 @@ urlpatterns = [
url(r'^sessao/(?P<pk>\d+)/(?P<iso>\d+)/(?P<oid>\d+)/retirar-leitura$', url(r'^sessao/(?P<pk>\d+)/(?P<iso>\d+)/(?P<oid>\d+)/retirar-leitura$',
retirar_leitura, name='retirar_leitura'), retirar_leitura, name='retirar_leitura'),
# Lista de Discurso
url(r'^sessao/(?P<pk>\d+)/lista-discurso/',
ListaDiscursoView.as_view(),
name='lista_discurso'),
url(r'^sistema/sessao-plenaria/lista-discurso/(?P<pk>\d+)/cronometro/create',
CronometroListaFormView.as_view(), name='cronometrolista_form'),
url(r'^sistema/salva-listadiscurso-parlamentares/$',
salva_listadiscurso_parlamentares, name='salva_listadiscurso_parlamentares'),
url(r'^sistema/salva-orador-listadiscurso/$',
salva_orador_listadiscurso, name='salva_orador_listadiscurso'),
url(r'^sistema/get-orador-lista/(?P<spk>\d+)/(?P<tpk>\d+)/$',
get_orador_listadiscurso, name='get_orador_listadiscurso'),
] ]

136
sapl/sessao/views.py

@ -48,14 +48,15 @@ from .forms import (AdicionarVariasMateriasFilterSet, ExpedienteForm,
PresencaForm, SessaoPlenariaFilterSet, PresencaForm, SessaoPlenariaFilterSet,
SessaoPlenariaForm, VotacaoEditForm, VotacaoForm, SessaoPlenariaForm, VotacaoEditForm, VotacaoForm,
VotacaoNominalForm, RetiradaPautaForm, OradorOrdemDiaForm, VotacaoNominalForm, RetiradaPautaForm, OradorOrdemDiaForm,
OrdemExpedienteLeituraForm) OrdemExpedienteLeituraForm,
CronometroListaForm)
from .models import (CargoMesa, ExpedienteMateria, ExpedienteSessao, OcorrenciaSessao, IntegranteMesa, from .models import (CargoMesa, ExpedienteMateria, ExpedienteSessao, OcorrenciaSessao, IntegranteMesa,
MateriaLegislativa, Orador, OradorExpediente, OrdemDia, MateriaLegislativa, Orador, OradorExpediente, OrdemDia,
PresencaOrdemDia, RegistroVotacao, ResumoOrdenacao, PresencaOrdemDia, RegistroVotacao, ResumoOrdenacao,
SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente, SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente,
TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar, TipoRetiradaPauta, TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar, TipoRetiradaPauta,
RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia, RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia, ORDENACAO_RESUMO,
ORDENACAO_RESUMO, RegistroLeitura) RegistroLeitura, TipoListaDiscurso, ListaDiscurso, CronometroLista, ParlamentarLista)
TipoSessaoCrud = CrudAux.build(TipoSessaoPlenaria, 'tipo_sessao_plenaria') TipoSessaoCrud = CrudAux.build(TipoSessaoPlenaria, 'tipo_sessao_plenaria')
@ -65,6 +66,19 @@ TipoResultadoVotacaoCrud = CrudAux.build(
TipoRetiradaPautaCrud = CrudAux.build(TipoRetiradaPauta, 'tipo_retirada_pauta') TipoRetiradaPautaCrud = CrudAux.build(TipoRetiradaPauta, 'tipo_retirada_pauta')
class TipoListaDiscursoCrud(CrudAux):
model = TipoListaDiscurso
class DetailView(CrudAux.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tipo_lista = context['object']
cronometros_lista = CronometroLista.objects.filter(tipo_lista=tipo_lista)
context['cronometros_lista'] = cronometros_lista
return context
def reordernar_materias_expediente(request, pk): def reordernar_materias_expediente(request, pk):
expedientes = ExpedienteMateria.objects.filter( expedientes = ExpedienteMateria.objects.filter(
sessao_plenaria_id=pk sessao_plenaria_id=pk
@ -386,7 +400,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
resultado = obj.registrovotacao_set.filter( resultado = obj.registrovotacao_set.filter(
materia_id=obj.materia_id).last() materia_id=obj.materia_id).last()
resultado_descricao = obj.resultado resultado_descricao = obj.resultado
resultado_observacao = obj.observacao resultado_observacao = resultado.observacao
if has_permission: if has_permission:
url = '' url = ''
@ -502,18 +516,16 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
def get_presencas_generic(model, sessao, legislatura): def get_presencas_generic(model, sessao, legislatura):
presencas = model.objects.filter( presentes = [p.parlamentar for p in model.objects.filter(sessao_plenaria=sessao)]
sessao_plenaria=sessao)
presentes = [p.parlamentar for p in presencas]
presentes = sorted( parlamentares_mandato = Mandato.objects.filter(
presentes, key=lambda x: remover_acentos(x.nome_parlamentar)) legislatura=legislatura,
data_inicio_mandato__lte=sessao.data_inicio,
data_fim_mandato__gte=sessao.data_inicio
).distinct().order_by(
'parlamentar__nome_parlamentar')
mandato = Mandato.objects.filter( for m in parlamentares_mandato:
legislatura=legislatura).order_by('parlamentar__nome_parlamentar')
for m in mandato:
parlamentar = m.parlamentar parlamentar = m.parlamentar
p_afastado = verifica_afastamento_parlamentar(parlamentar, sessao.data_inicio, sessao.data_fim) p_afastado = verifica_afastamento_parlamentar(parlamentar, sessao.data_inicio, sessao.data_fim)
if parlamentar in presentes: if parlamentar in presentes:
@ -788,6 +800,7 @@ class OradorExpedienteCrud(OradorCrud):
class ListView(MasterDetailCrud.ListView): class ListView(MasterDetailCrud.ListView):
ordering = ['numero_ordem']
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -1620,11 +1633,11 @@ def get_presenca_sessao(sessao_plenaria):
parlamentares_sessao = [p.parlamentar for p in SessaoPlenariaPresenca.objects.filter( parlamentares_sessao = [p.parlamentar for p in SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=sessao_plenaria.id sessao_plenaria_id=sessao_plenaria.id
).order_by('parlamentar__nome_parlamentar')] ).order_by('parlamentar__nome_parlamentar').distinct()]
ausentes_sessao = JustificativaAusencia.objects.filter( ausentes_sessao = JustificativaAusencia.objects.filter(
sessao_plenaria_id=sessao_plenaria.id sessao_plenaria_id=sessao_plenaria.id
).order_by('parlamentar__nome_parlamentar') ).distinct().order_by('parlamentar__nome_parlamentar')
return ({'presenca_sessao': parlamentares_sessao, return ({'presenca_sessao': parlamentares_sessao,
'justificativa_ausencia': ausentes_sessao}) 'justificativa_ausencia': ausentes_sessao})
@ -1718,7 +1731,7 @@ def get_oradores_expediente(sessao_plenaria):
def get_presenca_ordem_do_dia(sessao_plenaria): def get_presenca_ordem_do_dia(sessao_plenaria):
parlamentares_ordem = [p.parlamentar for p in PresencaOrdemDia.objects.filter( parlamentares_ordem = [p.parlamentar for p in PresencaOrdemDia.objects.filter(
sessao_plenaria_id=sessao_plenaria.id sessao_plenaria_id=sessao_plenaria.id
).order_by('parlamentar__nome_parlamentar')] ).distinct().order_by('parlamentar__nome_parlamentar')]
return {'presenca_ordem': parlamentares_ordem} return {'presenca_ordem': parlamentares_ordem}
@ -4513,3 +4526,92 @@ def retirar_leitura(request, pk, iso, oid):
ordem_expediente.save() ordem_expediente.save()
return HttpResponseRedirect(succ_url) return HttpResponseRedirect(succ_url)
class ListaDiscursoView(TemplateView):
template_name = 'sessao/lista_discurso.html'
def get(self, request, *args, **kwargs):
return TemplateView.get(self, request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = TemplateView.get_context_data(self, **kwargs)
sessao_pk = kwargs['pk']
sessao = SessaoPlenaria.objects.get(id=sessao_pk)
context.update({
'root_pk': sessao_pk,
'subnav_template_name': 'sessao/subnav.yaml',
'sessaoplenaria': sessao,
})
return context
class CronometroListaFormView(FormView):
form_class = CronometroListaForm
template_name = 'sessao/cronometrolista_form.html'
def get_initial(self):
initial = super().get_initial()
tipo_lista_pk = self.kwargs['pk']
tipo_lista = TipoListaDiscurso.objects.get(id=tipo_lista_pk)
initial['tipo_lista'] = tipo_lista
initial['nome_lista'] = tipo_lista.nome
return initial
def form_valid(self, form):
form.save()
return super().form_valid(form)
@property
def cancel_url(self):
return reverse('sapl.sessao:tipolistadiscurso_detail',
kwargs={'pk': self.kwargs['pk']})
def get_success_url(self):
return reverse('sapl.sessao:tipolistadiscurso_detail',
kwargs={'pk': self.kwargs['pk']})
def salva_listadiscurso_parlamentares(request):
parlamentares_selecionados_ids = request.GET.getlist('parlamentares_selecionados[]')
tipo_lista_id = request.GET.get('lista_selecionada')
sessao_id = request.GET.get('sessao_pk')
if parlamentares_selecionados_ids:
lista = ListaDiscurso.objects.get_or_create(tipo_id=tipo_lista_id, sessao_plenaria_id=sessao_id)[0]
ParlamentarLista.objects.filter(lista=lista).delete()
parlamentares_lista = []
for i,parlamentar_id in enumerate(parlamentares_selecionados_ids):
parlamentares_lista.append(ParlamentarLista(lista=lista, parlamentar_id=parlamentar_id, ordenacao=i+1))
ParlamentarLista.objects.bulk_create(parlamentares_lista)
else:
ParlamentarLista.objects.filter(lista__tipo_id=tipo_lista_id, lista__sessao_plenaria_id=sessao_id).delete()
ListaDiscurso.objects.filter(tipo_id=tipo_lista_id, sessao_plenaria_id=sessao_id).delete()
return JsonResponse({})
def salva_orador_listadiscurso(request):
tipo_id=request.GET.get('tipo_lista_pk')
sessao_id=request.GET.get('sessao_pk')
lista = ListaDiscurso.objects.get(tipo_id=tipo_id, sessao_plenaria_id=sessao_id)
orador_pk = request.GET.get('orador_pk')
if orador_pk != '-1':
lista.orador_atual = Parlamentar.objects.get(id=orador_pk)
else:
lista.orador_atual = None
lista.save()
return JsonResponse({})
def get_orador_listadiscurso(request, spk, tpk):
# Não foi utilizado .get para não quebrar caso não exista registro
lista = ListaDiscurso.objects.filter(tipo_id=tpk, sessao_plenaria_id=spk).first()
parlamentares = []
orador_pk = -1
orador_nome = ''
if lista:
if lista.orador_atual:
orador_pk = lista.orador_atual.pk
orador_nome = lista.orador_atual.nome_parlamentar
parlamentares = ParlamentarLista.objects.filter(lista=lista).order_by('ordenacao').values_list('parlamentar__id', 'parlamentar__nome_parlamentar')
return JsonResponse({'orador': (orador_pk, orador_nome),
'parlamentares': list(parlamentares)})

4
sapl/settings.py

@ -263,9 +263,9 @@ DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', cast=str, default='')
SERVER_EMAIL = config('SERVER_EMAIL', cast=str, default='') SERVER_EMAIL = config('SERVER_EMAIL', cast=str, default='')
EMAIL_RUNNING = None EMAIL_RUNNING = None
MAX_DOC_UPLOAD_SIZE = 80 * 1024 * 1024 # 80MB MAX_DOC_UPLOAD_SIZE = 150 * 1024 * 1024 # 150MB
MAX_IMAGE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB MAX_IMAGE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
DATA_UPLOAD_MAX_MEMORY_SIZE= 10 * 1024 * 1024 # 10MB DATA_UPLOAD_MAX_MEMORY_SIZE= 10 * 1024 * 1024 # 10MB
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/ # https://docs.djangoproject.com/en/1.8/topics/i18n/

0
sapl/static/sapl/frontend/css/chunk-vendors.f0333cff.css → sapl/static/sapl/frontend/css/chunk-vendors.c41142f2.css

0
sapl/static/sapl/frontend/css/chunk-vendors.f0333cff.css.gz → sapl/static/sapl/frontend/css/chunk-vendors.c41142f2.css.gz

1
sapl/static/sapl/frontend/css/compilacao.3d691cd9.css

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/css/compilacao.3d691cd9.css.gz

Binary file not shown.

1
sapl/static/sapl/frontend/css/compilacao.eff62463.css

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/css/compilacao.eff62463.css.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/css/painel.5d957a9b.css.gz

Binary file not shown.

2
sapl/static/sapl/frontend/css/painel.5d957a9b.css → sapl/static/sapl/frontend/css/painel.fd494bea.css

@ -1 +1 @@
.painel-principal{background:#1c1b1b;font-family:Verdana;font-size:x-large}.painel-principal .text-title{color:#4fa64d;margin:.5rem;font-weight:700}.painel-principal .text-subtitle{color:#459170;font-weight:700}.painel-principal .data-hora{font-size:180%}.painel-principal .text-value{color:#fff}.painel-principal .logo-painel{max-width:100%}.painel-principal .painels{-ms-flex-wrap:wrap;flex-wrap:wrap}.painel-principal .painel{margin-top:1rem}.painel-principal .painel table{width:100%}.painel-principal .painel h2{margin-bottom:.5rem}.painel-principal .painel #oradores_list,.painel-principal .painel #votacao{text-align:left;display:inline-block;margin-bottom:1rem} .painel-principal{background:#1c1b1b;font-family:Verdana;font-size:x-large}.painel-principal .text-title{color:#4fa64d;margin:.5rem;font-weight:700}.painel-principal .text-subtitle{color:#459170;font-weight:700}.painel-principal .data-hora{font-size:180%}.painel-principal .text-value{color:#fff}.painel-principal .logo-painel{max-width:100%}.painel-principal .painels{-ms-flex-wrap:wrap;flex-wrap:wrap}.painel-principal .painel{margin-top:1rem}.painel-principal .painel table{width:100%}.painel-principal .painel h2{margin-bottom:.5rem}.painel-principal .painel #oradores_list,.painel-principal .painel #votacao{text-align:left;display:inline-block;margin-bottom:1rem}.painel-principal body,.painel-principal html{max-width:100%;overflow-x:hidden}@media screen{.painel-principal li,.painel-principal ul{list-style-type:none}}

BIN
sapl/static/sapl/frontend/css/painel.fd494bea.css.gz

Binary file not shown.

1
sapl/static/sapl/frontend/css/sessao.559521f7.css

@ -0,0 +1 @@
.current{background:#38b883;background-color:#ff0}.btSelecionado{background-color:#00aeff}

0
sapl/static/sapl/frontend/js/chunk-09995afe.a6f4abba.js → sapl/static/sapl/frontend/js/chunk-09995afe.98f3367d.js

0
sapl/static/sapl/frontend/js/chunk-09995afe.a6f4abba.js.gz → sapl/static/sapl/frontend/js/chunk-09995afe.98f3367d.js.gz

0
sapl/static/sapl/frontend/js/chunk-2d0c4a82.75cb4df5.js → sapl/static/sapl/frontend/js/chunk-2d0c4a82.624674a7.js

0
sapl/static/sapl/frontend/js/chunk-2d0c4a82.75cb4df5.js.gz → sapl/static/sapl/frontend/js/chunk-2d0c4a82.624674a7.js.gz

0
sapl/static/sapl/frontend/js/chunk-2d0e8be2.d53f5a24.js → sapl/static/sapl/frontend/js/chunk-2d0e8be2.67905a03.js

0
sapl/static/sapl/frontend/js/chunk-2d0e8be2.d53f5a24.js.gz → sapl/static/sapl/frontend/js/chunk-2d0e8be2.67905a03.js.gz

0
sapl/static/sapl/frontend/js/chunk-31d76f93.fb085f5d.js → sapl/static/sapl/frontend/js/chunk-31d76f93.968a637a.js

0
sapl/static/sapl/frontend/js/chunk-31d76f93.fb085f5d.js.gz → sapl/static/sapl/frontend/js/chunk-31d76f93.968a637a.js.gz

0
sapl/static/sapl/frontend/js/chunk-681dd124.b923be8d.js → sapl/static/sapl/frontend/js/chunk-681dd124.08a628b0.js

0
sapl/static/sapl/frontend/js/chunk-681dd124.b923be8d.js.gz → sapl/static/sapl/frontend/js/chunk-681dd124.08a628b0.js.gz

BIN
sapl/static/sapl/frontend/js/chunk-vendors.3ef59900.js.gz

Binary file not shown.

73
sapl/static/sapl/frontend/js/chunk-vendors.3ef59900.js → sapl/static/sapl/frontend/js/chunk-vendors.dfcd310b.js

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/js/chunk-vendors.dfcd310b.js.gz

Binary file not shown.

1
sapl/static/sapl/frontend/js/compilacao.0684a9a9.js

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/js/compilacao.0684a9a9.js.gz

Binary file not shown.

1
sapl/static/sapl/frontend/js/compilacao.6b8172da.js

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/js/compilacao.6b8172da.js.gz

Binary file not shown.

7
sapl/static/sapl/frontend/js/global.3df943f5.js

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/js/global.3df943f5.js.gz

Binary file not shown.

7
sapl/static/sapl/frontend/js/global.ce91d2f8.js

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/js/global.ce91d2f8.js.gz

Binary file not shown.

2
sapl/static/sapl/frontend/js/online.487f3d5a.js → sapl/static/sapl/frontend/js/online.0f0e1e97.js

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/js/online.0f0e1e97.js.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/js/online.487f3d5a.js.gz

Binary file not shown.

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save