Browse Source

Impl google recaptcha (#3337)

* alterações de IDE

* Refatora forms de acomp via email e recup de senha

Implementa mixin de registro de email para encaminhamento de emails com
google recaptcha.

* Impl registro do Google Recaptcha

* Substitui urllib3 por requests

Co-authored-by: eribeiro <edwardr@senado.leg.br>
pull/3340/head
Leandro Roberto Silva 4 years ago
committed by GitHub
parent
commit
c79de3a89c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 90
      sapl/base/forms.py
  2. 23
      sapl/base/migrations/0043_auto_20210203_1442.py
  3. 20
      sapl/base/models.py
  4. 222
      sapl/base/views.py
  5. 14
      sapl/context_processors.py
  6. 39
      sapl/materia/forms.py
  7. 12
      sapl/materia/views.py
  8. 25
      sapl/protocoloadm/forms.py
  9. 13
      sapl/protocoloadm/views.py
  10. 2
      sapl/settings.py
  11. 3
      sapl/templates/base/layouts.yaml
  12. 5
      sapl/templates/base/login.html
  13. 4
      sapl/templates/base/recuperar_senha_email_form.html
  14. 8
      sapl/templates/materia/acompanhamento_materia.html
  15. 2
      sapl/templates/materia/materialegislativa_detail.html
  16. 2
      sapl/templates/materia/materialegislativa_filter.html
  17. 8
      sapl/templates/protocoloadm/acompanhamento_documento.html
  18. 2
      sapl/templates/protocoloadm/documentoadministrativo_filter.html
  19. 136
      sapl/utils.py

90
sapl/base/forms.py

@ -36,7 +36,8 @@ from sapl.utils import (autor_label, autor_modal, ChoiceWithoutValidationField,
FilterOverridesMetaMixin, FileFieldCheckMixin,
AnoNumeroOrderingFilter, ImageThumbnailFileInput,
models_with_gr_for_model, qs_override_django_filter,
RangeWidgetOverride, RANGE_ANOS, YES_NO_CHOICES)
RangeWidgetOverride, RANGE_ANOS, YES_NO_CHOICES,
GoogleRecapthaMixin)
from .models import AppConfig, CasaLegislativa
@ -116,7 +117,8 @@ class UsuarioCreateForm(ModelForm):
data = self.cleaned_data
if data['password1'] != data['password2']:
self.logger.warning('Erro de validação. Senhas informadas são diferentes.')
self.logger.warning(
'Erro de validação. Senhas informadas são diferentes.')
raise ValidationError('Senhas informadas são diferentes')
return data
@ -235,7 +237,8 @@ class UsuarioEditForm(ModelForm):
self.helper = SaplFormHelper()
self.helper.layout = Layout(
'username',
FieldWithButtons('token', StrictButton('Renovar', id="renovar-token", css_class="btn-outline-primary")),
FieldWithButtons('token', StrictButton(
'Renovar', id="renovar-token", css_class="btn-outline-primary")),
rows,
form_actions(
more=[
@ -250,7 +253,8 @@ class UsuarioEditForm(ModelForm):
data = self.cleaned_data
if data['password1'] and data['password1'] != data['password2']:
self.logger.warning("Erro de validação. Senhas informadas são diferentes.")
self.logger.warning(
"Erro de validação. Senhas informadas são diferentes.")
raise ValidationError('Senhas informadas são diferentes')
return data
@ -315,7 +319,8 @@ class SessaoLegislativaForm(FileFieldCheckMixin, ModelForm):
if numero <= ult and flag_edit:
self.logger.warning(
'O número da SessaoLegislativa ({}) é menor ou igual '
'que o de Sessões Legislativas passadas ({})'.format(numero, ult)
'que o de Sessões Legislativas passadas ({})'.format(
numero, ult)
)
raise ValidationError('O número da Sessão Legislativa não pode ser menor ou igual '
'que o de Sessões Legislativas passadas')
@ -325,7 +330,8 @@ class SessaoLegislativaForm(FileFieldCheckMixin, ModelForm):
self.logger.warning(
'A data de início ({}) da SessaoLegislativa está compreendida '
'fora da data início ({}) e fim ({}) da Legislatura '
'selecionada'.format(data_inicio, data_inicio_leg, data_fim_leg)
'selecionada'.format(
data_inicio, data_inicio_leg, data_fim_leg)
)
raise ValidationError('A data de início da Sessão Legislativa deve estar compreendida '
'entre a data início e fim da Legislatura selecionada')
@ -342,13 +348,15 @@ class SessaoLegislativaForm(FileFieldCheckMixin, ModelForm):
if data_inicio > data_fim:
self.logger.warning(
'Data início ({}) superior à data fim ({}).'.format(data_inicio, data_fim)
'Data início ({}) superior à data fim ({}).'.format(
data_inicio, data_fim)
)
raise ValidationError(
'Data início não pode ser superior à data fim')
if data_fim.year > data_inicio.year + 1:
raise ValidationError('A Sessão Legislativa só pode ter, no máximo, dois anos de período.')
raise ValidationError(
'A Sessão Legislativa só pode ter, no máximo, dois anos de período.')
data_inicio_intervalo = cleaned_data['data_inicio_intervalo']
data_fim_intervalo = cleaned_data['data_fim_intervalo']
@ -357,7 +365,8 @@ class SessaoLegislativaForm(FileFieldCheckMixin, ModelForm):
data_inicio_intervalo > data_fim_intervalo:
self.logger.warning(
'Data início de intervalo ({}) superior à '
'data fim de intervalo ({}).'.format(data_inicio_intervalo, data_fim_intervalo)
'data fim de intervalo ({}).'.format(
data_inicio_intervalo, data_fim_intervalo)
)
raise ValidationError('Data início de intervalo não pode ser '
'superior à data fim de intervalo')
@ -766,7 +775,8 @@ class AutorForm(ModelForm):
class AutorFilterSet(django_filters.FilterSet):
nome = django_filters.CharFilter(label=_('Nome do Autor'), lookup_expr='icontains')
nome = django_filters.CharFilter(
label=_('Nome do Autor'), lookup_expr='icontains')
class Meta:
model = Autor
@ -1063,20 +1073,24 @@ class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.fields['exibir_ordem_dia'] = forms.BooleanField(required=False, label='Exibir presença das Ordens do Dia')
self.form.fields['exibir_ordem_dia'] = forms.BooleanField(
required=False, label='Exibir presença das Ordens do Dia')
self.form.initial['exibir_ordem_dia'] = True
self.form.fields['exibir_somente_titular'] = forms.BooleanField(required=False, label='Exibir somente parlamentares titulares')
self.form.fields['exibir_somente_titular'] = forms.BooleanField(
required=False, label='Exibir somente parlamentares titulares')
self.form.initial['exibir_somente_titular'] = False
self.form.fields['exibir_somente_ativo'] = forms.BooleanField(required=False, label='Exibir somente parlamentares ativos')
self.form.fields['exibir_somente_ativo'] = forms.BooleanField(
required=False, label='Exibir somente parlamentares ativos')
self.form.initial['exibir_somente_ativo'] = False
self.form.fields['legislatura'].required = True
self.filters['data_inicio'].label = 'Período (Inicial - Final)'
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:
self.form.initial['tipo'] = tipo_sessao_ordinaria.first()
@ -1341,7 +1355,6 @@ class RelatorioMateriasTramitacaoFilterSet(django_filters.FilterSet):
label='Autor da Matéria',
queryset=Autor.objects.all())
@property
def qs(self):
parent = super(RelatorioMateriasTramitacaoFilterSet, self).qs
@ -1353,7 +1366,7 @@ class RelatorioMateriasTramitacaoFilterSet(django_filters.FilterSet):
model = MateriaEmTramitacao
fields = ['materia__ano', 'materia__tipo',
'tramitacao__unidade_tramitacao_destino',
'tramitacao__status','materia__autores']
'tramitacao__status', 'materia__autores']
def __init__(self, *args, **kwargs):
super(RelatorioMateriasTramitacaoFilterSet, self).__init__(
@ -1385,7 +1398,7 @@ class RelatorioMateriasTramitacaoFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Matéria em Tramitação'),
row1, row2, row3, row4,row5,
row1, row2, row3, row4, row5,
buttons,)
)
@ -1552,6 +1565,18 @@ class ConfiguracoesAppForm(ModelForm):
label=_('Mostrar brasão da Casa no painel?'),
required=False)
google_recaptcha_site_key = forms.CharField(
label=AppConfig._meta.get_field(
'google_recaptcha_site_key').verbose_name,
max_length=256,
required=False)
google_recaptcha_secret_key = forms.CharField(
label=AppConfig._meta.get_field(
'google_recaptcha_secret_key').verbose_name,
max_length=256,
required=False)
class Meta:
model = AppConfig
fields = ['documentos_administrativos',
@ -1575,7 +1600,9 @@ class ConfiguracoesAppForm(ModelForm):
'estatisticas_acesso_normas',
'escolher_numero_materia_proposicao',
'tramitacao_materia',
'tramitacao_documento']
'tramitacao_documento',
'google_recaptcha_site_key',
'google_recaptcha_secret_key']
def __init__(self, *args, **kwargs):
super(ConfiguracoesAppForm, self).__init__(*args, **kwargs)
@ -1609,35 +1636,29 @@ class ConfiguracoesAppForm(ModelForm):
return cleaned_data
class RecuperarSenhaForm(PasswordResetForm):
class RecuperarSenhaForm(GoogleRecapthaMixin, PasswordResetForm):
logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
row1 = to_row(
[('email', 12)])
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Insira o e-mail cadastrado com a sua conta'),
row1,
form_actions(label='Enviar'))
)
super(RecuperarSenhaForm, self).__init__(*args, **kwargs)
kwargs['title_label'] = _('Insira o e-mail cadastrado com a sua conta')
kwargs['action_label'] = _('Enviar')
super().__init__(*args, **kwargs)
def clean(self):
super(RecuperarSenhaForm, self).clean()
if not self.is_valid():
return self.cleaned_data
super(RecuperarSenhaForm, self).clean()
email_existente = User.objects.filter(
email_existente = get_user_model().objects.filter(
email=self.data['email']).exists()
if not email_existente:
msg = 'Não existe nenhum usuário cadastrado com este e-mail.'
self.logger.warning(
'Não existe nenhum usuário cadastrado com este e-mail ({}).'.format(self.data['email'])
'Não existe nenhum usuário cadastrado com este e-mail ({}).'.format(
self.data['email'])
)
raise ValidationError(msg)
@ -1726,7 +1747,8 @@ class AlterarSenhaForm(Form):
if user.is_anonymous:
self.logger.warning(
'Não é possível alterar senha de usuário anônimo ({}).'.format(username)
'Não é possível alterar senha de usuário anônimo ({}).'.format(
username)
)
raise ValidationError(
"Não é possível alterar senha de usuário anônimo")

23
sapl/base/migrations/0043_auto_20210203_1442.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.13 on 2021-02-03 17:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0042_auto_20201013_1151'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='google_recaptcha_secret_key',
field=models.CharField(default='', max_length=256, verbose_name='Chave privada gerada pelo Google Recaptcha'),
),
migrations.AddField(
model_name='appconfig',
name='google_recaptcha_site_key',
field=models.CharField(default='', max_length=256, verbose_name='Chave pública gerada pelo Google Recaptcha'),
),
]

