@ -56,6 +56,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'core.middleware.get_request.RequestMiddleware',
|
||||
]
|
||||
|
||||
|
||||
|
@ -20,6 +20,7 @@ from django.contrib.auth import views as auth_views
|
||||
from django.urls import include, path
|
||||
|
||||
from .views import HomeView
|
||||
from core.views import history
|
||||
|
||||
urlpatterns = [
|
||||
path('', HomeView.as_view(), name='home'),
|
||||
@ -29,7 +30,7 @@ urlpatterns = [
|
||||
path("account/", include("django.contrib.auth.urls")),
|
||||
path("organization/", include("access.urls")),
|
||||
path("itam/", include("itam.urls")),
|
||||
path("history/<str:model_name>/<int:model_pk>", HomeView.as_view(), name='_history'),
|
||||
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
|
||||
|
||||
]
|
||||
|
||||
|
0
app/core/middleware/__init__.py
Normal file
0
app/core/middleware/__init__.py
Normal file
21
app/core/middleware/get_request.py
Normal file
21
app/core/middleware/get_request.py
Normal file
@ -0,0 +1,21 @@
|
||||
import threading
|
||||
|
||||
request_local = threading.local()
|
||||
|
||||
def get_request():
|
||||
return getattr(request_local, 'request', None)
|
||||
|
||||
class RequestMiddleware():
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
request_local.request = request
|
||||
return self.get_response(request)
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
request_local.request = None
|
||||
|
||||
def process_template_response(self, request, response):
|
||||
request_local.request = None
|
||||
return response
|
44
app/core/migrations/0003_alter_notes_note_history.py
Normal file
44
app/core/migrations/0003_alter_notes_note_history.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.0.6 on 2024-05-23 03:08
|
||||
|
||||
import access.fields
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0001_initial'),
|
||||
('core', '0002_remove_notes_serial_number_alter_notes_device_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notes',
|
||||
name='note',
|
||||
field=models.TextField(blank=True, default=None, null=True, verbose_name='Note'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='History',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('before', models.TextField(blank=True, default=None, help_text='JSON Object before Change', null=True)),
|
||||
('after', models.TextField(blank=True, default=None, help_text='JSON Object After Change', null=True)),
|
||||
('action', models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, null=True)),
|
||||
('item_pk', models.IntegerField(default=None, null=True)),
|
||||
('item_class', models.CharField(default=None, max_length=50, null=True)),
|
||||
('item_parent_pk', models.IntegerField(default=None, null=True)),
|
||||
('item_parent_class', models.CharField(default=None, max_length=50, null=True)),
|
||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
),
|
||||
]
|
146
app/core/mixin/history_save.py
Normal file
146
app/core/mixin/history_save.py
Normal file
@ -0,0 +1,146 @@
|
||||
import json
|
||||
|
||||
from django.db import models
|
||||
|
||||
from core.middleware.get_request import get_request
|
||||
from core.models.history import History
|
||||
|
||||
|
||||
class SaveHistory(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return [ f.name for f in self._meta.fields + self._meta.many_to_many ]
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" OverRides save for keeping model history.
|
||||
|
||||
Not a Full-Override as this is just to add to existing.
|
||||
|
||||
Before to fetch from DB to ensure the changed value is the actual changed value and the after
|
||||
is the data that was saved to the DB.
|
||||
"""
|
||||
|
||||
remove_keys = [
|
||||
'_state',
|
||||
'created',
|
||||
'modified'
|
||||
]
|
||||
before = {}
|
||||
|
||||
try:
|
||||
before = self.__class__.objects.get(pk=self.pk).__dict__.copy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
clean = {}
|
||||
for entry in before:
|
||||
|
||||
if type(before[entry]) == type(int()):
|
||||
|
||||
value = int(before[entry])
|
||||
|
||||
elif type(before[entry]) == type(bool()):
|
||||
|
||||
value = bool(before[entry])
|
||||
|
||||
else:
|
||||
|
||||
value = str(before[entry])
|
||||
|
||||
|
||||
if entry not in remove_keys:
|
||||
clean[entry] = value
|
||||
|
||||
before_json = json.dumps(clean)
|
||||
|
||||
# Process the save
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
after = self.__dict__.copy()
|
||||
|
||||
clean = {}
|
||||
for entry in after:
|
||||
|
||||
if type(after[entry]) == type(int()):
|
||||
|
||||
value = int(after[entry])
|
||||
|
||||
elif type(after[entry]) == type(bool()):
|
||||
|
||||
value = bool(after[entry])
|
||||
|
||||
else:
|
||||
|
||||
value = str(after[entry])
|
||||
|
||||
|
||||
if entry not in remove_keys and str(before) != '{}':
|
||||
|
||||
if after[entry] != before[entry]:
|
||||
clean[entry] = value
|
||||
|
||||
elif entry not in remove_keys:
|
||||
|
||||
clean[entry] = value
|
||||
|
||||
|
||||
after = json.dumps(clean)
|
||||
|
||||
item_parent_pk = None
|
||||
item_parent_class = None
|
||||
|
||||
if self._meta.model_name == 'deviceoperatingsystem':
|
||||
|
||||
item_parent_pk = self.device.pk
|
||||
item_parent_class = self.device._meta.model_name
|
||||
|
||||
if self._meta.model_name == 'devicesoftware':
|
||||
|
||||
item_parent_pk = self.device.pk
|
||||
item_parent_class = self.device._meta.model_name
|
||||
|
||||
if self._meta.model_name == 'operatingsystemversion':
|
||||
|
||||
item_parent_pk = self.operating_system_id
|
||||
item_parent_class = self.operating_system._meta.model_name
|
||||
|
||||
|
||||
if self._meta.model_name == 'softwareversion':
|
||||
|
||||
item_parent_pk = self.software.pk
|
||||
item_parent_class = self.software._meta.model_name
|
||||
|
||||
|
||||
if not before:
|
||||
|
||||
action = History.Actions.ADD
|
||||
|
||||
elif before != after:
|
||||
|
||||
action = History.Actions.UPDATE
|
||||
|
||||
elif not after:
|
||||
|
||||
action = History.Actions.DELETE
|
||||
|
||||
|
||||
if before != after and after != '{}':
|
||||
entry = History.objects.create(
|
||||
organization = self.organization,
|
||||
is_global = False,
|
||||
before = before_json,
|
||||
after = after,
|
||||
user = get_request().user,
|
||||
action = action,
|
||||
item_pk = self.pk,
|
||||
item_class = self._meta.model_name,
|
||||
item_parent_pk = item_parent_pk,
|
||||
item_parent_class = item_parent_class,
|
||||
)
|
||||
|
||||
entry.save()
|
96
app/core/models/history.py
Normal file
96
app/core/models/history.py
Normal file
@ -0,0 +1,96 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
|
||||
|
||||
|
||||
class NotesCommonFields(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
|
||||
|
||||
class History(TenancyObject, NotesCommonFields):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'-created'
|
||||
]
|
||||
|
||||
|
||||
class Actions(models.TextChoices):
|
||||
ADD = '1', 'Create'
|
||||
UPDATE = '2', 'Update'
|
||||
DELETE = '3', 'Delete'
|
||||
|
||||
|
||||
before = models.TextField(
|
||||
help_text = 'JSON Object before Change',
|
||||
blank = True,
|
||||
default = None,
|
||||
null = True
|
||||
)
|
||||
|
||||
|
||||
after = models.TextField(
|
||||
help_text = 'JSON Object After Change',
|
||||
blank = True,
|
||||
default = None,
|
||||
null = True
|
||||
)
|
||||
|
||||
|
||||
action = models.IntegerField(
|
||||
choices=Actions,
|
||||
default=None,
|
||||
null=True,
|
||||
blank = False,
|
||||
)
|
||||
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.DO_NOTHING,
|
||||
blank= False,
|
||||
)
|
||||
|
||||
item_pk = models.IntegerField(
|
||||
default=None,
|
||||
null = True,
|
||||
blank = False,
|
||||
)
|
||||
|
||||
item_class = models.CharField(
|
||||
blank = False,
|
||||
default=None,
|
||||
null = True,
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
)
|
||||
|
||||
item_parent_pk = models.IntegerField(
|
||||
default=None,
|
||||
null = True,
|
||||
blank = False,
|
||||
)
|
||||
|
||||
item_parent_class = models.CharField(
|
||||
blank = False,
|
||||
default=None,
|
||||
null = True,
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
)
|
@ -44,7 +44,7 @@ class Notes(NotesCommonFields):
|
||||
|
||||
note = models.TextField(
|
||||
verbose_name = 'Note',
|
||||
blank = False,
|
||||
blank = True,
|
||||
default = None,
|
||||
null = True
|
||||
)
|
||||
|
60
app/core/templates/history.html.j2
Normal file
60
app/core/templates/history.html.j2
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% load json %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<script>
|
||||
|
||||
$('.clicker').click(function(){
|
||||
$(this).nextUntil('.clicker').slideToggle('normal');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.hidden {
|
||||
/*display: none;*/
|
||||
}
|
||||
.down {
|
||||
display: unset;
|
||||
}
|
||||
</style>
|
||||
<table style="max-width: 100%;">
|
||||
<thead>
|
||||
<th style="width: 25%;">Created</th>
|
||||
<th style="width: 25%;">Action</th>
|
||||
<th style="width: 25%;">Item</th>
|
||||
<th style="width: 25%;">User</th>
|
||||
</thead>
|
||||
{% for entry in history %}
|
||||
<tr class="clicker">
|
||||
<td>{{ entry.created }}</td>
|
||||
<td>
|
||||
{% if entry.action == 1 %}
|
||||
Create
|
||||
{% elif entry.action == 2 %}
|
||||
Update
|
||||
{% elif entry.action == 3 %}
|
||||
Delete
|
||||
{% else %}
|
||||
fuck knows
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ entry.item_class}}
|
||||
</td>
|
||||
<td>{{ entry.user }}</td>
|
||||
<tr class="hidden">
|
||||
<th colspan="2">Before</th>
|
||||
<th colspan="2">Changed</th>
|
||||
</tr>
|
||||
<tr class="hidden">
|
||||
<td colspan="2"><pre style="text-align: left; max-width: 300px;">{{ entry.before | json_pretty }}</pre></td>
|
||||
<td colspan="2"><pre style="text-align: left; max-width: 300px;">{{ entry.after | json_pretty }}</pre></td>
|
||||
</tr>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
13
app/core/templatetags/json.py
Normal file
13
app/core/templatetags/json.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
|
||||
import json
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
def json_pretty(value):
|
||||
|
||||
return json.dumps(json.loads(value), indent=4, sort_keys=True)
|
40
app/core/views/history.py
Normal file
40
app/core/views/history.py
Normal file
@ -0,0 +1,40 @@
|
||||
import markdown
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
from itam.models.device import Device, DeviceSoftware, DeviceOperatingSystem
|
||||
from itam.models.software import Software
|
||||
|
||||
|
||||
class View(OrganizationPermission, generic.View):
|
||||
|
||||
permission_required = [
|
||||
'itam.view_softwareversion'
|
||||
]
|
||||
|
||||
template_name = 'history.html.j2'
|
||||
|
||||
|
||||
def get(self, request, model_name, model_pk):
|
||||
if not request.user.is_authenticated and settings.LOGIN_REQUIRED:
|
||||
return redirect(f"{settings.LOGIN_URL}?next={request.path}")
|
||||
|
||||
context = {}
|
||||
|
||||
context['history'] = History.objects.filter(
|
||||
Q(item_pk = model_pk, item_class = model_name)
|
||||
|
|
||||
Q(item_parent_pk = model_pk, item_parent_class = model_name)
|
||||
)
|
||||
|
||||
context['content_title'] = 'History'
|
||||
|
||||
return render(request, self.template_name, context)
|
Reference in New Issue
Block a user