From fd529a53c84f8f1ee6f34b2303c0b1a618241441 Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO <yohann.danello@gmail.com>
Date: Mon, 24 Feb 2020 18:18:44 +0100
Subject: [PATCH] Logging support

---
 apps/api/__init__.py            |  4 ++
 apps/api/apps.py                | 10 +++++
 apps/logs/__init__.py           |  4 ++
 apps/logs/apps.py               | 14 +++++++
 apps/logs/models.py             | 63 ++++++++++++++++++++++++++++++
 apps/logs/signals.py            | 68 +++++++++++++++++++++++++++++++++
 apps/logs/urls.py               |  8 ++++
 locale/de/LC_MESSAGES/django.po | 56 +++++++++++++++++++++++----
 locale/fr/LC_MESSAGES/django.po | 62 ++++++++++++++++++++++++++----
 note_kfet/settings/base.py      |  1 +
 note_kfet/urls.py               |  2 +
 11 files changed, 276 insertions(+), 16 deletions(-)
 create mode 100644 apps/api/__init__.py
 create mode 100644 apps/api/apps.py
 create mode 100644 apps/logs/__init__.py
 create mode 100644 apps/logs/apps.py
 create mode 100644 apps/logs/models.py
 create mode 100644 apps/logs/signals.py
 create mode 100644 apps/logs/urls.py

