diff --git a/sapl/materia/migrations/0090_add_assinatura_digital_documentoacessorio.py b/sapl/materia/migrations/0090_add_assinatura_digital_documentoacessorio.py new file mode 100644 index 000000000..ba99f4825 --- /dev/null +++ b/sapl/materia/migrations/0090_add_assinatura_digital_documentoacessorio.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.28 on 2026-02-11 18:43 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import sapl.materia.models +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('materia', '0089_aumenta_justificativa_devolucao_750'), + ] + + operations = [ + migrations.AddField( + model_name='documentoacessorio', + name='assinado_em', + field=models.DateTimeField(blank=True, null=True, verbose_name='Data/Hora da Assinatura'), + ), + migrations.AddField( + model_name='documentoacessorio', + name='assinado_por', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='docacessorios_assinados', to=settings.AUTH_USER_MODEL, verbose_name='Assinado por'), + ), + migrations.AddField( + model_name='documentoacessorio', + name='assinatura_info', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Metadados do certificado digital usado na assinatura', null=True, verbose_name='Informações da Assinatura'), + ), + migrations.AddField( + model_name='documentoacessorio', + name='pdf_assinado', + field=models.FileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.anexo_upload_path, verbose_name='PDF Assinado'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 829da47b3..4add98c80 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -590,6 +590,35 @@ class DocumentoAcessorio(models.Model): data_ultima_atualizacao = models.DateTimeField( blank=True, null=True, auto_now=True, verbose_name=_('Data')) + # Campos para assinatura digital + pdf_assinado = models.FileField( + max_length=300, + upload_to=anexo_upload_path, + blank=True, + null=True, + verbose_name=_('PDF Assinado'), + storage=OverwriteStorage() + ) + assinatura_info = JSONField( + blank=True, + null=True, + verbose_name=_('Informações da Assinatura'), + help_text=_('Metadados do certificado digital usado na assinatura') + ) + assinado_em = models.DateTimeField( + blank=True, + null=True, + verbose_name=_('Data/Hora da Assinatura') + ) + assinado_por = models.ForeignKey( + get_settings_auth_user_model(), + verbose_name=_('Assinado por'), + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='docacessorios_assinados' + ) + class Meta: verbose_name = _('Documento Acessório') verbose_name_plural = _('Documentos Acessórios') diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index a90fb7398..e51f196f3 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -48,7 +48,9 @@ from sapl.materia.onlyoffice_materia_views import ( from sapl.materia.views_assinatura import ( materia_assinar_a1, materia_assinar_a3_preparar, materia_assinar_a3_finalizar, materia_pdf_assinado, materia_verificar_assinatura, materia_remover_assinatura, - detectar_aplicacao_a3 + detectar_aplicacao_a3, + docacessorio_assinar_a1, docacessorio_pdf_assinado, + docacessorio_verificar_assinatura, docacessorio_remover_assinatura ) from sapl.norma.views import NormaPesquisaSimplesView from sapl.protocoloadm.views import ( @@ -191,6 +193,16 @@ urlpatterns_materia = [ name='docacessorio_check_doc'), url(r'^materia/documentoacessorio/(?P\d+)/forcesave$', docacessorio_forcesave, name='docacessorio_forcesave'), + + # Assinatura Digital de Documento Acessório + url(r'^materia/documentoacessorio/(?P\d+)/assinar/a1/$', docacessorio_assinar_a1, + name='docacessorio_assinar_a1'), + url(r'^materia/documentoacessorio/(?P\d+)/pdf-assinado/$', docacessorio_pdf_assinado, + name='docacessorio_pdf_assinado'), + url(r'^materia/documentoacessorio/(?P\d+)/verificar-assinatura/$', docacessorio_verificar_assinatura, + name='docacessorio_verificar_assinatura'), + url(r'^materia/documentoacessorio/(?P\d+)/remover-assinatura/$', docacessorio_remover_assinatura, + name='docacessorio_remover_assinatura'), ] diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 3f68fa312..65c1deb97 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -1759,6 +1759,19 @@ class DocumentoAcessorioCrud(MasterDetailCrud): class DetailView(MasterDetailCrud.DetailView): template_name = "materia/documentoacessorio_detail.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + is_autor = False + if self.request.user.is_authenticated: + materia = self.object.materia + for autoria in materia.autoria_set.all(): + if autoria.autor.operadores.filter( + id=self.request.user.id).exists(): + is_autor = True + break + context['is_autor'] = is_autor + return context + class ListView(MasterDetailCrud.ListView): def hook_arquivo(self, obj, default, url): diff --git a/sapl/materia/views_assinatura.py b/sapl/materia/views_assinatura.py index c07686620..2ab8fc0ab 100644 --- a/sapl/materia/views_assinatura.py +++ b/sapl/materia/views_assinatura.py @@ -20,7 +20,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from sapl.base.models import AppConfig -from sapl.materia.models import MateriaLegislativa +from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa logger = logging.getLogger(__name__) @@ -808,3 +808,545 @@ def detectar_aplicacao_a3(request): 'portas_conhecidas': portas_conhecidas, 'instrucoes': 'O frontend deve tentar conectar a cada porta para detectar a aplicação.' }) + + +# ============================================================================= +# Views de Assinatura Digital para Documento Acessório +# ============================================================================= + +def _gerar_pdf_do_docacessorio(docacessorio, request): + """ + Gera o PDF do documento acessório para assinatura. + Se já é PDF, retorna direto. Se é DOCX, converte via OnlyOffice. + Retorna (bytes, None) ou (None, erro). + """ + import requests as http_requests + import xml.etree.ElementTree as ET + from django.urls import reverse + + if not docacessorio.arquivo: + return None, "Documento acessório não possui arquivo." + + file_name = docacessorio.arquivo.name.lower() + + # Se já é PDF, retorna diretamente + if file_name.endswith('.pdf'): + try: + with open(docacessorio.arquivo.path, 'rb') as f: + return f.read(), None + except Exception as e: + logger.error(f"Erro ao ler arquivo PDF: {e}") + return None, f"Erro ao ler o arquivo PDF: {e}" + + # Converter DOCX para PDF via OnlyOffice + from sapl.materia.onlyoffice_materia_views import generate_file_key + + download_url = request.build_absolute_uri( + reverse('sapl.materia:docacessorio_onlyoffice_download', kwargs={'pk': docacessorio.pk}) + ) + + conversion_url = f'{settings.ONLYOFFICE_URL}/ConvertService.ashx' + + conversion_data = { + "async": False, + "filetype": "docx", + "key": generate_file_key("docacessorio_sign", docacessorio.pk, request.user.pk), + "outputtype": "pdf", + "title": f"DocAcessorio_{docacessorio.pk}.pdf", + "url": download_url, + } + + if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None): + import jwt + token = jwt.encode(conversion_data, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + conversion_data['token'] = token + + try: + headers = {'Content-Type': 'application/json'} + + if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None): + import jwt + header_token = jwt.encode({"payload": conversion_data}, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + headers['Authorization'] = f'Bearer {header_token}' + + conversion_response = http_requests.post( + conversion_url, + json=conversion_data, + headers=headers, + timeout=60 + ) + + if conversion_response.status_code != 200: + return None, f"Erro na conversão OnlyOffice: status={conversion_response.status_code}" + + root = ET.fromstring(conversion_response.text) + + error_elem = root.find('Error') + if error_elem is not None: + return None, f"Erro na conversão do documento: código {error_elem.text}" + + file_url_elem = root.find('FileUrl') + if file_url_elem is None or not file_url_elem.text: + return None, "URL do PDF não retornada pelo OnlyOffice" + + pdf_url = file_url_elem.text + pdf_response = http_requests.get(pdf_url, timeout=60) + + if pdf_response.status_code != 200: + return None, f"Erro ao baixar PDF convertido: status={pdf_response.status_code}" + + return pdf_response.content, None + + except Exception as e: + logger.error(f"Erro na geração de PDF do doc acessório: {e}") + return None, f"Erro inesperado: {e}" + + +@login_required +@csrf_exempt +@require_http_methods(["POST"]) +def docacessorio_assinar_a1(request, pk): + """ + Assina o PDF do documento acessório com certificado A1 (arquivo .pfx/.p12). + """ + docacessorio = get_object_or_404(DocumentoAcessorio, pk=pk) + + if docacessorio.pdf_assinado: + return JsonResponse({ + 'success': False, + 'error': 'Este documento já possui um PDF assinado.' + }, status=400) + + certificado_file = request.FILES.get('certificado') + senha = request.POST.get('senha', '') + + if not certificado_file: + return JsonResponse({ + 'success': False, + 'error': 'Certificado não informado.' + }, status=400) + + if not senha: + return JsonResponse({ + 'success': False, + 'error': 'Senha do certificado não informada.' + }, status=400) + + pdf_bytes, error = _gerar_pdf_do_docacessorio(docacessorio, request) + if error: + return JsonResponse({ + 'success': False, + 'error': error + }, status=400) + + try: + from pyhanko.sign import signers, fields + from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter + from pyhanko.keys import load_cert_from_pemder + from pyhanko_certvalidator import ValidationContext + + cert_data = certificado_file.read() + + import tempfile as tmp_module + with tmp_module.NamedTemporaryFile(delete=False, suffix='.pfx') as tmp_cert: + tmp_cert.write(cert_data) + tmp_cert_path = tmp_cert.name + + try: + signer = signers.SimpleSigner.load_pkcs12( + pfx_file=tmp_cert_path, + passphrase=senha.encode('utf-8') + ) + if os.path.exists(tmp_cert_path): + os.unlink(tmp_cert_path) + except Exception as cert_error: + logger.error(f"Erro ao carregar certificado: {cert_error}") + if os.path.exists(tmp_cert_path): + os.unlink(tmp_cert_path) + error_msg = str(cert_error) + if 'password' in error_msg.lower() or 'mac' in error_msg.lower(): + error_detail = 'Senha incorreta.' + elif 'decode' in error_msg.lower() or 'parse' in error_msg.lower(): + error_detail = 'Arquivo não é um certificado válido (.pfx/.p12).' + else: + error_detail = f'Detalhes: {error_msg}' + return JsonResponse({ + 'success': False, + 'error': f'Erro ao carregar certificado: {error_detail}' + }, status=400) + + cert_info = signer.signing_cert + now = timezone.now() + valid_before = cert_info.not_valid_before + valid_after = cert_info.not_valid_after + if valid_before.tzinfo is None: + import pytz + valid_before = pytz.UTC.localize(valid_before) + if valid_after.tzinfo is None: + import pytz + valid_after = pytz.UTC.localize(valid_after) + if now < valid_before or now > valid_after: + return JsonResponse({ + 'success': False, + 'error': 'Certificado expirado ou ainda não válido.' + }, status=400) + + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_stamped: + temp_stamped_path = temp_stamped.name + + try: + from pyhanko.sign.fields import SigSeedSubFilter + from pyhanko.pdf_utils import text + from pyhanko.sign.general import SigningError + from PyPDF4 import PdfFileReader, PdfFileWriter + from reportlab.pdfgen import canvas + from reportlab.lib.units import mm + from reportlab.lib.utils import ImageReader + import re + + nome_assinante = request.user.get_full_name() or request.user.username + data_assinatura = timezone.localtime(timezone.now()) + data_formatada = data_assinatura.strftime('%d/%m/%Y %H:%M:%S') + + cargo = "Usuário do Sistema" + parlamentar = None + try: + from sapl.parlamentares.models import Parlamentar + parlamentar = Parlamentar.objects.filter( + usuario=request.user + ).first() + if parlamentar: + cargo = "Vereador(a)" + tipo_nome = AppConfig.attr('assinatura_nome') + if tipo_nome == 'C': + nome_assinante = parlamentar.nome_completo + else: + nome_assinante = parlamentar.nome_parlamentar + except: + pass + + issuer_str = str(cert_info.issuer).upper() + if 'ICP-BRASIL' in issuer_str or 'ICP BRASIL' in issuer_str: + tipo_cert = "ICP-Brasil" + else: + tipo_cert = "Certificado Digital" + + cpf = "" + try: + if parlamentar and parlamentar.cpf: + cpf = parlamentar.cpf + except: + pass + + if not cpf: + subject_str = str(cert_info.subject) + cpf_match = re.search(r'\d{3}\.?\d{3}\.?\d{3}-?\d{2}', subject_str) + if cpf_match: + cpf = cpf_match.group() + + data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M') + + # Criar carimbo visual e adicionar ao PDF ANTES de assinar + original_pdf = PdfFileReader(io.BytesIO(pdf_bytes)) + last_page = original_pdf.getPage(original_pdf.getNumPages() - 1) + page_box = last_page.mediaBox + page_width = float(page_box.getWidth()) + page_height = float(page_box.getHeight()) + + stamp_buffer = io.BytesIO() + c = canvas.Canvas(stamp_buffer, pagesize=(page_width, page_height)) + + y_pos = 15 * mm + x_pos = 10 * mm + largura_carimbo = 70 * mm + altura_carimbo = 22 * mm + logo_width = 18 * mm + + c.setStrokeColorRGB(0.5, 0.5, 0.5) + c.setLineWidth(0.5) + c.rect(x_pos, y_pos, largura_carimbo, altura_carimbo) + + c.setFont("Helvetica", 6) + c.setFillColorRGB(0.3, 0.3, 0.3) + c.drawString(x_pos + 3*mm, y_pos + 17*mm, "Assinado digitalmente por") + + c.setFont("Helvetica-Bold", 7) + c.setFillColorRGB(0, 0, 0) + nome_upper = nome_assinante.upper() + if len(nome_upper) > 28: + nome_upper = nome_upper[:28] + "..." + c.drawString(x_pos + 3*mm, y_pos + 12*mm, nome_upper) + + c.setFont("Helvetica", 6) + c.setFillColorRGB(0.3, 0.3, 0.3) + if cpf: + c.drawString(x_pos + 3*mm, y_pos + 7*mm, f"CPF: {cpf}") + c.drawString(x_pos + 3*mm, y_pos + 3*mm, f"Data: {data_simples}") + else: + c.drawString(x_pos + 3*mm, y_pos + 5*mm, f"Data: {data_simples}") + + try: + logo_path = None + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + possible_paths = [ + os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo-camara-padrao.png'), + os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo.png'), + os.path.join(base_dir, 'sapl/static/sapl/frontend/img/pdflogo.png'), + os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logo.png'), + os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logotipo.png'), + ] + + logo_dir = os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo') + if os.path.exists(logo_dir): + for f in os.listdir(logo_dir): + if f.lower().endswith(('.png', '.jpg', '.jpeg')): + possible_paths.insert(0, os.path.join(logo_dir, f)) + + for path in possible_paths: + if os.path.exists(path): + logo_path = path + break + + if logo_path: + logo = ImageReader(logo_path) + logo_x = x_pos + largura_carimbo - logo_width - 2*mm + logo_y = y_pos + 2*mm + c.drawImage(logo, logo_x, logo_y, + width=logo_width, height=logo_width, + preserveAspectRatio=True, mask='auto') + except Exception as logo_error: + logger.warning(f"Não foi possível adicionar logo: {logo_error}") + + c.save() + stamp_buffer.seek(0) + + stamp_pdf = PdfFileReader(stamp_buffer) + output_pdf = PdfFileWriter() + + for page_num in range(original_pdf.getNumPages()): + page = original_pdf.getPage(page_num) + if page_num == original_pdf.getNumPages() - 1: + page.mergePage(stamp_pdf.getPage(0)) + output_pdf.addPage(page) + + with open(temp_stamped_path, 'wb') as f: + output_pdf.write(f) + + # Assinar o PDF que já contém o carimbo + with open(temp_stamped_path, 'rb') as stamped_file: + stamped_bytes = stamped_file.read() + + signed_buffer = io.BytesIO() + with io.BytesIO(stamped_bytes) as inf: + w = IncrementalPdfFileWriter(inf) + + meta = signers.PdfSignatureMetadata( + field_name='AssinaturaDigital', + location='Câmara Municipal', + reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', + name=nome_assinante + ) + + signers.sign_pdf( + w, + meta, + signer=signer, + output=signed_buffer + ) + + signed_buffer.seek(0) + signed_pdf_content = signed_buffer.read() + + filename = f"docacessorio_{docacessorio.pk}_assinado_{int(timezone.now().timestamp())}.pdf" + docacessorio.pdf_assinado.save(filename, ContentFile(signed_pdf_content), save=False) + + docacessorio.assinatura_info = { + 'tipo_certificado': 'A1', + 'tipo_certificado_display': f'{tipo_cert} – A1', + 'subject': str(cert_info.subject), + 'issuer': str(cert_info.issuer), + 'serial': str(cert_info.serial_number), + 'valid_from': cert_info.not_valid_before.isoformat(), + 'valid_to': cert_info.not_valid_after.isoformat(), + 'signed_by': request.user.username, + 'nome_assinante': nome_assinante, + 'cargo': cargo, + 'data_assinatura': data_formatada, + 'validade_juridica': 'Assinatura Eletrônica Qualificada' + } + docacessorio.assinado_em = timezone.now() + docacessorio.assinado_por = request.user + docacessorio.save() + + logger.info(f"Documento acessório {docacessorio.pk} assinado por {request.user.username}") + + return JsonResponse({ + 'success': True, + 'message': 'PDF assinado com sucesso!', + 'certificado': { + 'nome': str(cert_info.subject), + 'validade': cert_info.not_valid_after.strftime('%d/%m/%Y') + } + }) + + finally: + if os.path.exists(temp_stamped_path): + os.unlink(temp_stamped_path) + + except ImportError: + logger.error("pyhanko não está instalado") + return JsonResponse({ + 'success': False, + 'error': 'Biblioteca de assinatura não instalada. Contate o administrador.' + }, status=500) + except Exception as e: + logger.error(f"Erro ao assinar PDF do doc acessório: {e}") + return JsonResponse({ + 'success': False, + 'error': f'Erro ao assinar o PDF: {str(e)}' + }, status=500) + + +@login_required +@require_http_methods(["GET"]) +def docacessorio_pdf_assinado(request, pk): + """ + Retorna o PDF assinado do documento acessório para download/visualização. + """ + docacessorio = get_object_or_404(DocumentoAcessorio, pk=pk) + + if not docacessorio.pdf_assinado: + messages.error(request, 'Este documento não possui PDF assinado.') + return redirect('sapl.materia:documentoacessorio_detail', + pk=docacessorio.materia.pk, dpk=pk) + + try: + with open(docacessorio.pdf_assinado.path, 'rb') as f: + content = f.read() + + filename = f"DocAcessorio_{docacessorio.pk}_{docacessorio.nome}_ASSINADO.pdf" + filename = filename.replace(' ', '_').replace('/', '-') + + response = HttpResponse(content, content_type='application/pdf') + response['Content-Disposition'] = f'inline; filename="{filename}"' + return response + + except Exception as e: + logger.error(f"Erro ao ler PDF assinado do doc acessório: {e}") + messages.error(request, 'Erro ao ler o arquivo PDF assinado.') + return redirect('sapl.materia:documentoacessorio_detail', + pk=docacessorio.materia.pk, dpk=pk) + + +@login_required +@require_http_methods(["GET"]) +def docacessorio_verificar_assinatura(request, pk): + """ + Verifica a assinatura digital do PDF do documento acessório. + """ + docacessorio = get_object_or_404(DocumentoAcessorio, pk=pk) + + if not docacessorio.pdf_assinado: + return JsonResponse({ + 'success': False, + 'error': 'Este documento não possui PDF assinado.', + 'assinado': False + }) + + try: + from pyhanko.sign.validation import validate_pdf_signature + from pyhanko.pdf_utils.reader import PdfFileReader + + with open(docacessorio.pdf_assinado.path, 'rb') as f: + reader = PdfFileReader(f) + + assinaturas = [] + + for sig_field_name in reader.embedded_signatures: + try: + sig = reader.embedded_signatures[sig_field_name] + + sig_info = { + 'campo': sig_field_name, + 'assinante': str(sig.signer_cert.subject) if sig.signer_cert else 'Desconhecido', + 'data': sig.self_reported_timestamp.isoformat() if sig.self_reported_timestamp else None, + } + + assinaturas.append(sig_info) + + except Exception as sig_error: + logger.warning(f"Erro ao verificar assinatura {sig_field_name}: {sig_error}") + assinaturas.append({ + 'campo': sig_field_name, + 'erro': str(sig_error) + }) + + return JsonResponse({ + 'success': True, + 'assinado': True, + 'total_assinaturas': len(assinaturas), + 'assinaturas': assinaturas, + 'info_salva': docacessorio.assinatura_info + }) + + except ImportError: + return JsonResponse({ + 'success': True, + 'assinado': True, + 'info_salva': docacessorio.assinatura_info, + 'verificacao_disponivel': False + }) + except Exception as e: + logger.error(f"Erro ao verificar assinatura do doc acessório: {e}") + return JsonResponse({ + 'success': False, + 'error': f'Erro ao verificar assinatura: {str(e)}', + 'assinado': True, + 'info_salva': docacessorio.assinatura_info + }) + + +@login_required +@require_http_methods(["POST"]) +def docacessorio_remover_assinatura(request, pk): + """ + Remove a assinatura digital do documento acessório. + Apenas superusuários podem executar esta ação. + """ + if not request.user.is_superuser: + return JsonResponse({ + 'success': False, + 'error': 'Apenas administradores podem remover assinaturas.' + }, status=403) + + docacessorio = get_object_or_404(DocumentoAcessorio, pk=pk) + + if not docacessorio.pdf_assinado: + return JsonResponse({ + 'success': False, + 'error': 'Este documento não possui PDF assinado.' + }, status=400) + + try: + docacessorio.pdf_assinado.delete(save=False) + + docacessorio.pdf_assinado = None + docacessorio.assinatura_info = None + docacessorio.assinado_em = None + docacessorio.assinado_por = None + docacessorio.save() + + logger.info(f"Assinatura do doc acessório {docacessorio.pk} removida por {request.user.username}") + + return JsonResponse({ + 'success': True, + 'message': 'Assinatura removida com sucesso.' + }) + + except Exception as e: + logger.error(f"Erro ao remover assinatura do doc acessório: {e}") + return JsonResponse({ + 'success': False, + 'error': f'Erro ao remover assinatura: {str(e)}' + }, status=500) diff --git a/sapl/templates/materia/assinatura_modal.html b/sapl/templates/materia/assinatura_modal.html index 0392399ac..4c1503532 100644 --- a/sapl/templates/materia/assinatura_modal.html +++ b/sapl/templates/materia/assinatura_modal.html @@ -250,8 +250,9 @@ document.addEventListener('DOMContentLoaded', function() { materiaId: null, a3AppInfo: null, - init: function(materiaId) { + init: function(materiaId, urlPrefix) { this.materiaId = materiaId; + this.urlPrefix = urlPrefix || ('/materia/' + materiaId); this.bindEvents(); }, @@ -408,7 +409,7 @@ document.addEventListener('DOMContentLoaded', function() { formData.append('certificado', certificado); formData.append('senha', senha); - fetch('/materia/' + this.materiaId + '/assinar/a1/', { + fetch(this.urlPrefix + '/assinar/a1/', { method: 'POST', body: formData, headers: { @@ -468,7 +469,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('sucesso-certificado-info').innerHTML = info; var btnBaixar = document.getElementById('btn-baixar-assinado'); - btnBaixar.href = '/materia/' + this.materiaId + '/pdf-assinado/'; + btnBaixar.href = this.urlPrefix + '/pdf-assinado/'; btnBaixar.style.display = 'inline-block'; document.getElementById('btn-fechar-modal').textContent = 'Fechar'; diff --git a/sapl/templates/materia/documentoacessorio_detail.html b/sapl/templates/materia/documentoacessorio_detail.html index 2ce82bf8c..12b9f77dc 100644 --- a/sapl/templates/materia/documentoacessorio_detail.html +++ b/sapl/templates/materia/documentoacessorio_detail.html @@ -1,5 +1,5 @@ {% extends "crud/detail.html" %} -{% load i18n %} +{% load i18n tz %} {% block sub_actions %} {{ block.super }} @@ -10,4 +10,80 @@ {% endif %} + {% if object.arquivo %} +
+ {% if object.pdf_assinado %} + + {% trans "Ver PDF Assinado" %} + + {% elif is_autor %} + + {% endif %} +
+ {% endif %} {% endblock sub_actions %} + +{% block detail_content %} + {{ block.super }} + {% if object.pdf_assinado %} +
+
+
+
+ {% trans "Assinatura Digital" %} +
+
+
+
+

{% trans "Status:" %} + {% trans "Documento Assinado" %} +

+ {% if object.assinado_em %} +

{% trans "Data/Hora:" %} {{ object.assinado_em|localtime|date:"d/m/Y H:i:s" }}

+ {% endif %} + {% if object.assinado_por %} +

{% trans "Assinado por:" %} {{ object.assinado_por.get_full_name|default:object.assinado_por.username }}

+ {% endif %} +
+
+ {% if object.assinatura_info %} +

{% trans "Tipo de Certificado:" %} {{ object.assinatura_info.tipo_certificado|default:"A1" }}

+ {% if object.assinatura_info.subject %} +

{% trans "Certificado:" %} {{ object.assinatura_info.subject|truncatechars:50 }}

+ {% endif %} + {% if object.assinatura_info.valid_to %} +

{% trans "Validade:" %} {{ object.assinatura_info.valid_to|slice:":10" }}

+ {% endif %} + {% endif %} +
+
+ +
+
+
+
+ {% endif %} +{% endblock detail_content %} + +{% block extra_js %} +{{ block.super }} +{% if object.arquivo and not object.pdf_assinado and is_autor %} + {% include "materia/assinatura_modal.html" %} + +{% endif %} +{% endblock extra_js %}