8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-22 19:33:11 +00:00
re2o/logs/views.py

607 lines
23 KiB
Python
Raw Normal View History

2017-01-15 23:01:18 +00:00
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Gabriel Détraz
# Copyright © 2018 Lara Kermarec
# Copyright © 2018 Augustin Lemesle
# Copyright © 2018 Hugo Levy-Falk
2017-01-15 23:01:18 +00:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2016-10-31 16:27:27 +00:00
# App de gestion des statistiques pour re2o
# Gabriel Détraz
# Gplv2
2017-10-14 02:17:42 +00:00
"""
Vues des logs et statistiques générales.
La vue index générale affiche une selection des dernières actions,
classées selon l'importance, avec date, et user formatés.
Stats_logs renvoie l'ensemble des logs.
Les autres vues sont thématiques, ensemble des statistiques et du
nombre d'objets par models, nombre d'actions par user, etc
"""
from __future__ import unicode_literals
2017-10-26 15:58:41 +00:00
from django.urls import reverse
2016-10-31 16:27:27 +00:00
from django.shortcuts import render, redirect
from django.contrib import messages
2017-12-28 15:32:48 +00:00
from django.contrib.auth.decorators import login_required
from django.http import Http404
2018-07-19 10:43:19 +00:00
from django.db.models import Count
from django.apps import apps
2018-07-19 10:43:19 +00:00
from django.utils.translation import ugettext as _
2016-10-31 16:27:27 +00:00
from reversion.models import Revision
from reversion.models import Version, ContentType
2016-10-31 16:27:27 +00:00
2017-10-27 00:44:19 +00:00
from users.models import (
User,
ServiceUser,
School,
ListRight,
ListShell,
Ban,
Whitelist,
Adherent,
Club,
2017-10-27 00:44:19 +00:00
)
from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation
2017-10-27 00:44:19 +00:00
from machines.models import (
Machine,
MachineType,
IpType,
Extension,
Interface,
Domain,
IpList,
OuverturePortList,
Service,
Vlan,
Nas,
SOA,
Mx,
Ns,
2017-10-27 00:44:19 +00:00
)
from topologie.models import (
Switch,
Port,
Room,
Stack,
ModelSwitch,
ConstructorSwitch,
AccessPoint,
2017-10-27 00:44:19 +00:00
)
from preferences.models import GeneralOption
2017-10-14 02:17:42 +00:00
from re2o.views import form
2017-12-28 15:32:48 +00:00
from re2o.utils import (
all_whitelisted,
all_baned,
all_has_access,
all_adherent,
all_active_assigned_interfaces_count,
all_active_interfaces_count,
2019-03-17 02:00:28 +00:00
)
from re2o.base import re2o_paginator, SortTable
2020-04-23 12:06:47 +00:00
from re2o.acl import can_view_all, can_view_app, can_edit_history, can_view
2016-11-15 14:25:27 +00:00
2020-04-23 16:06:21 +00:00
from .models import (
2020-04-24 13:37:05 +00:00
ActionsSearch,
2020-04-24 15:47:09 +00:00
RevisionAction,
2020-04-23 16:06:21 +00:00
MachineHistorySearch,
get_history_class
2020-04-23 16:06:21 +00:00
)
2020-04-24 13:37:05 +00:00
from .forms import (
ActionsSearchForm,
MachineHistorySearchForm
)
2020-04-22 16:17:06 +00:00
2016-10-31 16:27:27 +00:00
@login_required
@can_view_app("logs")
2016-10-31 16:27:27 +00:00
def index(request):
2017-10-14 02:17:42 +00:00
"""Affiche les logs affinés, date reformatées, selectionne
les event importants (ajout de droits, ajout de ban/whitelist)"""
pagination_number = GeneralOption.get_cached_value("pagination_number")
# The types of content kept for display
content_type_filter = ["ban", "whitelist", "vente", "interface", "user"]
# Select only wanted versions
2017-10-14 02:17:42 +00:00
versions = Version.objects.filter(
content_type__in=ContentType.objects.filter(model__in=content_type_filter)
).select_related("revision")
versions = SortTable.sort(
versions, request.GET.get("col"), request.GET.get("order"), SortTable.LOGS_INDEX
)
versions = re2o_paginator(request, versions, pagination_number)
# Force to have a list instead of QuerySet
versions.count(0)
# Items to remove later because invalid
to_remove = []
# Parse every item (max = pagination_number)
2017-10-14 02:17:42 +00:00
for i in range(len(versions.object_list)):
if versions.object_list[i].object:
version = versions.object_list[i]
versions.object_list[i] = {
"rev_id": version.revision.id,
"comment": version.revision.comment,
"datetime": version.revision.date_created,
"username": version.revision.user.get_username()
if version.revision.user
else "?",
"user_id": version.revision.user_id,
"version": version,
}
2017-10-14 02:17:42 +00:00
else:
to_remove.insert(0, i)
# Remove all tagged invalid items
2017-10-14 02:17:42 +00:00
for i in to_remove:
versions.object_list.pop(i)
return render(request, "logs/index.html", {"versions_list": versions})
2017-10-14 02:17:42 +00:00
@login_required
2017-12-28 15:32:48 +00:00
@can_view_all(GeneralOption)
def stats_logs(request):
2017-10-14 02:17:42 +00:00
"""Affiche l'ensemble des logs et des modifications sur les objets,
classés par date croissante, en vrac"""
2020-04-24 13:37:05 +00:00
actions_form = ActionsSearchForm(request.GET or None)
if actions_form.is_valid():
actions = ActionsSearch()
revisions = actions.get(actions_form.cleaned_data)
revisions = SortTable.sort(
revisions,
request.GET.get("col"),
request.GET.get("order"),
SortTable.LOGS_STATS_LOGS,
)
pagination_number = GeneralOption.get_cached_value("pagination_number")
revisions = re2o_paginator(request, revisions, pagination_number)
2020-04-24 16:45:37 +00:00
# Only do this now so it's not applied to objects which aren't displayed
# It can take a bit of time because it has to compute the diff of each version
revisions.object_list = [RevisionAction(r) for r in revisions.object_list]
2020-04-24 13:37:05 +00:00
return render(request, "logs/stats_logs.html", {"revisions_list": revisions})
return render(request, "logs/search_stats_logs.html", {"actions_form": actions_form})
2017-10-14 02:17:42 +00:00
@login_required
2017-12-28 15:32:48 +00:00
@can_edit_history
def revert_action(request, revision_id):
""" Annule l'action en question """
try:
revision = Revision.objects.get(id=revision_id)
except Revision.DoesNotExist:
2018-08-05 16:57:36 +00:00
messages.error(request, _("Nonexistent revision."))
if request.method == "POST":
revision.revert()
2018-08-05 16:57:36 +00:00
messages.success(request, _("The action was deleted."))
return redirect(reverse("logs:index"))
return form(
{"objet": revision, "objet_name": revision.__class__.__name__},
"logs/delete.html",
request,
)
2017-10-14 02:17:42 +00:00
2017-05-26 02:32:45 +00:00
@login_required
@can_view_all(IpList, Interface, User)
2017-05-26 02:32:45 +00:00
def stats_general(request):
2017-10-14 02:17:42 +00:00
"""Statistiques générales affinées sur les ip, activées, utilisées par
range, et les statistiques générales sur les users : users actifs,
cotisants, activés, archivés, etc"""
ip_dict = dict()
for ip_range in IpType.objects.select_related("vlan").all():
2017-05-26 02:32:45 +00:00
all_ip = IpList.objects.filter(ip_type=ip_range)
used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
active_ip = (
all_active_assigned_interfaces_count()
.filter(ipv4__in=IpList.objects.filter(ip_type=ip_range))
.count()
)
ip_dict[ip_range] = [
ip_range,
ip_range.vlan,
all_ip.count(),
used_ip,
active_ip,
all_ip.count() - used_ip,
]
_all_adherent = all_adherent(including_asso=False)
_all_has_access = all_has_access(including_asso=False)
2017-10-27 00:44:19 +00:00
_all_baned = all_baned()
_all_whitelisted = all_whitelisted()
_all_active_interfaces_count = all_active_interfaces_count()
_all_active_assigned_interfaces_count = all_active_assigned_interfaces_count()
2017-05-26 02:32:45 +00:00
stats = [
[ # First set of data (about users)
[ # Headers
2018-08-05 16:57:36 +00:00
_("Category"),
_("Number of users (members and clubs)"),
_("Number of members"),
_("Number of clubs"),
2018-04-13 20:37:04 +00:00
],
{ # Data
"active_users": [
2018-08-05 16:57:36 +00:00
_("Activated users"),
2018-04-13 20:37:04 +00:00
User.objects.filter(state=User.STATE_ACTIVE).count(),
(Adherent.objects.filter(state=Adherent.STATE_ACTIVE).count()),
Club.objects.filter(state=Club.STATE_ACTIVE).count(),
2018-04-13 20:37:04 +00:00
],
"inactive_users": [
2018-08-05 16:57:36 +00:00
_("Disabled users"),
2018-04-13 20:37:04 +00:00
User.objects.filter(state=User.STATE_DISABLED).count(),
(Adherent.objects.filter(state=Adherent.STATE_DISABLED).count()),
Club.objects.filter(state=Club.STATE_DISABLED).count(),
2018-04-13 20:37:04 +00:00
],
"archive_users": [
2018-08-05 16:57:36 +00:00
_("Archived users"),
2018-04-13 20:37:04 +00:00
User.objects.filter(state=User.STATE_ARCHIVE).count(),
(Adherent.objects.filter(state=Adherent.STATE_ARCHIVE).count()),
Club.objects.filter(state=Club.STATE_ARCHIVE).count(),
2019-03-17 02:00:28 +00:00
],
"full_archive_users": [
2019-11-16 14:03:08 +00:00
_("Fully archived users"),
2019-03-17 02:00:28 +00:00
User.objects.filter(state=User.STATE_FULL_ARCHIVE).count(),
(
Adherent.objects.filter(
state=Adherent.STATE_FULL_ARCHIVE
).count()
),
Club.objects.filter(state=Club.STATE_FULL_ARCHIVE).count(),
2018-04-13 20:37:04 +00:00
],
"not_active_users": [
_("Not yet active users"),
User.objects.filter(state=User.STATE_NOT_YET_ACTIVE).count(),
(
Adherent.objects.filter(
state=Adherent.STATE_NOT_YET_ACTIVE
).count()
),
Club.objects.filter(state=Club.STATE_NOT_YET_ACTIVE).count(),
],
"adherent_users": [
2018-08-05 16:57:36 +00:00
_("Contributing members"),
2018-04-13 20:37:04 +00:00
_all_adherent.count(),
_all_adherent.exclude(adherent__isnull=True).count(),
_all_adherent.exclude(club__isnull=True).count(),
2018-04-13 20:37:04 +00:00
],
"connexion_users": [
2018-08-05 16:57:36 +00:00
_("Users benefiting from a connection"),
2018-04-13 20:37:04 +00:00
_all_has_access.count(),
_all_has_access.exclude(adherent__isnull=True).count(),
_all_has_access.exclude(club__isnull=True).count(),
2018-04-13 20:37:04 +00:00
],
"ban_users": [
2018-08-05 16:57:36 +00:00
_("Banned users"),
2018-04-13 20:37:04 +00:00
_all_baned.count(),
_all_baned.exclude(adherent__isnull=True).count(),
_all_baned.exclude(club__isnull=True).count(),
2018-04-13 20:37:04 +00:00
],
"whitelisted_user": [
2018-08-05 16:57:36 +00:00
_("Users benefiting from a free connection"),
2018-04-13 20:37:04 +00:00
_all_whitelisted.count(),
_all_whitelisted.exclude(adherent__isnull=True).count(),
_all_whitelisted.exclude(club__isnull=True).count(),
2018-04-13 20:37:04 +00:00
],
2020-04-17 23:36:24 +00:00
"email_state_verified_users": [
2020-04-17 23:40:01 +00:00
_("Users with a confirmed email"),
2020-04-17 23:36:24 +00:00
User.objects.filter(email_state=User.EMAIL_STATE_VERIFIED).count(),
Adherent.objects.filter(email_state=User.EMAIL_STATE_VERIFIED).count(),
Club.objects.filter(email_state=User.EMAIL_STATE_VERIFIED).count(),
],
"email_state_unverified_users": [
2020-04-17 23:40:01 +00:00
_("Users with an unconfirmed email"),
2020-04-17 23:36:24 +00:00
User.objects.filter(email_state=User.EMAIL_STATE_UNVERIFIED).count(),
Adherent.objects.filter(email_state=User.EMAIL_STATE_UNVERIFIED).count(),
Club.objects.filter(email_state=User.EMAIL_STATE_UNVERIFIED).count(),
],
"email_state_pending_users": [
_("Users pending email confirmation"),
User.objects.filter(email_state=User.EMAIL_STATE_PENDING).count(),
Adherent.objects.filter(email_state=User.EMAIL_STATE_PENDING).count(),
Club.objects.filter(email_state=User.EMAIL_STATE_PENDING).count(),
],
"actives_interfaces": [
2018-08-05 16:57:36 +00:00
_("Active interfaces (with access to the network)"),
2018-04-13 20:37:04 +00:00
_all_active_interfaces_count.count(),
(
_all_active_interfaces_count.exclude(
machine__user__adherent__isnull=True
).count()
),
(
_all_active_interfaces_count.exclude(
machine__user__club__isnull=True
).count()
),
2018-04-13 20:37:04 +00:00
],
"actives_assigned_interfaces": [
2018-08-05 16:57:36 +00:00
_("Active interfaces assigned IPv4"),
2018-04-13 20:37:04 +00:00
_all_active_assigned_interfaces_count.count(),
(
_all_active_assigned_interfaces_count.exclude(
machine__user__adherent__isnull=True
).count()
),
(
_all_active_assigned_interfaces_count.exclude(
machine__user__club__isnull=True
).count()
),
],
},
2018-04-13 20:37:04 +00:00
],
[ # Second set of data (about ip adresses)
[ # Headers
2018-08-05 16:57:36 +00:00
_("IP range"),
_("VLAN"),
_("Total number of IP addresses"),
_("Number of assigned IP addresses"),
_("Number of IP address assigned to an activated machine"),
2019-11-16 14:03:08 +00:00
_("Number of unassigned IP addresses"),
2018-04-13 20:37:04 +00:00
],
ip_dict, # Data already prepared
],
2018-04-13 20:37:04 +00:00
]
return render(request, "logs/stats_general.html", {"stats_list": stats})
2017-05-26 02:32:45 +00:00
@login_required
@can_view_app("users", "cotisations", "machines", "topologie")
def stats_models(request):
2017-10-14 02:17:42 +00:00
"""Statistiques générales, affiche les comptages par models:
nombre d'users, d'écoles, de droits, de bannissements,
de factures, de ventes, de banque, de machines, etc"""
stats = {
2019-11-16 14:03:08 +00:00
_("Users (members and clubs)"): {
"users": [User._meta.verbose_name, User.objects.count()],
"adherents": [Adherent._meta.verbose_name, Adherent.objects.count()],
"clubs": [Club._meta.verbose_name, Club.objects.count()],
"serviceuser": [
ServiceUser._meta.verbose_name,
ServiceUser.objects.count(),
],
"school": [School._meta.verbose_name, School.objects.count()],
"listright": [ListRight._meta.verbose_name, ListRight.objects.count()],
"listshell": [ListShell._meta.verbose_name, ListShell.objects.count()],
"ban": [Ban._meta.verbose_name, Ban.objects.count()],
"whitelist": [Whitelist._meta.verbose_name, Whitelist.objects.count()],
2017-10-14 02:17:42 +00:00
},
2019-11-16 14:03:08 +00:00
Cotisation._meta.verbose_name_plural.title(): {
"factures": [Facture._meta.verbose_name, Facture.objects.count()],
"vente": [Vente._meta.verbose_name, Vente.objects.count()],
"cotisation": [Cotisation._meta.verbose_name, Cotisation.objects.count()],
"article": [Article._meta.verbose_name, Article.objects.count()],
"banque": [Banque._meta.verbose_name, Banque.objects.count()],
2017-10-14 02:17:42 +00:00
},
2019-11-16 14:03:08 +00:00
Machine._meta.verbose_name_plural.title(): {
"machine": [Machine._meta.verbose_name, Machine.objects.count()],
"typemachine": [
MachineType._meta.verbose_name,
MachineType.objects.count(),
],
"typeip": [IpType._meta.verbose_name, IpType.objects.count()],
"extension": [Extension._meta.verbose_name, Extension.objects.count()],
"interface": [Interface._meta.verbose_name, Interface.objects.count()],
"alias": [
Domain._meta.verbose_name,
Domain.objects.exclude(cname=None).count(),
],
"iplist": [IpList._meta.verbose_name, IpList.objects.count()],
"service": [Service._meta.verbose_name, Service.objects.count()],
"ouvertureportlist": [
2018-08-05 16:57:36 +00:00
OuverturePortList._meta.verbose_name,
OuverturePortList.objects.count(),
2017-10-27 00:44:19 +00:00
],
"vlan": [Vlan._meta.verbose_name, Vlan.objects.count()],
"SOA": [SOA._meta.verbose_name, SOA.objects.count()],
"Mx": [Mx._meta.verbose_name, Mx.objects.count()],
"Ns": [Ns._meta.verbose_name, Ns.objects.count()],
"nas": [Nas._meta.verbose_name, Nas.objects.count()],
2017-10-14 02:17:42 +00:00
},
2018-08-05 16:57:36 +00:00
_("Topology"): {
"switch": [Switch._meta.verbose_name, Switch.objects.count()],
"bornes": [AccessPoint._meta.verbose_name, AccessPoint.objects.count()],
"port": [Port._meta.verbose_name, Port.objects.count()],
"chambre": [Room._meta.verbose_name, Room.objects.count()],
"stack": [Stack._meta.verbose_name, Stack.objects.count()],
"modelswitch": [
2018-08-05 16:57:36 +00:00
ModelSwitch._meta.verbose_name,
ModelSwitch.objects.count(),
2017-10-27 00:44:19 +00:00
],
"constructorswitch": [
2018-08-05 16:57:36 +00:00
ConstructorSwitch._meta.verbose_name,
ConstructorSwitch.objects.count(),
2017-10-27 00:44:19 +00:00
],
2017-10-14 02:17:42 +00:00
},
_("Actions performed"): {
"revision": [_("Number of actions"), Revision.objects.count()]
2017-10-14 02:17:42 +00:00
},
}
return render(request, "logs/stats_models.html", {"stats_list": stats})
2017-10-14 02:17:42 +00:00
2016-11-01 02:11:32 +00:00
@login_required
@can_view_app("users")
2016-11-01 02:11:32 +00:00
def stats_users(request):
2017-10-14 02:17:42 +00:00
"""Affiche les statistiques base de données aggrégées par user :
nombre de machines par user, d'etablissements par user,
de moyens de paiements par user, de banque par user,
de bannissement par user, etc"""
2016-11-01 02:11:32 +00:00
stats = {
2019-11-16 14:03:08 +00:00
User._meta.verbose_name: {
Machine._meta.verbose_name_plural: User.objects.annotate(num=Count("machine")).order_by("-num")[
:10
],
2019-11-16 14:03:08 +00:00
Facture._meta.verbose_name_plural: User.objects.annotate(num=Count("facture")).order_by("-num")[
:10
],
2019-11-16 14:03:08 +00:00
Ban._meta.verbose_name_plural: User.objects.annotate(num=Count("ban")).order_by("-num")[:10],
Whitelist._meta.verbose_name_plural: User.objects.annotate(num=Count("whitelist")).order_by(
"-num"
)[:10],
2019-11-16 14:03:08 +00:00
_("rights"): User.objects.annotate(num=Count("groups")).order_by("-num")[
:10
],
2017-10-14 02:17:42 +00:00
},
2019-11-16 14:03:08 +00:00
School._meta.verbose_name: {
User._meta.verbose_name_plural: School.objects.annotate(num=Count("user")).order_by("-num")[:10]
2017-10-14 02:17:42 +00:00
},
2019-11-16 14:03:08 +00:00
Paiement._meta.verbose_name: {
User._meta.verbose_name_plural: Paiement.objects.annotate(num=Count("facture")).order_by("-num")[
:10
]
2017-10-14 02:17:42 +00:00
},
2019-11-16 14:03:08 +00:00
Banque._meta.verbose_name: {
User._meta.verbose_name_plural: Banque.objects.annotate(num=Count("facture")).order_by("-num")[
:10
]
2017-10-14 02:17:42 +00:00
},
2016-11-01 02:11:32 +00:00
}
return render(request, "logs/stats_users.html", {"stats_list": stats})
2017-10-14 02:17:42 +00:00
2016-11-01 02:11:32 +00:00
@login_required
@can_view_app("users")
2016-11-01 02:11:32 +00:00
def stats_actions(request):
2017-10-14 02:17:42 +00:00
"""Vue qui affiche les statistiques de modifications d'objets par
utilisateurs.
Affiche le nombre de modifications aggrégées par utilisateurs"""
2016-11-01 02:11:32 +00:00
stats = {
2019-11-16 14:03:08 +00:00
User._meta.verbose_name: {
_("actions"): User.objects.annotate(num=Count("revision")).order_by("-num")[
:40
]
}
2016-11-01 02:11:32 +00:00
}
return render(request, "logs/stats_users.html", {"stats_list": stats})
2020-04-22 16:17:06 +00:00
@login_required
@can_view_app("users")
2020-04-22 16:32:56 +00:00
def stats_search_machine_history(request):
"""View which displays the history of machines with the given
une IP or MAC adresse"""
history_form = MachineHistorySearchForm(request.GET or None)
2020-04-22 16:17:06 +00:00
if history_form.is_valid():
history = MachineHistorySearch()
events = history.get(
history_form.cleaned_data.get("q", ""),
history_form.cleaned_data
)
max_result = GeneralOption.get_cached_value("pagination_number")
events = re2o_paginator(
request,
events,
max_result
)
2020-04-22 16:17:06 +00:00
return render(
request,
"logs/machine_history.html",
{ "events": events },
2020-04-22 16:17:06 +00:00
)
return render(request, "logs/search_machine_history.html", {"history_form": history_form})
def get_history_object(request, model, object_name, object_id):
2020-04-23 16:06:21 +00:00
"""Get the objet of type model with the given object_id
Handles permissions and DoesNotExist errors
"""
try:
object_name_id = object_name + "id"
kwargs = {object_name_id: object_id}
instance = model.get_instance(**kwargs)
except model.DoesNotExist:
instance = None
if instance is None:
authorized = can_view_app("logs")
msg = None
else:
authorized, msg, _permissions = instance.can_view(request.user)
if not authorized:
2020-04-23 16:06:21 +00:00
messages.error(
request, msg or _("You don't have the right to access this menu.")
)
return False, redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
return True, instance
@login_required
def history(request, application, object_name, object_id):
"""Render history for a model.
The model is determined using the `HISTORY_BIND` dictionnary if none is
found, raises a Http404. The view checks if the user is allowed to see the
history using the `can_view` method of the model, or the generic
`can_view_app("logs")` for deleted objects (see `get_history_object`).
Args:
request: The request sent by the user.
application: Name of the application.
object_name: Name of the model.
object_id: Id of the object you want to acces history.
Returns:
The rendered page of history if access is granted, else the user is
redirected to their profile page, with an error message.
Raises:
Http404: This kind of models doesn't have history.
"""
try:
model = apps.get_model(application, object_name)
except LookupError:
2018-07-19 10:43:19 +00:00
raise Http404(_("No model found."))
2020-04-23 16:06:21 +00:00
authorized, instance = get_history_object(request, model, object_name, object_id)
2020-04-23 16:06:21 +00:00
if not can_view:
return instance
history = get_history_class(model)
events = history.get(int(object_id), model)
# Events is None if object wasn't found
if events is None:
messages.error(request, _("Nonexistent entry."))
return redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
# Generate the pagination with the objects
max_result = GeneralOption.get_cached_value("pagination_number")
events = re2o_paginator(request, events, max_result)
# Add a default title in case the object was deleted
title = instance or "{} ({})".format(history.name, _("Deleted"))
return render(
request,
2020-04-25 10:45:36 +00:00
"re2o/history.html",
{"title": title, "events": events, "related_history": history.related},
)