Browse Source

Tela de pesquisa de AuditLog (#3622)

* Tela de pesquisa de AuditLog

* Add template tags

* Corrige erro em paginação
pull/3628/merge
Edward 2 years ago
committed by Edward Oliveira
parent
commit
ac1a3a9b06
  1. 3
      docker/start.sh
  2. 44
      sapl/base/forms.py
  3. 38
      sapl/base/management/commands/backfill_auditlog.py
  4. 23
      sapl/base/migrations/0056_auto_20221118_1330.py
  5. 7
      sapl/base/models.py
  6. 16
      sapl/base/receivers.py
  7. 30
      sapl/base/templatetags/common_tags.py
  8. 4
      sapl/base/urls.py
  9. 75
      sapl/base/views.py
  10. 88
      sapl/templates/base/auditlog_filter.html
  11. 3
      sapl/templates/navbar.yaml

3
docker/start.sh

@ -114,6 +114,9 @@ if [ $lack_pwd -eq 0 ]; then
# return -1 # return -1
fi fi
# Backfilling AuditLog's JSON field
time ./manage.py backfill_auditlog &
echo "-------------------------------------" echo "-------------------------------------"
echo "| ███████╗ █████╗ ██████╗ ██╗ |" echo "| ███████╗ █████╗ ██████╗ ██╗ |"
echo "| ██╔════╝██╔══██╗██╔══██╗██║ |" echo "| ██╔════╝██╔══██╗██╔══██╗██║ |"

44
sapl/base/forms.py

@ -21,7 +21,7 @@ import django_filters
from haystack.forms import ModelSearchForm from haystack.forms import ModelSearchForm
from sapl.audiencia.models import AudienciaPublica from sapl.audiencia.models import AudienciaPublica
from sapl.base.models import Autor, TipoAutor, OperadorAutor from sapl.base.models import Autor, AuditLog, TipoAutor, OperadorAutor
from sapl.comissoes.models import Reuniao from sapl.comissoes.models import Reuniao
from sapl.crispy_layout_mixin import (form_actions, to_column, to_row, from sapl.crispy_layout_mixin import (form_actions, to_column, to_row,
SaplFormHelper, SaplFormLayout) SaplFormHelper, SaplFormLayout)
@ -741,6 +741,48 @@ class AutorFilterSet(django_filters.FilterSet):
form_actions(label='Pesquisar'))) form_actions(label='Pesquisar')))
def get_username():
return [(u, u) for u in get_user_model().objects.all().order_by('username').values_list('username', flat=True)]
def get_models():
return [(m, m) for m in AuditLog.objects.distinct('model_name').order_by('model_name').values_list('model_name', flat=True)]
class AuditLogFilterSet(django_filters.FilterSet):
OPERATION_CHOICES = (
('U', 'Atualizado'),
('C', 'Criado'),
('D', 'Excluído'),
)
username = django_filters.ChoiceFilter(choices=get_username(), label=_('Usuário'))
object_id = django_filters.NumberFilter(label=_('Id'))
operation = django_filters.ChoiceFilter(choices=OPERATION_CHOICES, label=_('Operação'))
model_name = django_filters.ChoiceFilter(choices=get_models, label=_('Tipo de Registro'))
timestamp = django_filters.DateRangeFilter(label=_('Período'))
class Meta:
model = AuditLog
fields = ['username', 'operation', 'model_name', 'timestamp', 'object_id']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
row0 = to_row([('username', 2),
('operation', 2),
('model_name', 4),
('object_id', 2),
('timestamp', 2)])
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Filtros'),
row0,
form_actions(label='Aplicar Filtro')))
class OperadorAutorForm(ModelForm): class OperadorAutorForm(ModelForm):
class Meta: class Meta:

38
sapl/base/management/commands/backfill_auditlog.py

