From 5af1f5ab59f5c3adbda5b30aef3ca5b2ddddb628 Mon Sep 17 00:00:00 2001 From: "tapumar@gmail.com" Date: Tue, 25 Sep 2018 15:13:43 -0100 Subject: [PATCH] Fix #2252 --- sapl/protocoloadm/email_utils.py | 205 ++++++++++++++++++ sapl/protocoloadm/forms.py | 27 ++- .../0008_acompanhamentodocumento.py | 32 +++ sapl/protocoloadm/models.py | 27 +++ sapl/protocoloadm/urls.py | 14 +- sapl/protocoloadm/views.py | 151 ++++++++++++- .../templates/email/acompanhar_documento.html | 25 +++ sapl/templates/email/acompanhar_documento.txt | 16 ++ .../acompanhamento_documento.html | 21 ++ .../documentoadministrativo_filter.html | 3 + 10 files changed, 510 insertions(+), 11 deletions(-) create mode 100644 sapl/protocoloadm/email_utils.py create mode 100644 sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py create mode 100644 sapl/templates/email/acompanhar_documento.html create mode 100644 sapl/templates/email/acompanhar_documento.txt create mode 100644 sapl/templates/protocoloadm/acompanhamento_documento.html diff --git a/sapl/protocoloadm/email_utils.py b/sapl/protocoloadm/email_utils.py new file mode 100644 index 000000000..84ea352b0 --- /dev/null +++ b/sapl/protocoloadm/email_utils.py @@ -0,0 +1,205 @@ +from datetime import datetime as dt + +from django.core.mail import EmailMultiAlternatives, get_connection, send_mail +from django.core.urlresolvers import reverse +from django.template import Context, loader +from django.utils import timezone + +from sapl.base.models import CasaLegislativa +from sapl.settings import EMAIL_SEND_USER + +from .models import AcompanhamentoDocumento + + +def load_email_templates(templates, context={}): + + emails = [] + for t in templates: + tpl = loader.get_template(t) + email = tpl.render(Context(context)) + if t.endswith(".html"): + email = email.replace('\n', '').replace('\r', '') + emails.append(email) + return emails + + +def enviar_emails(sender, recipients, messages): + ''' + Recipients is a string list of email addresses + + Messages is an array of dicts of the form: + {'recipient': 'address', # useless???? + 'subject': 'subject text', + 'txt_message': 'text message', + 'html_message': 'html message' + } + ''' + + if len(messages) == 1: + # sends an email simultaneously to all recipients + send_mail(messages[0]['subject'], + messages[0]['txt_message'], + sender, + recipients, + html_message=messages[0]['html_message'], + fail_silently=False) + + elif len(recipients) > len(messages): + raise ValueError("Message list should have size 1 \ + or equal recipient list size. \ + recipients: %s, messages: %s" % (recipients, messages) + ) + + else: + # sends an email simultaneously to all reciepients + for (d, m) in zip(recipients, messages): + send_mail(m['subject'], + m['txt_message'], + sender, + [d], + html_message=m['html_message'], + fail_silently=False) + + +def criar_email_confirmacao(base_url, casa_legislativa, documento, hash_txt=''): + + if not casa_legislativa: + raise ValueError("Casa Legislativa é obrigatória") + + if not documento: + raise ValueError("Documento é obrigatório") + + # FIXME i18n + casa_nome = (casa_legislativa.nome + ' de ' + + casa_legislativa.municipio + '-' + + casa_legislativa.uf) + + documento_url = reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': documento.id}) + confirmacao_url = reverse('sapl.protocoloadm:acompanhar_confirmar', + kwargs={'pk': documento.id}) + + + templates = load_email_templates(['email/acompanhar.txt', + 'email/acompanhar.html'], + {"casa_legislativa": casa_nome, + "logotipo": casa_legislativa.logotipo, + "descricao_documento": documento.assunto, + "hash_txt": hash_txt, + "base_url": base_url, + "documento": str(documento), + "documento_url": documento_url, + "confirmacao_url": confirmacao_url, }) + return templates + + +def do_envia_email_confirmacao(base_url, casa, documento, destinatario): + # + # Envia email de confirmacao para atualizações de tramitação + # + + sender = EMAIL_SEND_USER + # FIXME i18n + subject = "[SAPL] " + str(documento) + " - Ative o Acompanhamento do Documento" + messages = [] + recipients = [] + + email_texts = criar_email_confirmacao(base_url, + casa, + documento, + destinatario.hash,) + recipients.append(destinatario.email) + messages.append({ + 'recipient': destinatario.email, + 'subject': subject, + 'txt_message': email_texts[0], + 'html_message': email_texts[1] + }) + + enviar_emails(sender, recipients, messages) + + +def criar_email_tramitacao(base_url, casa_legislativa, documento, status, + unidade_destino, hash_txt=''): + + if not casa_legislativa: + raise ValueError("Casa Legislativa é obrigatória") + + if not documento: + raise ValueError("Documento é obrigatória") + + # FIXME i18n + casa_nome = (casa_legislativa.nome + ' de ' + + casa_legislativa.municipio + '-' + + casa_legislativa.uf) + + url_documento = reverse('sapl.documento:tramitacao_list', + kwargs={'pk': documento.id}) + url_excluir = reverse('sapl.documento:acompanhar_excluir', + kwargs={'pk': documento.id}) + + tramitacao = documento.tramitacao_set.last() + + templates = load_email_templates(['email/tramitacao.txt', + 'email/tramitacao.html'], + {"casa_legislativa": casa_nome, + "data_registro": dt.strftime( + timezone.now(), + "%d/%m/%Y"), + "cod_documento": documento.id, + "logotipo": casa_legislativa.logotipo, + "descricao_documento": documento.assunto, + "data": tramitacao.data_tramitacao, + "status": status, + "localizacao": unidade_destino, + "texto_acao": tramitacao.texto, + "hash_txt": hash_txt, + "documento": str(documento), + "base_url": base_url, + "documento_url": url_documento, + "excluir_url": url_excluir}) + return templates + + +def do_envia_email_tramitacao(base_url, documento, status, unidade_destino): + # + # Envia email de tramitacao para usuarios cadastrados + # + destinatarios = AcompanhamentoDocumento.objects.filter(documento=documento, + confirmado=True) + casa = CasaLegislativa.objects.first() + + sender = EMAIL_SEND_USER + # FIXME i18n + subject = "[SAPL] " + str(documento) + \ + " - Acompanhamento de Documento Administrativo" + + connection = get_connection() + connection.open() + + for destinatario in destinatarios: + try: + email_texts = criar_email_tramitacao(base_url, + casa, + documento, + status, + unidade_destino, + destinatario.hash,) + + email = EmailMultiAlternatives( + subject, + email_texts[0], + sender, + [destinatario.email], + connection=connection) + email.attach_alternative(email_texts[1], "text/html") + email.send() + + # Garantia de que, mesmo com o lançamento de qualquer exceção, + # a conexão será fechada + except Exception: + connection.close() + raise Exception( + 'Erro ao enviar e-mail de acompanhamento de documento.') + + connection.close() diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 9985f522f..8c441ad93 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -2,7 +2,7 @@ import django_filters from crispy_forms.bootstrap import InlineRadios from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button, Fieldset, Layout +from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout from django import forms from django.core.exceptions import (MultipleObjectsReturned, ObjectDoesNotExist, ValidationError) @@ -19,7 +19,8 @@ from sapl.materia.models import (MateriaLegislativa, TipoMateriaLegislativa, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, AnoNumeroOrderingFilter, RangeWidgetOverride, autor_label, autor_modal) -from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, +from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, + DocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) @@ -39,6 +40,28 @@ EM_TRAMITACAO = [('', '---------'), (0, 'Sim'), (1, 'Não')] +class AcompanhamentoDocumentoForm(ModelForm): + + class Meta: + model = AcompanhamentoDocumento + fields = ['email'] + + def __init__(self, *args, **kwargs): + + row1 = to_row([('email', 10)]) + + row1.append( + Column(form_actions(label='Cadastrar'), css_class='col-md-2') + ) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + _('Acompanhamento de Documento por e-mail'), row1 + ) + ) + super(AcompanhamentoDocumentoForm, self).__init__(*args, **kwargs) + class ProtocoloFilterSet(django_filters.FilterSet): diff --git a/sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py b/sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py new file mode 100644 index 000000000..cd71df05c --- /dev/null +++ b/sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-09-25 15:31 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0007_auto_20180924_1724'), + ] + + operations = [ + migrations.CreateModel( + name='AcompanhamentoDocumento', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('usuario', models.CharField(max_length=50)), + ('email', models.EmailField(max_length=100, verbose_name='E-mail')), + ('data_cadastro', models.DateField(auto_now_add=True)), + ('hash', models.CharField(max_length=8)), + ('confirmado', models.BooleanField(default=False)), + ('documento', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='protocoloadm.DocumentoAdministrativo')), + ], + options={ + 'verbose_name_plural': 'Acompanhamentos de Documento', + 'verbose_name': 'Acompanhamento de Documento', + }, + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 52bd9ad22..908aac542 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -298,3 +298,30 @@ class TramitacaoAdministrativo(models.Model): return _('%(documento)s - %(status)s') % { 'documento': self.documento, 'status': self.status } + +@reversion.register() +class AcompanhamentoDocumento(models.Model): + usuario = models.CharField(max_length=50) + documento = models.ForeignKey(DocumentoAdministrativo, on_delete=models.CASCADE) + email = models.EmailField( + max_length=100, verbose_name=_('E-mail')) + data_cadastro = models.DateField(auto_now_add=True) + hash = models.CharField(max_length=8) + confirmado = models.BooleanField(default=False) + + class Meta: + verbose_name = _('Acompanhamento de Documento') + verbose_name_plural = _('Acompanhamentos de Documento') + + def __str__(self): + if self.data_cadastro is None: + return _('%(documento)s - %(email)s') % { + 'documento': self.documento, + 'email': self.email + } + else: + return _('%(documento)s - %(email)s - Registrado em: %(data)s') % { + 'documento': self.documento, + 'email': self.email, + 'data': str(self.data_cadastro.strftime('%d/%m/%Y')) + } diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index 8ed9216f2..67d5edb65 100644 --- a/sapl/protocoloadm/urls.py +++ b/sapl/protocoloadm/urls.py @@ -1,6 +1,9 @@ from django.conf.urls import include, url -from sapl.protocoloadm.views import (AnularProtocoloAdmView, +from sapl.protocoloadm.views import (AcompanhamentoDocumentoView, + AcompanhamentoConfirmarView, + AcompanhamentoExcluirView, + AnularProtocoloAdmView, ComprovanteProtocoloView, CriarDocumentoProtocolo, DocumentoAcessorioAdministrativoCrud, @@ -56,6 +59,15 @@ urlpatterns_protocolo = [ url(r'^protocoloadm/(?P\d+)/protocolo-mostrar$', ProtocoloMostrarView.as_view(), name='protocolo_mostrar'), + url(r'^docadm/(?P\d+)/acompanhar-documento/$', + AcompanhamentoDocumentoView.as_view(), name='acompanhar_documento'), + url(r'^docadm/(?P\d+)/acompanhar-confirmar$', + AcompanhamentoConfirmarView.as_view(), + name='acompanhar_confirmar'), + url(r'^docadm/(?P\d+)/acompanhar-excluir$', + AcompanhamentoExcluirView.as_view(), + name='acompanhar_excluir'), + url(r'^protocoloadm/(?P\d+)/continuar$', diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index d58b1e11b..8933c502f 100644 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -1,3 +1,6 @@ +from datetime import datetime +from random import choice +from string import ascii_letters, digits from braces.views import FormValidMessageMixin from django.contrib import messages @@ -18,24 +21,27 @@ from django.views.generic.edit import FormView from django_filters.views import FilterView import sapl -from sapl.base.models import Autor +from sapl.base.models import Autor, CasaLegislativa from sapl.comissoes.models import Comissao from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Legislatura, Parlamentar from sapl.protocoloadm.models import Protocolo -from sapl.utils import (create_barcode, get_client_ip, +from sapl.utils import (create_barcode, get_base_url, get_client_ip, get_mime_type_from_file_extension, show_results_filter_set) - -from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm, +from .email_utils import do_envia_email_confirmacao +from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm, + DocumentoAcessorioAdministrativoForm, DocumentoAdministrativoFilterSet, DocumentoAdministrativoForm, ProtocoloDocumentForm, ProtocoloFilterSet, ProtocoloMateriaForm, - TramitacaoAdmEditForm, TramitacaoAdmForm, DesvincularDocumentoForm, DesvincularMateriaForm, - filtra_tramitacao_adm_destino_and_status, filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status) -from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, - StatusTramitacaoAdministrativo, + TramitacaoAdmEditForm, TramitacaoAdmForm, + DesvincularDocumentoForm, DesvincularMateriaForm, + filtra_tramitacao_adm_destino_and_status, + filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status) +from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, + DocumentoAdministrativo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) TipoDocumentoAdministrativoCrud = CrudAux.build( @@ -89,6 +95,135 @@ def doc_texto_integral(request, pk): return response raise Http404 +class AcompanhamentoConfirmarView(TemplateView): + + def get_redirect_url(self, email): + msg = _('Este documento está sendo acompanhado pelo e-mail: %s') % ( + email) + messages.add_message(self.request, messages.SUCCESS, msg) + return reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': self.kwargs['pk']}) + + def get(self, request, *args, **kwargs): + documento_id = kwargs['pk'] + hash_txt = request.GET.get('hash_txt', '') + + try: + acompanhar = AcompanhamentoDocumento.objects.get( + documento_id=documento_id, + hash=hash_txt) + except ObjectDoesNotExist: + raise Http404() + # except MultipleObjectsReturned: + # A melhor solução deve ser permitir que a exceção + # (MultipleObjectsReturned) seja lançada e vá para o log, + # pois só poderá ser causada por um erro de desenvolvimente + + acompanhar.confirmado = True + acompanhar.save() + + return HttpResponseRedirect(self.get_redirect_url(acompanhar.email)) + + +class AcompanhamentoExcluirView(TemplateView): + + def get_success_url(self): + msg = _('Você parou de acompanhar este Documento.') + messages.add_message(self.request, messages.INFO, msg) + return reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': self.kwargs['pk']}) + + def get(self, request, *args, **kwargs): + materia_id = kwargs['pk'] + hash_txt = request.GET.get('hash_txt', '') + + try: + AcompanhamentoDocumento.objects.get(documento_id=documento_id, + hash=hash_txt).delete() + except ObjectDoesNotExist: + pass + + return HttpResponseRedirect(self.get_success_url()) + + +class AcompanhamentoDocumentoView(CreateView): + template_name = "protocoloadm/acompanhamento_documento.html" + + def get_random_chars(self): + s = ascii_letters + digits + return ''.join(choice(s) for i in range(choice([6, 7]))) + + def get(self, request, *args, **kwargs): + pk = self.kwargs['pk'] + documento = DocumentoAdministrativo.objects.get(id=pk) + + return self.render_to_response( + {'form': AcompanhamentoDocumentoForm(), + 'documento': documento}) + + def post(self, request, *args, **kwargs): + form = AcompanhamentoDocumentoForm(request.POST) + pk = self.kwargs['pk'] + documento = DocumentoAdministrativo.objects.get(id=pk) + + if form.is_valid(): + email = form.cleaned_data['email'] + usuario = request.user + + hash_txt = self.get_random_chars() + + acompanhar = AcompanhamentoDocumento.objects.get_or_create( + documento=documento, + email=form.data['email']) + + # Se o segundo elemento do retorno do get_or_create for True + # quer dizer que o elemento não existia + if acompanhar[1]: + acompanhar = acompanhar[0] + acompanhar.hash = hash_txt + acompanhar.usuario = usuario.username + acompanhar.confirmado = False + acompanhar.save() + + base_url = get_base_url(request) + + destinatario = AcompanhamentoDocumento.objects.get( + documento=documento, + email=email, + confirmado=False) + casa = CasaLegislativa.objects.first() + + do_envia_email_confirmacao(base_url, + casa, + documento, + destinatario) + + msg = _('Foi enviado um e-mail de confirmação. Confira sua caixa \ + de mensagens e clique no link que nós enviamos para \ + confirmar o acompanhamento deste documento.') + messages.add_message(request, messages.SUCCESS, msg) + + # Caso esse Acompanhamento já exista + # avisa ao usuário que esse documento já está sendo acompanhado + else: + msg = _('Este e-mail já está acompanhando esse documento.') + messages.add_message(request, messages.INFO, msg) + + return self.render_to_response( + {'form': form, + 'documento': documento, + 'error': _('Esse documento já está\ + sendo acompanhada por este e-mail.')}) + return HttpResponseRedirect(self.get_success_url()) + else: + return self.render_to_response( + {'form': form, + 'documento': documento}) + + def get_success_url(self): + return reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': self.kwargs['pk']}) + class DocumentoAdministrativoMixin: diff --git a/sapl/templates/email/acompanhar_documento.html b/sapl/templates/email/acompanhar_documento.html new file mode 100644 index 000000000..f90558568 --- /dev/null +++ b/sapl/templates/email/acompanhar_documento.html @@ -0,0 +1,25 @@ +{% load i18n %} +{% load static %} + + +