20
sapl/base/models.py

@ -179,17 +179,27 @@ class AppConfig(models.Model):
choices=YES_NO_CHOICES, default=False)
escolher_numero_materia_proposicao = models.BooleanField(
verbose_name=_('Indicar número da matéria a ser gerada na proposição?'),
verbose_name=_(
'Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False)
tramitacao_materia = models.BooleanField(
verbose_name=_('Tramitar matérias anexadas junto com as matérias principais?'),
verbose_name=_(
'Tramitar matérias anexadas junto com as matérias principais?'),
choices=YES_NO_CHOICES, default=True)
tramitacao_documento = models.BooleanField(
verbose_name=_('Tramitar documentos anexados junto com os documentos principais?'),
verbose_name=_(
'Tramitar documentos anexados junto com os documentos principais?'),
choices=YES_NO_CHOICES, default=True)
google_recaptcha_site_key = models.CharField(
verbose_name=_('Chave pública gerada pelo Google Recaptcha'),
max_length=256, default='')
google_recaptcha_secret_key = models.CharField(
verbose_name=_('Chave privada gerada pelo Google Recaptcha'),
max_length=256, default='')
class Meta:
verbose_name = _('Configurações da Aplicação')
verbose_name_plural = _('Configurações da Aplicação')
@ -219,7 +229,8 @@ class TipoAutor(models.Model):
descricao = models.CharField(
max_length=50,
verbose_name=_('Descrição'),
help_text=_('Obs: Não crie tipos de autores semelhante aos tipos fixos. ')
help_text=_(
'Obs: Não crie tipos de autores semelhante aos tipos fixos. ')
)
content_type = models.OneToOneField(
@ -325,4 +336,3 @@ class AuditLog(models.Model):
self.model_name,
self.username,
)

222
sapl/base/views.py

@ -1,62 +1,67 @@
from collections import OrderedDict
import collections
import itertools
import datetime
import itertools
import logging
import os
from collections import OrderedDict
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Group, User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import (PasswordResetView,PasswordResetConfirmView, PasswordResetCompleteView,
from django.contrib.auth.views import (PasswordResetView, PasswordResetConfirmView, PasswordResetCompleteView,
PasswordResetDoneView)
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from django.core.mail import send_mail
from django.urls import reverse, reverse_lazy
from django.db import connection
from django.db.models import Count, Q, ProtectedError, Max
from django.shortcuts import render
from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, redirect
from django.template import TemplateDoesNotExist
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DetailView, DeleteView, FormView, ListView, UpdateView)
from django.views.generic import (
CreateView, DetailView, DeleteView, FormView, ListView, UpdateView)
from django.views.generic.base import RedirectView, TemplateView
from django_filters.views import FilterView
from haystack.views import SearchView
from haystack.query import SearchQuerySet
from sapl.relatorios.views import (relatorio_materia_em_tramitacao, relatorio_materia_por_autor,
relatorio_materia_por_ano_autor, relatorio_presenca_sessao,
relatorio_historico_tramitacao, relatorio_fim_prazo_tramitacao, relatorio_atas,
relatorio_audiencia, relatorio_normas_mes, relatorio_normas_vigencia,
relatorio_historico_tramitacao_adm, relatorio_reuniao,
relatorio_estatisticas_acesso_normas, relatorio_normas_por_autor,
relatorio_documento_acessorio)
from haystack.views import SearchView
from rest_framework.authtoken.models import Token
from sapl import settings
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica
from sapl.base.models import Autor, TipoAutor
from sapl.base.forms import (AutorForm, AutorFormForAdmin, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm,
NovaSenhaForm)
from sapl.base.models import Autor, TipoAutor
from sapl.comissoes.models import Comissao, Reuniao
from sapl.crud.base import CrudAux, make_pagination
from sapl.materia.models import (Anexada, Autoria, DocumentoAcessorio, MateriaEmTramitacao, MateriaLegislativa,
Proposicao, StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, UnidadeTramitacao,
Tramitacao)
from sapl.norma.models import NormaJuridica, TipoNormaJuridica
from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato, Parlamentar, SessaoLegislativa)
from sapl.parlamentares.models import (
Filiacao, Legislatura, Mandato, Parlamentar, SessaoLegislativa)
from sapl.protocoloadm.models import (Anexado, DocumentoAdministrativo, Protocolo, StatusTramitacaoAdministrativo,
TipoDocumentoAdministrativo)
from sapl.sessao.models import (Bancada, PresencaOrdemDia, SessaoPlenaria, SessaoPlenariaPresenca, TipoSessaoPlenaria)
from sapl.relatorios.views import (relatorio_materia_em_tramitacao, relatorio_materia_por_autor,
relatorio_materia_por_ano_autor, relatorio_presenca_sessao,
relatorio_historico_tramitacao, relatorio_fim_prazo_tramitacao, relatorio_atas,
relatorio_audiencia, relatorio_normas_mes, relatorio_normas_vigencia,
relatorio_historico_tramitacao_adm, relatorio_reuniao,
relatorio_estatisticas_acesso_normas, relatorio_normas_por_autor,
relatorio_documento_acessorio)
from sapl.sessao.models import (
Bancada, PresencaOrdemDia, SessaoPlenaria, SessaoPlenariaPresenca, TipoSessaoPlenaria)
from sapl.settings import EMAIL_SEND_USER
from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured, parlamentares_ativos,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, num_materias_por_tipo)
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, num_materias_por_tipo,
google_recaptcha_configured)
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet,
RelatorioAudienciaFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet,
RelatorioHistoricoTramitacaoFilterSet, RelatorioMateriasPorAnoAutorTipoFilterSet,
@ -67,8 +72,6 @@ from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm,
RelatorioNormasPorAutorFilterSet)
from .models import AppConfig, CasaLegislativa
from rest_framework.authtoken.models import Token
def get_casalegislativa():
return CasaLegislativa.objects.first()
@ -87,6 +90,8 @@ class ConfirmarEmailView(TemplateView):
class RecuperarSenhaEmailView(PasswordResetView):
logger = logging.getLogger(__name__)
success_url = reverse_lazy('sapl.base:recuperar_senha_finalizado')
email_template_name = 'base/recuperar_senha_email.html'
html_email_template_name = 'base/recuperar_senha_email.html'
@ -94,6 +99,24 @@ class RecuperarSenhaEmailView(PasswordResetView):
from_email = EMAIL_SEND_USER
form_class = RecuperarSenhaForm
def get(self, request, *args, **kwargs):
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
return PasswordResetView.get(self, request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
return PasswordResetView.post(self, request, *args, **kwargs)
class RecuperarSenhaFinalizadoView(PasswordResetDoneView):
template_name = 'base/recupera_senha_email_enviado.html'
@ -330,7 +353,8 @@ class PesquisarAutorView(FilterView):
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(page_obj.number, paginator.num_pages)
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhum Autor encontrado!'
@ -354,13 +378,14 @@ class PesquisarAutorView(FilterView):
filter_url=url,
numero_res=len(self.object_list))
context['show_results'] = show_results_filter_set(self.request.GET.copy())
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context)
class RelatoriosListView(TemplateView):
template_name='base/relatorios_list.html'
template_name = 'base/relatorios_list.html'
def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs)
@ -471,11 +496,13 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
cd = self.filterset.form.cleaned_data
if not cd['data_inicio'] and not cd['sessao_legislativa'] \
and not cd['legislatura']:
msg = _("Formulário inválido! Preencha pelo menos algum dos campos Período, Legislatura ou Sessão Legislativa.")
msg = _(
"Formulário inválido! Preencha pelo menos algum dos campos Período, Legislatura ou Sessão Legislativa.")
messages.error(self.request, msg)
return context
# Caso a data tenha sido preenchida, verifica se foi preenchida corretamente
# Caso a data tenha sido preenchida, verifica se foi preenchida
# corretamente
if self.request.GET.get('data_inicio_0') and not self.request.GET.get('data_inicio_1'):
msg = _("Formulário inválido! Preencha a data do Período Final.")
messages.error(self.request, msg)
@ -497,14 +524,16 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
sessao_legislativa_pk = self.request.GET.get('sessao_legislativa')
if sessao_legislativa_pk:
param0['sessao_plenaria__sessao_legislativa_id'] = sessao_legislativa_pk
sessao_legislativa = SessaoLegislativa.objects.get(id=sessao_legislativa_pk)
sessao_legislativa = SessaoLegislativa.objects.get(
id=sessao_legislativa_pk)
context['sessao_legislativa'] = sessao_legislativa
tipo_sessao_plenaria_pk = self.request.GET.get('tipo')
context['tipo'] = ''
if tipo_sessao_plenaria_pk:
param0['sessao_plenaria__tipo_id'] = tipo_sessao_plenaria_pk
context['tipo'] = TipoSessaoPlenaria.objects.get(id=tipo_sessao_plenaria_pk)
context['tipo'] = TipoSessaoPlenaria.objects.get(
id=tipo_sessao_plenaria_pk)
_range = []
@ -517,29 +546,35 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
_range = [legislatura.data_inicio, legislatura.data_fim]
elif sessao_legislativa_pk:
_range = [sessao_legislativa.data_inicio, sessao_legislativa.data_fim]
_range = [sessao_legislativa.data_inicio,
sessao_legislativa.data_fim]
param0.update({'sessao_plenaria__data_inicio__range': _range})
# Parlamentares com Mandato no intervalo de tempo (Ativos)
parlamentares_qs = parlamentares_ativos(_range[0], _range[1]).order_by('nome_parlamentar')
parlamentares_qs = parlamentares_ativos(
_range[0], _range[1]).order_by('nome_parlamentar')
parlamentares_id = parlamentares_qs.values_list('id', flat=True)
# Presenças de cada Parlamentar em Sessões
presenca_sessao = SessaoPlenariaPresenca.objects.filter(**param0).values_list('parlamentar_id').annotate(sessao_count=Count('id'))
presenca_sessao = SessaoPlenariaPresenca.objects.filter(
**param0).values_list('parlamentar_id').annotate(sessao_count=Count('id'))
# Presenças de cada Ordem do Dia
presenca_ordem = PresencaOrdemDia.objects.filter(**param0).values_list('parlamentar_id').annotate(sessao_count=Count('id'))
presenca_ordem = PresencaOrdemDia.objects.filter(
**param0).values_list('parlamentar_id').annotate(sessao_count=Count('id'))
total_ordemdia = PresencaOrdemDia.objects.filter(**param0).distinct('sessao_plenaria__id').order_by('sessao_plenaria__id').count()
total_ordemdia = PresencaOrdemDia.objects.filter(
**param0).distinct('sessao_plenaria__id').order_by('sessao_plenaria__id').count()
total_sessao = context['object_list'].count()
username = self.request.user.username
context['exibir_somente_titular'] = self.request.GET.get('exibir_somente_titular') == 'on'
context['exibir_somente_ativo'] = self.request.GET.get('exibir_somente_ativo') == 'on'
context['exibir_somente_titular'] = self.request.GET.get(
'exibir_somente_titular') == 'on'
context['exibir_somente_ativo'] = self.request.GET.get(
'exibir_somente_ativo') == 'on'
# Completa o dicionario as informacoes parlamentar/sessao/ordem
parlamentares_presencas = []
@ -594,17 +629,21 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
continue
try:
self.logger.debug(F'user={username}. Tentando obter presença do parlamentar (pk={p.id}).')
self.logger.debug(
F'user={username}. Tentando obter presença do parlamentar (pk={p.id}).')
sessao_count = presenca_sessao.get(parlamentar_id=p.id)[1]
except ObjectDoesNotExist as e:
self.logger.error(F'user={username}. Erro ao obter presença do parlamentar (pk={p.id}). Definido como 0. {str(e)}')
self.logger.error(
F'user={username}. Erro ao obter presença do parlamentar (pk={p.id}). Definido como 0. {str(e)}')
sessao_count = 0
try:
# Presenças de cada Ordem do Dia
self.logger.info(F'user={username}. Tentando obter PresencaOrdemDia para o parlamentar pk={p.id}.')
self.logger.info(
F'user={username}. Tentando obter PresencaOrdemDia para o parlamentar pk={p.id}.')
ordemdia_count = presenca_ordem.get(parlamentar_id=p.id)[1]
except ObjectDoesNotExist:
self.logger.error(F'user={username}. Erro ao obter PresencaOrdemDia para o parlamentar pk={p.id}. Definido como 0.')
self.logger.error(
F'user={username}. Erro ao obter PresencaOrdemDia para o parlamentar pk={p.id}. Definido como 0.')
ordemdia_count = 0
parlamentar.update({
@ -613,9 +652,11 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
})
if total_sessao != 0:
parlamentar.update({'sessao_porc': round(sessao_count * 100 / total_sessao, 2)})
parlamentar.update({'sessao_porc': round(
sessao_count * 100 / total_sessao, 2)})
if total_ordemdia != 0:
parlamentar.update({'ordemdia_porc': round(ordemdia_count * 100 / total_ordemdia, 2)})
parlamentar.update({'ordemdia_porc': round(
ordemdia_count * 100 / total_ordemdia, 2)})
parlamentares_presencas.append(parlamentar)
@ -626,10 +667,12 @@ class RelatorioPresencaSessaoView(RelatorioMixin, FilterView):
context['periodo'] = f"{self.request.GET['data_inicio_0']} - {self.request.GET['data_inicio_1']}"
context['sessao_legislativa'] = ''
context['legislatura'] = ''
context['exibir_ordem'] = self.request.GET.get('exibir_ordem_dia') == 'on'
context['exibir_ordem'] = self.request.GET.get(
'exibir_ordem_dia') == 'on'
if sessao_legislativa_pk:
context['sessao_legislativa'] = SessaoLegislativa.objects.get(id=sessao_legislativa_pk)
context['sessao_legislativa'] = SessaoLegislativa.objects.get(
id=sessao_legislativa_pk)
if legislatura_pk:
context['legislatura'] = Legislatura.objects.get(id=legislatura_pk)
# =====================================================================
@ -650,7 +693,8 @@ class RelatorioHistoricoTramitacaoView(RelatorioMixin, FilterView):
def get_context_data(self, **kwargs):
context = super(RelatorioHistoricoTramitacaoView,
self).get_context_data(**kwargs)
context['title'] = _('Histórico de Tramitações de Matérias Legislativas')
context['title'] = _(
'Histórico de Tramitações de Matérias Legislativas')
if not self.filterset.form.is_valid():
return context
qr = self.request.GET.copy()
@ -852,7 +896,8 @@ class RelatorioMateriasTramitacaoView(RelatorioMixin, FilterView):
qs = qs.filter(**kwargs)
data['queryset'] = qs
self.total_resultados_tipos = num_materias_por_tipo(qs, "materia__tipo")
self.total_resultados_tipos = num_materias_por_tipo(
qs, "materia__tipo")
return data
@ -1072,8 +1117,8 @@ class RelatorioNormasPublicadasMesView(RelatorioMixin, FilterView):
context['ano'] = self.request.GET['ano']
normas_mes = collections.OrderedDict()
meses = {1: 'Janeiro', 2: 'Fevereiro', 3:'Março', 4: 'Abril', 5: 'Maio', 6:'Junho',
7: 'Julho', 8: 'Agosto', 9:'Setembro', 10:'Outubro', 11:'Novembro', 12:'Dezembro'}
meses = {1: 'Janeiro', 2: 'Fevereiro', 3: 'Março', 4: 'Abril', 5: 'Maio', 6: 'Junho',
7: 'Julho', 8: 'Agosto', 9: 'Setembro', 10: 'Outubro', 11: 'Novembro', 12: 'Dezembro'}
for norma in context['object_list']:
if not meses[norma.data.month] in normas_mes:
normas_mes[meses[norma.data.month]] = []
@ -1110,16 +1155,17 @@ class RelatorioNormasVigenciaView(RelatorioMixin, FilterView):
if vigencia == 'True':
qs_dt_not_null = qs.filter(data_vigencia__isnull=True)
qs = (qs_dt_not_null | qs.filter(data_vigencia__gte=datetime.datetime.now().date())).distinct()
qs = (qs_dt_not_null | qs.filter(
data_vigencia__gte=datetime.datetime.now().date())).distinct()
else:
qs = qs.filter(data_vigencia__lt=datetime.datetime.now().date())
qs = qs.filter(
data_vigencia__lt=datetime.datetime.now().date())
kwargs.update({
'queryset': qs
})
return kwargs
def get_context_data(self, **kwargs):
context = super(RelatorioNormasVigenciaView,
self).get_context_data(**kwargs)
@ -1129,17 +1175,20 @@ class RelatorioNormasVigenciaView(RelatorioMixin, FilterView):
if not self.filterset.form.is_valid():
return context
normas_totais = NormaJuridica.objects.filter(ano=self.request.GET['ano'])
normas_totais = NormaJuridica.objects.filter(
ano=self.request.GET['ano'])
context['quant_total'] = len(normas_totais)
if self.request.GET['vigencia'] == 'True':
context['vigencia'] = 'Vigente'
context['quant_vigente'] = len(context['object_list'])
context['quant_nao_vigente'] = context['quant_total'] - context['quant_vigente']
context['quant_nao_vigente'] = context['quant_total'] - \
context['quant_vigente']
else:
context['vigencia'] = 'Não vigente'
context['quant_nao_vigente'] = len(context['object_list'])
context['quant_vigente'] = context['quant_total'] - context['quant_nao_vigente']
context['quant_vigente'] = context['quant_total'] - \
context['quant_nao_vigente']
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
@ -1178,8 +1227,8 @@ class EstatisticasAcessoNormas(TemplateView):
rows = cursor.fetchall()
normas_mes = collections.OrderedDict()
meses = {1: 'Janeiro', 2: 'Fevereiro', 3:'Março', 4: 'Abril', 5: 'Maio', 6:'Junho',
7: 'Julho', 8: 'Agosto', 9:'Setembro', 10:'Outubro', 11:'Novembro', 12:'Dezembro'}
meses = {1: 'Janeiro', 2: 'Fevereiro', 3: 'Março', 4: 'Abril', 5: 'Maio', 6: 'Junho',
7: 'Julho', 8: 'Agosto', 9: 'Setembro', 10: 'Outubro', 11: 'Novembro', 12: 'Dezembro'}
for row in rows:
if not meses[int(row[2])] in normas_mes:
@ -1189,7 +1238,8 @@ class EstatisticasAcessoNormas(TemplateView):
# Ordena por acesso e limita em 5
for n in normas_mes:
sorted_by_value = sorted(normas_mes[n], key=lambda kv: kv[1], reverse=True)
sorted_by_value = sorted(
normas_mes[n], key=lambda kv: kv[1], reverse=True)
normas_mes[n] = sorted_by_value[0:5]
context['normas_mes'] = normas_mes
@ -1293,6 +1343,7 @@ class ListarInconsistenciasView(PermissionRequiredMixin, ListView):
)
return tabela
def materias_anexadas_ciclicas():
ciclos = []
@ -1306,7 +1357,8 @@ def materias_anexadas_ciclicas():
ma = anexadas.pop()
if ma not in visitados:
visitados.append(ma)
anexadas.extend([a.materia_anexada for a in Anexada.objects.filter(materia_principal=ma)])
anexadas.extend(
[a.materia_anexada for a in Anexada.objects.filter(materia_principal=ma)])
else:
ciclo_list = visitados + [ma]
ciclos.append(ciclo_list)
@ -1320,6 +1372,7 @@ def materias_anexadas_ciclicas():
return ciclos_unique
def is_ciclo_unique(ciclo, ciclos_set):
if set(ciclo) not in ciclos_set:
ciclos_set.append(set(ciclo))
@ -1327,6 +1380,7 @@ def is_ciclo_unique(ciclo, ciclos_set):
else:
return False
def anexados_ciclicos(ofMateriaLegislativa):
ciclicos = []
@ -1367,7 +1421,8 @@ def anexados_ciclicos(ofMateriaLegislativa):
)
anexados_temp.extend(anexados_anexado)
else:
ciclicos.append((anexado.data_anexacao, anexado.materia_principal, anexado.materia_anexada))
ciclicos.append(
(anexado.data_anexacao, anexado.materia_principal, anexado.materia_anexada))
else:
if anexado.documento_anexado not in anexados_total:
if not principal['documento_principal'] == anexado.documento_anexado.pk:
@ -1377,7 +1432,8 @@ def anexados_ciclicos(ofMateriaLegislativa):
)
anexados_temp.extend(anexados_anexado)
else:
ciclicos.append((anexado.data_anexacao, anexado.documento_principal, anexado.documento_anexado))
ciclicos.append(
(anexado.data_anexacao, anexado.documento_principal, anexado.documento_anexado))
return ciclicos
@ -1547,10 +1603,12 @@ def parlamentares_filiacoes_intersecao():
for c in combinacoes:
data_filiacao1 = c[0].data
data_desfiliacao1 = c[0].data_desfiliacao if c[0].data_desfiliacao else timezone.now().date()
data_desfiliacao1 = c[0].data_desfiliacao if c[0].data_desfiliacao else timezone.now(
).date()
data_filiacao2 = c[1].data
data_desfiliacao2 = c[1].data_desfiliacao if c[1].data_desfiliacao else timezone.now().date()
data_desfiliacao2 = c[1].data_desfiliacao if c[1].data_desfiliacao else timezone.now(
).date()
if data_filiacao1 and data_filiacao2:
exists = intervalos_tem_intersecao(
@ -1593,10 +1651,12 @@ def parlamentares_mandatos_intersecao():
for c in combinacoes:
data_inicio_mandato1 = c[0].data_inicio_mandato
data_fim_mandato1 = c[0].data_fim_mandato if c[0].data_fim_mandato else timezone.now().date()
data_fim_mandato1 = c[0].data_fim_mandato if c[0].data_fim_mandato else timezone.now(
).date()
data_inicio_mandato2 = c[1].data_inicio_mandato
data_fim_mandato2 = c[1].data_fim_mandato if c[1].data_fim_mandato else timezone.now().date()
data_fim_mandato2 = c[1].data_fim_mandato if c[1].data_fim_mandato else timezone.now(
).date()
if data_inicio_mandato1 and data_inicio_mandato2:
exists = intervalos_tem_intersecao(
@ -1669,13 +1729,16 @@ def get_estatistica(request):
normas = NormaJuridica.objects.all()
datas = [
materias.order_by('-data_ultima_atualizacao').values_list('data_ultima_atualizacao', flat=True)
materias.order_by(
'-data_ultima_atualizacao').values_list('data_ultima_atualizacao', flat=True)
.exclude(data_ultima_atualizacao__isnull=True).first(),
normas.order_by('-data_ultima_atualizacao').values_list('data_ultima_atualizacao', flat=True)
normas.order_by(
'-data_ultima_atualizacao').values_list('data_ultima_atualizacao', flat=True)
.exclude(data_ultima_atualizacao__isnull=True).first()
]
max_data = max(datas) if datas[0] and datas[1] else next(iter([i for i in datas if i is not None]), '')
max_data = max(datas) if datas[0] and datas[1] else next(
iter([i for i in datas if i is not None]), '')
return JsonResponse({
"data_ultima_atualizacao": max_data,
@ -1964,7 +2027,8 @@ class DeleteUsuarioView(PermissionRequiredMixin, DeleteView):
try:
super(DeleteUsuarioView, self).delete(request, *args, **kwargs)
except ProtectedError as exception:
error_url = reverse_lazy('sapl.base:user_delete', kwargs={'pk': self.kwargs['pk']})
error_url = reverse_lazy('sapl.base:user_delete', kwargs={
'pk': self.kwargs['pk']})
error_message = "O usuário não pode ser removido, pois é referenciado por:<br><ul>"
for e in exception.protected_objects:
@ -2099,13 +2163,15 @@ class AppConfigCrud(CrudAux):
recibo_prop_atual = AppConfig.objects.last().receber_recibo_proposicao
recibo_prop_novo = self.request.POST['receber_recibo_proposicao']
if recibo_prop_novo == 'False' and recibo_prop_atual:
props = Proposicao.objects.filter(hash_code='', data_recebimento__isnull=True).exclude(data_envio__isnull=True)
props = Proposicao.objects.filter(
hash_code='', data_recebimento__isnull=True).exclude(data_envio__isnull=True)
for prop in props:
try:
self.gerar_hash(prop)
except ValidationError as e:
form.add_error('receber_recibo_proposicao',e)
msg = _("Não foi possível mudar a configuração porque a Proposição {} não possui texto original vinculado!".format(prop))
form.add_error('receber_recibo_proposicao', e)
msg = _(
"Não foi possível mudar a configuração porque a Proposição {} não possui texto original vinculado!".format(prop))
messages.error(self.request, msg)
return super().form_invalid(form)
return super().form_valid(form)
@ -2117,7 +2183,8 @@ class AppConfigCrud(CrudAux):
inst.texto_original.path, str(inst.pk))
inst.save()
except IOError:
raise ValidationError("Existem proposicoes com arquivos inexistentes.")
raise ValidationError(
"Existem proposicoes com arquivos inexistentes.")
elif inst.texto_articulado.exists():
ta = inst.texto_articulado.first()
inst.hash_code = 'P' + ta.hash() + SEPARADOR_HASH_PROPOSICAO + str(inst.pk)
@ -2136,7 +2203,6 @@ class AppConfigCrud(CrudAux):
reverse('sapl.base:appconfig_update',
kwargs={'pk': app_config.pk}))
class UpdateView(CrudAux.UpdateView):
template_name = 'base/AppConfig.html'
@ -2258,6 +2324,7 @@ class LogotipoView(RedirectView):
logo = casa and casa.logotipo and casa.logotipo.name
return os.path.join(settings.MEDIA_URL, logo) if logo else STATIC_LOGO
def filtro_campos(dicionario):
chaves_desejadas = ['ementa',
@ -2280,6 +2347,7 @@ def filtro_campos(dicionario):
return dicionario
def pesquisa_textual(request):
if 'q' not in request.GET:
@ -2299,7 +2367,8 @@ def pesquisa_textual(request):
try:
sec_dict['pk'] = e.object.pk
except:
# Index and db are out of sync. Object has been deleted from database
# Index and db are out of sync. Object has been deleted from
# database
continue
dici = filtro_campos(e.object.__dict__)
sec_dict['objeto'] = str(dici)
@ -2309,7 +2378,6 @@ def pesquisa_textual(request):
json_dict['resultados'].append(sec_dict)
return JsonResponse(json_dict)
@ -2322,7 +2390,8 @@ class RelatorioHistoricoTramitacaoAdmView(RelatorioMixin, FilterView):
def get_context_data(self, **kwargs):
context = super(RelatorioHistoricoTramitacaoAdmView,
self).get_context_data(**kwargs)
context['title'] = _('Histórico de Tramitações de Documento Administrativo')
context['title'] = _(
'Histórico de Tramitações de Documento Administrativo')
if not self.filterset.form.is_valid():
return context
qr = self.request.GET.copy()
@ -2361,6 +2430,7 @@ class RelatorioHistoricoTramitacaoAdmView(RelatorioMixin, FilterView):
return context
class RelatorioNormasPorAutorView(RelatorioMixin, FilterView):
model = NormaJuridica
filterset_class = RelatorioNormasPorAutorFilterSet

14
sapl/context_processors.py

@ -1,13 +1,14 @@
import logging
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from sapl.base.views import get_casalegislativa
from sapl.utils import google_recaptcha_configured as google_recaptcha_configured_utils
from sapl.utils import mail_service_configured as mail_service_configured_utils
def parliament_info(request):
from sapl.base.views import get_casalegislativa
casa = get_casalegislativa()
if casa:
return casa.__dict__
@ -22,3 +23,12 @@ def mail_service_configured(request):
logger.warning(_('Servidor de email não configurado.'))
return {'mail_service_configured': False}
return {'mail_service_configured': True}
def google_recaptcha_configured(request):
if not google_recaptcha_configured_utils():
logger = logging.getLogger(__name__)
logger.warning(_('Google Recaptcha não configurado.'))
return {'google_recaptcha_configured': False}
return {'google_recaptcha_configured': True}

39
sapl/materia/forms.py

@ -7,7 +7,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.base import File
from django.db import models, transaction
from django.db import transaction
from django.db.models import F, Max, Q
from django.forms import ModelChoiceField, ModelForm, widgets
from django.forms.forms import Form
@ -15,9 +15,6 @@ from django.forms.models import ModelMultipleChoiceField
from django.forms.widgets import CheckboxSelectMultiple, HiddenInput, Select
from django.urls import reverse
from django.utils import timezone
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
import django_filters
@ -28,15 +25,15 @@ from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PRIVATE)
from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
SaplFormLayout, to_column, to_row)
from sapl.materia.models import (AssuntoMateria, Autoria, MateriaAssunto,
from sapl.materia.models import (AssuntoMateria, MateriaAssunto,
MateriaLegislativa, Orgao,
RegimeTramitacao, StatusTramitacao,
TipoDocumento, TipoProposicao,
UnidadeTramitacao, ConfigEtiquetaMateriaLegislativa)
ConfigEtiquetaMateriaLegislativa)
from sapl.norma.models import (LegislacaoCitada, NormaJuridica,
TipoNormaJuridica)
from sapl.parlamentares.models import Legislatura, Partido, Parlamentar
from sapl.protocoloadm.models import (Anexado, DocumentoAdministrativo,
from sapl.parlamentares.models import Legislatura, Partido
from sapl.protocoloadm.models import (DocumentoAdministrativo,
Protocolo)
from sapl.utils import (autor_label, autor_modal,
ChoiceWithoutValidationField,
@ -44,8 +41,9 @@ from sapl.utils import (autor_label, autor_modal,
FilterOverridesMetaMixin, gerar_hash_arquivo,
lista_anexados, MateriaPesquisaOrderingFilter,
models_with_gr_for_model, qs_override_django_filter,
RangeWidgetOverride, SEPARADOR_HASH_PROPOSICAO,
validar_arquivo, YES_NO_CHOICES)
SEPARADOR_HASH_PROPOSICAO,
validar_arquivo, YES_NO_CHOICES,
GoogleRecapthaMixin)
from .models import (AcompanhamentoMateria, Anexada, Autoria,
DespachoInicial, DocumentoAcessorio, Numeracao,
@ -317,7 +315,7 @@ class UnidadeTramitacaoForm(ModelForm):
return unidade
class AcompanhamentoMateriaForm(ModelForm):
class AcompanhamentoMateriaForm(GoogleRecapthaMixin, ModelForm):
class Meta:
model = AcompanhamentoMateria
@ -325,17 +323,10 @@ class AcompanhamentoMateriaForm(ModelForm):
def __init__(self, *args, **kwargs):
row1 = to_row([('email', 12)])
kwargs['title_label'] = _('Acompanhamento de Matéria por e-mail')
kwargs['action_label'] = _('Cadastrar')
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Acompanhamento de Matéria por e-mail'),
row1,
form_actions(label='Cadastrar')
)
)
super(AcompanhamentoMateriaForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
class DocumentoAcessorioForm(FileFieldCheckMixin, ModelForm):
@ -2789,7 +2780,8 @@ class FichaSelecionaForm(forms.Form):
class StatusTramitacaoFilterSet(django_filters.FilterSet):
descricao = django_filters.CharFilter(label=_("Descrição do Status"), method='multifield_filter')
descricao = django_filters.CharFilter(
label=_("Descrição do Status"), method='multifield_filter')
class Meta:
model = StatusTramitacao
@ -2806,7 +2798,8 @@ class StatusTramitacaoFilterSet(django_filters.FilterSet):
self.form.helper = SaplFormHelper()
self.form.helper.form_method = "GET"
self.form.helper.layout = Layout(
Fieldset(_("Pesquisa de Status de Tramitacao"), row0, form_actions(label="Pesquisar"))
Fieldset(_("Pesquisa de Status de Tramitacao"),
row0, form_actions(label="Pesquisar"))
)

12
sapl/materia/views.py

@ -56,7 +56,8 @@ from sapl.settings import MAX_DOC_UPLOAD_SIZE, MEDIA_ROOT
from sapl.utils import (autor_label, autor_modal, gerar_hash_arquivo, get_base_url,
get_client_ip, get_mime_type_from_file_extension, lista_anexados,
mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO,
show_results_filter_set, YES_NO_CHOICES, get_tempfile_dir)
show_results_filter_set, YES_NO_CHOICES, get_tempfile_dir,
google_recaptcha_configured)
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet,
@ -2090,6 +2091,10 @@ class AcompanhamentoMateriaView(CreateView):
messages.error(request, _('Serviço de Acompanhamento de '
'Matérias não foi configurado'))
return redirect('/')
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
pk = self.kwargs['pk']
materia = MateriaLegislativa.objects.get(id=pk)
@ -2106,6 +2111,11 @@ class AcompanhamentoMateriaView(CreateView):
'Matérias não foi configurado'))
return redirect('/')
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
form = AcompanhamentoMateriaForm(request.POST)
pk = self.kwargs['pk']
materia = MateriaLegislativa.objects.get(id=pk)

