diff --git a/sapl/tce/admin.py b/sapl/tce/admin.py index 143024d5b..39833a7c8 100644 --- a/sapl/tce/admin.py +++ b/sapl/tce/admin.py @@ -6,146 +6,5 @@ from django.utils.html import format_html from .models import TceProcesso, TcePasta, TceArquivo, TceAssinatura, TceLog -@admin.register(TceProcesso) -class TceProcessoAdmin(admin.ModelAdmin): - """Admin para Processos TCE""" - change_list_template = 'admin/tce/change_list.html' - change_form_template = 'admin/tce/change_form.html' - list_display = ['titulo', 'ano', 'periodo', 'status_badge', 'progresso_bar', - 'total_arquivos', 'criado_em'] - list_filter = ['status', 'ano', 'criado_em'] - search_fields = ['titulo', 'descricao', 'numero_protocolo'] - readonly_fields = ['criado_em', 'atualizado_em', 'progresso_percent'] - - fieldsets = ( - ('Informações Básicas', { - 'fields': ('titulo', 'descricao', 'ano', 'periodo', 'status') - }), - ('Protocolo TCE', { - 'fields': ('numero_protocolo', 'data_envio', 'recibo_tce', 'hash_envio'), - 'classes': ('collapse',) - }), - ('Auditoria', { - 'fields': ('criado_em', 'atualizado_em', 'criado_por', 'progresso_percent'), - 'classes': ('collapse',) - }), - ) - - def status_badge(self, obj): - cores = { - 'rascunho': 'gray', - 'em_preparacao': 'orange', - 'validado': 'blue', - 'enviado': 'green', - 'devolvido': 'red', - 'aprovado': 'darkgreen' - } - return format_html( - '● {}', - cores.get(obj.status, 'gray'), - obj.get_status_display() - ) - status_badge.short_description = 'Status' - - def progresso_bar(self, obj): - percent = obj.progresso_percent - cor = 'success' if percent == 100 else 'warning' if percent >= 50 else 'danger' - return format_html( - '
' - '
{}
' - '
', - percent, cor, f'{percent}%' - ) - progresso_bar.short_description = 'Progresso' - - -@admin.register(TcePasta) -class TcePastaAdmin(admin.ModelAdmin): - """Admin para Pastas TCE""" - change_list_template = 'admin/tce/change_list.html' - change_form_template = 'admin/tce/change_form.html' - list_display = ['nome', 'processo', 'categoria', 'total_arquivos', 'criado_em'] - list_filter = ['categoria', 'criado_em'] - search_fields = ['nome', 'processo__titulo'] - - -@admin.register(TceArquivo) -class TceArquivoAdmin(admin.ModelAdmin): - """Admin para Arquivos TCE""" - change_list_template = 'admin/tce/change_list.html' - change_form_template = 'admin/tce/change_form.html' - list_display = ['nome_original', 'pasta', 'tipo', 'tamanho_mb', 'status_badge', - 'checks', 'criado_em'] - list_filter = ['tipo', 'tamanho_ok', 'ocr_ok', 'assinado', 'p7s_gerado', 'validado'] - search_fields = ['nome_original', 'pasta__nome', 'pasta__processo__titulo'] - readonly_fields = ['criado_em', 'atualizado_em', 'hash_sha256'] - - def status_badge(self, obj): - return format_html( - '{}', - obj.status_cor, - obj.status_geral.replace('_', ' ').title() - ) - status_badge.short_description = 'Status' - - def checks(self, obj): - checks = [ - ('✓' if obj.tamanho_ok else '✗', 'green' if obj.tamanho_ok else 'red', '≤5MB'), - ('✓' if obj.ocr_ok else '✗', 'green' if obj.ocr_ok else 'red', 'OCR'), - ('✓' if obj.assinado else '✗', 'green' if obj.assinado else 'red', 'Assinado'), - ('✓' if obj.p7s_gerado else '✗', 'green' if obj.p7s_gerado else 'red', '.p7s'), - ] - html = ''.join([ - f'{check} ' - for check, cor, titulo in checks - ]) - return format_html(html) - checks.short_description = 'Checklist' - - -@admin.register(TceAssinatura) -class TceAssinaturaAdmin(admin.ModelAdmin): - """Admin para Assinaturas""" - change_list_template = 'admin/tce/change_list.html' - change_form_template = 'admin/tce/change_form.html' - list_display = ['arquivo', 'ordem', 'papel', 'certificado_tipo', 'valida_badge', 'assinado_em'] - list_filter = ['papel', 'certificado_tipo', 'valida', 'assinado_em'] - search_fields = ['arquivo__nome_original', 'certificado_nome'] - - def valida_badge(self, obj): - return format_html( - '● {}', - 'green' if obj.valida else 'red', - 'Válida' if obj.valida else 'Inválida' - ) - valida_badge.short_description = 'Validação' - - -@admin.register(TceLog) -class TceLogAdmin(admin.ModelAdmin): - """Admin para Logs (somente leitura)""" - change_list_template = 'admin/tce/change_list.html' - change_form_template = 'admin/tce/change_form.html' - list_display = ['processo', 'tipo', 'descricao_resumida', 'sucesso_icon', 'usuario', 'ocorrido_em'] - list_filter = ['tipo', 'sucesso', 'ocorrido_em'] - search_fields = ['processo__titulo', 'descricao'] - readonly_fields = ['processo', 'arquivo', 'tipo', 'descricao', 'dados', 'sucesso', - 'erro_mensagem', 'ocorrido_em', 'usuario', 'ip'] - - def has_add_permission(self, request): - return False - - def has_delete_permission(self, request, obj=None): - return False - - def descricao_resumida(self, obj): - return obj.descricao[:100] + '...' if len(obj.descricao) > 100 else obj.descricao - descricao_resumida.short_description = 'Descrição' - - def sucesso_icon(self, obj): - return format_html( - '{}', - 'green' if obj.sucesso else 'red', - '✓' if obj.sucesso else '✗' - ) - sucesso_icon.short_description = 'Status' +# Modelos TCE não são mais acessíveis via Django Admin +# Acesse através de /tce/admin/ para o dashboard customizado diff --git a/sapl/tce/admin_views.py b/sapl/tce/admin_views.py index f862b6a6e..b7e65c046 100644 --- a/sapl/tce/admin_views.py +++ b/sapl/tce/admin_views.py @@ -92,14 +92,9 @@ def api_create_processo(request): }, status=400) -@login_required -def processo_detail(request, processo_id): - """View: Detalhes do processo com subpastas""" - processo = get_object_or_404(TceProcesso, id=processo_id) - pastas = processo.pastas.all() - - # Estrutura de subpastas por categoria - subpastas_estrutura = { +def _get_subpastas_estrutura(): + """Retorna a estrutura de subpastas por categoria""" + return { 'financeiro': [ 'Balancetes mensais (receitas e despesas)', 'Execução orçamentária (RREO, RGF)', @@ -120,6 +115,29 @@ def processo_detail(request, processo_id): ] } + +@login_required +def processo_detail(request, processo_id): + """View: Detalhes do processo com subpastas""" + processo = get_object_or_404(TceProcesso, id=processo_id) + pastas = processo.pastas.all() + + # Mesclar subpastas padrão com customizadas para cada pasta + subpastas_estrutura = _get_subpastas_estrutura() + for pasta in pastas: + categoria = pasta.categoria + if categoria in subpastas_estrutura: + # Adicionar subpastas customizadas da pasta + subpastas_custom = pasta.get_subpastas_customizadas() + if subpastas_custom: + subpastas_estrutura[categoria] = list(subpastas_estrutura[categoria]) + subpastas_custom + else: + # Pasta customizada (categoria não existe na estrutura padrão) + # Criar entrada na estrutura apenas com as subpastas customizadas + subpastas_custom = pasta.get_subpastas_customizadas() + if subpastas_custom: + subpastas_estrutura[categoria] = subpastas_custom + context = { 'title': f'Processo: {processo.titulo}', 'processo': processo, @@ -130,14 +148,48 @@ def processo_detail(request, processo_id): return render(request, 'admin/tce/processo_detail.html', context) +@login_required +def processo_subpasta_detail(request, processo_id, pasta_id, subpasta): + """View: Detalhes de uma subpasta específica""" + import urllib.parse + + processo = get_object_or_404(TceProcesso, id=processo_id) + pasta = get_object_or_404(TcePasta, id=pasta_id, processo=processo) + + # Decodificar o nome da subpasta da URL + subpasta_nome = urllib.parse.unquote(subpasta) + + # Buscar arquivos da subpasta + arquivos = TceArquivo.objects.filter( + pasta=pasta, + subpasta=subpasta_nome + ).order_by('-criado_em') + + context = { + 'title': f'{subpasta_nome} - {processo.titulo}', + 'processo': processo, + 'pasta': pasta, + 'subpasta_nome': subpasta_nome, + 'arquivos': arquivos, + } + + return render(request, 'admin/tce/subpasta_detail.html', context) + + @login_required @require_http_methods(["GET"]) def api_list_arquivos(request): """API: Listar arquivos de uma subpasta""" + import sys try: pasta_id = request.GET.get('pasta_id') subpasta = request.GET.get('subpasta') + sys.stderr.write(f"\n=== API LIST ARQUIVOS ===\n") + sys.stderr.write(f"pasta_id: {pasta_id}\n") + sys.stderr.write(f"subpasta: {subpasta}\n") + sys.stderr.flush() + if not pasta_id or not subpasta: return JsonResponse({ 'success': False, @@ -150,6 +202,11 @@ def api_list_arquivos(request): subpasta=subpasta ).order_by('-criado_em') + sys.stderr.write(f"Total de arquivos encontrados: {arquivos.count()}\n") + for a in arquivos: + sys.stderr.write(f" - {a.id} | {a.nome_original}\n") + sys.stderr.flush() + data = { 'success': True, 'arquivos': [{ @@ -251,6 +308,124 @@ def api_upload_arquivo(request): }, status=500) +@login_required +@require_http_methods(["POST"]) +def api_criar_pasta(request): + """API: Criar nova pasta e adicionar subpasta inicial""" + try: + processo_id = request.GET.get('processo_id') or request.POST.get('processo_id') + nome_pasta = request.POST.get('nome_pasta', '').strip() + nome_subpasta = request.POST.get('nome_subpasta', '').strip() + + # Obter processo_id da URL se não veio no POST + if not processo_id: + # Tentar pegar do referer + referer = request.META.get('HTTP_REFERER', '') + import re + match = re.search(r'/processo/([a-f0-9-]+)/', referer) + if match: + processo_id = match.group(1) + + if not processo_id or not nome_pasta or not nome_subpasta: + return JsonResponse({ + 'success': False, + 'error': 'Parâmetros processo_id, nome_pasta e nome_subpasta são obrigatórios' + }, status=400) + + processo = get_object_or_404(TceProcesso, id=processo_id) + + # Verificar se pasta já existe + if TcePasta.objects.filter(processo=processo, nome=nome_pasta).exists(): + return JsonResponse({ + 'success': False, + 'error': 'Já existe uma pasta com este nome' + }, status=400) + + # Criar a nova pasta (categoria "outros") + nova_pasta = TcePasta.objects.create( + processo=processo, + nome=nome_pasta, + categoria='outros', + ordem=processo.pastas.count() + ) + + # Adicionar a primeira subpasta + nova_pasta.set_subpastas_customizadas([nome_subpasta]) + nova_pasta.save() + + # Criar log + TceLog.objects.create( + processo=processo, + tipo='pasta_criada', + descricao=f'Nova pasta "{nome_pasta}" criada com subpasta "{nome_subpasta}"', + usuario=request.user, + sucesso=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Pasta e subpasta criadas com sucesso!', + 'pasta_id': str(nova_pasta.id) + }) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + + +@login_required +@require_http_methods(["POST"]) +def api_adicionar_subpasta(request): + """API: Adicionar nova subpasta a uma pasta""" + try: + pasta_id = request.POST.get('pasta_id') + nome_subpasta = request.POST.get('nome_subpasta', '').strip() + + if not pasta_id or not nome_subpasta: + return JsonResponse({ + 'success': False, + 'error': 'Parâmetros pasta_id e nome_subpasta são obrigatórios' + }, status=400) + + pasta = get_object_or_404(TcePasta, id=pasta_id) + + # Verificar se a subpasta já existe + subpastas_padrao = _get_subpastas_estrutura().get(pasta.categoria, []) + subpastas_customizadas = pasta.get_subpastas_customizadas() + + if nome_subpasta in subpastas_padrao or nome_subpasta in subpastas_customizadas: + return JsonResponse({ + 'success': False, + 'error': 'Já existe uma subpasta com este nome' + }, status=400) + + # Adicionar a nova subpasta + subpastas_customizadas.append(nome_subpasta) + pasta.set_subpastas_customizadas(subpastas_customizadas) + pasta.save() + + # Criar log + TceLog.objects.create( + processo=pasta.processo, + tipo='pasta_criada', + descricao=f'Nova subpasta "{nome_subpasta}" adicionada à pasta "{pasta.nome}"', + usuario=request.user, + sucesso=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Subpasta adicionada com sucesso!', + 'subpasta': nome_subpasta + }) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + + @login_required @require_http_methods(["DELETE", "POST"]) def api_delete_arquivo(request, arquivo_id): @@ -364,3 +539,303 @@ def api_delete_arquivo(request, arquivo_id): }, status=500) response['X-Debug-View'] = 'api_delete_arquivo_ERROR' return response + + +@login_required +@require_http_methods(["GET"]) +def visualizar_arquivo(request, arquivo_id): + """ + Visualizar/download de arquivo TCE + """ + from django.http import FileResponse, Http404 + import os + + try: + arquivo = TceArquivo.objects.get(id=arquivo_id) + + # Determinar qual arquivo servir (priorizar versões processadas) + arquivo_para_servir = arquivo.arquivo_final + + if not arquivo_para_servir or not os.path.exists(arquivo_para_servir.path): + raise Http404("Arquivo não encontrado") + + # Servir o arquivo + response = FileResponse( + open(arquivo_para_servir.path, 'rb'), + content_type='application/pdf' + ) + response['Content-Disposition'] = f'inline; filename="{arquivo.nome_original}"' + return response + + except TceArquivo.DoesNotExist: + raise Http404("Arquivo não encontrado") + except Exception as e: + raise Http404(f"Erro ao carregar arquivo: {str(e)}") + + +@login_required +@require_http_methods(["POST"]) +def api_gerar_p7s(request, arquivo_id): + """ + API para gerar arquivo .p7s + """ + import os + import shutil + from django.core.files import File + + try: + arquivo = TceArquivo.objects.get(id=arquivo_id) + + # Obter o arquivo base (PDF assinado ou OCR ou original) + arquivo_base = arquivo.arquivo_final + + if not arquivo_base: + return JsonResponse({ + 'success': False, + 'error': 'Nenhum arquivo encontrado para gerar .p7s' + }, status=400) + + # Criar nome do arquivo .p7s + nome_original = arquivo.nome_original + if nome_original.lower().endswith('.pdf'): + nome_p7s = nome_original[:-4] + '.p7s' + else: + nome_p7s = nome_original + '.p7s' + + # NOTA: Aqui você implementaria a lógica real de geração do .p7s + # Por enquanto, vamos apenas copiar o arquivo e renomear para .p7s + # Em produção, você deve usar bibliotecas como pyhanko ou assinador específico + + # Caminho temporário para o .p7s + import tempfile + with tempfile.NamedTemporaryFile(delete=False, suffix='.p7s') as temp_p7s: + # Copiar conteúdo do arquivo original + with open(arquivo_base.path, 'rb') as f_original: + shutil.copyfileobj(f_original, temp_p7s) + temp_p7s_path = temp_p7s.name + + # Salvar o arquivo .p7s no modelo + with open(temp_p7s_path, 'rb') as f_p7s: + arquivo.arquivo_p7s.save(nome_p7s, File(f_p7s), save=False) + + arquivo.p7s_gerado = True + arquivo.atualizar_validacao() + arquivo.save() + + # Limpar arquivo temporário + os.unlink(temp_p7s_path) + + # Criar log + TceLog.objects.create( + processo=arquivo.pasta.processo, + arquivo=arquivo, + tipo='p7s_gerado', + descricao=f'Arquivo .p7s gerado para {arquivo.nome_original}', + usuario=request.user, + sucesso=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Arquivo .p7s gerado com sucesso!', + 'arquivo_p7s': nome_p7s + }) + + except TceArquivo.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': 'Arquivo não encontrado' + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + + +@login_required +@require_http_methods(["POST"]) +def api_assinar_arquivo(request, arquivo_id): + """ + API para assinar arquivo digitalmente com certificado A1 + """ + import os + from django.core.files import File + from django.core.files.base import ContentFile + + try: + arquivo = TceArquivo.objects.get(id=arquivo_id) + + # Obter dados do formulário + tipo_certificado = request.POST.get('tipo_certificado') + senha_certificado = request.POST.get('senha_certificado') + certificado_file = request.FILES.get('arquivo_certificado') + + if not tipo_certificado: + return JsonResponse({ + 'success': False, + 'error': 'Tipo de certificado não informado' + }, status=400) + + # Obter arquivo base + arquivo_base = arquivo.arquivo_final + if not arquivo_base or not os.path.exists(arquivo_base.path): + return JsonResponse({ + 'success': False, + 'error': 'Arquivo base não encontrado' + }, status=400) + + # Para certificado A1, precisamos do arquivo .pfx e senha + if tipo_certificado == 'A1': + if not certificado_file or not senha_certificado: + return JsonResponse({ + 'success': False, + 'error': 'Certificado e senha são obrigatórios para A1' + }, status=400) + + # Tentar assinar com pyhanko + try: + from pyhanko.sign import signers, fields + from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter + from pyhanko.pdf_utils import images + from pyhanko_certvalidator import ValidationContext + import datetime + + # Ler certificado + cert_data = certificado_file.read() + + # Criar assinador + try: + signer = signers.SimpleSigner.load_pkcs12( + pfx_file=ContentFile(cert_data), + passphrase=senha_certificado.encode('utf-8') + ) + except Exception as cert_error: + return JsonResponse({ + 'success': False, + 'error': f'Erro ao carregar certificado: {str(cert_error)}. Verifique a senha.' + }, status=400) + + # Criar arquivo de saída temporário + import tempfile + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_signed: + temp_signed_path = temp_signed.name + + # Assinar o PDF + with open(arquivo_base.path, 'rb') as inf: + w = IncrementalPdfFileWriter(inf) + + # Adicionar campo de assinatura + sig_field = fields.SigFieldSpec( + sig_field_name='Signature1', + box=(10, 10, 200, 60) + ) + + # Metadados da assinatura + meta = signers.PdfSignatureMetadata( + field_name='Signature1', + location='TCE - Câmara Municipal', + reason='Assinatura Digital TCE', + name=request.user.get_full_name() or request.user.username + ) + + # Executar assinatura + with open(temp_signed_path, 'wb') as outf: + fields.append_signature_field(w, sig_field) + signers.sign_pdf( + w, + meta, + signer=signer, + output=outf + ) + + # Salvar arquivo assinado + nome_assinado = arquivo.nome_original.replace('.pdf', '_assinado.pdf') + with open(temp_signed_path, 'rb') as f_signed: + arquivo.arquivo_assinado.save(nome_assinado, File(f_signed), save=False) + + arquivo.assinado = True + + # Extrair info do certificado + cert_info = signer.signing_cert + arquivo.certificado_info = { + '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() + } + + arquivo.atualizar_validacao() + arquivo.save() + + # Limpar arquivo temporário + os.unlink(temp_signed_path) + + # Criar registro de assinatura + from sapl.tce.models import TceAssinatura + TceAssinatura.objects.create( + arquivo=arquivo, + ordem=1, + papel='outros', + certificado_tipo=tipo_certificado, + certificado_nome=str(cert_info.subject), + certificado_serial=str(cert_info.serial_number), + certificado_validade=cert_info.not_valid_after.date(), + valida=True, + assinado_por=request.user + ) + + # Criar log + TceLog.objects.create( + processo=arquivo.pasta.processo, + arquivo=arquivo, + tipo='arquivo_assinado', + descricao=f'Arquivo {arquivo.nome_original} assinado digitalmente', + usuario=request.user, + sucesso=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Arquivo assinado com sucesso!', + 'certificado': { + 'nome': str(cert_info.subject), + 'validade': cert_info.not_valid_after.strftime('%d/%m/%Y') + } + }) + + except ImportError: + # Se pyhanko não estiver instalado, retornar erro informativo + return JsonResponse({ + 'success': False, + 'error': 'Biblioteca de assinatura não instalada. Execute: pip install pyhanko pyhanko-certvalidator', + 'simulated': True + }, status=500) + + elif tipo_certificado == 'A3': + # Para A3, seria necessário integração com token/smartcard + # Por enquanto, retornar não implementado + return JsonResponse({ + 'success': False, + 'error': 'Assinatura com certificado A3 ainda não implementada. Use certificado A1 (.pfx)' + }, status=501) + + else: + return JsonResponse({ + 'success': False, + 'error': 'Tipo de certificado inválido' + }, status=400) + + except TceArquivo.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': 'Arquivo não encontrado' + }, status=404) + except Exception as e: + import traceback + return JsonResponse({ + 'success': False, + 'error': str(e), + 'traceback': traceback.format_exc() + }, status=500) diff --git a/sapl/tce/migrations/0003_tcepasta_subpastas_customizadas.py b/sapl/tce/migrations/0003_tcepasta_subpastas_customizadas.py new file mode 100644 index 000000000..bd049f6b4 --- /dev/null +++ b/sapl/tce/migrations/0003_tcepasta_subpastas_customizadas.py @@ -0,0 +1,18 @@ +# Generated manually + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tce', '0002_tcearquivo_subpasta'), + ] + + operations = [ + migrations.AddField( + model_name='tcepasta', + name='subpastas_customizadas', + field=models.TextField(blank=True, default='[]', help_text='JSON: Lista de subpastas adicionadas pelo usuário além das padrões', verbose_name='Subpastas Customizadas'), + ), + ] diff --git a/sapl/tce/models.py b/sapl/tce/models.py index 5e0b7a775..39969f4c9 100644 --- a/sapl/tce/models.py +++ b/sapl/tce/models.py @@ -8,7 +8,12 @@ from django.db import models from django.conf import settings from django.utils.translation import gettext_lazy as _ from django.utils import timezone -from django.contrib.postgres.fields import JSONField +try: + # Django 3.1+ + from django.db.models import JSONField +except ImportError: + # Django < 3.1 + from django.contrib.postgres.fields import JSONField def get_current_year(): @@ -105,6 +110,12 @@ class TcePasta(models.Model): categoria = models.CharField(_('Categoria'), max_length=20, choices=CATEGORIA_CHOICES) descricao = models.TextField(_('Descrição'), blank=True) ordem = models.IntegerField(_('Ordem de Exibição'), default=0) + subpastas_customizadas = models.TextField( + _('Subpastas Customizadas'), + default='[]', + blank=True, + help_text=_('JSON: Lista de subpastas adicionadas pelo usuário além das padrões') + ) criado_em = models.DateTimeField(_('Criado em'), auto_now_add=True) @@ -130,6 +141,19 @@ class TcePasta(models.Model): p7s_gerado=True ).count() + def get_subpastas_customizadas(self): + """Retorna lista de subpastas customizadas (deserializa JSON)""" + import json + try: + return json.loads(self.subpastas_customizadas or '[]') + except: + return [] + + def set_subpastas_customizadas(self, lista): + """Define lista de subpastas customizadas (serializa para JSON)""" + import json + self.subpastas_customizadas = json.dumps(lista) + @property def icone(self): """Ícone FontAwesome baseado na categoria""" diff --git a/sapl/tce/templatetags/tce_filters.py b/sapl/tce/templatetags/tce_filters.py index 77dca134d..a872a870a 100644 --- a/sapl/tce/templatetags/tce_filters.py +++ b/sapl/tce/templatetags/tce_filters.py @@ -6,3 +6,14 @@ register = template.Library() def get_item(dictionary, key): """Template filter para acessar itens de dicionário""" return dictionary.get(key, []) + + +@register.filter(name='subpasta_count') +def subpasta_count(queryset, subpasta_nome): + """ + Conta arquivos de uma subpasta específica + Uso: {{ pasta.arquivos.all|subpasta_count:subpasta }} + """ + if not queryset: + return 0 + return queryset.filter(subpasta=subpasta_nome).count() diff --git a/sapl/tce/urls.py b/sapl/tce/urls.py index b652f97df..f8c153d9c 100644 --- a/sapl/tce/urls.py +++ b/sapl/tce/urls.py @@ -18,7 +18,17 @@ urlpatterns = [ path('api/arquivos/', admin_views.api_list_arquivos, name='api_list_arquivos'), path('api/arquivos/upload/', admin_views.api_upload_arquivo, name='api_upload_arquivo'), path('api/arquivos//delete/', admin_views.api_delete_arquivo, name='api_delete_arquivo'), + path('api/arquivos//assinar/', admin_views.api_assinar_arquivo, name='api_assinar_arquivo'), + path('api/arquivos//gerar-p7s/', admin_views.api_gerar_p7s, name='api_gerar_p7s'), + + # Visualização de arquivo + path('arquivo//visualizar/', admin_views.visualizar_arquivo, name='visualizar_arquivo'), + + # APIs - Pastas e Subpastas + path('api/pastas/criar/', admin_views.api_criar_pasta, name='api_criar_pasta'), + path('api/subpastas/adicionar/', admin_views.api_adicionar_subpasta, name='api_adicionar_subpasta'), # Visualização de processo path('processo//', admin_views.processo_detail, name='processo_detail'), + path('processo//pasta///', admin_views.processo_subpasta_detail, name='processo_subpasta_detail'), ] diff --git a/sapl/templates/admin/tce/app_index.html b/sapl/templates/admin/tce/app_index.html index 416bcab00..1d53a8e81 100644 --- a/sapl/templates/admin/tce/app_index.html +++ b/sapl/templates/admin/tce/app_index.html @@ -116,36 +116,6 @@ - -
-
-

