Repository: interlegis/sapl

Issue:
Ter no SAPL a opção para cada casa adicionar documentos particionados ou sequencial na parte de documentos acessórios
<!--- Forneça um resumo geral da _issue_ no título acima -->

Hoje ao adicionar um [documento acessório] na parte de matérias legislativas, o usuário deve adicionar cada NOVO documento gerado em relação àquela matéria, o usuário externo verá uma lista de documento para baixar um de cada vez, solicitamos uma NOVA opção de cadastro de documentos acessórios, aonde esses novos documentos que fossem inseridos  o SAPL colocasse isso apenas em PDF único, aonde o usuário externo pudesse baixar apenas uma único arquivo contendo todos os documentos relacionados aquela matéria.

Além disso o usuário que fosse cadastrar por exemplo documentos acessórios de uma determinada matéria, não precisaria digitalizar UM DOCUMENTO POR VEZ, pois dá forma que está hoje fica muito moroso esse processo. 

## Comportamento Esperado
<!--- Se você está descrevendo um _bug_, conte-nos o que deveria acontecer. -->
<!--- Se você está sugerindo uma mudança/melhoria, conte-nos como deve funcionar. -->
Qualquer documento acessório gerado e cadastrado seria juntando a um único ARQUIVO (PDF) único daquela matéria.

## Comportamento Atual
<!--- Se está descrevendo um bug, conte-nos o que acontece em vez do comportamento esperado. -->
<!--- Se está sugerindo uma mudança/melhoria, explique a diferença com o comportamento atual. -->Hoje o usuário cadastra arquivo por arquivo, tendo que especificar que tipo de arquivo é, esse processo é muito moroso.

## Possível Solução
<!--- Não é obrigatório, mas sugira uma possível correção/razão para o bug -->
<!--- ou ideias de como implementar a adição/mudança. -->

## Passos para Reproduzir (para bugs)
<!--- Forneça um link para um exemplo, ou um conjunto de passos inequívocos -->
<!--- para reproduzir esse bug. Inclua código para reproduzir, se relevante. -->
1.
2.
3.
4.

## Contexto
<!--- Como esse problema o afeta? O que você está tentando realizar? -->
<!--- Fornecer o contexto nos ajuda a encontrar uma solução que seja mais útil no mundo real -->

## Imagens do Ocorrido
<!--- Representação visual em vídeo ou imagem do ocorrido -->
<!--- Se está descrevendo um bug poste imagens ou vídeos na reprodução do bug citado, caso se aplique -->

## Seu Ambiente
<!--- Inclua detalhes relevantes sobre o ambiente em que você presenciou/experienciou o bug. -->
* Versão usada (_Release_):
* Nome e versão do navegador:
* Nome e versão do Sistema Operacional (desktop ou mobile):
* Link para o seu projeto (Caso de fork deste projeto):



Discussion:
Ao digitalizar os documentos vocês podem agrupar tudo em um documento só e cadastrar de uma única vez. Existem ferramentas que tanto digitalizar vários documentos como um só quanto ferramentas que permitem agrupar um conjunto de PDFs já digitalizados em um único arquivo. Não existe necessidade do SAPL fazer esse tipo de operação quando existem ferramentas especializadas avulsas pra isso.
Vc não entendeu, o problema é no momento do cadastro, o usuário deve
escolher os TIPOS DE DOCUMENTOS, UM POR UM, é aqui o problema, deveria ter
uma opção para cadastrar o processo de forma ÚNICO.

Em qua., 18 de mar. de 2020 às 16:04, Edward <notifications@github.com>
escreveu:

> Ao digitalizar os documentos vocês podem agrupar tudo em um documento só e
> cadastrar de uma única vez. Existem ferramentas que tanto digitalizar
> vários documentos como um só quanto ferramentas que permitem agrupar um
> conjunto de PDFs já digitalizados em um único arquivo. Não existe
> necessidade do SAPL fazer esse tipo de operação quando existem ferramentas
> especializadas avulsas pra isso.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/interlegis/sapl/issues/3127#issuecomment-600807903>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AASPXD4IHYFBAR4TYPQJUX3RIELLLANCNFSM4LMU2HCQ>
> .
>


-- 
São Paulo Realmente é aquilo que

Lamento, mas cadastrar o processo de forma ÚNICO está totalmente fora de cogitação.
@edwardoliveira pq vc fechou ? essa funcionalidade é importante para nós, posso refazer ela explicando com mais detalhes ?
> Lamento, mas cadastrar o processo de forma ÚNICO está totalmente fora de cogitação.