{{casa_legislativa}} +
+ Sistema de Apoio ao Processo Legislativo +

+ +

Registramos seu pedido para acompanhamento por e-mail do documento administrativo identificado a seguir:

+{{documento}} - {{descricao_documento}}
+{{assunto}}
+ +

+

Para garantia de sua privacidade, solicitamos que ative o recebimento das futuras mensagens clicando no link:

+ +

+ {{base_url}}{{confirmacao_url}}?hash_txt={{hash_txt}} +

+
+
+

Caso não tenha realizado o cadastramento em nosso sistema, favor desconsiderar a presente mensagem
+Esta é uma mensagem automática. Por favor, não responda.

+ + diff --git a/sapl/templates/email/acompanhar_documento.txt b/sapl/templates/email/acompanhar_documento.txt new file mode 100644 index 000000000..d4d04fbd7 --- /dev/null +++ b/sapl/templates/email/acompanhar_documento.txt @@ -0,0 +1,16 @@ +{{casa_legislativa}} + +Sistema de Apoio ao Processo Legislativo + +>Registramos seu pedido para acompanhamento por e-mail do documento administrativo identificad a seguir: + +{{base_url}}{{documento_url}} - {{documento}} - {{descricao_documento}} + +{{assunto}} + +Para garantia de sua privacidade, solicitamos que ative o recebimento das futuras mensagens acessando no link: + +{{base_url}}{{url_confirmar}}?hash_txt={{hash_txt}} + +Caso não tenha realizado o cadastramento em nosso sistema, favor desconsiderar a presente mensagem +Esta é uma mensagem automática. Por favor, não responda. diff --git a/sapl/templates/protocoloadm/acompanhamento_documento.html b/sapl/templates/protocoloadm/acompanhamento_documento.html new file mode 100644 index 000000000..b6526cc52 --- /dev/null +++ b/sapl/templates/protocoloadm/acompanhamento_documento.html @@ -0,0 +1,21 @@ +{% extends "crud/detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% block actions %} {% endblock %} +{% block detail_content %} + +

Acompanhamento de Documento

+
+
+
Tipo: {{documento.tipo.sigla}} - {{documento.tipo.descricao}}
+
Número: {{documento.numero}}
+
Ano: {{documento.ano}}
+ +
+
+
Assunto: {{documento.assunto|safe}}
+
+ +{% if error %}
{{ error }}
{% endif %} +{% crispy form %} +{% endblock %} diff --git a/sapl/templates/protocoloadm/documentoadministrativo_filter.html b/sapl/templates/protocoloadm/documentoadministrativo_filter.html index e87962273..6dbd95276 100644 --- a/sapl/templates/protocoloadm/documentoadministrativo_filter.html +++ b/sapl/templates/protocoloadm/documentoadministrativo_filter.html @@ -63,6 +63,9 @@ {% if d.texto_integral %} Texto Integral
{% endif %} + {% if d.tramitacao %} + Acompanhar Documento + {% endif %}