@ -0,0 +1,38 @@
import json
import logging
from django.core.management.base import BaseCommand
from sapl.base.models import AuditLog
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def handle(self, **options):
print("Backfilling AuditLog JSON Field...")
logs = AuditLog.objects.filter(data__isnull=True)
error_counter = 0
if logs:
update_list = []
for log in logs:
try:
obj = log.object[1:-1] \
if log.object.startswith('[') else log.object
data = json.loads(obj)
log.data = data
except Exception as e:
error_counter += 1
logging.error(e)
log.data = None
else:
update_list.append(log)
if len(update_list) == 1000:
AuditLog.objects.bulk_update(update_list, ['data'])
update_list = []
if update_list:
AuditLog.objects.bulk_update(update_list, ['data'])
print(f"Logs backfilled: {len(logs) - error_counter}")
print(f"Logs with errors: {error_counter}")
print("Finished backfilling")

23
sapl/base/migrations/0056_auto_20221118_1330.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2022-11-18 16:30
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0055_appconfig_mostrar_voto'),
]
operations = [
migrations.AlterModelOptions(
name='auditlog',
options={'ordering': ('-id', '-timestamp'), 'verbose_name': 'AuditLog', 'verbose_name_plural': 'AuditLogs'},
),
migrations.AddField(
model_name='auditlog',
name='data',
field=django.contrib.postgres.fields.jsonb.JSONField(null=True, verbose_name='data'),
),
]

7
sapl/base/models.py

