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 django.conf.urls import url, include
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
@ -8,6 +9,10 @@ from .viewsets import UserViewSet
|
|||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register('user', UserViewSet)
|
router.register('user', UserViewSet)
|
||||||
|
|
||||||
|
if "logs" in settings.INSTALLED_APPS:
|
||||||
|
from logs.api.views import ChangelogViewSet
|
||||||
|
router.register('logs', ChangelogViewSet)
|
||||||
|
|
||||||
app_name = 'api'
|
app_name = 'api'
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# 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',
|
'crispy_forms',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
|
'logs',
|
||||||
'mailer',
|
'mailer',
|
||||||
'polymorphic',
|
'polymorphic',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
Loading…
Reference in New Issue
Block a user