25
sapl/protocoloadm/forms.py

@ -2,12 +2,12 @@ import logging
import re
from crispy_forms.bootstrap import InlineRadios, Alert, FormActions
from crispy_forms.layout import (Button, Column, Div, Fieldset, HTML,
from crispy_forms.layout import (Button, Div, Fieldset, HTML,
Layout, Submit)
from django import forms
from django.core.exceptions import (MultipleObjectsReturned,
ObjectDoesNotExist, ValidationError)
from django.db import models, transaction
from django.db import transaction
from django.db.models import Max
from django.forms import ModelForm
from django.utils import timezone
@ -20,14 +20,14 @@ from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
from sapl.materia.models import (MateriaLegislativa,
TipoMateriaLegislativa,
UnidadeTramitacao)
from sapl.protocoloadm.models import Protocolo
from sapl.utils import (AnoNumeroOrderingFilter, autor_label, autor_modal,
choice_anos_com_documentoadministrativo,
choice_anos_com_materias,
choice_anos_com_protocolo, choice_force_optional,
FileFieldCheckMixin, FilterOverridesMetaMixin,
lista_anexados, RangeWidgetOverride, RANGE_ANOS,
validar_arquivo, YES_NO_CHOICES)
lista_anexados, RANGE_ANOS,
validar_arquivo, YES_NO_CHOICES,
GoogleRecapthaMixin)
from .models import (Anexado, AcompanhamentoDocumento,
DocumentoAcessorioAdministrativo,
@ -48,7 +48,7 @@ NATUREZA_PROCESSO = [('0', 'Administrativo'),
EM_TRAMITACAO = [(0, 'Sim'), (1, 'Não')]
class AcompanhamentoDocumentoForm(ModelForm):
class AcompanhamentoDocumentoForm(GoogleRecapthaMixin, ModelForm):
class Meta:
model = AcompanhamentoDocumento
@ -56,17 +56,10 @@ class AcompanhamentoDocumentoForm(ModelForm):
def __init__(self, *args, **kwargs):
row1 = to_row([('email', 12)])
kwargs['title_label'] = _('Acompanhamento de Documento por e-mail')
kwargs['action_label'] = _('Cadastrar')
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Acompanhamento de Documento por e-mail'),
row1,
form_actions(label='Cadastrar')
)
)
super(AcompanhamentoDocumentoForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
class ProtocoloFilterSet(django_filters.FilterSet):

13
sapl/protocoloadm/views.py

@ -40,7 +40,8 @@ from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo
from sapl.relatorios.views import relatorio_doc_administrativos
from sapl.utils import (create_barcode, get_base_url, get_client_ip,
get_mime_type_from_file_extension, lista_anexados,
show_results_filter_set, mail_service_configured, from_date_to_datetime_utc)
show_results_filter_set, mail_service_configured, from_date_to_datetime_utc,
google_recaptcha_configured)
from .forms import (AcompanhamentoDocumentoForm, AnexadoEmLoteFilterSet, AnexadoForm,
AnularProtocoloAdmForm, compara_tramitacoes_doc,
@ -200,6 +201,11 @@ class AcompanhamentoDocumentoView(CreateView):
'Documentos não foi configurado'))
return redirect('/')
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
pk = self.kwargs['pk']
documento = DocumentoAdministrativo.objects.get(id=pk)
@ -214,6 +220,11 @@ class AcompanhamentoDocumentoView(CreateView):
'Documentos não foi configurado'))
return redirect('/')
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
form = AcompanhamentoDocumentoForm(request.POST)
pk = self.kwargs['pk']
documento = DocumentoAdministrativo.objects.get(id=pk)