diff --git a/apps/api/__init__.py b/apps/api/__init__.py
new file mode 100644
index 00000000..1b17aec6
--- /dev/null
+++ b/apps/api/__init__.py
@@ -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'
diff --git a/apps/api/apps.py b/apps/api/apps.py
new file mode 100644
index 00000000..11d78652
--- /dev/null
+++ b/apps/api/apps.py
@@ -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')
diff --git a/apps/logs/__init__.py b/apps/logs/__init__.py
new file mode 100644
index 00000000..58ee5b08
--- /dev/null
+++ b/apps/logs/__init__.py
@@ -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'
diff --git a/apps/logs/apps.py b/apps/logs/apps.py
new file mode 100644
index 00000000..f48820c7
--- /dev/null
+++ b/apps/logs/apps.py
@@ -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
diff --git a/apps/logs/models.py b/apps/logs/models.py
new file mode 100644
index 00000000..17e8c710
--- /dev/null
+++ b/apps/logs/models.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+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'),
+    )
+
+    model = models.CharField(
+        max_length=255,
+        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."))
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
new file mode 100644
index 00000000..646739fa
--- /dev/null
+++ b/apps/logs/signals.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import inspect
+
+from django.core import serializers
+from django.db.models.signals import pre_save, pre_delete
+from django.dispatch import receiver
+from .models import Changelog
+
+
+def get_user_in_signal(sender, **kwargs):
+    user = None
+    for entry in reversed(inspect.stack()):
+        try:
+            user = entry[0].f_locals['request'].user
+            break
+        except:
+            pass
+
+    if not user:
+        print("WARNING: Attempt to save " + str(sender) + " with no user")
+
+    return user
+
+EXCLUDED = [
+        'Changelog',
+        'Migration',
+        'Session',
+    ]
+
+@receiver(pre_save)
+def save_object(sender, instance, **kwargs):
+    model_name = sender.__name__
+    if model_name in EXCLUDED:
+        return
+
+    previous = sender.objects.filter(pk=instance.pk).all()
+
+    user = get_user_in_signal(sender, **kwargs)
+    if previous.exists:
+        previous_json = serializers.serialize('json', previous)[1:-1]
+    else:
+        previous_json = None
+    instance_json = serializers.serialize('json', [instance, ],)[1:-1]
+    Changelog.objects.create(user=user,
+                                        model=model_name,
+                                        instance_pk=instance.pk,
+                                        previous=previous_json,
+                                        data=instance_json,
+                                        action=("edit" if previous.exists() else "create")
+                                        )#.save()
+
+@receiver(pre_delete)
+def delete_object(sender, instance, **kwargs):
+    model_name = sender.__name__
+    if model_name in EXCLUDED:
+        return
+
+    user = get_user_in_signal(sender, **kwargs)
+    instance_json = serializers.serialize('json', [instance, ])[1:-1]
+    Changelog.objects.create(user=user,
+                                        model=model_name,
+                                        instance_pk=instance.pk,
+                                        previous=instance_json,
+                                        data=None,
+                                        action="delete"
+                                        ).save()
diff --git a/apps/logs/urls.py b/apps/logs/urls.py
new file mode 100644
index 00000000..6d76674c
--- /dev/null
+++ b/apps/logs/urls.py
@@ -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 = [
+]
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index 3aadf83e..618447da 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-02-21 13:50+0100\n"
+"POT-Creation-Date: 2020-02-24 17:15+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -82,6 +82,46 @@ msgstr ""
 msgid "guests"
 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
 msgid "member"
 msgstr ""
@@ -244,10 +284,6 @@ msgstr ""
 msgid "This alias is already taken."
 msgstr ""
 
-#: apps/note/models/notes.py:105
-msgid "user"
-msgstr ""
-
 #: apps/note/models/notes.py:109
 msgid "one's note"
 msgstr ""
@@ -358,15 +394,19 @@ msgstr ""
 msgid "Transfer money from your account to one or others"
 msgstr ""
 
-#: note_kfet/settings/base.py:148
+#: apps/note/views.py:143
+msgid "Consommations"
+msgstr ""
+
+#: note_kfet/settings/base.py:150
 msgid "German"
 msgstr ""
 
-#: note_kfet/settings/base.py:149
+#: note_kfet/settings/base.py:151
 msgid "English"
 msgstr ""
 
-#: note_kfet/settings/base.py:150
+#: note_kfet/settings/base.py:152
 msgid "French"
 msgstr ""
 
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index bdf4fc8f..fdb98479 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-02-21 13:50+0100\n"
+"POT-Creation-Date: 2020-02-24 17:15+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -77,6 +77,50 @@ msgstr "invité"
 msgid "guests"
 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
 msgid "member"
 msgstr "adhérent"
@@ -244,10 +288,6 @@ msgstr "Note"
 msgid "This alias is already taken."
 msgstr "Cet alias est déjà pris."
 
-#: apps/note/models/notes.py:105
-msgid "user"
-msgstr "utilisateur"
-
 #: apps/note/models/notes.py:109
 msgid "one's note"
 msgstr "note d'un utilisateur"
@@ -358,15 +398,21 @@ msgstr "transactions d'adhésion"
 msgid "Transfer money from your account to one or others"
 msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres"
 
-#: note_kfet/settings/base.py:148
+#: apps/note/views.py:143
+#, fuzzy
+#| msgid "transactions"
+msgid "Consommations"
+msgstr "transactions"
+
+#: note_kfet/settings/base.py:150
 msgid "German"
 msgstr ""
 
-#: note_kfet/settings/base.py:149
+#: note_kfet/settings/base.py:151
 msgid "English"
 msgstr ""
 
-#: note_kfet/settings/base.py:150
+#: note_kfet/settings/base.py:152
 msgid "French"
 msgstr ""
 
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index ab4453e4..eb0787cd 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -61,6 +61,7 @@ INSTALLED_APPS = [
     'member',
     'note',
     'api',
+    'logs',
 ]
 LOGIN_REDIRECT_URL = '/note/transfer/'
 
diff --git a/note_kfet/urls.py b/note_kfet/urls.py
index 303e229a..fe87cc05 100644
--- a/note_kfet/urls.py
+++ b/note_kfet/urls.py
@@ -21,4 +21,6 @@ urlpatterns = [
 
     # Include Django REST API
     path('api/', include('api.urls')),
+
+    path('logs/', include('logs.urls')),
 ]
-- 
GitLab