mirror of
https://gitlab.com/animath/si/plateforme-corres2math.git
synced 2024-12-05 02:06:52 +00:00
Log DB modifications
This commit is contained in:
parent
ca4b0729e7
commit
7ae31f8a61
@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from rest_framework import routers
|
||||
|
||||
@ -8,6 +9,10 @@ from .viewsets import UserViewSet
|
||||
router = routers.DefaultRouter()
|
||||
router.register('user', UserViewSet)
|
||||
|
||||
if "logs" in settings.INSTALLED_APPS:
|
||||
from logs.api.views import ChangelogViewSet
|
||||
router.register('logs', ChangelogViewSet)
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
# Wire up our API using automatic URL routing.
|
||||
|
4
apps/logs/__init__.py
Normal file
4
apps/logs/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'logs.apps.LogsConfig'
|
0
apps/logs/api/__init__.py
Normal file
0
apps/logs/api/__init__.py
Normal file
19
apps/logs/api/serializers.py
Normal file
19
apps/logs/api/serializers.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Changelog
|
||||
|
||||
|
||||
class ChangelogSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Changelog types.
|
||||
The djangorestframework plugin will analyse the model `Changelog` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Changelog
|
||||
fields = '__all__'
|
||||
# noinspection PyProtectedMember
|
||||
read_only_fields = [f.name for f in model._meta.get_fields()] # Changelogs are read-only protected
|
11
apps/logs/api/urls.py
Normal file
11
apps/logs/api/urls.py
Normal file
@ -0,0 +1,11 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ChangelogViewSet
|
||||
|
||||
|
||||
def register_logs_urls(router, path):
|
||||
"""
|
||||
Configure router for Activity REST API.
|
||||
"""
|
||||
router.register(path, ChangelogViewSet)
|
28
apps/logs/api/views.py
Normal file
28
apps/logs/api/views.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from .serializers import ChangelogSerializer
|
||||
from ..models import Changelog
|
||||
|
||||
|
||||
class ChangelogViewSet(ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/logs/
|
||||
"""
|
||||
|
||||
def check_permissions(self, request):
|
||||
# Only superusers can get access to logs
|
||||
return self.request.user and self.request.user.is_superuser
|
||||
|
||||
queryset = Changelog.objects.all()
|
||||
serializer_class = ChangelogSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
|
||||
ordering_fields = ['timestamp', 'id', ]
|
||||
ordering = ['-id', ]
|
18
apps/logs/apps.py
Normal file
18
apps/logs/apps.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import pre_save, post_save, post_delete
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class LogsConfig(AppConfig):
|
||||
name = 'logs'
|
||||
verbose_name = _('Logs')
|
||||
|
||||
def ready(self):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import signals
|
||||
pre_save.connect(signals.pre_save_object)
|
||||
post_save.connect(signals.save_object)
|
||||
post_delete.connect(signals.delete_object)
|
37
apps/logs/migrations/0001_initial.py
Normal file
37
apps/logs/migrations/0001_initial.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-21 15:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Changelog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address')),
|
||||
('instance_pk', models.CharField(max_length=255, verbose_name='identifier')),
|
||||
('previous', models.TextField(blank=True, default='', verbose_name='previous data')),
|
||||
('data', models.TextField(blank=True, default='', verbose_name='new data')),
|
||||
('action', models.CharField(choices=[('create', 'create'), ('edit', 'edit'), ('delete', 'delete')], default='edit', max_length=16, verbose_name='action')),
|
||||
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='timestamp')),
|
||||
('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype', verbose_name='model')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'changelog',
|
||||
'verbose_name_plural': 'changelogs',
|
||||
},
|
||||
),
|
||||
]
|
0
apps/logs/migrations/__init__.py
Normal file
0
apps/logs/migrations/__init__.py
Normal file
88
apps/logs/models.py
Normal file
88
apps/logs/models.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Changelog(models.Model):
|
||||
"""
|
||||
Store each modification in the database (except sessions and logging),
|
||||
including creating, editing and deleting models.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
verbose_name=_('user'),
|
||||
)
|
||||
|
||||
ip = models.GenericIPAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("IP Address")
|
||||
)
|
||||
|
||||
model = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_('model'),
|
||||
)
|
||||
|
||||
instance_pk = models.CharField(
|
||||
max_length=255,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_('identifier'),
|
||||
)
|
||||
|
||||
previous = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_('previous data'),
|
||||
)
|
||||
|
||||
data = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_('new data'),
|
||||
)
|
||||
|
||||
action = models.CharField( # create, edit or delete
|
||||
max_length=16,
|
||||
null=False,
|
||||
blank=False,
|
||||
choices=[
|
||||
('create', _('create')),
|
||||
('edit', _('edit')),
|
||||
('delete', _('delete')),
|
||||
],
|
||||
default='edit',
|
||||
verbose_name=_('action'),
|
||||
)
|
||||
|
||||
timestamp = models.DateTimeField(
|
||||
null=False,
|
||||
blank=False,
|
||||
default=timezone.now,
|
||||
name='timestamp',
|
||||
verbose_name=_('timestamp'),
|
||||
)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("changelog")
|
||||
verbose_name_plural = _("changelogs")
|
||||
|
||||
def __str__(self):
|
||||
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
||||
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
136
apps/logs/signals.py
Normal file
136
apps/logs/signals.py
Normal file
@ -0,0 +1,136 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from corres2math.middlewares import get_current_authenticated_user, get_current_ip
|
||||
|
||||
from .models import Changelog
|
||||
|
||||
import getpass
|
||||
|
||||
|
||||
# Ces modèles ne nécessitent pas de logs
|
||||
EXCLUDED = [
|
||||
'admin.logentry',
|
||||
'authtoken.token',
|
||||
'contenttypes.contenttype',
|
||||
'logs.changelog', # Never remove this line
|
||||
'mailer.dontsendentry',
|
||||
'mailer.message',
|
||||
'mailer.messagelog',
|
||||
'migrations.migration',
|
||||
'sessions.session',
|
||||
]
|
||||
|
||||
|
||||
def pre_save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get saved, we get the previous instance that is currently in the database
|
||||
"""
|
||||
qs = sender.objects.filter(pk=instance.pk).all()
|
||||
if qs.exists():
|
||||
instance._previous = qs.get()
|
||||
else:
|
||||
instance._previous = None
|
||||
|
||||
|
||||
def save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
||||
in order to store each modification made
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
return
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
previous = instance._previous
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
|
||||
if user is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
ip = "127.0.0.1"
|
||||
username = getpass.getuser()
|
||||
if User.objects.filter(username=username).exists():
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
if user is not None and instance._meta.label_lower == "auth.user" and previous:
|
||||
# On n'enregistre pas les connexions
|
||||
if instance.last_login != previous.last_login:
|
||||
return
|
||||
|
||||
changed_fields = '__all__'
|
||||
if previous:
|
||||
# On ne garde que les champs modifiés
|
||||
changed_fields = []
|
||||
for field in instance._meta.fields:
|
||||
if field.name.endswith("_ptr"):
|
||||
# A field ending with _ptr is a OneToOneRel with a subclass, e.g. NoteClub.note_ptr -> Note
|
||||
continue
|
||||
if getattr(instance, field.name) != getattr(previous, field.name):
|
||||
changed_fields.append(field.name)
|
||||
|
||||
if len(changed_fields) == 0:
|
||||
# Pas de log s'il n'y a pas de modification
|
||||
return
|
||||
|
||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles avec uniquement les champs modifiés
|
||||
class CustomSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = instance.__class__
|
||||
fields = changed_fields
|
||||
|
||||
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else ""
|
||||
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
|
||||
|
||||
Changelog.objects.create(user=user,
|
||||
ip=ip,
|
||||
model=ContentType.objects.get_for_model(instance),
|
||||
instance_pk=instance.pk,
|
||||
previous=previous_json,
|
||||
data=instance_json,
|
||||
action=("edit" if previous else "create")
|
||||
).save()
|
||||
|
||||
|
||||
def delete_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Each time a model is deleted, an entry in the table `Changelog` is added in the database
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
return
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
|
||||
if user is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
ip = "127.0.0.1"
|
||||
username = getpass.getuser()
|
||||
if User.objects.filter(username=username).exists():
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
||||
class CustomSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = instance.__class__
|
||||
fields = '__all__'
|
||||
|
||||
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
|
||||
|
||||
Changelog.objects.create(user=user,
|
||||
ip=ip,
|
||||
model=ContentType.objects.get_for_model(instance),
|
||||
instance_pk=instance.pk,
|
||||
previous=instance_json,
|
||||
data="",
|
||||
action="delete"
|
||||
).save()
|
@ -54,6 +54,7 @@ INSTALLED_APPS = [
|
||||
'crispy_forms',
|
||||
'django_extensions',
|
||||
'django_tables2',
|
||||
'logs',
|
||||
'mailer',
|
||||
'polymorphic',
|
||||
'rest_framework',
|
||||
|
Loading…
Reference in New Issue
Block a user