Browse Source

Fix(Proposições): Correção da geração de hash_code para documentos PDF

- Corrige geração de hash_code ao enviar proposições com texto_original (PDF)
- Adiciona verificação de arquivo existente antes de gerar hash no recibo
- Melhora UI/UX: filtros clicáveis na lista de proposições
- Aumenta largura máxima dos containers para 1200px
- Permite acesso de administradores ao detalhe da proposição
- Adiciona botão Receber na lista de proposições pendentes
- Corrige cor do título/cabeçalho das tabelas para preto
- Remove títulos duplicados nos templates
pull/3858/head
rangelbruno 8 months ago
parent
commit
ee19820322
  1. 61
      sapl/materia/views.py
  2. 21
      sapl/templates/materia/prop_pendentes_list.html
  3. 204
      sapl/templates/materia/proposicao_base.html
  4. 6
      sapl/templates/materia/proposicao_detail.html
  5. 48
      sapl/templates/materia/proposicao_list.html
  6. 15
      sapl/templates/materia/receber_proposicao.html

61
sapl/materia/views.py

@ -553,7 +553,6 @@ class ProposicaoDevolvida(PermissionRequiredMixin, ListView):
context['page_range'] = make_pagination( context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages) page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição devolvida.' context['NO_ENTRIES_MSG'] = 'Nenhuma proposição devolvida.'
context['subnav_template_name'] = 'materia/subnav_prop.yaml'
return context return context
@ -582,8 +581,6 @@ class ProposicaoPendente(PermissionRequiredMixin, ListView):
context['page_range'] = make_pagination( context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages) page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição pendente.' context['NO_ENTRIES_MSG'] = 'Nenhuma proposição pendente.'
context['subnav_template_name'] = 'materia/subnav_prop.yaml'
qr = self.request.GET.copy() qr = self.request.GET.copy()
context['filter_url'] = ('&o=' + qr['o']) if 'o' in qr.keys() else '' context['filter_url'] = ('&o=' + qr['o']) if 'o' in qr.keys() else ''
return context return context
@ -610,13 +607,12 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView):
context['page_range'] = make_pagination( context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages) page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição recebida.' context['NO_ENTRIES_MSG'] = 'Nenhuma proposição recebida.'
context['subnav_template_name'] = 'materia/subnav_prop.yaml'
return context return context
class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView):
app_label = sapl.protocoloadm.apps.AppConfig.label app_label = sapl.protocoloadm.apps.AppConfig.label
template_name = "crud/form.html" template_name = "materia/receber_proposicao.html"
form_class = ReceberProposicaoForm form_class = ReceberProposicaoForm
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -662,7 +658,6 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ReceberProposicao, self).get_context_data(**kwargs) context = super(ReceberProposicao, self).get_context_data(**kwargs)
context['subnav_template_name'] = 'materia/subnav_prop.yaml'
return context return context
@ -918,18 +913,22 @@ class ProposicaoCrud(Crud):
'Proposição não possui texto. Use o botão "Editar com OnlyOffice" para criar o documento, ' 'Proposição não possui texto. Use o botão "Editar com OnlyOffice" para criar o documento, '
'e depois clique em "Salvar e Voltar" para garantir que o texto seja salvo antes de enviar.') 'e depois clique em "Salvar e Voltar" para garantir que o texto seja salvo antes de enviar.')
else: else:
receber_recibo = BaseAppConfig.attr(
'receber_recibo_proposicao')
if p.texto_articulado.exists(): if p.texto_articulado.exists():
ta = p.texto_articulado.first() ta = p.texto_articulado.first()
ta.privacidade = STATUS_TA_IMMUTABLE_RESTRICT ta.privacidade = STATUS_TA_IMMUTABLE_RESTRICT
ta.editing_locked = True ta.editing_locked = True
ta.save() ta.save()
receber_recibo = BaseAppConfig.attr(
'receber_recibo_proposicao')
if not receber_recibo: if not receber_recibo:
ta = p.texto_articulado.first()
p.hash_code = 'P' + ta.hash() + SEPARADOR_HASH_PROPOSICAO + str(p.pk) p.hash_code = 'P' + ta.hash() + SEPARADOR_HASH_PROPOSICAO + str(p.pk)
elif p.texto_original and not receber_recibo:
# Gerar hash para arquivos PDF/documentos
p.hash_code = gerar_hash_arquivo(
p.texto_original.path,
str(p.pk))
p.data_devolucao = None p.data_devolucao = None
p.data_envio = timezone.now() p.data_envio = timezone.now()
@ -1204,16 +1203,32 @@ class ProposicaoCrud(Crud):
def hook_header_status_proposicao(self): def hook_header_status_proposicao(self):
return 'Status' return 'Status'
def get_queryset(self):
qs = super().get_queryset()
status_filter = self.request.GET.get('status', '')
if status_filter == 'elaboracao':
qs = qs.filter(data_envio__isnull=True, cancelado=False)
elif status_filter == 'aguardando':
qs = qs.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False)
elif status_filter == 'incorporada':
qs = qs.filter(data_recebimento__isnull=False, cancelado=False)
elif status_filter == 'devolvida':
qs = qs.filter(data_devolucao__isnull=False, cancelado=False)
return qs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Primeiro, buscar os dados antes de chamar super() # Primeiro, buscar os dados antes de chamar super()
# pois get_rows modifica os objetos # pois get_rows modifica os objetos
qs = self.get_queryset() # Usar queryset base (sem filtro) para as estatísticas
qs_base = super().get_queryset()
stats = { stats = {
'total': qs.count(), 'total': qs_base.count(),
'elaboracao': qs.filter(data_envio__isnull=True, cancelado=False).count(), 'elaboracao': qs_base.filter(data_envio__isnull=True, cancelado=False).count(),
'aguardando': qs.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False).count(), 'aguardando': qs_base.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False).count(),
'incorporada': qs.filter(data_recebimento__isnull=False, cancelado=False).count(), 'incorporada': qs_base.filter(data_recebimento__isnull=False, cancelado=False).count(),
'devolvida': qs.filter(data_devolucao__isnull=False, cancelado=False).count(), 'devolvida': qs_base.filter(data_devolucao__isnull=False, cancelado=False).count(),
} }
# Processar proposições ANTES de chamar super() que chama get_rows # Processar proposições ANTES de chamar super() que chama get_rows
@ -1274,6 +1289,7 @@ class ProposicaoCrud(Crud):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['stats'] = stats context['stats'] = stats
context['proposicoes'] = proposicoes context['proposicoes'] = proposicoes
context['status_filter'] = self.request.GET.get('status', '')
# Paginação # Paginação
context['is_paginated'] = paginator.num_pages > 1 context['is_paginated'] = paginator.num_pages > 1
@ -1328,10 +1344,15 @@ class ReciboProposicaoView(TemplateView):
**kwargs) **kwargs)
proposicao = Proposicao.objects.get(pk=self.kwargs['pk']) proposicao = Proposicao.objects.get(pk=self.kwargs['pk'])
_hash = None
if proposicao.texto_original: if proposicao.texto_original:
_hash = gerar_hash_arquivo( import os
proposicao.texto_original.path, if os.path.exists(proposicao.texto_original.path):
self.kwargs['pk']) _hash = gerar_hash_arquivo(
proposicao.texto_original.path,
self.kwargs['pk'])
else:
_hash = proposicao.hash_code if proposicao.hash_code else 'ARQUIVO_NAO_ENCONTRADO'
elif proposicao.texto_articulado.exists(): elif proposicao.texto_articulado.exists():
ta = proposicao.texto_articulado.first() ta = proposicao.texto_articulado.first()
# FIXME hash para textos articulados # FIXME hash para textos articulados
@ -1404,8 +1425,6 @@ class HistoricoProposicaoView(PermissionRequiredMixin, ListView):
context['page_range'] = make_pagination( context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages) page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição' context['NO_ENTRIES_MSG'] = 'Nenhuma proposição'
if self.request.user.is_superuser:
context['subnav_template_name'] = 'materia/subnav_prop.yaml'
return context return context