2
sapl/settings.py

@ -195,6 +195,8 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages',
'sapl.context_processors.parliament_info',
'sapl.context_processors.mail_service_configured',
'sapl.context_processors.google_recaptcha_configured',
],
'debug': DEBUG
},

3
sapl/templates/base/layouts.yaml

@ -55,6 +55,9 @@ AppConfig:
- cronometro_ordem cronometro_consideracoes
- mostrar_brasao_painel
{% trans 'Segurança' %}:
- google_recaptcha_site_key google_recaptcha_secret_key
TipoAutor:
{% trans 'Tipo Autor' %}:
- descricao

5
sapl/templates/base/login.html

@ -41,7 +41,12 @@
</tr>
</table>
</p>
{% if google_recaptcha_configured %}
<h5><a href="{% url 'sapl.base:recuperar_senha_email' %}"><center>Esqueceu sua senha?</center></a></h6>
{% else %}
<br>
{% endif %}
<p class="bs-component">
<center>
<input class="btn btn-lg btn-success btn-block" type="submit" value="login" />

4
sapl/templates/base/recuperar_senha_email_form.html

@ -1 +1,5 @@
{% extends "crud/form.html" %}
{% block head_content %}{{block.super}}
<script src='https://www.google.com/recaptcha/api.js'></script>
{% endblock %}

