Browse Source

Tela de pesquisa de AuditLog

pull/3622/head
Edward Ribeiro 2 years ago
parent
commit
1e3bf10384
  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. 4
      sapl/base/urls.py
  8. 69
      sapl/base/views.py
  9. 89
      sapl/templates/base/auditlog_filter.html
  10. 3
      sapl/templates/navbar.yaml

3
docker/start.sh

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

44
sapl/base/forms.py

@ -21,7 +21,7 @@ import django_filters
from haystack.forms import ModelSearchForm
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.crispy_layout_mixin import (form_actions, to_column, to_row,
SaplFormHelper, SaplFormLayout)
@ -741,6 +741,48 @@ class AutorFilterSet(django_filters.FilterSet):
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 = (
('C', 'Criar'),
('D', 'Apagar'),
('U', 'Atualizar'),
)
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=_('Objeto'))
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', 3),
('operation', 2),
('model_name', 4),
('object_id', 1),
('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 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)
timestamp = models.DateTimeField(verbose_name=_('timestamp'),
db_index=True)
# DEPRECATED FIELD! TO BE REMOVED (EVENTUALLY)
object = models.CharField(max_length=MAX_DATA_LENGTH,
blank=True,
verbose_name=_('object'))
data = JSONField(null=True, verbose_name=_('data'))
object_id = models.PositiveIntegerField(verbose_name=_('object_id'),
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)
app_name = models.CharField(max_length=100,
verbose_name=_('app'),
@ -433,7 +436,7 @@ class AuditLog(models.Model):
class Meta:
verbose_name = _('AuditLog')
verbose_name_plural = _('AuditLogs')
ordering = ('-id',)
ordering = ('-id', '-timestamp')
def __str__(self):
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__
app_name = instance._meta.app_label
object_id = instance.id
data = serializers.serialize('json', [instance])
if len(data) > AuditLog.MAX_DATA_LENGTH:
data = data[:AuditLog.MAX_DATA_LENGTH]
try:
import json
# [1:-1] below removes the surrounding square brackets
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:
username = user.username
@ -136,7 +141,8 @@ def audit_log_function(sender, **kwargs):
app_name=app_name,
timestamp=timezone.now(),
object_id=object_id,
object=data)
object='',
data=data)
except Exception as e:
logger.error('Error saving auditing log object')
logger.error(e)

4
sapl/base/urls.py

@ -15,7 +15,7 @@ from sapl.settings import MEDIA_URL, LOGOUT_REDIRECT_URL
from .apps import AppConfig
from .forms import LoginForm
from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
HelpTopicView, LogotipoView, RelatorioAtasView,
HelpTopicView, LogotipoView, RelatorioAtasView, PesquisarAuditLogView,
RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView,
RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView,
RelatorioMateriasTramitacaoView, RelatorioPresencaSessaoView, RelatorioReuniaoView, SaplSearchView,
@ -179,6 +179,8 @@ urlpatterns = [
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
url(r'^(sapl/)?XSLT/HTML/(?P<path>.*)$', RedirectView.as_view(
url=os.path.join(MEDIA_URL, 'sapl/public/XSLT/HTML/%(path)s'),

69
sapl/base/views.py

@ -39,9 +39,9 @@ from ratelimit.decorators import ratelimit
from sapl import settings
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica
from sapl.base.forms import (AutorForm, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm,
NovaSenhaForm, UserAdminForm,
NovaSenhaForm, UserAdminForm, AuditLogFilterSet,
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.crud.base import CrudAux, make_pagination, Crud,\
ListWithSearchForm, MasterDetailCrud
@ -2256,6 +2256,71 @@ class SaplSearchView(SearchView):
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"]
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):
from sapl.settings import LOGIN_URL

89
sapl/templates/base/auditlog_filter.html

@ -0,0 +1,89 @@
{% extends "crud/list.html" %}
{% load i18n %}
{% 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>Objeto</th>
<th>Id</th>
<th>Campos</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 }}</td>
<td>{{ obj.model_name }}</td>
<td>{{obj.data.pk}}</td>
<td>
<strong>Campos ({{obj.data.fields|length}})</strong><br/>
<hr/>
{% for key, value in obj.data.fields.items %}
<ul>
<li>
{{key}}:
{% if key == "password" %}*********{%else%}{{value|default_if_none:""}}{%endif%}<br/>
</li>
</ul>
{% if forloop.counter == 10 %}
<div id="{{obj.id}}" style="display:none;">
{%endif%}
{% 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 %}
</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' %}
url: {% url 'sapl.base:lista_inconsistencias' %}
check_permission: user.is_superuser
- title: {% trans 'Logs de Auditoria' %}
url: {% url 'sapl.base:pesquisar_auditlog' %}
check_permission: user.is_superuser
{% comment %}
<li class="nav__sub-item"><a class="nav__sub-link" href="#">Provedor LexML</a></li>

Loading…
Cancel
Save