Não é cadastrar um processo de uma forma ÚNICO, não falei isso, mas o usuário pode cadastrar vários arquivos , em um dia ele cadastrar 10 arquivos por exemplo, em outro dias ele cadastrar 5, no final esses arquivo estarão em um único arquivo no SAPL, em vez de está particionado como é hoje
> Não é cadastrar um processo de uma forma ÚNICO, não falei isso

Isso foi exatamente o que vc falou! Olha o screenshot abaixo da tua resposta.

<img width="663" alt="Screen Shot 2020-04-15 at 14 27 17" src="https://user-images.githubusercontent.com/9326037/79368097-3f418f00-7f25-11ea-9d7f-d83360bbb354.png">

> Não é cadastrar um processo de uma forma ÚNICO, não falei isso, mas o usuário pode cadastrar vários arquivos , em um dia ele cadastrar 10 arquivos por exemplo, em outro dias ele cadastrar 5, no final esses arquivo estarão em um único arquivo no SAPL, em vez de está particionado como é hoje

~~Eu posso reabrir a issue sem problemas, pronto. __Mas que fique claro infelizmente não temos como e nem vamos implementar essa funcionalidade num futuro próximo__, pois isso implica incorrer em uma série de potenciais problemas de segurança e integridade do arquivo final. Se essa funcionalidade é importante para vcs eu sugiro que __vcs se comprometam em desenvolvê-la__, visto que o SAPL é software livre e precisamos de colaborações de código também. Caso vcs implementem teremos total prazer em testar e validar a solução. :)~~

Atualização: em vista dos esclarecimentos abaixo, favor desconsiderar o texto acima.
Me expressei errado, isso acontece, no final da frase quis dizer o
seguinte: é aqui o problema, deveria ter uma opção quem embora eu faça o
cadastro particionado do arquivos do processo, mas que no final no SAPL ele
fosse oferecido para  download por exemplo em um ÚNICO ARQUIVO,

 Exemplo como funciona hoje:

ANEXA VÁRIOS ARQUIVOS EM DOCUMENTOS ACESSÓRIOS E SE EU QUERO FAZER
DOWNLOADS FAÇO DOWNLOADS DESSES VÁRIOS ARQUIVO ASSIM  COMO FOI INSERIDO
TIPO:                N (anexos) -------> N (baixa vários)


Exemplo do outro modelo de funcionamento:

Anexa vários arquivo em documentos acessórios assim como no modelo
anterior, porém ao baixar no SAPL tudo que foi anexado estaria em um único
arquivo

N (anexos)  --------> 1 (baixa um ÚNICO ARQUIVO)



Em qua., 15 de abr. de 2020 às 14:28, Edward <notifications@github.com>
escreveu:

> Não é cadastrar um processo de uma forma ÚNICO, não falei isso
>
> Isso foi exatamente o que vc falou! Olha o screenshot abaixo da tua
> resposta.
>
> [image: Screen Shot 2020-04-15 at 14 27 17]
> <https://user-images.githubusercontent.com/9326037/79368097-3f418f00-7f25-11ea-9d7f-d83360bbb354.png>
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/interlegis/sapl/issues/3127#issuecomment-614174060>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AASPXD5WQRDSTUXHMOI7QXTRMXVDJANCNFSM4LMU2HCQ>
> .
>


-- 
São Paulo Realmente é aquilo que

Todo esse tempo vc quis dizer isso?! O texto desta issue está meio confuso, mistura várias coisas (cadastro, download, etc), a exceção deste último comentário. Para deixar bem claro:

1) Isso que vc acabou de explicar é factível desde que a gente ainda preserve os arquivos originais. Ainda vão existir os arquivos individuais, que o usuário pode baixar um por um, mas **também** podemos disponibilizar um link onde o usuário -- logado ou não -- possar baixar um único arquivo com todos os documentos em um único PDF. A gente aqui (mais de uma pessoa) tinha entendido isso na primeira mensagem que abre essa issue, e mais algo além totalmente fora do que podemos/devemos fazer (a parte 2).

2) O problema dessa issue, que ficou muito confuso, foi isso aqui:
> Além disso o usuário que fosse cadastrar por exemplo documentos acessórios de uma determinada matéria, não precisaria digitalizar UM DOCUMENTO POR VEZ, pois dá forma que está hoje fica muito moroso esse processo.

