Merge branch 'logging' into 'master'

Logs

See merge request bde/nk20!28
This commit is contained in:
ynerant 2020-03-07 10:36:28 +01:00
commit 5b995f16f1
14 changed files with 343 additions and 0 deletions

4
apps/api/__init__.py Normal file
View 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 = 'api.apps.APIConfig'

10
apps/api/apps.py Normal file
View File

@ -0,0 +1,10 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class APIConfig(AppConfig):
name = 'api'
verbose_name = _('API')

4
apps/logs/__init__.py Normal file
View 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'

14
apps/logs/apps.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class LogsConfig(AppConfig):
name = 'logs'
verbose_name = _('Logs')
def ready(self):
# noinspection PyUnresolvedReferences
import logs.signals

View File

71
apps/logs/models.py Normal file
View File

@ -0,0 +1,71 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
class Changelog(models.Model):
"""
Store each modification on 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(
null=True,
verbose_name=_('previous data'),
)
data = models.TextField(
null=True,
verbose_name=_('new data'),
)
action = models.CharField( # create, edit or delete
max_length=16,
null=False,
blank=False,
verbose_name=_('action'),
)
timestamp = models.DateTimeField(
null=False,
blank=False,
auto_now_add=True,
name='timestamp',
verbose_name=_('timestamp'),
)
def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed."))

119
apps/logs/signals.py Normal file
View File

@ -0,0 +1,119 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import inspect
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.db.models.signals import pre_save, post_save, post_delete
from django.dispatch import receiver
from .models import Changelog
def get_request_in_signal(sender):
req = None
for entry in reversed(inspect.stack()):
try:
req = entry[0].f_locals['request']
# Check if there is a user
# noinspection PyStatementEffect
req.user
break
except:
pass
if not req:
print("WARNING: Attempt to save " + str(sender) + " with no user")
return req
def get_user_and_ip(sender):
req = get_request_in_signal(sender)
try:
user = req.user
if 'HTTP_X_FORWARDED_FOR' in req.META:
ip = req.META.get('HTTP_X_FORWARDED_FOR')
else:
ip = req.META.get('REMOTE_ADDR')
except:
user = None
ip = None
return user, ip
EXCLUDED = [
'admin.logentry',
'authtoken.token',
'cas_server.user',
'cas_server.userattributes',
'contenttypes.contenttype',
'logs.changelog',
'migrations.migration',
'note.noteuser',
'note.noteclub',
'note.notespecial',
'sessions.session',
'reversion.revision',
'reversion.version',
]
@receiver(pre_save)
def pre_save_object(sender, instance, **kwargs):
qs = sender.objects.filter(pk=instance.pk).all()
if qs.exists():
instance._previous = qs.get()
else:
instance._previous = None
@receiver(post_save)
def save_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
return
previous = instance._previous
user, ip = get_user_and_ip(sender)
if user is not None and instance._meta.label_lower == "auth.user" and previous:
# Don't save last login modifications
if instance.last_login != previous.last_login:
return
previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None
instance_json = serializers.serialize('json', [instance, ])[1:-1]
if previous_json == instance_json:
# No modification
return
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()
@receiver(post_delete)
def delete_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
return
user, ip = get_user_and_ip(sender)
instance_json = serializers.serialize('json', [instance, ])[1:-1]
Changelog.objects.create(user=user,
ip=ip,
model=ContentType.objects.get_for_model(instance),
instance_pk=instance.pk,
previous=instance_json,
data=None,
action="delete"
).save()

8
apps/logs/urls.py Normal file
View File

@ -0,0 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
app_name = 'logs'
# TODO User interface
urlpatterns = [
]

View File

@ -2,9 +2,22 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .signals import save_user_profile
class MemberConfig(AppConfig): class MemberConfig(AppConfig):
name = 'member' name = 'member'
verbose_name = _('member') verbose_name = _('member')
def ready(self):
"""
Define app internal signals to interact with other apps
"""
post_save.connect(
save_user_profile,
sender=settings.AUTH_USER_MODEL,
)

View File

@ -1,2 +1,15 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
def save_user_profile(instance, created, raw, **_kwargs):
"""
Hook to create and save a profile when an user is updated if it is not registered with the signup form
"""
if raw:
# When provisionning data, do not try to autocreate
return
if created:
from .models import Profile
Profile.objects.get_or_create(user=instance)
instance.profile.save()

View File

@ -82,6 +82,46 @@ msgstr ""
msgid "guests" msgid "guests"
msgstr "" msgstr ""
#: apps/api/apps.py:10
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
msgid "Logs"
msgstr ""
#: apps/logs/models.py:20 apps/note/models/notes.py:105
msgid "user"
msgstr ""
#: apps/logs/models.py:27
msgid "model"
msgstr ""
#: apps/logs/models.py:34
msgid "identifier"
msgstr ""
#: apps/logs/models.py:39
msgid "previous data"
msgstr ""
#: apps/logs/models.py:44
msgid "new data"
msgstr ""
#: apps/logs/models.py:51
msgid "action"
msgstr ""
#: apps/logs/models.py:59
msgid "timestamp"
msgstr ""
#: apps/logs/models.py:63
msgid "Logs cannot be destroyed."
msgstr ""
#: apps/member/apps.py:10 #: apps/member/apps.py:10
msgid "member" msgid "member"
msgstr "" msgstr ""

View File

@ -77,6 +77,50 @@ msgstr "invité"
msgid "guests" msgid "guests"
msgstr "invités" msgstr "invités"
#: apps/api/apps.py:10
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
msgid "Logs"
msgstr ""
#: apps/logs/models.py:20 apps/note/models/notes.py:105
msgid "user"
msgstr "utilisateur"
#: apps/logs/models.py:27
msgid "model"
msgstr "Modèle"
#: apps/logs/models.py:34
msgid "identifier"
msgstr "Identifiant"
#: apps/logs/models.py:39
msgid "previous data"
msgstr "Données précédentes"
#: apps/logs/models.py:44
#, fuzzy
#| msgid "end date"
msgid "new data"
msgstr "Nouvelles données"
#: apps/logs/models.py:51
#, fuzzy
#| msgid "section"
msgid "action"
msgstr "Action"
#: apps/logs/models.py:59
msgid "timestamp"
msgstr "Date"
#: apps/logs/models.py:63
msgid "Logs cannot be destroyed."
msgstr "Les logs ne peuvent pas être détruits."
#: apps/member/apps.py:10 #: apps/member/apps.py:10
msgid "member" msgid "member"
msgstr "adhérent" msgstr "adhérent"

View File

@ -64,6 +64,7 @@ INSTALLED_APPS = [
'member', 'member',
'note', 'note',
'api', 'api',
'logs',
] ]
LOGIN_REDIRECT_URL = '/note/transfer/' LOGIN_REDIRECT_URL = '/note/transfer/'

View File

@ -32,6 +32,8 @@ urlpatterns = [
# Include Django REST API # Include Django REST API
path('api/', include('api.urls')), path('api/', include('api.urls')),
path('logs/', include('logs.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)