@ -419,12 +419,15 @@ class AuditLog(models.Model):
db_index=True) db_index=True)
timestamp = models.DateTimeField(verbose_name=_('timestamp'), timestamp = models.DateTimeField(verbose_name=_('timestamp'),
db_index=True) db_index=True)
# DEPRECATED FIELD! TO BE REMOVED (EVENTUALLY)
object = models.CharField(max_length=MAX_DATA_LENGTH, object = models.CharField(max_length=MAX_DATA_LENGTH,
blank=True, blank=True,
verbose_name=_('object')) verbose_name=_('object'))
data = JSONField(null=True, verbose_name=_('data'))
object_id = models.PositiveIntegerField(verbose_name=_('object_id'), object_id = models.PositiveIntegerField(verbose_name=_('object_id'),
db_index=True) db_index=True)
model_name = models.CharField(max_length=100, verbose_name=_('model'), model_name = models.CharField(max_length=100,
verbose_name=_('model'),
db_index=True) db_index=True)
app_name = models.CharField(max_length=100, app_name = models.CharField(max_length=100,
verbose_name=_('app'), verbose_name=_('app'),
@ -433,7 +436,7 @@ class AuditLog(models.Model):
class Meta: class Meta:
verbose_name = _('AuditLog') verbose_name = _('AuditLog')
verbose_name_plural = _('AuditLogs') verbose_name_plural = _('AuditLogs')
ordering = ('-id',) ordering = ('-id', '-timestamp')
def __str__(self): def __str__(self):
return "[%s] %s %s.%s %s" % (self.timestamp, return "[%s] %s %s.%s %s" % (self.timestamp,

16
sapl/base/receivers.py

@ -120,10 +120,15 @@ def audit_log_function(sender, **kwargs):
model_name = instance.__class__.__name__ model_name = instance.__class__.__name__
app_name = instance._meta.app_label app_name = instance._meta.app_label
object_id = instance.id object_id = instance.id
data = serializers.serialize('json', [instance]) try:
import json
if len(data) > AuditLog.MAX_DATA_LENGTH: # [1:-1] below removes the surrounding square brackets
data = data[:AuditLog.MAX_DATA_LENGTH] str_data = serializers.serialize('json', [instance])[1:-1]
data = json.loads(str_data)
except:
# old version capped string at AuditLog.MAX_DATA_LENGTH
# so there can be invalid json fields in Prod.
data = None
if user: if user:
username = user.username username = user.username
@ -136,7 +141,8 @@ def audit_log_function(sender, **kwargs):
app_name=app_name, app_name=app_name,
timestamp=timezone.now(), timestamp=timezone.now(),
object_id=object_id, object_id=object_id,
object=data) object='',
data=data)
except Exception as e: except Exception as e:
logger.error('Error saving auditing log object') logger.error('Error saving auditing log object')
logger.error(e) logger.error(e)

30
sapl/base/templatetags/common_tags.py

@ -29,6 +29,17 @@ def define(arg):
return arg return arg
@register.simple_tag
def describe_operation(value):
if value == "C":
return "Criar"
elif value == "D":
return "Apagar"
elif value == "U":
return "Atualizar"
return ""
@register.simple_tag @register.simple_tag
def field_verbose_name(instance, field_name): def field_verbose_name(instance, field_name):
return instance._meta.get_field(field_name).verbose_name return instance._meta.get_field(field_name).verbose_name
@ -51,6 +62,25 @@ def model_verbose_name_plural(class_name):
model = get_class(class_name) model = get_class(class_name)
return model._meta.verbose_name_plural return model._meta.verbose_name_plural
@register.filter
def obfuscate_value(value, key):
if key in ["hash", "google_recaptcha_secret_key", "password", "google_recaptcha_site_key", "hash_code"]:
return "***************"
return value
@register.filter
def desc_operation(value):
if value == "C":
return "Criado"
elif value == "D":
return "Excluido"
elif value == "U":
return "Atualizado"
return ""
@register.filter @register.filter
def format_user(user): def format_user(user):
if user.first_name: if user.first_name:

4
sapl/base/urls.py

@ -15,7 +15,7 @@ from sapl.settings import MEDIA_URL, LOGOUT_REDIRECT_URL
from .apps import AppConfig from .apps import AppConfig
from .forms import LoginForm from .forms import LoginForm
from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud, from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
HelpTopicView, LogotipoView, RelatorioAtasView, HelpTopicView, LogotipoView, RelatorioAtasView, PesquisarAuditLogView,
RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView, RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView,
RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView, RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView,
RelatorioMateriasTramitacaoView, RelatorioPresencaSessaoView, RelatorioReuniaoView, SaplSearchView, RelatorioMateriasTramitacaoView, RelatorioPresencaSessaoView, RelatorioReuniaoView, SaplSearchView,
@ -179,6 +179,8 @@ urlpatterns = [
url(r'^sistema/search/', SaplSearchView(), name='haystack_search'), url(r'^sistema/search/', SaplSearchView(), name='haystack_search'),
url(r'^sistema/auditlog/$', PesquisarAuditLogView.as_view(), name='pesquisar_auditlog'),
# Folhas XSLT e extras referenciadas por documentos migrados do sapl 2.5 # Folhas XSLT e extras referenciadas por documentos migrados do sapl 2.5
url(r'^(sapl/)?XSLT/HTML/(?P<path>.*)$', RedirectView.as_view( url(r'^(sapl/)?XSLT/HTML/(?P<path>.*)$', RedirectView.as_view(
url=os.path.join(MEDIA_URL, 'sapl/public/XSLT/HTML/%(path)s'), url=os.path.join(MEDIA_URL, 'sapl/public/XSLT/HTML/%(path)s'),

75
sapl/base/views.py

@ -39,9 +39,9 @@ from ratelimit.decorators import ratelimit
from sapl import settings from sapl import settings
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica
from sapl.base.forms import (AutorForm, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm, from sapl.base.forms import (AutorForm, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm,
NovaSenhaForm, UserAdminForm, NovaSenhaForm, UserAdminForm, AuditLogFilterSet,
OperadorAutorForm, LoginForm, SaplSearchForm) OperadorAutorForm, LoginForm, SaplSearchForm)
from sapl.base.models import Autor, TipoAutor, OperadorAutor from sapl.base.models import AuditLog, Autor, TipoAutor, OperadorAutor
from sapl.comissoes.models import Comissao, Reuniao from sapl.comissoes.models import Comissao, Reuniao
from sapl.crud.base import CrudAux, make_pagination, Crud,\ from sapl.crud.base import CrudAux, make_pagination, Crud,\
ListWithSearchForm, MasterDetailCrud ListWithSearchForm, MasterDetailCrud
@ -2256,6 +2256,77 @@ class SaplSearchView(SearchView):
return context return context
class PesquisarAuditLogView(FilterView):
model = AuditLog
filterset_class = AuditLogFilterSet
paginate_by = 20
permission_required = ('base.list_appconfig',)
def get_filterset_kwargs(self, filterset_class):
super(PesquisarAuditLogView, self).get_filterset_kwargs(
filterset_class
)
return ({
"data": self.request.GET or None,
"queryset": self.get_queryset().order_by("-id")
})
def get_context_data(self, **kwargs):
context = super(PesquisarAuditLogView, self).get_context_data(
**kwargs
)
paginator = context["paginator"]
page_obj = context["page_obj"]
qr = self.request.GET.copy()
if 'page' in qr:
del qr['page']
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
context.update({
"page_range": make_pagination(
page_obj.number, paginator.num_pages
),
"NO_ENTRIES_MSG": "Nenhum registro de log encontrado!",
"title": _("Pesquisar Logs de Auditoria")
})
return context
def get(self, request, *args, **kwargs):
super(PesquisarAuditLogView, self).get(request)
data = self.filterset.data
url = ''
if data:
url = '&' + str(self.request.META["QUERY_STRING"])
if url.startswith("&page"):
url = ''
resultados = self.object_list
# if 'page' in self.request.META['QUERY_STRING']:
# resultados = self.object_list
# else:
# resultados = []
context = self.get_context_data(filter=self.filterset,
object_list=resultados,
filter_url=url,
numero_res=len(resultados)
)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context)
class AlterarSenha(FormView): class AlterarSenha(FormView):
from sapl.settings import LOGIN_URL from sapl.settings import LOGIN_URL

88
sapl/templates/base/auditlog_filter.html

@ -0,0 +1,88 @@
{% extends "crud/list.html" %}
{% load i18n common_tags %}
{% load tz %}
{% load crispy_forms_tags staticfiles %}
{% block head_extra_css %}
created {
background-color: green;
color: #FFF;
}
deleted {
background-color: red;
color: #FFF;
}
{% endblock head_extra_css %}
{% block base_content %}
{% crispy filter.form %}
<br>
{% if numero_res > 0 %}
{% if numero_res == 1 %}
<h3>Foi encontrado {{ numero_res }} resultado</h3>
{% else %}
<h3>Foram encontrados {{ numero_res }} resultados</h3>
{% endif %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Data/Hora</th>
<th>Usuário</th>
<th>Operação</th>
<th>Registro</th>
<th>Id</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for obj in page_obj %}
<tr class="background:{%if obj.operation == 'D' %}red{%else%}lightgray{%endif%}">
<td>{{ obj.timestamp|localtime|date:"d/m/Y, H:i:s" }}</td>
<td>{{ obj.username|default:"Não informado" }}</td>
<td>{{ obj.operation|desc_operation }}</td>
<td>{{ obj.model_name }}</td>
<td>{{obj.data.pk}}</td>
<td>
<strong>Atributos ({{obj.data.fields|length}})</strong><br/>
<hr/>
<ul>
{% for key, value in obj.data.fields.items %}
{% if forloop.counter == 11 %}
<div id="{{obj.id}}" style="display:none;">
{%endif%}
<li>
{{key}}: {{ value|default_if_none:""|obfuscate_value:key }}<br/>
</li>
{% if forloop.last and forloop.counter > 10 %}
</div>
<input class="btn btn-primary btn-sm" type="button" value="Expandir/Colapsar" onclick="toggleDetails({{obj.id}})"/>
{% endif %}
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<font size="4"><p align="center">{{ NO_ENTRIES_MSG }}</p></font>
{% endif %}
<br/>
{% include 'paginacao.html'%}
<br /><br /><br />
{% endblock base_content %}
{% block extra_js %}
<script language="Javascript">
function toggleDetails(id) {
let curr = document.getElementById(id);
if (curr.style.display == "none") {
document.getElementById(id).style.display = "block";
}
else {
document.getElementById(id).style.display = "none";
}
}
</script>
{% endblock extra_js %}

3
sapl/templates/navbar.yaml

@ -97,6 +97,9 @@
- title: {% trans 'Inconsistências de Dados' %} - title: {% trans 'Inconsistências de Dados' %}
url: {% url 'sapl.base:lista_inconsistencias' %} url: {% url 'sapl.base:lista_inconsistencias' %}
check_permission: user.is_superuser check_permission: user.is_superuser
- title: {% trans 'Logs de Auditoria' %}
url: {% url 'sapl.base:pesquisar_auditlog' %}
check_permission: user.is_superuser
{% comment %} {% comment %}
<li class="nav__sub-item"><a class="nav__sub-link" href="#">Provedor LexML</a></li> <li class="nav__sub-item"><a class="nav__sub-link" href="#">Provedor LexML</a></li>

Loading…
Cancel
Save