Quando falei de algo fora de cogitação foi exatamente essa parte 2), que dá a entender que seriam **cadastrados** mais de um arquivo de uma única vez, em um único formulário, e anexado tudo em um único arquivo PDF logo no cadastro. Isso não tem como fazermos. MAS cadastrar todos os anexos da forma como é hoje, e depois baixar tudo (em um único PDF ou então como um ZIP, por exemplo) é OK.

Reiterando, o texto dessa issue ficou bem confuso, mas o que vc explicou agora é exequível. Iremos trabalhar nisso. 




Resolving patch:
diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py
--- a/sapl/materia/urls.py
+++ b/sapl/materia/urls.py
@@ -27,7 +27,7 @@
                                 proposicao_texto, recuperar_materia,
                                 ExcluirTramitacaoEmLoteView, RetornarProposicao,
                                 MateriaPesquisaSimplesView,
-                                DespachoInicialMultiCreateView)
+                                DespachoInicialMultiCreateView, get_zip_docacessorios, get_pdf_docacessorios)
 from sapl.norma.views import NormaPesquisaSimplesView
 from sapl.protocoloadm.views import (
     FichaPesquisaAdmView, FichaSelecionaAdmView)
@@ -118,6 +118,10 @@
         name='tramitacao_em_lote'),
     url(r'^materia/excluir-tramitacao-em-lote', ExcluirTramitacaoEmLoteView.as_view(),
         name='excluir_tramitacao_em_lote'),
+    url(r'^materia/docacessorio/zip/(?P<pk>\d+)$', get_zip_docacessorios,
+        name='compress_docacessorios'),
+    url(r'^materia/docacessorio/pdf/(?P<pk>\d+)$', get_pdf_docacessorios,
+        name='merge_docacessorios')
 ]
 
 
diff --git a/sapl/materia/views.py b/sapl/materia/views.py
--- a/sapl/materia/views.py
+++ b/sapl/materia/views.py
@@ -6,11 +6,15 @@
 import shutil
 import tempfile
 import weasyprint
+import time
 
 from crispy_forms.layout import HTML
 from datetime import datetime
 from random import choice
 from string import ascii_letters, digits
+from datetime import datetime
+from PyPDF4 import PdfFileReader, PdfFileMerger
+import zipfile
 
 from django.conf import settings
 from django.contrib import messages
@@ -52,7 +56,7 @@
 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)
+                        show_results_filter_set, YES_NO_CHOICES,get_tempfile_dir)
 
 from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
                     AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet,
@@ -2725,3 +2729,127 @@ def form_valid(self, form):
                 self.object.save()
 
             return fv