8
sapl/templates/materia/acompanhamento_materia.html

@ -1,7 +1,13 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block actions %} {% endblock %}
{% block head_content %}{{block.super}}
<script src='https://www.google.com/recaptcha/api.js'></script>
{% endblock %}
{% block actions %}{% endblock %}
{% block detail_content %}
<h1>Acompanhamento de Matéria</h1>

2
sapl/templates/materia/materialegislativa_detail.html

@ -3,7 +3,7 @@
{% block sub_actions %}
{{ block.super }}
{% if object.em_tramitacao and mail_service_configured %}
{% if object.em_tramitacao and mail_service_configured and google_recaptcha_configured %}
<div class="actions btn-group btn-group-sm" role="group">
<a href="{% url 'sapl.materia:acompanhar_materia' object.id %}" class="btn btn-outline-primary">Acompanhar Matéria</a>
</div>

2
sapl/templates/materia/materialegislativa_filter.html

@ -156,7 +156,7 @@
{% endfor %}
{% endif %}
<p></p>
{% if m.em_tramitacao and mail_service_configured %}
{% if m.em_tramitacao and mail_service_configured and google_recaptcha_configured %}
<a href="{% url 'sapl.materia:acompanhar_materia' m.id %}">Acompanhar Matéria</a>
{% endif %}
{% endif %}