21
sapl/templates/materia/prop_pendentes_list.html

@ -1,10 +1,13 @@
{% extends "base.html" %} {% extends "materia/proposicao_base.html" %}
{% load i18n %} {% load i18n %}
{% load tz %} {% load tz %}
{% load common_tags %} {% load common_tags %}
{% block base_content %} {% block base_content %}
<fieldset> <div class="proposicao-content">
<legend>Proposições Não Recebidas</legend> <div class="proposicao-header">
<h2>Proposicoes Pendentes</h2>
<p class="text-muted">Proposicoes enviadas aguardando recebimento</p>
</div>
{% if not object_list %} {% if not object_list %}
<p>{{ NO_ENTRIES_MSG }}</p> <p>{{ NO_ENTRIES_MSG }}</p>
{% else %} {% else %}
@ -29,6 +32,7 @@
{% if not AppConfig.receber_recibo_proposicao %} {% if not AppConfig.receber_recibo_proposicao %}
<th>Código do Documento</th> <th>Código do Documento</th>
{% endif %} {% endif %}
<th>Ações</th>
{% endwith %} {% endwith %}
</tr> </tr>
</thead> </thead>
@ -64,13 +68,22 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
<td>
{% if prop.hash_code %}
<a href="{% url 'sapl.materia:proposicao-confirmar' prop.hash_code|strip_hash prop.pk %}" class="btn btn-sm btn-primary" title="Receber Proposição">
<i class="fas fa-check"></i> Receber
</a>
{% else %}
<span class="text-muted">Sem hash</span>
{% endif %}
</td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
</fieldset> </div>
{% include 'paginacao.html'%} {% include 'paginacao.html'%}
{% endblock %} {% endblock %}

204
sapl/templates/materia/proposicao_base.html

@ -0,0 +1,204 @@
{% extends "base.html" %}
{% load i18n %}
{% block sections_nav %}{% endblock sections_nav %}
{% block content_subnav %}
<div class="proposicao-subnav">
<div class="container">
<nav class="proposicao-nav">
<a href="{% url 'sapl.materia:receber-proposicao' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/receber/' %} active{% endif %}">
<i class="fas fa-inbox"></i>
<span>Receber</span>
</a>
<a href="{% url 'sapl.materia:proposicao-pendente' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/pendente/' %} active{% endif %}">
<i class="fas fa-clock"></i>
<span>Pendentes</span>
</a>
<a href="{% url 'sapl.materia:proposicao-devolvida' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/devolvida/' %} active{% endif %}">
<i class="fas fa-undo"></i>
<span>Devolvidas</span>
</a>
<a href="{% url 'sapl.materia:proposicao-recebida' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/recebida/' %} active{% endif %}">
<i class="fas fa-check-circle"></i>
<span>Incorporadas</span>
</a>
<a href="{% url 'sapl.materia:historico-proposicao' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/historico' %} active{% endif %}">
<i class="fas fa-history"></i>
<span>Historico</span>
</a>
</nav>
</div>
</div>
<style>
.proposicao-subnav {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
padding: 0;
}
.proposicao-nav {
display: flex;
gap: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.proposicao-nav::-webkit-scrollbar {
display: none;
}
.proposicao-nav-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem 1.25rem;
color: #6c757d;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: all 0.15s ease;
}
.proposicao-nav-item:hover {
color: #495057;
background: rgba(0,0,0,0.02);
text-decoration: none;
}
.proposicao-nav-item.active {
color: #0d6efd;
border-bottom-color: #0d6efd;
background: #fff;
}
.proposicao-nav-item i {
font-size: 0.9rem;
opacity: 0.8;
}
.proposicao-nav-item.active i {
opacity: 1;
}
@media (max-width: 768px) {
.proposicao-nav-item {
padding: 0.75rem 1rem;
font-size: 0.8rem;
}
.proposicao-nav-item span {
display: none;
}
.proposicao-nav-item i {
font-size: 1.1rem;
}
}
/* Estilos de conteudo */
.proposicao-content {
padding-top: 1.5rem;
}
.proposicao-header {
margin-bottom: 1.5rem;
}
.proposicao-header h2 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.25rem;
color: #000 !important;
}
.proposicao-header p {
font-size: 0.875rem;
margin-bottom: 0;
}
/* Tabelas */
.proposicao-content .table {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.proposicao-content .table thead th {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.025em;
color: #000 !important;
padding: 0.875rem 1rem;
}
.proposicao-content .table thead th a {
color: #000 !important;
text-decoration: none;
}
.proposicao-content .table thead th a:hover {
color: #0d6efd !important;
text-decoration: underline;
}
.proposicao-content .table tbody td {
padding: 0.875rem 1rem;
vertical-align: middle;
font-size: 0.875rem;
}
.proposicao-content .table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,0.015);
}
.proposicao-content .table-hover tbody tr:hover {
background-color: rgba(13,110,253,0.04);
}
/* Formulario de receber */
.receber-form-container {
width: 100%;
background: #fff;
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.receber-form-container fieldset {
border: none;
padding: 0;
margin: 0;
}
.receber-form-container legend {
font-size: 1.1rem;
font-weight: 600;
color: #495057;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e9ecef;
}
/* Mensagem vazia */
.proposicao-content > p {
color: #6c757d;
font-style: italic;
}
</style>
{% endblock content_subnav %}
{% block title %}{% endblock title %}

6
sapl/templates/materia/proposicao_detail.html

@ -2,11 +2,13 @@
{% load i18n common_tags %} {% load i18n common_tags %}
{% load tz %} {% load tz %}
{% block title %}{% endblock title %}
{% block webpack_loader_chunks_css %} {% block webpack_loader_chunks_css %}
{{ block.super }} {{ block.super }}
<style> <style>
.prop-detail { .prop-detail {
max-width: 800px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 20px 0; padding: 20px 0;
} }
@ -260,7 +262,7 @@
{% block base_content %} {% block base_content %}
<div class="prop-detail"> <div class="prop-detail">
{% if user in object.autor.operadores.all %} {% if user in object.autor.operadores.all or user.is_superuser or perms.materia.detail_proposicao %}
<!-- Header --> <!-- Header -->
<div class="prop-detail-header"> <div class="prop-detail-header">
<a href="{% url 'sapl.materia:proposicao_list' %}" class="prop-back"> <a href="{% url 'sapl.materia:proposicao_list' %}" class="prop-back">

48
sapl/templates/materia/proposicao_list.html

@ -1,11 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n common_tags crispy_forms_tags %} {% load i18n common_tags crispy_forms_tags %}
{% block title %}{% endblock title %}
{% block webpack_loader_chunks_css %} {% block webpack_loader_chunks_css %}
{{ block.super }} {{ block.super }}
<style> <style>
.prop-page { .prop-page {
max-width: 900px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
@ -27,15 +29,35 @@
.prop-stats { .prop-stats {
display: flex; display: flex;
gap: 16px; gap: 8px;
font-size: 0.85rem; font-size: 0.85rem;
color: #666;
} }
.prop-stats span { .prop-stats a {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
padding: 6px 12px;
border-radius: 20px;
text-decoration: none;
color: #666;
background: #f5f5f5;
transition: all 0.15s;
}
.prop-stats a:hover {
background: #e8e8e8;
color: #333;
text-decoration: none;
}
.prop-stats a.active {
background: #1976d2;
color: #fff;
}
.prop-stats a.active:hover {
background: #1565c0;
} }
.prop-actions { .prop-actions {
@ -185,9 +207,21 @@
<div class="prop-header"> <div class="prop-header">
<h2>{% trans "Proposições" %}</h2> <h2>{% trans "Proposições" %}</h2>
<div class="prop-stats"> <div class="prop-stats">
<span><strong>{{ stats.total|default:"0" }}</strong> total</span> <a href="?" class="{% if not status_filter %}active{% endif %}">
<span><strong>{{ stats.elaboracao|default:"0" }}</strong> em elaboração</span> <strong>{{ stats.total|default:"0" }}</strong> total
<span><strong>{{ stats.aguardando|default:"0" }}</strong> enviadas</span> </a>
<a href="?status=elaboracao" class="{% if status_filter == 'elaboracao' %}active{% endif %}">
<strong>{{ stats.elaboracao|default:"0" }}</strong> em elaboração
</a>
<a href="?status=aguardando" class="{% if status_filter == 'aguardando' %}active{% endif %}">
<strong>{{ stats.aguardando|default:"0" }}</strong> enviadas
</a>
<a href="?status=incorporada" class="{% if status_filter == 'incorporada' %}active{% endif %}">
<strong>{{ stats.incorporada|default:"0" }}</strong> incorporadas
</a>
<a href="?status=devolvida" class="{% if status_filter == 'devolvida' %}active{% endif %}">
<strong>{{ stats.devolvida|default:"0" }}</strong> devolvidas
</a>
</div> </div>
</div> </div>

15
sapl/templates/materia/receber_proposicao.html

@ -0,0 +1,15 @@
{% extends "materia/proposicao_base.html" %}
{% load i18n crispy_forms_tags %}
{% block base_content %}
<div class="proposicao-content">
<div class="proposicao-header">
<h2>Receber Proposicao</h2>
<p class="text-muted">Informe o codigo do recibo para validar e receber a proposicao</p>
</div>
<div class="receber-form-container">
{% crispy form %}
</div>
</div>
{% endblock base_content %}
Loading…
Cancel
Save