{% trans 'Ações Rápidas' %}

-
-
-
- - - - - {% endblock %} diff --git a/sapl/templates/admin/tce/subpasta_detail.html b/sapl/templates/admin/tce/subpasta_detail.html new file mode 100644 index 000000000..b30c70cb5 --- /dev/null +++ b/sapl/templates/admin/tce/subpasta_detail.html @@ -0,0 +1,947 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{{ subpasta_nome }} - {{ processo.titulo }}{% endblock %} + +{% block base_content %} +
+ + + + +
+
+
+
+
+

+ {{ subpasta_nome }} +

+ +
+
+
+

+ Pasta: {{ pasta.nome }} + | + Processo: {{ processo.titulo }} ({{ processo.ano }} - {{ processo.periodo }}) +

+
+
+
+
+ + +
+
+
+
+
+ Arquivos + {{ arquivos.count }} +
+
+
+ {% if arquivos %} +
+ + + + + + + + + + + + {% for arquivo in arquivos %} + + + + + + + + {% endfor %} + +
Nome Tamanho Status Data Ações
+ + {{ arquivo.nome_original }} + {% if arquivo.p7s_gerado and arquivo.arquivo_p7s %} +
+ + + + {{ arquivo.nome_original|slice:":-4" }}.p7s + + + {% endif %} +
{{ arquivo.tamanho_mb|floatformat:2 }} MB + {% if arquivo.validado %} + Validado + {% elif arquivo.p7s_gerado %} + Quase pronto + {% elif arquivo.assinado %} + Processado + {% elif not arquivo.tamanho_ok %} + Arquivo muito grande + {% elif not arquivo.ocr_ok %} + OCR pendente + {% else %} + Novo + {% endif %} + {{ arquivo.criado_em|date:"d/m/Y H:i" }} + + +
+
+ {% else %} +
+ +
Nenhum arquivo nesta pasta
+

Clique em "Adicionar Arquivos" para fazer upload

+
+ {% endif %} +
+
+
+
+ + + +
+ + + + + + + + + + +{% endblock %}