8
sapl/templates/protocoloadm/acompanhamento_documento.html

@ -1,7 +1,13 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block actions %} {% endblock %}
{% block head_content %}{{block.super}}
<script src='https://www.google.com/recaptcha/api.js'></script>
{% endblock %}
{% block actions %}{% endblock %}
{% block detail_content %}
<h1>Acompanhamento de Documento</h1>

2
sapl/templates/protocoloadm/documentoadministrativo_filter.html

@ -63,7 +63,7 @@
{% if d.texto_integral %}
<strong><a href="{{ d.texto_integral.url }}">Texto Integral</a></strong></br>
{% endif %}
{% if d.tramitacao and mail_service_configured %}
{% if d.tramitacao and mail_service_configured and google_recaptcha_configured %}
<a href="{% url 'sapl.protocoloadm:acompanhar_documento' d.id %}">Acompanhar Documento</a>
{% endif %}
</td>

136
sapl/utils.py

@ -1,23 +1,18 @@
from itertools import groupby
import django_filters
from functools import wraps
import hashlib
from itertools import groupby
import logging
import magic
from operator import itemgetter
import os
import re
import unicodedata
import platform
import re
import requests
import tempfile
from crispy_forms.layout import Button, HTML
from easy_thumbnails import source_generators
from floppyforms import ClearableFileInput
from functools import wraps
from operator import itemgetter
from reversion_compare.admin import CompareVersionAdmin
from unicodedata import normalize as unicodedata_normalize
from unipath.path import Path
import unicodedata
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Button, HTML, Fieldset, Div
from django import forms
from django.apps import apps
from django.conf import settings
@ -35,6 +30,12 @@ from django.forms.widgets import SplitDateTimeWidget
from django.utils import six, timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
import django_filters
from easy_thumbnails import source_generators
from floppyforms import ClearableFileInput
import magic
from reversion_compare.admin import CompareVersionAdmin
from unipath.path import Path
from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
SaplFormLayout, to_row)
@ -64,7 +65,7 @@ def num_materias_por_tipo(qs, attr_tipo='tipo'):
# select_related eh importante por questoes de desempenho, pois caso
# contrario ele realizara uma consulta ao banco para cada iteracao,
# na key do groupby (uma alternativa é só usar tipo_id, na chave).
qs2 = qs.select_related(attr_tipo).order_by(attr_tipo+'_id')
qs2 = qs.select_related(attr_tipo).order_by(attr_tipo + '_id')
for key, values in groupby(qs2, key=sort_function):
# poderia usar qtdes[key] = len(list(values)) aqui, mas
@ -76,16 +77,16 @@ def num_materias_por_tipo(qs, attr_tipo='tipo'):
def validar_arquivo(arquivo, nome_campo):
if len(arquivo.name) > 200:
raise ValidationError(
"Certifique-se de que o nome do arquivo no " \
"campo '" + nome_campo + "' tenha no máximo 200 caracteres " \
"Certifique-se de que o nome do arquivo no "
"campo '" + nome_campo + "' tenha no máximo 200 caracteres "
"(ele possui {})".format(len(arquivo.name))
)
if arquivo.size > MAX_DOC_UPLOAD_SIZE:
raise ValidationError(
"O arquivo " + nome_campo + " deve ser menor que " \
"O arquivo " + nome_campo + " deve ser menor que "
"{0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb".format(
(MAX_DOC_UPLOAD_SIZE/1024)/1024,
(arquivo.size/1024)/1024
(MAX_DOC_UPLOAD_SIZE / 1024) / 1024,
(arquivo.size / 1024) / 1024
)
)
@ -178,7 +179,9 @@ def montar_row_autor(name):
return autor_row
#TODO: Esta função é utilizada?
# TODO: Esta função é utilizada?
def montar_helper_autor(self):
autor_row = montar_row_autor('nome')
self.helper = SaplFormHelper()
@ -1008,14 +1011,21 @@ def mail_service_configured(request=None):
return settings.EMAIL_RUNNING
def google_recaptcha_configured():
from sapl.base.models import AppConfig
return not AppConfig.attr('google_recaptcha_site_key') == ''
def lista_anexados(principal, isMateriaLegislativa=True):
anexados_total = []
if isMateriaLegislativa: #MateriaLegislativa
if isMateriaLegislativa: # MateriaLegislativa
from sapl.materia.models import Anexada
anexados_iterator = Anexada.objects.filter(materia_principal=principal)
else: #DocAdm
else: # DocAdm
from sapl.protocoloadm.models import Anexado
anexados_iterator = Anexado.objects.filter(documento_principal=principal)
anexados_iterator = Anexado.objects.filter(
documento_principal=principal)
anexadas_temp = list(anexados_iterator)
@ -1024,12 +1034,14 @@ def lista_anexados(principal, isMateriaLegislativa=True):
if isMateriaLegislativa:
if anx.materia_anexada not in anexados_total:
anexados_total.append(anx.materia_anexada)
anexados_anexado = Anexada.objects.filter(materia_principal=anx.materia_anexada)
anexados_anexado = Anexada.objects.filter(
materia_principal=anx.materia_anexada)
anexadas_temp.extend(anexados_anexado)
else:
if anx.documento_anexado not in anexados_total:
anexados_total.append(anx.documento_anexado)
anexados_anexado = Anexado.objects.filter(documento_principal=anx.documento_anexado)
anexados_anexado = Anexado.objects.filter(
documento_principal=anx.documento_anexado)
anexadas_temp.extend(anexados_anexado)
if principal in anexados_total:
anexados_total.remove(principal)
@ -1058,10 +1070,84 @@ class OverwriteStorage(FileSystemStorage):
Muda o comportamento padrão do Django e o faz sobrescrever arquivos de
mesmo nome que foram carregados pelo usuário ao invés de renomeá-los.
'''
def get_available_name(self, name, max_length=None):
if self.exists(name):
os.remove(os.path.join(settings.MEDIA_ROOT, name))
return name
def get_tempfile_dir():
return '/tmp' if platform.system() == 'Darwin' else tempfile.gettempdir()
class GoogleRecapthaMixin:
logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
from sapl.base.models import AppConfig
title_label = kwargs.pop('title_label')
action_label = kwargs.pop('action_label')
row1 = to_row(
[
(Div(
css_class="g-recaptcha float-right", # if not settings.DEBUG else '',
data_sitekey=AppConfig.attr('google_recaptcha_site_key')
), 5),
('email', 7),
]
)
self.helper = FormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(
title_label,
row1
),
actions=form_actions(label=action_label)
)
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
cd = self.cleaned_data
recaptcha = self.data.get('g-recaptcha-response', '')
if not recaptcha:
raise ValidationError(
_('Verificação do reCAPTCHA não efetuada.'))
from sapl.base.models import AppConfig
url = ('https://www.google.com/recaptcha/api/siteverify?'
'secret=%s'
'&response=%s' % (AppConfig.attr('google_recaptcha_secret_key'),
recaptcha))
try:
r = requests.post(url)
if r.ok:
jdata = r.json()
else:
raise ValidationError(
_('Ocorreu um erro na validação do reCAPTCHA.'))
except Exception as e:
logging.error(e)
raise ValidationError(
_('Ocorreu um erro na validação do reCAPTCHA.'))
if jdata['success']:
return cd
else:
raise ValidationError(
_('Ocorreu um erro na validação do reCAPTCHA.'))
return cd

Loading…
Cancel
Save