core/counter: add generic operation logs and implements it for Sellings and Refilling deletions

This commit is contained in:
Antoine Bartuccio 2019-11-13 23:14:21 +01:00
parent 129f2e53ee
commit e634cda318
Signed by: klmp200
GPG Key ID: E7245548C53F904B
13 changed files with 467 additions and 192 deletions

View File

@ -23,6 +23,7 @@
#
import importlib
import threading
from django.conf import settings
from django.utils.functional import SimpleLazyObject
from django.contrib.auth import get_user
@ -54,3 +55,22 @@ class AuthenticationMiddleware(DjangoAuthenticationMiddleware):
"'account.middleware.AuthenticationMiddleware'."
)
request.user = SimpleLazyObject(lambda: get_cached_user(request))
class RequestMiddleware:
"""
Allow to access current request in signals
This is a hack that looks into the thread
!!! Do not use if your operation is asynchronus !!!
Mainly used for log purpose
"""
def __init__(self, get_response, thread_local=threading.local()):
self.get_response = get_response
self.thread_local = thread_local
def __call__(self, request):
self.thread_local.current_request = request
response = self.get_response(request)
return response

View File

@ -0,0 +1,52 @@
# Generated by Django 2.2.6 on 2019-11-13 21:17
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"),
],
default="SELLING_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,27 @@ class Gift(models.Model):
def is_owned_by(self, user):
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,
default=settings.SITH_LOG_OPERATION_TYPE[0][0],
)
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.get_payment_method_display() }}</td>
{% 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 %}
</tr>
{% endfor %}

View File

@ -13,6 +13,7 @@
{% if user.is_root %}
<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: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>
{% endif %}
{% if user.can_create_subscription or user.is_root %}

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# Copyright 2016,2017,2019
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
@ -21,3 +22,5 @@
# 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

65
counter/signals.py Normal file
View File

@ -0,0 +1,65 @@
# -*- 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 RequestMiddleware
from core.models import OperationLog
from counter.models import Selling, Refilling, Counter
def write_log(instance, operation_type):
def get_user():
request = RequestMiddleware(get_response=None).thread_local.current_request
# Get a random barmen if deletion is from a counter
session_token = request.session.get("counter_token", None)
if session_token:
counter = Counter.objects.filter(token=session_token).first()
if counter:
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
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
@ -33,4 +34,5 @@ urlpatterns = [
DeleteAllForumUserMessagesView.as_view(),
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.views.generic.edit import FormView
from django.views.generic import ListView
from django.urls import reverse
from django import forms
from django.core.exceptions import PermissionDenied
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 forum.models import ForumMessageMeta
@ -165,3 +167,14 @@ class DeleteAllForumUserMessagesView(FormView):
def get_success_url(self):
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.security.SecurityMiddleware",
"core.middleware.AuthenticationMiddleware",
"core.middleware.RequestMiddleware",
)
ROOT_URLCONF = "sith.urls"
@ -443,6 +444,11 @@ SITH_PEDAGOGY_UV_RESULT_GRADE = [
("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_ECOCUP_CONS = 1152