+
+
+def create_zip_docacessorios(materia):
+    logger = logging.getLogger(__name__)
+    docs = materia.documentoacessorio_set.\
+        all().values_list('arquivo', flat=True)    
+    if not docs:
+        return None, None
+
+    docs_path = [os.path.join(MEDIA_ROOT, i) for i in docs]
+
+    if not docs_path:
+        raise FileNotFoundError("Não há arquivos PDF cadastrados em documentos acessorios.")
+    logger.info("Gerando compilado PDF de documentos acessorios com {} documentos".format(docs_path))
+    zipfilename = '{}/mat_{}_{}_docacessorios.zip'.format(
+        get_tempfile_dir(),
+        materia.pk,
+        time.mktime(datetime.now().timetuple()))
+    with zipfile.ZipFile(zipfilename, 'w', zipfile.ZIP_DEFLATED) as zipf:
+        for f in docs_path:
+            zipf.write(f, f.split(os.sep)[-1])
+
+    external_name = "mat_{}_{}_docacessorios.zip".format(materia.numero, materia.ano)
+    return external_name, zipfilename
+
+
+def get_zip_docacessorios(request, pk):
+    logger = logging.getLogger(__name__)
+    username = request.user.username
+    materia = get_object_or_404(MateriaLegislativa, pk=pk)
+    try:
+        external_name, zipfilename = create_zip_docacessorios(materia)
+        logger.info("user= {}. Gerou o zip compilado de documento acessorios")
+    except FileNotFoundError:
+        logger.error("user= {}.Não há arquivos cadastrados".format(username))
+        msg=_('Não há arquivos cadastrados nesses documentos acessórios.')
+        messages.add_message(request, messages.ERROR, msg)
+        return redirect(reverse('sapl.materia:documentoacessorio_list',
+                                kwargs={'pk': pk}))
+    except Exception as e:
+        logger.error("user={}. Um erro inesperado ocorreu na criação do pdf de documentos acessorios: {}"
+                    .format(username,str(e)))
+        msg=_('Um erro inesperado ocorreu. Entre em contato com o suporte do SAPL.')
+        messages.add_message(request, messages.ERROR, msg)
+        return redirect(reverse('sapl.materia:documentoacessorio_list',
+                                kwargs={'pk': pk}))
+
+    if not zipfilename:
+        msg=_('Não há nenhum documento acessório cadastrado.')
+        messages.add_message(request, messages.ERROR, msg)
+        return redirect(reverse('sapl.materia:documentoacessorio_list',
+                                kwargs={'pk': pk}))
+
+    with open(os.path.join(get_tempfile_dir(), zipfilename), 'rb') as f:
+        data = f.read()
+    response = HttpResponse(data, content_type='application/zip')
+    response['Content-Disposition'] = ('attachment; filename="%s"'
+                                       % external_name)
+    return response
+
+
+def create_pdf_docacessorios(materia):
+    logger = logging.getLogger(__name__)
+    docs = materia.documentoacessorio_set. \
+        all().values_list('arquivo', flat=True)
+    if not docs:
+        return None, None
+
+    # TODO: o for-comprehension abaixo filtra os arquivos não PDF.
+    # TODO: o que fazer com os arquivos não PDF? converter? ignorar?
+    docs_path = [os.path.join(MEDIA_ROOT, i) for i in docs if i.lower().endswith('pdf')]
+    if not docs_path:
+        raise FileNotFoundError("Não há arquivos PDF cadastrados em documentos acessorios.")
+    logger.info("Gerando compilado PDF de documentos acessorios com {} documentos"
+                .format(docs_path))
+    merged_pdf = '{}/mat_{}_{}_docacessorios.pdf'.format(
+        get_tempfile_dir(),
+        materia.pk,
+        time.mktime(datetime.now().timetuple()))
+
+    merger = PdfFileMerger()
+    for f in docs_path:
+        merger.append(fileobj=f)
+    merger.write(fileobj=open(merged_pdf, "wb"))
+    merger.close()
+
+    external_name = "mat_{}_{}_docacessorios.pdf".format(materia.numero, materia.ano)
+    return external_name, merged_pdf
+
+
+def get_pdf_docacessorios(request, pk):
+    materia = get_object_or_404(MateriaLegislativa, pk=pk)
+    logger = logging.getLogger(__name__)
+    username = request.user.username
+    try:
+        external_name, pdffilename = create_pdf_docacessorios(materia)
+        logger.info("user= {}. Gerou o pdf compilado de documento acessorios")
+    except FileNotFoundError:
+        logger.error("user= {}.Não há arquivos cadastrados".format(username))
+        msg=_('Não há arquivos cadastrados nesses documentos acessórios.')
+        messages.add_message(request, messages.ERROR, msg)
+        return redirect(reverse('sapl.materia:documentoacessorio_list',
+                                kwargs={'pk': pk}))
+    except Exception as e:
+        logger.error("user= {}.Um erro inesperado ocorreu na criação do pdf de documentos acessorios: {}"
+                    .format(username,str(e)))
+        msg=_('Um erro inesperado ocorreu. Entre em contato com o suporte do SAPL.')
+        messages.add_message(request, messages.ERROR, msg)
+        return redirect(reverse('sapl.materia:documentoacessorio_list',
+                                kwargs={'pk': pk}))
+
+    if not pdffilename:
+        msg=_('Não há nenhum documento acessório PDF cadastrado.')
+        messages.add_message(request, messages.ERROR, msg)
+        return redirect(reverse('sapl.materia:documentoacessorio_list',
+                                kwargs={'pk': pk}))
+
+    with open(os.path.join(get_tempfile_dir(), pdffilename), 'rb') as f:
+        data = f.read()
+    response = HttpResponse(data, content_type='application/pdf')
+    response['Content-Disposition'] = ('attachment; filename="%s"'
+                                       % external_name)
+    return response
+
diff --git a/sapl/utils.py b/sapl/utils.py
--- a/sapl/utils.py
+++ b/sapl/utils.py
@@ -7,7 +7,8 @@
 import os
 import re
 import unicodedata
-
+import platform
+import tempfile
 from crispy_forms.layout import Button, HTML
 from easy_thumbnails import source_generators
 from floppyforms import ClearableFileInput
@@ -1061,3 +1062,6 @@ 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()


Test patch: