Merge branch 'deletion_logs' into 'master'

Add generic operation logs and implements it for Sellings and Refilling deletions

See merge request ae/Sith!259
This commit is contained in:
Antoine Bartuccio 2019-11-14 19:29:55 +01:00
commit 50c2f8164d
13 changed files with 472 additions and 193 deletions

View File

@ -23,6 +23,7 @@
# #
import importlib import importlib
import threading
from django.conf import settings from django.conf import settings
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.contrib.auth import get_user from django.contrib.auth import get_user
@ -49,8 +50,31 @@ class AuthenticationMiddleware(DjangoAuthenticationMiddleware):
def process_request(self, request): def process_request(self, request):
assert hasattr(request, "session"), ( assert hasattr(request, "session"), (
"The Django authentication middleware requires session middleware " "The Django authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert " "to be installed. Edit your MIDDLEWARE setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before " "'django.contrib.sessions.middleware.SessionMiddleware' before "
"'account.middleware.AuthenticationMiddleware'." "'account.middleware.AuthenticationMiddleware'."
) )
request.user = SimpleLazyObject(lambda: get_cached_user(request)) request.user = SimpleLazyObject(lambda: get_cached_user(request))
_threadlocal = threading.local()
def get_signal_request():
"""
!!! Do not use if your operation is asynchronus !!!
Allow to access current request in signals
This is a hack that looks into the thread
Mainly used for log purpose
"""
return getattr(_threadlocal, "request", None)
class SignalRequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
setattr(_threadlocal, "request", request)
return self.get_response(request)

View File

@ -0,0 +1,51 @@
# Generated by Django 2.2.6 on 2019-11-14 15:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("core", "0033_auto_20191006_0049"),
]
operations = [
migrations.CreateModel(
name="OperationLog",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True, verbose_name="date")),
("label", models.CharField(max_length=255, verbose_name="label")),
(
"operation_type",
models.CharField(
choices=[
("SELLING_DELETION", "Selling deletion"),
("REFILLING_DELETION", "Refilling deletion"),
],
max_length=40,
verbose_name="operation type",
),
),
(
"operator",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="logs",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@ -1454,3 +1454,24 @@ class Gift(models.Model):
def is_owned_by(self, user): def is_owned_by(self, user):
return user.is_board_member or user.is_root return user.is_board_member or user.is_root
class OperationLog(models.Model):
"""
General purpose log object to register operations
"""
date = models.DateTimeField(_("date"), auto_now_add=True)
label = models.CharField(_("label"), max_length=255)
operator = models.ForeignKey(
User, related_name="logs", on_delete=models.SET_NULL, null=True
)
operation_type = models.CharField(
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE,
)
def is_owned_by(self, user):
return user.is_root
def __str__(self):
return "%s - %s - %s" % (self.operation_type, self.label, self.operator)

View File

@ -63,7 +63,7 @@
<td>{{ i.amount }} €</td> <td>{{ i.amount }} €</td>
<td>{{ i.get_payment_method_display() }}</td> <td>{{ i.get_payment_method_display() }}</td>
{% if i.is_owned_by(user) %} {% if i.is_owned_by(user) %}
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">Delete</a></td> <td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -13,6 +13,7 @@
{% if user.is_root %} {% if user.is_root %}
<li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li> <li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li> <li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li> <li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
{% endif %} {% endif %}
{% if user.can_create_subscription or user.is_root %} {% if user.can_create_subscription or user.is_root %}

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -* # -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017,2019
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
@ -21,3 +22,5 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
default_app_config = "counter.app.CounterConfig"

34
counter/app.py Normal file
View File

@ -0,0 +1,34 @@
# -*- coding:utf-8 -*
#
# Copyright 2019
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class CounterConfig(AppConfig):
name = "counter"
verbose_name = _("counter")
def ready(self):
import counter.signals

69
counter/signals.py Normal file
View File

@ -0,0 +1,69 @@
# -*- coding:utf-8 -*
#
# Copyright 2019
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.conf import settings
from core.middleware import get_signal_request
from core.models import OperationLog
from counter.models import Selling, Refilling, Counter
def write_log(instance, operation_type):
def get_user():
request = get_signal_request()
if not request:
return None
# Get a random barmen if deletion is from a counter
session = getattr(request, "session", {})
session_token = session.get("counter_token", None)
if session_token:
counter = Counter.objects.filter(token=session_token).first()
if counter and len(counter.get_barmen_list()) > 0:
return counter.get_random_barman()
# Get the current logged user if not from a counter
if request.user and not request.user.is_anonymous:
return request.user
# Return None by default
return None
log = OperationLog(
label=str(instance), operator=get_user(), operation_type=operation_type,
).save()
@receiver(pre_delete, sender=Refilling, dispatch_uid="write_log_refilling_deletion")
def write_log_refilling_deletion(sender, instance, **kwargs):
write_log(instance, "REFILLING_DELETION")
@receiver(pre_delete, sender=Selling, dispatch_uid="write_log_refilling_deletion")
def write_log_selling_deletion(sender, instance, **kwargs):
write_log(instance, "SELLING_DELETION")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import paginate %}
{% block title %}
{% trans %}Operation logs{% endtrans %}
{% endblock %}
{% block content %}
<table>
<thead>
<tr>
<th>{% trans %}Date{% endtrans %}</th>
<th>{% trans %}Operation type{% endtrans %}</th>
<th>{% trans %}Label{% endtrans %}</th>
<th>{% trans %}Operator{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for log in object_list %}
<tr>
<td>{{ log.date }} </td>
<td>{{ log.get_operation_type_display() }}</td>
<td>{{ log.label }}</td>
<td>{{ log.operator }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<br>
{{ paginate(page_obj, paginator) }}
{% endblock content %}

View File

@ -2,6 +2,7 @@
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
@ -33,4 +34,5 @@ urlpatterns = [
DeleteAllForumUserMessagesView.as_view(), DeleteAllForumUserMessagesView.as_view(),
name="delete_forum_messages", name="delete_forum_messages",
), ),
re_path(r"^logs$", OperationLogListView.as_view(), name="operation_logs"),
] ]

View File

@ -25,13 +25,15 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from django.views.generic import ListView
from django.urls import reverse from django.urls import reverse
from django import forms from django import forms
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from ajax_select.fields import AutoCompleteSelectField from ajax_select.fields import AutoCompleteSelectField
from core.models import User from core.views import CanEditPropMixin
from core.models import User, OperationLog
from counter.models import Customer from counter.models import Customer
from forum.models import ForumMessageMeta from forum.models import ForumMessageMeta
@ -165,3 +167,14 @@ class DeleteAllForumUserMessagesView(FormView):
def get_success_url(self): def get_success_url(self):
return reverse("core:user_profile", kwargs={"user_id": self.user.id}) return reverse("core:user_profile", kwargs={"user_id": self.user.id})
class OperationLogListView(ListView, CanEditPropMixin):
"""
List all logs
"""
model = OperationLog
template_name = "rootplace/logs.jinja"
ordering = ["-date"]
paginate_by = 100

View File

@ -106,6 +106,7 @@ MIDDLEWARE = (
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"core.middleware.AuthenticationMiddleware", "core.middleware.AuthenticationMiddleware",
"core.middleware.SignalRequestMiddleware",
) )
ROOT_URLCONF = "sith.urls" ROOT_URLCONF = "sith.urls"
@ -443,6 +444,11 @@ SITH_PEDAGOGY_UV_RESULT_GRADE = [
("ABS", _("Abs")), ("ABS", _("Abs")),
] ]
SITH_LOG_OPERATION_TYPE = [
(("SELLING_DELETION"), _("Selling deletion")),
(("REFILLING_DELETION"), _("Refilling deletion")),
]
SITH_PEDAGOGY_UTBM_API = "https://extranet1.utbm.fr/gpedago/api/guide" SITH_PEDAGOGY_UTBM_API = "https://extranet1.utbm.fr/gpedago/api/guide"
SITH_ECOCUP_CONS = 1152 SITH_ECOCUP_CONS = 1152