diff --git a/cotisations/forms.py b/cotisations/forms.py index ba4c066f..3459f0de 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -45,9 +45,9 @@ from preferences.models import OptionalUser from users.models import User from re2o.field_permissions import FieldPermissionFormMixin +from re2o.mixins import FormRevMixin - -class NewFactureForm(ModelForm): +class NewFactureForm(FormRevMixin, ModelForm): """Creation d'une facture, moyen de paiement, banque et numero de cheque""" def __init__(self, *args, **kwargs): @@ -96,7 +96,7 @@ class CreditSoldeForm(NewFactureForm): montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) -class SelectUserArticleForm(Form): +class SelectUserArticleForm(FormRevMixin, Form): """Selection d'un article lors de la creation d'une facture""" article = forms.ModelChoiceField( queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')), @@ -158,7 +158,7 @@ class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): self.fields['valid'].label = 'Validité de la facture' -class ArticleForm(ModelForm): +class ArticleForm(FormRevMixin, ModelForm): """Creation d'un article. Champs : nom, cotisation, durée""" class Meta: model = Article @@ -170,7 +170,7 @@ class ArticleForm(ModelForm): self.fields['name'].label = "Désignation de l'article" -class DelArticleForm(Form): +class DelArticleForm(FormRevMixin, Form): """Suppression d'un ou plusieurs articles en vente. Choix parmis les modèles""" articles = forms.ModelMultipleChoiceField( @@ -188,7 +188,7 @@ class DelArticleForm(Form): self.fields['articles'].queryset = Article.objects.all() -class PaiementForm(ModelForm): +class PaiementForm(FormRevMixin, ModelForm): """Creation d'un moyen de paiement, champ text moyen et type permettant d'indiquer si il s'agit d'un chèque ou non pour le form""" class Meta: @@ -202,7 +202,7 @@ class PaiementForm(ModelForm): self.fields['type_paiement'].label = 'Type de paiement à ajouter' -class DelPaiementForm(Form): +class DelPaiementForm(FormRevMixin, Form): """Suppression d'un ou plusieurs moyens de paiements, selection parmis les models""" paiements = forms.ModelMultipleChoiceField( @@ -220,7 +220,7 @@ class DelPaiementForm(Form): self.fields['paiements'].queryset = Paiement.objects.all() -class BanqueForm(ModelForm): +class BanqueForm(FormRevMixin, ModelForm): """Creation d'une banque, field name""" class Meta: model = Banque @@ -232,7 +232,7 @@ class BanqueForm(ModelForm): self.fields['name'].label = 'Banque à ajouter' -class DelBanqueForm(Form): +class DelBanqueForm(FormRevMixin, Form): """Selection d'une ou plusieurs banques, pour suppression""" banques = forms.ModelMultipleChoiceField( queryset=Banque.objects.none(), @@ -283,7 +283,7 @@ class NewFactureSoldeForm(NewFactureForm): return cleaned_data -class RechargeForm(Form): +class RechargeForm(FormRevMixin, Form): value = forms.FloatField( label='Valeur', min_value=0.01, diff --git a/cotisations/models.py b/cotisations/models.py index cbf44b79..cdf73a39 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -57,9 +57,10 @@ from django.utils import timezone from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin -from re2o.mixins import AclMixin +from re2o.mixins import AclMixin, RevMixin -class Facture(AclMixin, FieldPermissionModelMixin, models.Model): + +class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ Définition du modèle des factures. Une facture regroupe une ou plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement et si il y a lieu un numero pour les chèques. Possède les valeurs @@ -87,6 +88,11 @@ class Facture(AclMixin, FieldPermissionModelMixin, models.Model): ("change_all_facture", "Superdroit, peut modifier toutes les factures"), ) + def linked_objects(self): + """Return linked objects : machine and domain. + Usefull in history display""" + return self.vente_set.all() + def prix(self): """Renvoie le prix brut sans les quantités. Méthode dépréciée""" @@ -180,7 +186,7 @@ def facture_post_delete(sender, **kwargs): user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) -class Vente(AclMixin, models.Model): +class Vente(RevMixin, AclMixin, models.Model): """Objet vente, contient une quantité, une facture parente, un nom, un prix. Peut-être relié à un objet cotisation, via le boolean iscotisation""" @@ -325,7 +331,7 @@ def vente_post_delete(sender, **kwargs): user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) -class Article(AclMixin, models.Model): +class Article(RevMixin, AclMixin, models.Model): """Liste des articles en vente : prix, nom, et attribut iscotisation et duree si c'est une cotisation""" PRETTY_NAME = "Articles en vente" @@ -381,7 +387,7 @@ class Article(AclMixin, models.Model): return self.name -class Banque(AclMixin, models.Model): +class Banque(RevMixin, AclMixin, models.Model): """Liste des banques""" PRETTY_NAME = "Banques enregistrées" @@ -396,7 +402,7 @@ class Banque(AclMixin, models.Model): return self.name -class Paiement(AclMixin, models.Model): +class Paiement(RevMixin, AclMixin, models.Model): """Moyens de paiement""" PRETTY_NAME = "Moyens de paiement" PAYMENT_TYPES = ( @@ -426,7 +432,7 @@ class Paiement(AclMixin, models.Model): super(Paiement, self).save(*args, **kwargs) -class Cotisation(AclMixin, models.Model): +class Cotisation(RevMixin, AclMixin, models.Model): """Objet cotisation, debut et fin, relié en onetoone à une vente""" PRETTY_NAME = "Cotisations" diff --git a/cotisations/views.py b/cotisations/views.py index a9c996d6..545f9ebe 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -28,7 +28,6 @@ import os from django.urls import reverse from django.shortcuts import render, redirect -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.validators import MaxValueValidator from django.contrib.auth.decorators import login_required, permission_required from django.contrib import messages @@ -39,14 +38,13 @@ from django.forms import modelformset_factory, formset_factory from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.views.decorators.debug import sensitive_variables -from reversion import revisions as reversion -from reversion.models import Version # Import des models, forms et fonctions re2o +from reversion import revisions as reversion from users.models import User from re2o.settings import LOGO_PATH from re2o import settings from re2o.views import form -from re2o.utils import SortTable +from re2o.utils import SortTable, re2o_paginator from re2o.acl import ( can_create, can_edit, @@ -126,10 +124,7 @@ def new_facture(request, user, userid): 'users:profil', kwargs={'userid': userid} )) - with transaction.atomic(), reversion.create_revision(): - new_facture_instance.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_facture_instance.save() for art_item in articles: if art_item.cleaned_data: article = art_item.cleaned_data['article'] @@ -142,10 +137,7 @@ def new_facture(request, user, userid): duration=article.duration, number=quantity ) - with transaction.atomic(), reversion.create_revision(): - new_vente.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_vente.save() if any(art_item.cleaned_data['article'].type_cotisation for art_item in articles if art_item.cleaned_data): messages.success( @@ -257,13 +249,9 @@ def edit_facture(request, facture, factureid): ) vente_form = vente_form_set(request.POST or None, queryset=ventes_objects) if facture_form.is_valid() and vente_form.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if facture_form.changed_data: facture_form.save() - vente_form.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for form in vente_form for field - in facture_form.changed_data + form.changed_data)) + vente_form.save() messages.success(request, "La facture a bien été modifiée") return redirect(reverse('cotisations:index')) return form({ @@ -278,9 +266,7 @@ def del_facture(request, facture, factureid): """Suppression d'une facture. Supprime en cascade les ventes et cotisations filles""" if request.method == "POST": - with transaction.atomic(), reversion.create_revision(): - facture.delete() - reversion.set_user(request.user) + facture.delete() messages.success(request, "La facture a été détruite") return redirect(reverse('cotisations:index')) return form({ @@ -297,21 +283,15 @@ def credit_solde(request, user, userid): facture = CreditSoldeForm(request.POST or None) if facture.is_valid(): facture_instance = facture.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - facture_instance.user = user - facture_instance.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + facture_instance.user = user + facture_instance.save() new_vente = Vente.objects.create( facture=facture_instance, name="solde", prix=facture.cleaned_data['montant'], number=1 ) - with transaction.atomic(), reversion.create_revision(): - new_vente.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_vente.save() messages.success(request, "Solde modifié") return redirect(reverse('cotisations:index')) return form({'factureform': facture, 'action_name' : 'Créditer'}, 'cotisations/facture.html', request) @@ -329,10 +309,7 @@ def add_article(request): PAS de conséquence sur les ventes déjà faites""" article = ArticleForm(request.POST or None) if article.is_valid(): - with transaction.atomic(), reversion.create_revision(): - article.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + article.save() messages.success(request, "L'article a été ajouté") return redirect(reverse('cotisations:index-article')) return form({'factureform': article, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) @@ -345,15 +322,9 @@ def edit_article(request, article_instance, articleid): Réservé au trésorier""" article = ArticleForm(request.POST or None, instance=article_instance) if article.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if article.changed_data: article.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in article.changed_data - ) - ) - messages.success(request, "Type d'article modifié") + messages.success(request, "Type d'article modifié") return redirect(reverse('cotisations:index-article')) return form({'factureform': article, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) @@ -365,9 +336,7 @@ def del_article(request, instances): article = DelArticleForm(request.POST or None, instances=instances) if article.is_valid(): article_del = article.cleaned_data['articles'] - with transaction.atomic(), reversion.create_revision(): - article_del.delete() - reversion.set_user(request.user) + article_del.delete() messages.success(request, "Le/les articles ont été supprimé") return redirect(reverse('cotisations:index-article')) return form({'factureform': article, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) @@ -380,10 +349,7 @@ def add_paiement(request): via foreign key""" paiement = PaiementForm(request.POST or None) if paiement.is_valid(): - with transaction.atomic(), reversion.create_revision(): - paiement.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + paiement.save() messages.success(request, "Le moyen de paiement a été ajouté") return redirect(reverse('cotisations:index-paiement')) return form({'factureform': paiement, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) @@ -395,15 +361,9 @@ def edit_paiement(request, paiement_instance, paiementid): """Edition d'un moyen de paiement""" paiement = PaiementForm(request.POST or None, instance=paiement_instance) if paiement.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if paiement.changed_data: paiement.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in paiement.changed_data - ) - ) - messages.success(request, "Type de paiement modifié") + messages.success(request, "Type de paiement modifié") return redirect(reverse('cotisations:index-paiement')) return form({'factureform': paiement, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) @@ -417,10 +377,7 @@ def del_paiement(request, instances): paiement_dels = paiement.cleaned_data['paiements'] for paiement_del in paiement_dels: try: - with transaction.atomic(), reversion.create_revision(): - paiement_del.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") + paiement_del.delete() messages.success( request, "Le moyen de paiement a été supprimé" @@ -441,10 +398,7 @@ def add_banque(request): """Ajoute une banque à la liste des banques""" banque = BanqueForm(request.POST or None) if banque.is_valid(): - with transaction.atomic(), reversion.create_revision(): - banque.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + banque.save() messages.success(request, "La banque a été ajoutée") return redirect(reverse('cotisations:index-banque')) return form({'factureform': banque, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) @@ -456,15 +410,9 @@ def edit_banque(request, banque_instance, banqueid): """Edite le nom d'une banque""" banque = BanqueForm(request.POST or None, instance=banque_instance) if banque.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if banque.changed_data: banque.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in banque.changed_data - ) - ) - messages.success(request, "Banque modifiée") + messages.success(request, "Banque modifiée") return redirect(reverse('cotisations:index-banque')) return form({'factureform': banque, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) @@ -478,10 +426,7 @@ def del_banque(request, instances): banque_dels = banque.cleaned_data['banques'] for banque_del in banque_dels: try: - with transaction.atomic(), reversion.create_revision(): - banque_del.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") + banque_del.delete() messages.success(request, "La banque a été supprimée") except ProtectedError: messages.error(request, "La banque %s est affectée à au moins\ @@ -509,20 +454,11 @@ def control(request): fields=('control', 'valid'), extra=0 ) - paginator = Paginator(facture_list, pagination_number) - page = request.GET.get('page') - try: - facture_list = paginator.page(page) - except PageNotAnInteger: - facture_list = paginator.page(1) - except EmptyPage: - facture_list = paginator.page(paginator.num.pages) + facture_list = re2o_paginator(request, facture_list, pagination_number) controlform = controlform_set(request.POST or None, queryset=facture_list.object_list) if controlform.is_valid(): - with transaction.atomic(), reversion.create_revision(): - controlform.save() - reversion.set_user(request.user) - reversion.set_comment("Controle trésorier") + controlform.save() + reversion.set_comment("Controle") return redirect(reverse('cotisations:control')) return render(request, 'cotisations/control.html', { 'facture_list': facture_list, @@ -573,16 +509,7 @@ def index(request): request.GET.get('order'), SortTable.COTISATIONS_INDEX ) - paginator = Paginator(facture_list, pagination_number) - page = request.GET.get('page') - try: - facture_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - facture_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - facture_list = paginator.page(paginator.num_pages) + facture_list = re2o_paginator(request, facture_list, pagination_number) return render(request, 'cotisations/index.html', { 'facture_list': facture_list }) @@ -630,10 +557,7 @@ def new_facture_solde(request, userid): 'users:profil', kwargs={'userid': userid} )) - with transaction.atomic(), reversion.create_revision(): - facture.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + facture.save() for art_item in articles: if art_item.cleaned_data: article = art_item.cleaned_data['article'] @@ -646,10 +570,7 @@ def new_facture_solde(request, userid): duration=article.duration, number=quantity ) - with transaction.atomic(), reversion.create_revision(): - new_vente.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_vente.save() if any(art_item.cleaned_data['article'].type_cotisation for art_item in articles if art_item.cleaned_data): messages.success( diff --git a/logs/views.py b/logs/views.py index 5dd87eaa..84ff8180 100644 --- a/logs/views.py +++ b/logs/views.py @@ -39,7 +39,6 @@ from __future__ import unicode_literals from django.urls import reverse from django.shortcuts import render, redirect -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db.models import Count, Max @@ -100,6 +99,7 @@ from re2o.utils import ( all_baned, all_has_access, all_adherent, + re2o_paginator, ) from re2o.acl import ( can_view_all, @@ -139,17 +139,7 @@ def index(request): request.GET.get('order'), SortTable.LOGS_INDEX ) - paginator = Paginator(versions, pagination_number) - page = request.GET.get('page') - try: - versions = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - versions = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - versions = paginator.page(paginator.num_pages) - + versions = re2o_paginator(request, versions, pagination_number) # Force to have a list instead of QuerySet versions.count(0) # Items to remove later because invalid @@ -191,16 +181,7 @@ def stats_logs(request): request.GET.get('order'), SortTable.LOGS_STATS_LOGS ) - paginator = Paginator(revisions, pagination_number) - page = request.GET.get('page') - try: - revisions = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - revisions = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - revisions = paginator.page(paginator.num_pages) + revisions = re2o_paginator(request, revisions, pagination_number) return render(request, 'logs/stats_logs.html', { 'revisions_list': revisions }) diff --git a/machines/forms.py b/machines/forms.py index 5dc9dd8b..6ece03e8 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -39,6 +39,7 @@ from django.forms import ModelForm, Form from django import forms from re2o.field_permissions import FieldPermissionFormMixin +from re2o.mixins import FormRevMixin from .models import ( Domain, @@ -61,7 +62,7 @@ from .models import ( ) -class EditMachineForm(FieldPermissionFormMixin, ModelForm): +class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Formulaire d'édition d'une machine""" class Meta: model = Machine @@ -79,7 +80,7 @@ class NewMachineForm(EditMachineForm): fields = ['name'] -class EditInterfaceForm(FieldPermissionFormMixin, ModelForm): +class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Edition d'une interface. Edition complète""" class Meta: model = Interface @@ -125,7 +126,7 @@ class AddInterfaceForm(EditInterfaceForm): fields = ['type', 'ipv4', 'mac_address', 'details'] -class AliasForm(ModelForm): +class AliasForm(FormRevMixin, ModelForm): """Ajout d'un alias (et edition), CNAME, contenant nom et extension""" class Meta: model = Domain @@ -142,7 +143,7 @@ class AliasForm(ModelForm): ) -class DomainForm(ModelForm): +class DomainForm(FormRevMixin, ModelForm): """Ajout et edition d'un enregistrement de nom, relié à interface""" class Meta: model = Domain @@ -158,7 +159,7 @@ class DomainForm(ModelForm): super(DomainForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelAliasForm(Form): +class DelAliasForm(FormRevMixin, Form): """Suppression d'un ou plusieurs objets alias""" alias = forms.ModelMultipleChoiceField( queryset=Domain.objects.all(), @@ -174,7 +175,7 @@ class DelAliasForm(Form): ) -class MachineTypeForm(ModelForm): +class MachineTypeForm(FormRevMixin, ModelForm): """Ajout et edition d'un machinetype, relié à un iptype""" class Meta: model = MachineType @@ -187,7 +188,7 @@ class MachineTypeForm(ModelForm): self.fields['ip_type'].label = "Type d'ip relié" -class DelMachineTypeForm(Form): +class DelMachineTypeForm(FormRevMixin, Form): """Suppression d'un ou plusieurs machinetype""" machinetypes = forms.ModelMultipleChoiceField( queryset=MachineType.objects.none(), @@ -204,7 +205,7 @@ class DelMachineTypeForm(Form): self.fields['machinetypes'].queryset = MachineType.objects.all() -class IpTypeForm(ModelForm): +class IpTypeForm(FormRevMixin, ModelForm): """Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de stop après creation""" class Meta: @@ -226,7 +227,7 @@ class EditIpTypeForm(IpTypeForm): 'ouverture_ports'] -class DelIpTypeForm(Form): +class DelIpTypeForm(FormRevMixin, Form): """Suppression d'un ou plusieurs iptype""" iptypes = forms.ModelMultipleChoiceField( queryset=IpType.objects.none(), @@ -243,7 +244,7 @@ class DelIpTypeForm(Form): self.fields['iptypes'].queryset = IpType.objects.all() -class ExtensionForm(ModelForm): +class ExtensionForm(FormRevMixin, ModelForm): """Formulaire d'ajout et edition d'une extension""" class Meta: model = Extension @@ -258,7 +259,7 @@ class ExtensionForm(ModelForm): self.fields['soa'].label = 'En-tête SOA à utiliser' -class DelExtensionForm(Form): +class DelExtensionForm(FormRevMixin, Form): """Suppression d'une ou plusieurs extensions""" extensions = forms.ModelMultipleChoiceField( queryset=Extension.objects.none(), @@ -275,7 +276,7 @@ class DelExtensionForm(Form): self.fields['extensions'].queryset = Extension.objects.all() -class Ipv6ListForm(FieldPermissionFormMixin, ModelForm): +class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Gestion des ipv6 d'une machine""" class Meta: model = Ipv6List @@ -286,7 +287,7 @@ class Ipv6ListForm(FieldPermissionFormMixin, ModelForm): super(Ipv6ListForm, self).__init__(*args, prefix=prefix, **kwargs) -class SOAForm(ModelForm): +class SOAForm(FormRevMixin, ModelForm): """Ajout et edition d'un SOA""" class Meta: model = SOA @@ -297,7 +298,7 @@ class SOAForm(ModelForm): super(SOAForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelSOAForm(Form): +class DelSOAForm(FormRevMixin, Form): """Suppression d'un ou plusieurs SOA""" soa = forms.ModelMultipleChoiceField( queryset=SOA.objects.none(), @@ -314,7 +315,7 @@ class DelSOAForm(Form): self.fields['soa'].queryset = SOA.objects.all() -class MxForm(ModelForm): +class MxForm(FormRevMixin, ModelForm): """Ajout et edition d'un MX""" class Meta: model = Mx @@ -327,7 +328,7 @@ class MxForm(ModelForm): interface_parent=None ).select_related('extension') -class DelMxForm(Form): +class DelMxForm(FormRevMixin, Form): """Suppression d'un ou plusieurs MX""" mx = forms.ModelMultipleChoiceField( queryset=Mx.objects.none(), @@ -344,7 +345,7 @@ class DelMxForm(Form): self.fields['mx'].queryset = Mx.objects.all() -class NsForm(ModelForm): +class NsForm(FormRevMixin, ModelForm): """Ajout d'un NS pour une zone On exclue les CNAME dans les objets domain (interdit par la rfc) donc on prend uniquemet """ @@ -360,7 +361,7 @@ class NsForm(ModelForm): ).select_related('extension') -class DelNsForm(Form): +class DelNsForm(FormRevMixin, Form): """Suppresion d'un ou plusieurs NS""" ns = forms.ModelMultipleChoiceField( queryset=Ns.objects.none(), @@ -377,7 +378,7 @@ class DelNsForm(Form): self.fields['ns'].queryset = Ns.objects.all() -class TxtForm(ModelForm): +class TxtForm(FormRevMixin, ModelForm): """Ajout d'un txt pour une zone""" class Meta: model = Txt @@ -388,7 +389,7 @@ class TxtForm(ModelForm): super(TxtForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelTxtForm(Form): +class DelTxtForm(FormRevMixin, Form): """Suppression d'un ou plusieurs TXT""" txt = forms.ModelMultipleChoiceField( queryset=Txt.objects.none(), @@ -405,7 +406,7 @@ class DelTxtForm(Form): self.fields['txt'].queryset = Txt.objects.all() -class SrvForm(ModelForm): +class SrvForm(FormRevMixin, ModelForm): """Ajout d'un srv pour une zone""" class Meta: model = Srv @@ -416,7 +417,7 @@ class SrvForm(ModelForm): super(SrvForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelSrvForm(Form): +class DelSrvForm(FormRevMixin, Form): """Suppression d'un ou plusieurs Srv""" srv = forms.ModelMultipleChoiceField( queryset=Srv.objects.none(), @@ -433,7 +434,7 @@ class DelSrvForm(Form): self.fields['srv'].queryset = Srv.objects.all() -class NasForm(ModelForm): +class NasForm(FormRevMixin, ModelForm): """Ajout d'un type de nas (machine d'authentification, swicths, bornes...)""" class Meta: @@ -445,7 +446,7 @@ class NasForm(ModelForm): super(NasForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelNasForm(Form): +class DelNasForm(FormRevMixin, Form): """Suppression d'un ou plusieurs nas""" nas = forms.ModelMultipleChoiceField( queryset=Nas.objects.none(), @@ -462,7 +463,7 @@ class DelNasForm(Form): self.fields['nas'].queryset = Nas.objects.all() -class ServiceForm(ModelForm): +class ServiceForm(FormRevMixin, ModelForm): """Ajout et edition d'une classe de service : dns, dhcp, etc""" class Meta: model = Service @@ -482,7 +483,7 @@ class ServiceForm(ModelForm): return instance -class DelServiceForm(Form): +class DelServiceForm(FormRevMixin, Form): """Suppression d'un ou plusieurs service""" service = forms.ModelMultipleChoiceField( queryset=Service.objects.none(), @@ -499,7 +500,7 @@ class DelServiceForm(Form): self.fields['service'].queryset = Service.objects.all() -class VlanForm(ModelForm): +class VlanForm(FormRevMixin, ModelForm): """Ajout d'un vlan : id, nom""" class Meta: model = Vlan @@ -510,7 +511,7 @@ class VlanForm(ModelForm): super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelVlanForm(Form): +class DelVlanForm(FormRevMixin, Form): """Suppression d'un ou plusieurs vlans""" vlan = forms.ModelMultipleChoiceField( queryset=Vlan.objects.none(), @@ -527,7 +528,7 @@ class DelVlanForm(Form): self.fields['vlan'].queryset = Vlan.objects.all() -class EditOuverturePortConfigForm(ModelForm): +class EditOuverturePortConfigForm(FormRevMixin, ModelForm): """Edition de la liste des profils d'ouverture de ports pour l'interface""" class Meta: @@ -543,7 +544,7 @@ class EditOuverturePortConfigForm(ModelForm): ) -class EditOuverturePortListForm(ModelForm): +class EditOuverturePortListForm(FormRevMixin, ModelForm): """Edition de la liste des ports et profils d'ouverture des ports""" class Meta: diff --git a/machines/models.py b/machines/models.py index d4368b2e..dc2dba65 100644 --- a/machines/models.py +++ b/machines/models.py @@ -27,6 +27,7 @@ from datetime import timedelta import re from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress from ipaddress import IPv6Address +from itertools import chain from django.db import models from django.db.models.signals import post_save, post_delete @@ -39,13 +40,13 @@ from django.core.validators import MaxValueValidator from macaddress.fields import MACAddressField from re2o.field_permissions import FieldPermissionModelMixin -from re2o.mixins import AclMixin +from re2o.mixins import AclMixin, RevMixin import users.models import preferences.models -class Machine(FieldPermissionModelMixin, models.Model): +class Machine(RevMixin, FieldPermissionModelMixin, models.Model): """ Class définissant une machine, object parent user, objets fils interfaces""" PRETTY_NAME = "Machine" @@ -72,6 +73,11 @@ class Machine(FieldPermissionModelMixin, models.Model): """ return Machine.objects.get(pk=machineid) + def linked_objects(self): + """Return linked objects : machine and domain. + Usefull in history display""" + return chain(self.interface_set.all(), Domain.objects.filter(interface_parent__in=self.interface_set.all())) + @staticmethod def can_change_user(user_request, *args, **kwargs): """Checks if an user is allowed to change the user who owns a @@ -163,7 +169,7 @@ class Machine(FieldPermissionModelMixin, models.Model): return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name) -class MachineType(AclMixin, models.Model): +class MachineType(RevMixin, AclMixin, models.Model): """ Type de machine, relié à un type d'ip, affecté aux interfaces""" PRETTY_NAME = "Type de machine" @@ -203,7 +209,7 @@ class MachineType(AclMixin, models.Model): return self.type -class IpType(AclMixin, models.Model): +class IpType(RevMixin, AclMixin, models.Model): """ Type d'ip, définissant un range d'ip, affecté aux machine types""" PRETTY_NAME = "Type d'ip" @@ -333,7 +339,7 @@ class IpType(AclMixin, models.Model): return self.type -class Vlan(AclMixin, models.Model): +class Vlan(RevMixin, AclMixin, models.Model): """ Un vlan : vlan_id et nom On limite le vlan id entre 0 et 4096, comme défini par la norme""" PRETTY_NAME = "Vlans" @@ -351,7 +357,7 @@ class Vlan(AclMixin, models.Model): return self.name -class Nas(AclMixin, models.Model): +class Nas(RevMixin, AclMixin, models.Model): """ Les nas. Associé à un machine_type. Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour le radius. Champ autocapture de la mac à true ou false""" @@ -390,7 +396,7 @@ class Nas(AclMixin, models.Model): return self.name -class SOA(AclMixin, models.Model): +class SOA(RevMixin, AclMixin, models.Model): """ Un enregistrement SOA associé à une extension Les valeurs par défault viennent des recommandations RIPE : @@ -467,7 +473,7 @@ class SOA(AclMixin, models.Model): -class Extension(AclMixin, models.Model): +class Extension(RevMixin, AclMixin, models.Model): """ Extension dns type example.org. Précise si tout le monde peut l'utiliser, associé à un origin (ip d'origine)""" PRETTY_NAME = "Extensions dns" @@ -530,7 +536,7 @@ class Extension(AclMixin, models.Model): super(Extension, self).clean(*args, **kwargs) -class Mx(AclMixin, models.Model): +class Mx(RevMixin, AclMixin, models.Model): """ Entrées des MX. Enregistre la zone (extension) associée et la priorité Todo : pouvoir associer un MX à une interface """ @@ -555,7 +561,7 @@ class Mx(AclMixin, models.Model): return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) -class Ns(AclMixin, models.Model): +class Ns(RevMixin, AclMixin, models.Model): """Liste des enregistrements name servers par zone considéérée""" PRETTY_NAME = "Enregistrements NS" @@ -576,7 +582,7 @@ class Ns(AclMixin, models.Model): return str(self.zone) + ' ' + str(self.ns) -class Txt(AclMixin, models.Model): +class Txt(RevMixin, AclMixin, models.Model): """ Un enregistrement TXT associé à une extension""" PRETTY_NAME = "Enregistrement TXT" @@ -599,7 +605,7 @@ class Txt(AclMixin, models.Model): return str(self.field1).ljust(15) + " IN TXT " + str(self.field2) -class Srv(AclMixin, models.Model): +class Srv(RevMixin, AclMixin, models.Model): PRETTY_NAME = "Enregistrement Srv" TCP = 'TCP' @@ -661,7 +667,7 @@ class Srv(AclMixin, models.Model): str(self.port) + ' ' + str(self.target) + '.' -class Interface(AclMixin, FieldPermissionModelMixin,models.Model): +class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): """ Une interface. Objet clef de l'application machine : - une address mac unique. Possibilité de la rendre unique avec le typemachine @@ -908,7 +914,7 @@ class Interface(AclMixin, FieldPermissionModelMixin,models.Model): return self.ipv4 and not self.has_private_ip() -class Ipv6List(AclMixin, FieldPermissionModelMixin, models.Model): +class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): PRETTY_NAME = 'Enregistrements Ipv6 des machines' ipv6 = models.GenericIPAddressField( @@ -1012,7 +1018,7 @@ class Ipv6List(AclMixin, FieldPermissionModelMixin, models.Model): return str(self.ipv6) -class Domain(AclMixin, models.Model): +class Domain(RevMixin, AclMixin, models.Model): """ Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les alias et les nom de machines, suivant si interface_parent ou cname sont remplis""" @@ -1170,7 +1176,7 @@ class Domain(AclMixin, models.Model): return str(self.name) + str(self.extension) -class IpList(AclMixin, models.Model): +class IpList(RevMixin, AclMixin, models.Model): PRETTY_NAME = "Addresses ipv4" ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True) @@ -1202,7 +1208,7 @@ class IpList(AclMixin, models.Model): return self.ipv4 -class Service(AclMixin, models.Model): +class Service(RevMixin, AclMixin, models.Model): """ Definition d'un service (dhcp, dns, etc)""" PRETTY_NAME = "Services à générer (dhcp, dns, etc)" @@ -1256,7 +1262,7 @@ def regen(service): return -class Service_link(AclMixin, models.Model): +class Service_link(RevMixin, AclMixin, models.Model): """ Definition du lien entre serveurs et services""" PRETTY_NAME = "Relation entre service et serveur" @@ -1287,7 +1293,7 @@ class Service_link(AclMixin, models.Model): return str(self.server) + " " + str(self.service) -class OuverturePortList(AclMixin, models.Model): +class OuverturePortList(RevMixin, AclMixin, models.Model): """Liste des ports ouverts sur une interface.""" PRETTY_NAME = "Profil d'ouverture de ports" @@ -1346,7 +1352,7 @@ class OuverturePortList(AclMixin, models.Model): ) -class OuverturePort(AclMixin, models.Model): +class OuverturePort(RevMixin, AclMixin, models.Model): """ Représente un simple port ou une plage de ports. diff --git a/machines/templates/machines/aff_alias.html b/machines/templates/machines/aff_alias.html index fb3f0486..f19b6482 100644 --- a/machines/templates/machines/aff_alias.html +++ b/machines/templates/machines/aff_alias.html @@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_edit alias %} {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='alias' id=alias.id %} + {% include 'buttons/history.html' with href='machines:history' name='domain' id=alias.id %} {% endfor %} diff --git a/machines/views.py b/machines/views.py index eb7086c4..ee23504d 100644 --- a/machines/views.py +++ b/machines/views.py @@ -33,13 +33,11 @@ from django.http import HttpResponse from django.shortcuts import render, redirect from django.shortcuts import get_object_or_404 from django.template.context_processors import csrf -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.db.models import ProtectedError, F from django.forms import ValidationError, modelformset_factory -from django.db import transaction from django.contrib.auth import authenticate, login from django.views.decorators.csrf import csrf_exempt @@ -124,6 +122,7 @@ from re2o.utils import ( all_has_access, filter_active_interfaces, SortTable, + re2o_paginator, ) from re2o.acl import ( can_create, @@ -238,20 +237,11 @@ def new_machine(request, user, userid): domain.instance.interface_parent = new_interface if domain.is_valid(): new_domain = domain.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - new_machine.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_machine.save() new_interface.machine = new_machine - with transaction.atomic(), reversion.create_revision(): - new_interface.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_interface.save() new_domain.interface_parent = new_interface - with transaction.atomic(), reversion.create_revision(): - new_domain.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_domain.save() messages.success(request, "La machine a été créée") return redirect(reverse( 'users:profil', @@ -287,18 +277,12 @@ def edit_interface(request, interface_instance, interfaceid): new_machine = machine_form.save(commit=False) new_interface = interface_form.save(commit=False) new_domain = domain_form.save(commit=False) - with transaction.atomic(), reversion.create_revision(): + if machine_form.changed_data: new_machine.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machine_form.changed_data)) - with transaction.atomic(), reversion.create_revision(): + if interface_form.changed_data: new_interface.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in interface_form.changed_data)) - with transaction.atomic(), reversion.create_revision(): + if domain_form.changed_data: new_domain.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) messages.success(request, "La machine a été modifiée") return redirect(reverse( 'users:profil', @@ -318,9 +302,7 @@ def edit_interface(request, interface_instance, interfaceid): def del_machine(request, machine, machineid): """ Supprime une machine, interfaces en mode cascade""" if request.method == "POST": - with transaction.atomic(), reversion.create_revision(): - machine.delete() - reversion.set_user(request.user) + machine.delete() messages.success(request, "La machine a été détruite") return redirect(reverse( 'users:profil', @@ -342,15 +324,9 @@ def new_interface(request, machine, machineid): new_interface.machine = machine if domain_form.is_valid(): new_domain = domain_form.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - new_interface.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_interface.save() new_domain.interface_parent = new_interface - with transaction.atomic(), reversion.create_revision(): - new_domain.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_domain.save() messages.success(request, "L'interface a été ajoutée") return redirect(reverse( 'users:profil', @@ -370,11 +346,9 @@ def del_interface(request, interface, interfaceid): """ Supprime une interface. Domain objet en mode cascade""" if request.method == "POST": machine = interface.machine - with transaction.atomic(), reversion.create_revision(): - interface.delete() - if not machine.interface_set.all(): - machine.delete() - reversion.set_user(request.user) + interface.delete() + if not machine.interface_set.all(): + machine.delete() messages.success(request, "L'interface a été détruite") return redirect(reverse( 'users:profil', @@ -390,10 +364,7 @@ def new_ipv6list(request, interface, interfaceid): ipv6list_instance = Ipv6List(interface=interface) ipv6 = Ipv6ListForm(request.POST or None, instance=ipv6list_instance, user=request.user) if ipv6.is_valid(): - with transaction.atomic(), reversion.create_revision(): - ipv6.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + ipv6.save() messages.success(request, "Ipv6 ajoutée") return redirect(reverse( 'machines:index-ipv6', @@ -407,11 +378,9 @@ def edit_ipv6list(request, ipv6list_instance, ipv6listid): """Edition d'une ipv6""" ipv6 = Ipv6ListForm(request.POST or None, instance=ipv6list_instance, user=request.user) if ipv6.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if ipv6.changed_data: ipv6.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ipv6.changed_data)) - messages.success(request, "Ipv6 modifiée") + messages.success(request, "Ipv6 modifiée") return redirect(reverse( 'machines:index-ipv6', kwargs={'interfaceid':str(ipv6list_instance.interface.id)} @@ -424,9 +393,7 @@ def del_ipv6list(request, ipv6list, ipv6listid): """ Supprime une ipv6""" if request.method == "POST": interfaceid = ipv6list.interface.id - with transaction.atomic(), reversion.create_revision(): - ipv6list.delete() - reversion.set_user(request.user) + ipv6list.delete() messages.success(request, "L'ipv6 a été détruite") return redirect(reverse( 'machines:index-ipv6', @@ -441,10 +408,7 @@ def add_iptype(request): iptype = IpTypeForm(request.POST or None) if iptype.is_valid(): - with transaction.atomic(), reversion.create_revision(): - iptype.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + iptype.save() messages.success(request, "Ce type d'ip a été ajouté") return redirect(reverse('machines:index-iptype')) return form({'iptypeform': iptype, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -456,11 +420,9 @@ def edit_iptype(request, iptype_instance, iptypeid): iptype = EditIpTypeForm(request.POST or None, instance=iptype_instance) if iptype.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if iptype.changed_data: iptype.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data)) - messages.success(request, "Type d'ip modifié") + messages.success(request, "Type d'ip modifié") return redirect(reverse('machines:index-iptype')) return form({'iptypeform': iptype, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -473,9 +435,7 @@ def del_iptype(request, instances): iptype_dels = iptype.cleaned_data['iptypes'] for iptype_del in iptype_dels: try: - with transaction.atomic(), reversion.create_revision(): - iptype_del.delete() - reversion.set_user(request.user) + iptype_del.delete() messages.success(request, "Le type d'ip a été supprimé") except ProtectedError: messages.error(request, "Le type d'ip %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % iptype_del) @@ -488,10 +448,7 @@ def add_machinetype(request): machinetype = MachineTypeForm(request.POST or None) if machinetype.is_valid(): - with transaction.atomic(), reversion.create_revision(): - machinetype.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + machinetype.save() messages.success(request, "Ce type de machine a été ajouté") return redirect(reverse('machines:index-machinetype')) return form({'machinetypeform': machinetype, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -499,14 +456,11 @@ def add_machinetype(request): @login_required @can_edit(MachineType) def edit_machinetype(request, machinetype_instance, machinetypeid): - machinetype = MachineTypeForm(request.POST or None, instance=machinetype_instance) if machinetype.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if machinetype.changed_data: machinetype.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data)) - messages.success(request, "Type de machine modifié") + messages.success(request, "Type de machine modifié") return redirect(reverse('machines:index-machinetype')) return form({'machinetypeform': machinetype, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -518,9 +472,7 @@ def del_machinetype(request, instances): machinetype_dels = machinetype.cleaned_data['machinetypes'] for machinetype_del in machinetype_dels: try: - with transaction.atomic(), reversion.create_revision(): - machinetype_del.delete() - reversion.set_user(request.user) + machinetype_del.delete() messages.success(request, "Le type de machine a été supprimé") except ProtectedError: messages.error(request, "Le type de machine %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % machinetype_del) @@ -530,13 +482,9 @@ def del_machinetype(request, instances): @login_required @can_create(Extension) def add_extension(request): - extension = ExtensionForm(request.POST or None) if extension.is_valid(): - with transaction.atomic(), reversion.create_revision(): - extension.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + extension.save() messages.success(request, "Cette extension a été ajoutée") return redirect(reverse('machines:index-extension')) return form({'extensionform': extension, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -544,14 +492,11 @@ def add_extension(request): @login_required @can_edit(Extension) def edit_extension(request, extension_instance, extensionid): - extension = ExtensionForm(request.POST or None, instance=extension_instance) if extension.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if extension.changed_data: extension.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data)) - messages.success(request, "Extension modifiée") + messages.success(request, "Extension modifiée") return redirect(reverse('machines:index-extension')) return form({'extensionform': extension, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -563,9 +508,7 @@ def del_extension(request, instances): extension_dels = extension.cleaned_data['extensions'] for extension_del in extension_dels: try: - with transaction.atomic(), reversion.create_revision(): - extension_del.delete() - reversion.set_user(request.user) + extension_del.delete() messages.success(request, "L'extension a été supprimée") except ProtectedError: messages.error(request, "L'extension %s est affectée à au moins un type de machine, vous ne pouvez pas la supprimer" % extension_del) @@ -575,13 +518,9 @@ def del_extension(request, instances): @login_required @can_create(SOA) def add_soa(request): - soa = SOAForm(request.POST or None) if soa.is_valid(): - with transaction.atomic(), reversion.create_revision(): - soa.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + soa.save() messages.success(request, "Cet enregistrement SOA a été ajouté") return redirect(reverse('machines:index-extension')) return form({'soaform': soa, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -589,14 +528,11 @@ def add_soa(request): @login_required @can_edit(SOA) def edit_soa(request, soa_instance, soaid): - soa = SOAForm(request.POST or None, instance=soa_instance) if soa.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if soa.changed_data: soa.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in soa.changed_data)) - messages.success(request, "SOA modifié") + messages.success(request, "SOA modifié") return redirect(reverse('machines:index-extension')) return form({'soaform': soa, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -608,9 +544,7 @@ def del_soa(request, instances): soa_dels = soa.cleaned_data['soa'] for soa_del in soa_dels: try: - with transaction.atomic(), reversion.create_revision(): - soa_del.delete() - reversion.set_user(request.user) + soa_del.delete() messages.success(request, "Le SOA a été supprimée") except ProtectedError: messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del) @@ -620,13 +554,9 @@ def del_soa(request, instances): @login_required @can_create(Mx) def add_mx(request): - mx = MxForm(request.POST or None) if mx.is_valid(): - with transaction.atomic(), reversion.create_revision(): - mx.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + mx.save() messages.success(request, "Cet enregistrement mx a été ajouté") return redirect(reverse('machines:index-extension')) return form({'mxform': mx, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -634,14 +564,11 @@ def add_mx(request): @login_required @can_edit(Mx) def edit_mx(request, mx_instance, mxid): - mx = MxForm(request.POST or None, instance=mx_instance) if mx.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if mx.changed_data: mx.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data)) - messages.success(request, "Mx modifié") + messages.success(request, "Mx modifié") return redirect(reverse('machines:index-extension')) return form({'mxform': mx, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -653,9 +580,7 @@ def del_mx(request, instances): mx_dels = mx.cleaned_data['mx'] for mx_del in mx_dels: try: - with transaction.atomic(), reversion.create_revision(): - mx_del.delete() - reversion.set_user(request.user) + mx_del.delete() messages.success(request, "L'mx a été supprimée") except ProtectedError: messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del) @@ -665,13 +590,9 @@ def del_mx(request, instances): @login_required @can_create(Ns) def add_ns(request): - ns = NsForm(request.POST or None) if ns.is_valid(): - with transaction.atomic(), reversion.create_revision(): - ns.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + ns.save() messages.success(request, "Cet enregistrement ns a été ajouté") return redirect(reverse('machines:index-extension')) return form({'nsform': ns, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -679,14 +600,11 @@ def add_ns(request): @login_required @can_edit(Ns) def edit_ns(request, ns_instance, nsid): - ns = NsForm(request.POST or None, instance=ns_instance) if ns.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if ns.changed_data: ns.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data)) - messages.success(request, "Ns modifié") + messages.success(request, "Ns modifié") return redirect(reverse('machines:index-extension')) return form({'nsform': ns, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -698,9 +616,7 @@ def del_ns(request, instances): ns_dels = ns.cleaned_data['ns'] for ns_del in ns_dels: try: - with transaction.atomic(), reversion.create_revision(): - ns_del.delete() - reversion.set_user(request.user) + ns_del.delete() messages.success(request, "Le ns a été supprimée") except ProtectedError: messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del) @@ -710,13 +626,9 @@ def del_ns(request, instances): @login_required @can_create(Txt) def add_txt(request): - txt = TxtForm(request.POST or None) if txt.is_valid(): - with transaction.atomic(), reversion.create_revision(): - txt.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + txt.save() messages.success(request, "Cet enregistrement text a été ajouté") return redirect(reverse('machines:index-extension')) return form({'txtform': txt, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -724,14 +636,11 @@ def add_txt(request): @login_required @can_edit(Txt) def edit_txt(request, txt_instance, txtid): - txt = TxtForm(request.POST or None, instance=txt_instance) if txt.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if txt.changed_data: txt.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data)) - messages.success(request, "Txt modifié") + messages.success(request, "Txt modifié") return redirect(reverse('machines:index-extension')) return form({'txtform': txt, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -743,9 +652,7 @@ def del_txt(request, instances): txt_dels = txt.cleaned_data['txt'] for txt_del in txt_dels: try: - with transaction.atomic(), reversion.create_revision(): - txt_del.delete() - reversion.set_user(request.user) + txt_del.delete() messages.success(request, "Le txt a été supprimé") except ProtectedError: messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del) @@ -755,13 +662,9 @@ def del_txt(request, instances): @login_required @can_create(Srv) def add_srv(request): - srv = SrvForm(request.POST or None) if srv.is_valid(): - with transaction.atomic(), reversion.create_revision(): - srv.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + srv.save() messages.success(request, "Cet enregistrement srv a été ajouté") return redirect(reverse('machines:index-extension')) return form({'srvform': srv, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -769,14 +672,11 @@ def add_srv(request): @login_required @can_edit(Srv) def edit_srv(request, srv_instance, srvid): - srv = SrvForm(request.POST or None, instance=srv_instance) if srv.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if srv.changed_data: srv.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in srv.changed_data)) - messages.success(request, "Srv modifié") + messages.success(request, "Srv modifié") return redirect(reverse('machines:index-extension')) return form({'srvform': srv, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -788,9 +688,7 @@ def del_srv(request, instances): srv_dels = srv.cleaned_data['srv'] for srv_del in srv_dels: try: - with transaction.atomic(), reversion.create_revision(): - srv_del.delete() - reversion.set_user(request.user) + srv_del.delete() messages.success(request, "L'srv a été supprimée") except ProtectedError: messages.error(request, "Erreur le Srv suivant %s ne peut être supprimé" % srv_del) @@ -801,15 +699,11 @@ def del_srv(request, instances): @can_create(Domain) @can_edit(Interface) def add_alias(request, interface, interfaceid): - alias = AliasForm(request.POST or None, user=request.user) if alias.is_valid(): alias = alias.save(commit=False) alias.cname = interface.domain - with transaction.atomic(), reversion.create_revision(): - alias.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + alias.save() messages.success(request, "Cet alias a été ajouté") return redirect(reverse( 'machines:index-alias', @@ -820,14 +714,11 @@ def add_alias(request, interface, interfaceid): @login_required @can_edit(Domain) def edit_alias(request, domain_instance, domainid): - alias = AliasForm(request.POST or None, instance=domain_instance, user=request.user) if alias.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if alias.changed_data: domain_instance = alias.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data)) - messages.success(request, "Alias modifié") + messages.success(request, "Alias modifié") return redirect(reverse( 'machines:index-alias', kwargs={'interfaceid':str(domain_instance.cname.interface_parent.id)} @@ -842,9 +733,7 @@ def del_alias(request, interface, interfaceid): alias_dels = alias.cleaned_data['alias'] for alias_del in alias_dels: try: - with transaction.atomic(), reversion.create_revision(): - alias_del.delete() - reversion.set_user(request.user) + alias_del.delete() messages.success(request, "L'alias %s a été supprimé" % alias_del) except ProtectedError: messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del) @@ -858,13 +747,9 @@ def del_alias(request, interface, interfaceid): @login_required @can_create(Service) def add_service(request): - service = ServiceForm(request.POST or None) if service.is_valid(): - with transaction.atomic(), reversion.create_revision(): - service.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + service.save() messages.success(request, "Cet enregistrement service a été ajouté") return redirect(reverse('machines:index-service')) return form({'serviceform': service, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -872,14 +757,11 @@ def add_service(request): @login_required @can_edit(Service) def edit_service(request, service_instance, serviceid): - service = ServiceForm(request.POST or None, instance=service_instance) if service.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if service.changed_data: service.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data)) - messages.success(request, "Service modifié") + messages.success(request, "Service modifié") return redirect(reverse('machines:index-service')) return form({'serviceform': service, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -891,9 +773,7 @@ def del_service(request, instances): service_dels = service.cleaned_data['service'] for service_del in service_dels: try: - with transaction.atomic(), reversion.create_revision(): - service_del.delete() - reversion.set_user(request.user) + service_del.delete() messages.success(request, "Le service a été supprimée") except ProtectedError: messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del) @@ -903,13 +783,9 @@ def del_service(request, instances): @login_required @can_create(Vlan) def add_vlan(request): - vlan = VlanForm(request.POST or None) if vlan.is_valid(): - with transaction.atomic(), reversion.create_revision(): - vlan.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + vlan.save() messages.success(request, "Cet enregistrement vlan a été ajouté") return redirect(reverse('machines:index-vlan')) return form({'vlanform': vlan, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -917,14 +793,11 @@ def add_vlan(request): @login_required @can_edit(Vlan) def edit_vlan(request, vlan_instance, vlanid): - vlan = VlanForm(request.POST or None, instance=vlan_instance) if vlan.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if vlan.changed_data: vlan.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data)) - messages.success(request, "Vlan modifié") + messages.success(request, "Vlan modifié") return redirect(reverse('machines:index-vlan')) return form({'vlanform': vlan, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -936,9 +809,7 @@ def del_vlan(request, instances): vlan_dels = vlan.cleaned_data['vlan'] for vlan_del in vlan_dels: try: - with transaction.atomic(), reversion.create_revision(): - vlan_del.delete() - reversion.set_user(request.user) + vlan_del.delete() messages.success(request, "Le vlan a été supprimée") except ProtectedError: messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del) @@ -948,13 +819,9 @@ def del_vlan(request, instances): @login_required @can_create(Nas) def add_nas(request): - nas = NasForm(request.POST or None) if nas.is_valid(): - with transaction.atomic(), reversion.create_revision(): - nas.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + nas.save() messages.success(request, "Cet enregistrement nas a été ajouté") return redirect(reverse('machines:index-nas')) return form({'nasform': nas, 'action_name' : 'Créer'}, 'machines/machine.html', request) @@ -962,14 +829,11 @@ def add_nas(request): @login_required @can_edit(Nas) def edit_nas(request, nas_instance, nasid): - nas = NasForm(request.POST or None, instance=nas_instance) if nas.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if nas.changed_data: nas.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data)) - messages.success(request, "Nas modifié") + messages.success(request, "Nas modifié") return redirect(reverse('machines:index-nas')) return form({'nasform': nas, 'action_name' : 'Editer'}, 'machines/machine.html', request) @@ -981,9 +845,7 @@ def del_nas(request, instances): nas_dels = nas.cleaned_data['nas'] for nas_del in nas_dels: try: - with transaction.atomic(), reversion.create_revision(): - nas_del.delete() - reversion.set_user(request.user) + nas_del.delete() messages.success(request, "Le nas a été supprimé") except ProtectedError: messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del) @@ -1001,16 +863,7 @@ def index(request): request.GET.get('order'), SortTable.MACHINES_INDEX ) - paginator = Paginator(machines_list, pagination_large_number) - page = request.GET.get('page') - try: - machines_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - machines_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - machines_list = paginator.page(paginator.num_pages) + machines_list = re2o_paginator(request, machines_list, pagination_large_number) return render(request, 'machines/index.html', {'machines_list': machines_list}) @login_required @@ -1084,7 +937,6 @@ def index_portlist(request): @login_required @can_edit(OuverturePortList) def edit_portlist(request, ouvertureportlist_instance, ouvertureportlistid): - port_list = EditOuverturePortListForm(request.POST or None, instance=ouvertureportlist_instance) port_formset = modelformset_factory( OuverturePort, @@ -1095,7 +947,10 @@ def edit_portlist(request, ouvertureportlist_instance, ouvertureportlistid): validate_min=True, )(request.POST or None, queryset=ouvertureportlist_instance.ouvertureport_set.all()) if port_list.is_valid() and port_formset.is_valid(): - pl = port_list.save() + if port_list.changed_data: + pl = port_list.save() + else: + pl = ouvertureportlist_instance instances = port_formset.save(commit=False) for to_delete in port_formset.deleted_objects: to_delete.delete() @@ -1116,7 +971,6 @@ def del_portlist(request, port_list_instance, ouvertureportlistid): @login_required @can_create(OuverturePortList) def add_portlist(request): - port_list = EditOuverturePortListForm(request.POST or None) port_formset = modelformset_factory( OuverturePort, @@ -1152,8 +1006,9 @@ def configure_ports(request, interface_instance, interfaceid): messages.error(request, "Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas d'effet en v4") interface = EditOuverturePortConfigForm(request.POST or None, instance=interface_instance) if interface.is_valid(): - interface.save() - messages.success(request, "Configuration des ports mise à jour.") + if interface.changed_data: + interface.save() + messages.success(request, "Configuration des ports mise à jour.") return redirect(reverse('machines:index')) return form({'interfaceform' : interface, 'action_name' : 'Editer la configuration'}, 'machines/machine.html', request) diff --git a/preferences/views.py b/preferences/views.py index 43befc72..22341c28 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -32,7 +32,6 @@ from __future__ import unicode_literals from django.urls import reverse from django.shortcuts import render, redirect -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.db.models import ProtectedError diff --git a/re2o/mixins.py b/re2o/mixins.py index 09c197d5..307074ff 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -4,6 +4,7 @@ # quelques clics. # # Copyright © 2018 Gabriel Détraz +# Copyright © 2017 Charlie Jacomme # # 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 @@ -19,6 +20,29 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from reversion import revisions as reversion + + +class RevMixin(object): + def save(self, *args, **kwargs): + if self.pk is None: + reversion.set_comment("Création") + return super(RevMixin, self).save(*args, **kwargs) + + def delete(self, *args, **kwargs): + reversion.set_comment("Suppresion") + return super(RevMixin, self).delete(*args, **kwargs) + + +class FormRevMixin(object): + def save(self, *args, **kwargs): + if reversion.get_comment() != "" and self.changed_data != []: + reversion.set_comment(reversion.get_comment() + ",%s" % ', '.join(field for field in self.changed_data)) + elif self.changed_data: + reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in self.changed_data)) + return super(FormRevMixin, self).save(*args, **kwargs) + + class AclMixin(object): """This mixin is used in nearly every class/models defined in re2o apps. It is used by acl, in models (decorators can_...) and in templates tags diff --git a/re2o/script_utils.py b/re2o/script_utils.py new file mode 100644 index 00000000..e72ea626 --- /dev/null +++ b/re2o/script_utils.py @@ -0,0 +1,86 @@ +# ⁻*- mode: python; coding: utf-8 -*- +# 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 Lev-Arcady Sellem +# +# 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. + +import os, sys, pwd + +proj_path="/var/www/re2o" +os.environ.setdefault("DJANGO_SETTINGS_MODULE","re2o.settings") +sys.path.append(proj_path) +os.chdir(proj_path) +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + + +from django.core.management.base import CommandError +from users.models import User + +from django.utils.html import strip_tags +from reversion import revisions as reversion +from django.db import transaction +from getpass import getpass + + +def get_user(pseudo): + """Cherche un utilisateur re2o à partir de son pseudo""" + user = User.objects.filter(pseudo=pseudo) + if len(user)==0: + raise CommandError("Utilisateur invalide") + if len(user)>1: + raise CommandError("Plusieurs utilisateurs correspondant à ce pseudo. Ceci NE DEVRAIT PAS arriver") + return user[0] + + +def get_system_user(): + """Retourne l'utilisateur système ayant lancé la commande""" + return pwd.getpwuid(int(os.getenv("SUDO_UID") or os.getuid())).pw_name + + +def form_cli(Form,user,action,*args,**kwargs): + """ + Remplit un formulaire à partir de la ligne de commande + Form : le formulaire (sous forme de classe) à remplir + user : l'utilisateur re2o faisant la modification + action : l'action réalisée par le formulaire (pour les logs) + Les arguments suivants sont transmis tels quels au formulaire. + """ + data={} + dumb_form = Form(user=user,*args,**kwargs) + for key in dumb_form.fields: + if not dumb_form.fields[key].widget.input_type=='hidden': + if dumb_form.fields[key].widget.input_type=='password': + data[key]=getpass("%s : " % dumb_form.fields[key].label) + else: + data[key]=input("%s : " % dumb_form.fields[key].label) + + form = Form(data,user=user,*args,**kwargs) + if not form.is_valid(): + sys.stderr.write("Erreurs : \n") + for err in form.errors: + #Oui, oui, on gère du HTML là où d'autres ont eu la lumineuse idée de le mettre + sys.stderr.write("\t%s : %s\n" % (err,strip_tags(form.errors[err]))) + raise CommandError("Formulaire invalide") + + with transaction.atomic(), reversion.create_revision(): + form.save() + reversion.set_user(user) + reversion.set_comment(action) + + sys.stdout.write("%s : effectué. La modification peut prendre quelques minutes pour s'appliquer.\n" % action) diff --git a/re2o/settings.py b/re2o/settings.py index d1c27192..c101460b 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -89,6 +89,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'reversion.middleware.RevisionMiddleware', ) ROOT_URLCONF = 're2o.urls' diff --git a/re2o/templates/re2o/aff_history.html b/re2o/templates/re2o/aff_history.html index 8f143522..d7be350c 100644 --- a/re2o/templates/re2o/aff_history.html +++ b/re2o/templates/re2o/aff_history.html @@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Date - Cableur + Effectué par Commentaire diff --git a/re2o/utils.py b/re2o/utils.py index aaefca8d..8336aab5 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -42,6 +42,7 @@ from django.db.models import Q from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from cotisations.models import Cotisation, Facture, Paiement, Vente from machines.models import Domain, Interface, Machine @@ -280,6 +281,22 @@ class SortTable: else: return request +def re2o_paginator(request, query_set, pagination_number): + """Paginator script for list display in re2o. + :request: + :query_set: Query_set to paginate + :pagination_number: Number of entries to display""" + paginator = Paginator(query_set, pagination_number) + page = request.GET.get('page') + try: + results = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + results = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + results = paginator.page(paginator.num_pages) + return results def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ diff --git a/re2o/views.py b/re2o/views.py index 9965dcda..991b8702 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -31,7 +31,6 @@ from django.urls import reverse from django.shortcuts import render, redirect from django.template.context_processors import csrf from django.contrib.auth.decorators import login_required, permission_required -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from reversion.models import Version from django.contrib import messages from preferences.models import Service @@ -40,7 +39,9 @@ from django.conf import settings from contributors import contributeurs import os import time +from itertools import chain import users, preferences, cotisations, topologie, machines +from .utils import re2o_paginator def form(ctx, template, request): """Form générique, raccourci importé par les fonctions views du site""" @@ -88,7 +89,7 @@ HISTORY_BIND = { 'machines' : { 'machine' : machines.models.Machine, 'interface' : machines.models.Interface, - 'alias' : machines.models.Domain, + 'domain' : machines.models.Domain, 'machinetype' : machines.models.MachineType, 'iptype' : machines.models.IpType, 'extension' : machines.models.Extension, @@ -146,16 +147,10 @@ def history(request, application, object_name, object_id): )) pagination_number = GeneralOption.get_cached_value('pagination_number') reversions = Version.objects.get_for_object(instance) - paginator = Paginator(reversions, pagination_number) - page = request.GET.get('page') - try: - reversions = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - reversions = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of result - reversions = paginator.page(paginator.num_pages) + if hasattr(instance, 'linked_objects'): + for related_object in chain(instance.linked_objects()): + reversions = reversions | Version.objects.get_for_object(related_object) + reversions = re2o_paginator(request, reversions, pagination_number) return render( request, 're2o/history.html', diff --git a/topologie/forms.py b/topologie/forms.py index 7b8f0955..b8c3d8d1 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -50,9 +50,9 @@ from .models import ( ConstructorSwitch, AccessPoint ) +from re2o.mixins import FormRevMixin - -class PortForm(ModelForm): +class PortForm(FormRevMixin, ModelForm): """Formulaire pour la création d'un port d'un switch Relié directement au modèle port""" class Meta: @@ -64,7 +64,7 @@ class PortForm(ModelForm): super(PortForm, self).__init__(*args, prefix=prefix, **kwargs) -class EditPortForm(ModelForm): +class EditPortForm(FormRevMixin, ModelForm): """Form pour l'édition d'un port de switche : changement des reglages radius ou vlan, ou attribution d'une chambre, autre port ou machine @@ -89,7 +89,7 @@ class EditPortForm(ModelForm): )) -class AddPortForm(ModelForm): +class AddPortForm(FormRevMixin, ModelForm): """Permet d'ajouter un port de switch. Voir EditPortForm pour plus d'informations""" class Meta(PortForm.Meta): @@ -108,7 +108,7 @@ class AddPortForm(ModelForm): )) -class StackForm(ModelForm): +class StackForm(FormRevMixin, ModelForm): """Permet d'edition d'une stack : stack_id, et switches membres de la stack""" class Meta: @@ -149,7 +149,7 @@ class NewSwitchForm(NewMachineForm): fields = ['name', 'location', 'number', 'stack', 'stack_member_id'] -class EditRoomForm(ModelForm): +class EditRoomForm(FormRevMixin, ModelForm): """Permet d'éediter le nom et commentaire d'une prise murale""" class Meta: model = Room @@ -166,7 +166,7 @@ class CreatePortsForm(forms.Form): end = forms.IntegerField(label="Fin :", min_value=0) -class EditModelSwitchForm(ModelForm): +class EditModelSwitchForm(FormRevMixin, ModelForm): """Permet d'éediter un modèle de switch : nom et constructeur""" class Meta: model = ModelSwitch @@ -177,7 +177,7 @@ class EditModelSwitchForm(ModelForm): super(EditModelSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) -class EditConstructorSwitchForm(ModelForm): +class EditConstructorSwitchForm(FormRevMixin, ModelForm): """Permet d'éediter le nom d'un constructeur""" class Meta: model = ConstructorSwitch diff --git a/topologie/management/commands/graph_topo.py b/topologie/management/commands/graph_topo.py new file mode 100644 index 00000000..b5d61a80 --- /dev/null +++ b/topologie/management/commands/graph_topo.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +import sys +import json + +import six +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from topologie.management.modelviz import ModelGraph, generate_dot +from django_extensions.management.utils import signalcommand + +try: + import pygraphviz + HAS_PYGRAPHVIZ = True +except ImportError: + HAS_PYGRAPHVIZ = False + +try: + try: + import pydotplus as pydot + except ImportError: + import pydot + HAS_PYDOT = True +except ImportError: + HAS_PYDOT = False + + +class Command(BaseCommand): + help = "Creates a GraphViz dot file for the specified app names. You can pass multiple app names and they will all be combined into a single model. Output is usually directed to a dot file." + + can_import_settings = True + + def __init__(self, *args, **kwargs): + """Allow defaults for arguments to be set in settings.GRAPH_MODELS. + Each argument in self.arguments is a dict where the key is the + space-separated args and the value is our kwarg dict. + The default from settings is keyed as the long arg name with '--' + removed and any '-' replaced by '_'. + """ + self.arguments = { + '--pygraphviz': { + 'action': 'store_true', 'dest': 'pygraphviz', + 'help': 'Use PyGraphViz to generate the image.'}, + + '--pydot': {'action': 'store_true', 'dest': 'pydot', + 'help': 'Use PyDot(Plus) to generate the image.'}, + + '--disable-fields -d': { + 'action': 'store_true', 'dest': 'disable_fields', + 'help': 'Do not show the class member fields'}, + + '--group-models -g': { + 'action': 'store_true', 'dest': 'group_models', + 'help': 'Group models together respective to their ' + 'application'}, + + '--all-applications -a': { + 'action': 'store_true', 'dest': 'all_applications', + 'help': 'Automatically include all applications from ' + 'INSTALLED_APPS'}, + + '--output -o': { + 'action': 'store', 'dest': 'outputfile', + 'help': 'Render output file. Type of output dependend on file ' + 'extensions. Use png or jpg to render graph to image.'}, + + '--layout -l': { + 'action': 'store', 'dest': 'layout', 'default': 'dot', + 'help': 'Layout to be used by GraphViz for visualization. ' + 'Layouts: circo dot fdp neato nop nop1 nop2 twopi'}, + + '--verbose-names -n': { + 'action': 'store_true', 'dest': 'verbose_names', + 'help': 'Use verbose_name of models and fields'}, + + '--language -L': { + 'action': 'store', 'dest': 'language', + 'help': 'Specify language used for verbose_name localization'}, + + '--exclude-columns -x': { + 'action': 'store', 'dest': 'exclude_columns', + 'help': 'Exclude specific column(s) from the graph. ' + 'Can also load exclude list from file.'}, + + '--exclude-models -X': { + 'action': 'store', 'dest': 'exclude_models', + 'help': 'Exclude specific model(s) from the graph. Can also ' + 'load exclude list from file. Wildcards (*) are allowed.'}, + + '--include-models -I': { + 'action': 'store', 'dest': 'include_models', + 'help': 'Restrict the graph to specified models. Wildcards ' + '(*) are allowed.'}, + + '--inheritance -e': { + 'action': 'store_true', 'dest': 'inheritance', 'default': True, + 'help': 'Include inheritance arrows (default)'}, + + '--no-inheritance -E': { + 'action': 'store_false', 'dest': 'inheritance', + 'help': 'Do not include inheritance arrows'}, + + '--hide-relations-from-fields -R': { + 'action': 'store_false', 'dest': 'relations_as_fields', + 'default': True, + 'help': 'Do not show relations as fields in the graph.'}, + + '--disable-sort-fields -S': { + 'action': 'store_false', 'dest': 'sort_fields', + 'default': True, 'help': 'Do not sort fields'}, + + '--json': {'action': 'store_true', 'dest': 'json', + 'help': 'Output graph data as JSON'} + } + + defaults = getattr(settings, 'GRAPH_MODELS', None) + + if defaults: + for argument in self.arguments: + arg_split = argument.split(' ') + setting_opt = arg_split[0].lstrip('-').replace('-', '_') + if setting_opt in defaults: + self.arguments[argument]['default'] = defaults[setting_opt] + + super(Command, self).__init__(*args, **kwargs) + + def add_arguments(self, parser): + """Unpack self.arguments for parser.add_arguments.""" + parser.add_argument('app_label', nargs='*') + for argument in self.arguments: + parser.add_argument(*argument.split(' '), + **self.arguments[argument]) + + @signalcommand + def handle(self, *args, **options): + args = options['app_label'] + if len(args) < 1 and not options['all_applications']: + raise CommandError("need one or more arguments for appname") + + use_pygraphviz = options.get('pygraphviz', False) + use_pydot = options.get('pydot', False) + use_json = options.get('json', False) + if use_json and (use_pydot or use_pygraphviz): + raise CommandError("Cannot specify --json with --pydot or --pygraphviz") + + cli_options = ' '.join(sys.argv[2:]) + graph_models = ModelGraph(args, cli_options=cli_options, **options) + graph_models.generate_graph_data() + graph_data = graph_models.get_graph_data(as_json=use_json) + if use_json: + self.render_output_json(graph_data, **options) + return + + dotdata = generate_dot(graph_data) + if not six.PY3: + dotdata = dotdata.encode('utf-8') + if options['outputfile']: + if not use_pygraphviz and not use_pydot: + if HAS_PYGRAPHVIZ: + use_pygraphviz = True + elif HAS_PYDOT: + use_pydot = True + if use_pygraphviz: + self.render_output_pygraphviz(dotdata, **options) + elif use_pydot: + self.render_output_pydot(dotdata, **options) + else: + raise CommandError("Neither pygraphviz nor pydotplus could be found to generate the image") + else: + self.print_output(dotdata) + + def print_output(self, dotdata): + if six.PY3 and isinstance(dotdata, six.binary_type): + dotdata = dotdata.decode() + + print(dotdata) + + def render_output_json(self, graph_data, **kwargs): + output_file = kwargs.get('outputfile') + if output_file: + with open(output_file, 'wt') as json_output_f: + json.dump(graph_data, json_output_f) + else: + print(json.dumps(graph_data)) + + def render_output_pygraphviz(self, dotdata, **kwargs): + """Renders the image using pygraphviz""" + if not HAS_PYGRAPHVIZ: + raise CommandError("You need to install pygraphviz python module") + + version = pygraphviz.__version__.rstrip("-svn") + try: + if tuple(int(v) for v in version.split('.')) < (0, 36): + # HACK around old/broken AGraph before version 0.36 (ubuntu ships with this old version) + import tempfile + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write(dotdata) + tmpfile.seek(0) + dotdata = tmpfile.name + except ValueError: + pass + + graph = pygraphviz.AGraph(dotdata) + graph.layout(prog=kwargs['layout']) + graph.draw(kwargs['outputfile']) + + def render_output_pydot(self, dotdata, **kwargs): + """Renders the image using pydot""" + if not HAS_PYDOT: + raise CommandError("You need to install pydot python module") + + graph = pydot.graph_from_dot_data(dotdata) + if not graph: + raise CommandError("pydot returned an error") + if isinstance(graph, (list, tuple)): + if len(graph) > 1: + sys.stderr.write("Found more then one graph, rendering only the first one.\n") + graph = graph[0] + + output_file = kwargs['outputfile'] + formats = ['bmp', 'canon', 'cmap', 'cmapx', 'cmapx_np', 'dot', 'dia', 'emf', + 'em', 'fplus', 'eps', 'fig', 'gd', 'gd2', 'gif', 'gv', 'imap', + 'imap_np', 'ismap', 'jpe', 'jpeg', 'jpg', 'metafile', 'pdf', + 'pic', 'plain', 'plain-ext', 'png', 'pov', 'ps', 'ps2', 'svg', + 'svgz', 'tif', 'tiff', 'tk', 'vml', 'vmlz', 'vrml', 'wbmp', 'xdot'] + ext = output_file[output_file.rfind('.') + 1:] + format = ext if ext in formats else 'raw' + graph.write(output_file, format=format) diff --git a/topologie/management/commands/modelviz.py b/topologie/management/commands/modelviz.py new file mode 100644 index 00000000..0f01bb4d --- /dev/null +++ b/topologie/management/commands/modelviz.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +""" +modelviz.py - DOT file generator for Django Models +Based on: + Django model to DOT (Graphviz) converter + by Antonio Cavedoni + Adapted to be used with django-extensions +""" + +import datetime +import os +import re + +import six +from django.apps import apps +from django.db.models.fields.related import ( + ForeignKey, ManyToManyField, OneToOneField, RelatedField, +) +from django.contrib.contenttypes.fields import GenericRelation +from django.template import Context, Template, loader +from django.utils.encoding import force_bytes, force_str +from django.utils.safestring import mark_safe +from django.utils.translation import activate as activate_language + + +__version__ = "1.1" +__license__ = "Python" +__author__ = "Bas van Oostveen ", +__contributors__ = [ + "Antonio Cavedoni " + "Stefano J. Attardi ", + "limodou ", + "Carlo C8E Miron", + "Andre Campos ", + "Justin Findlay ", + "Alexander Houben ", + "Joern Hees ", + "Kevin Cherepski ", + "Jose Tomas Tocino ", + "Adam Dobrawy ", + "Mikkel Munch Mortensen ", + "Andrzej Bistram ", +] + + +def parse_file_or_list(arg): + if not arg: + return [] + if isinstance(arg, (list, tuple, set)): + return arg + if ',' not in arg and os.path.isfile(arg): + return [e.strip() for e in open(arg).readlines()] + return [e.strip() for e in arg.split(',')] + + +class ModelGraph(object): + def __init__(self, app_labels, **kwargs): + self.graphs = [] + self.cli_options = kwargs.get('cli_options', None) + self.disable_fields = kwargs.get('disable_fields', False) + self.include_models = parse_file_or_list( + kwargs.get('include_models', "") + ) + self.all_applications = kwargs.get('all_applications', False) + self.use_subgraph = kwargs.get('group_models', False) + self.verbose_names = kwargs.get('verbose_names', False) + self.inheritance = kwargs.get('inheritance', True) + self.relations_as_fields = kwargs.get("relations_as_fields", True) + self.sort_fields = kwargs.get("sort_fields", True) + self.language = kwargs.get('language', None) + if self.language is not None: + activate_language(self.language) + self.exclude_columns = parse_file_or_list( + kwargs.get('exclude_columns', "") + ) + self.exclude_models = parse_file_or_list( + kwargs.get('exclude_models', "") + ) + if self.all_applications: + self.app_labels = [app.label for app in apps.get_app_configs()] + else: + self.app_labels = app_labels + + def generate_graph_data(self): + self.process_apps() + + nodes = [] + for graph in self.graphs: + nodes.extend([e['name'] for e in graph['models']]) + + for graph in self.graphs: + for model in graph['models']: + for relation in model['relations']: + if relation is not None: + if relation['target'] in nodes: + relation['needs_node'] = False + + def get_graph_data(self, as_json=False): + now = datetime.datetime.now() + graph_data = { + 'created_at': now.strftime("%Y-%m-%d %H:%M"), + 'cli_options': self.cli_options, + 'disable_fields': self.disable_fields, + 'use_subgraph': self.use_subgraph, + } + + if as_json: + graph_data['graphs'] = [context.flatten() for context in self.graphs] + else: + graph_data['graphs'] = self.graphs + + return graph_data + + def add_attributes(self, field, abstract_fields): + if self.verbose_names and field.verbose_name: + label = force_bytes(field.verbose_name) + if label.islower(): + label = label.capitalize() + else: + label = field.name + + t = type(field).__name__ + if isinstance(field, (OneToOneField, ForeignKey)): + remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported + t += " ({0})".format(remote_field.field_name) + # TODO: ManyToManyField, GenericRelation + + return { + 'name': field.name, + 'label': label, + 'type': t, + 'blank': field.blank, + 'abstract': field in abstract_fields, + 'relation': isinstance(field, RelatedField), + 'primary_key': field.primary_key, + } + + def add_relation(self, field, model, extras=""): + if self.verbose_names and field.verbose_name: + label = force_bytes(field.verbose_name) + if label.islower(): + label = label.capitalize() + else: + label = field.name + + # show related field name + if hasattr(field, 'related_query_name'): + related_query_name = field.related_query_name() + if self.verbose_names and related_query_name.islower(): + related_query_name = related_query_name.replace('_', ' ').capitalize() + label = '{} ({})'.format(label, force_str(related_query_name)) + + # handle self-relationships and lazy-relationships + remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported + remote_field_model = remote_field.model if hasattr(remote_field, 'model') else remote_field.to # Remove me after Django 1.8 is unsupported + if isinstance(remote_field_model, six.string_types): + if remote_field_model == 'self': + target_model = field.model + else: + if '.' in remote_field_model: + app_label, model_name = remote_field_model.split('.', 1) + else: + app_label = field.model._meta.app_label + model_name = remote_field_model + target_model = apps.get_model(app_label, model_name) + else: + target_model = remote_field_model + + _rel = self.get_relation_context(target_model, field, label, extras) + + if _rel not in model['relations'] and self.use_model(_rel['target']): + return _rel + + def get_abstract_models(self, appmodels): + abstract_models = [] + for appmodel in appmodels: + abstract_models += [abstract_model for abstract_model in + appmodel.__bases__ if + hasattr(abstract_model, '_meta') and + abstract_model._meta.abstract] + abstract_models = list(set(abstract_models)) # remove duplicates + return abstract_models + + def get_app_context(self, app): + return Context({ + 'name': '"%s"' % app.name, + 'app_name': "%s" % app.name, + 'cluster_app_name': "cluster_%s" % app.name.replace(".", "_"), + 'models': [] + }) + + def get_appmodel_attributes(self, appmodel): + if self.relations_as_fields: + attributes = [field for field in appmodel._meta.local_fields] + else: + # Find all the 'real' attributes. Relations are depicted as graph edges instead of attributes + attributes = [field for field in appmodel._meta.local_fields if not + isinstance(field, RelatedField)] + return attributes + + def get_appmodel_abstracts(self, appmodel): + return [abstract_model.__name__ for abstract_model in + appmodel.__bases__ if + hasattr(abstract_model, '_meta') and + abstract_model._meta.abstract] + + def get_appmodel_context(self, appmodel, appmodel_abstracts): + context = { + 'app_name': appmodel.__module__.replace(".", "_"), + 'name': appmodel.__name__, + 'abstracts': appmodel_abstracts, + 'fields': [], + 'relations': [] + } + + if self.verbose_names and appmodel._meta.verbose_name: + context['label'] = force_bytes(appmodel._meta.verbose_name) + else: + context['label'] = context['name'] + + return context + + def get_bases_abstract_fields(self, c): + _abstract_fields = [] + for e in c.__bases__: + if hasattr(e, '_meta') and e._meta.abstract: + _abstract_fields.extend(e._meta.fields) + _abstract_fields.extend(self.get_bases_abstract_fields(e)) + return _abstract_fields + + def get_inheritance_context(self, appmodel, parent): + label = "multi-table" + if parent._meta.abstract: + label = "abstract" + if appmodel._meta.proxy: + label = "proxy" + label += r"\ninheritance" + return { + 'target_app': parent.__module__.replace(".", "_"), + 'target': parent.__name__, + 'type': "inheritance", + 'name': "inheritance", + 'label': label, + 'arrows': '[arrowhead=empty, arrowtail=none, dir=both]', + 'needs_node': True, + } + + def get_models(self, app): + appmodels = list(app.get_models()) + return appmodels + + def get_relation_context(self, target_model, field, label, extras): + return { + 'target_app': target_model.__module__.replace('.', '_'), + 'target': target_model.__name__, + 'type': type(field).__name__, + 'name': field.name, + 'label': label, + 'arrows': extras, + 'needs_node': True + } + + def process_attributes(self, field, model, pk, abstract_fields): + newmodel = model.copy() + if self.skip_field(field) or pk and field == pk: + return newmodel + newmodel['fields'].append(self.add_attributes(field, abstract_fields)) + return newmodel + + def process_apps(self): + for app_label in self.app_labels: + app = apps.get_app_config(app_label) + if not app: + continue + app_graph = self.get_app_context(app) + app_models = self.get_models(app) + abstract_models = self.get_abstract_models(app_models) + app_models = abstract_models + app_models + + for appmodel in app_models: + if not self.use_model(appmodel._meta.object_name): + continue + appmodel_abstracts = self.get_appmodel_abstracts(appmodel) + abstract_fields = self.get_bases_abstract_fields(appmodel) + model = self.get_appmodel_context(appmodel, appmodel_abstracts) + attributes = self.get_appmodel_attributes(appmodel) + + # find primary key and print it first, ignoring implicit id if other pk exists + pk = appmodel._meta.pk + if pk and not appmodel._meta.abstract and pk in attributes: + model['fields'].append(self.add_attributes(pk, abstract_fields)) + + for field in attributes: + model = self.process_attributes(field, model, pk, abstract_fields) + + if self.sort_fields: + model = self.sort_model_fields(model) + + for field in appmodel._meta.local_fields: + model = self.process_local_fields(field, model, abstract_fields) + + for field in appmodel._meta.local_many_to_many: + model = self.process_local_many_to_many(field, model) + + if self.inheritance: + # add inheritance arrows + for parent in appmodel.__bases__: + model = self.process_parent(parent, appmodel, model) + + app_graph['models'].append(model) + if app_graph['models']: + self.graphs.append(app_graph) + + def process_local_fields(self, field, model, abstract_fields): + newmodel = model.copy() + if (field.attname.endswith('_ptr_id') or # excluding field redundant with inheritance relation + field in abstract_fields or # excluding fields inherited from abstract classes. they too show as local_fields + self.skip_field(field)): + return newmodel + if isinstance(field, OneToOneField): + newmodel['relations'].append(self.add_relation(field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]')) + elif isinstance(field, ForeignKey): + newmodel['relations'].append(self.add_relation(field, newmodel, '[arrowhead=none, arrowtail=dot, dir=both]')) + return newmodel + + def process_local_many_to_many(self, field, model): + newmodel = model.copy() + if self.skip_field(field): + return newmodel + if isinstance(field, ManyToManyField): + remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported + if hasattr(remote_field.through, '_meta') and remote_field.through._meta.auto_created: + newmodel['relations'].append(self.add_relation(field, newmodel, '[arrowhead=dot arrowtail=dot, dir=both]')) + elif isinstance(field, GenericRelation): + newmodel['relations'].append(self.add_relation(field, newmodel, mark_safe('[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]'))) + return newmodel + + def process_parent(self, parent, appmodel, model): + newmodel = model.copy() + if hasattr(parent, "_meta"): # parent is a model + _rel = self.get_inheritance_context(appmodel, parent) + # TODO: seems as if abstract models aren't part of models.getModels, which is why they are printed by this without any attributes. + if _rel not in newmodel['relations'] and self.use_model(_rel['target']): + newmodel['relations'].append(_rel) + return newmodel + + def sort_model_fields(self, model): + newmodel = model.copy() + newmodel['fields'] = sorted(newmodel['fields'], key=lambda field: (not field['primary_key'], not field['relation'], field['label'])) + return newmodel + + def use_model(self, model_name): + """ + Decide whether to use a model, based on the model name and the lists of + models to exclude and include. + """ + # Check against exclude list. + if self.exclude_models: + for model_pattern in self.exclude_models: + model_pattern = '^%s$' % model_pattern.replace('*', '.*') + if re.search(model_pattern, model_name): + return False + # Check against exclude list. + elif self.include_models: + for model_pattern in self.include_models: + model_pattern = '^%s$' % model_pattern.replace('*', '.*') + if re.search(model_pattern, model_name): + return True + # Return `True` if `include_models` is falsey, otherwise return `False`. + return not self.include_models + + def skip_field(self, field): + if self.exclude_columns: + if self.verbose_names and field.verbose_name: + if field.verbose_name in self.exclude_columns: + return True + if field.name in self.exclude_columns: + return True + return False + + +def generate_dot(graph_data, template='django_extensions/graph_models/digraph.dot'): + t = loader.get_template(template) + + if not isinstance(t, Template) and not (hasattr(t, 'template') and isinstance(t.template, Template)): + raise Exception("Default Django template loader isn't used. " + "This can lead to the incorrect template rendering. " + "Please, check the settings.") + + c = Context(graph_data).flatten() + dot = t.render(c) + + return dot + + +def generate_graph_data(*args, **kwargs): + generator = ModelGraph(*args, **kwargs) + generator.generate_graph_data() + return generator.get_graph_data() + + +def use_model(model, include_models, exclude_models): + generator = ModelGraph([], include_models=include_models, exclude_models=exclude_models) + return generator.use_model(model) + diff --git a/topologie/models.py b/topologie/models.py index 9cef5c44..abbdfa7a 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -48,9 +48,9 @@ from django.db import transaction from reversion import revisions as reversion from machines.models import Machine, Interface, regen -from re2o.mixins import AclMixin +from re2o.mixins import AclMixin, RevMixin -class Stack(AclMixin, models.Model): +class Stack(AclMixin, RevMixin, models.Model): """Un objet stack. Regrouppe des switchs en foreign key ,contient une id de stack, un switch id min et max dans le stack""" @@ -102,6 +102,9 @@ class AccessPoint(AclMixin, Machine): ("view_accesspoint", "Peut voir une borne"), ) + def __str__(self): + return str(self.interface_set.first()) + class Switch(AclMixin, Machine): """ Definition d'un switch. Contient un nombre de ports (number), @@ -187,7 +190,7 @@ class Switch(AclMixin, Machine): return str(self.interface_set.first()) -class ModelSwitch(AclMixin, models.Model): +class ModelSwitch(AclMixin, RevMixin, models.Model): """Un modèle (au sens constructeur) de switch""" PRETTY_NAME = "Modèle de switch" reference = models.CharField(max_length=255) @@ -205,7 +208,7 @@ class ModelSwitch(AclMixin, models.Model): return str(self.constructor) + ' ' + self.reference -class ConstructorSwitch(AclMixin, models.Model): +class ConstructorSwitch(AclMixin, RevMixin, models.Model): """Un constructeur de switch""" PRETTY_NAME = "Constructeur de switch" name = models.CharField(max_length=255) @@ -219,7 +222,7 @@ class ConstructorSwitch(AclMixin, models.Model): return self.name -class Port(AclMixin, models.Model): +class Port(AclMixin, RevMixin, models.Model): """ Definition d'un port. Relié à un switch(foreign_key), un port peut etre relié de manière exclusive à : - une chambre (room) @@ -335,7 +338,7 @@ class Port(AclMixin, models.Model): return str(self.switch) + " - " + str(self.port) -class Room(AclMixin, models.Model): +class Room(AclMixin, RevMixin, models.Model): """Une chambre/local contenant une prise murale""" PRETTY_NAME = "Chambre/ Prise murale" diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html index 1eef75b8..8a1268a3 100644 --- a/topologie/templates/topologie/aff_port.html +++ b/topologie/templates/topologie/aff_port.html @@ -24,6 +24,64 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} + +
+ + + + + {% for port in port_list|slice:"::2" %} + + {% endfor %} + + + + + {% for port in port_list|slice:"::2" %} + + {% endfor %} + + + + + + {% for port in port_list|slice:"1::2" %} + + {% endfor %} + + + + + {% for port in port_list|slice:"1::2" %} + + {% endfor %} + + +
{{ port.port }}
+ {% if port.room %} + {{ port.room }} + {% elif port.machine_interface %} + {{ port.machine_interface }} + {% elif port.related%} + {{ port.related }} + {% else %} + Vide + {% endif %} +
{{ port.port }}
+ {% if port.room %} + {{ port.room }} + {% elif port.machine_interface %} + {{ port.machine_interface }} + {% elif port.related%} + {{ port.related }} + {% else %} + Vide + {% endif %} +
+
+ + +
@@ -76,3 +134,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
+
diff --git a/topologie/templates/topologie/index_p.html b/topologie/templates/topologie/index_p.html index 4fc1a61d..0bd62039 100644 --- a/topologie/templates/topologie/index_p.html +++ b/topologie/templates/topologie/index_p.html @@ -35,7 +35,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Ajouter un port Ajouter des ports {% acl_end %} - {% include "topologie/aff_port.html" with port_list=port_list %} +
+{% include "topologie/aff_port.html" with port_list=port_list %}


diff --git a/topologie/views.py b/topologie/views.py index b57a5255..2534c371 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -43,9 +43,6 @@ from django.db import IntegrityError from django.db import transaction from django.db.models import ProtectedError, Prefetch from django.core.exceptions import ValidationError -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from reversion import revisions as reversion -from reversion.models import Version from topologie.models import ( Switch, @@ -68,7 +65,7 @@ from topologie.forms import ( EditAccessPointForm ) from users.views import form -from re2o.utils import SortTable +from re2o.utils import re2o_paginator, SortTable from re2o.acl import ( can_create, can_edit, @@ -105,16 +102,7 @@ def index(request): SortTable.TOPOLOGIE_INDEX ) pagination_number = GeneralOption.get_cached_value('pagination_number') - paginator = Paginator(switch_list, pagination_number) - page = request.GET.get('page') - try: - switch_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - switch_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - switch_list = paginator.page(paginator.num_pages) + switch_list = re2o_paginator(request, switch_list, pagination_number) return render(request, 'topologie/index.html', { 'switch_list': switch_list }) @@ -160,16 +148,7 @@ def index_room(request): SortTable.TOPOLOGIE_INDEX_ROOM ) pagination_number = GeneralOption.get_cached_value('pagination_number') - paginator = Paginator(room_list, pagination_number) - page = request.GET.get('page') - try: - room_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - room_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - room_list = paginator.page(paginator.num_pages) + room_list = re2o_paginator(request, room_list, pagination_number) return render(request, 'topologie/index_room.html', { 'room_list': room_list }) @@ -191,16 +170,7 @@ def index_ap(request): SortTable.TOPOLOGIE_INDEX_BORNE ) pagination_number = GeneralOption.get_cached_value('pagination_number') - paginator = Paginator(ap_list, pagination_number) - page = request.GET.get('page') - try: - ap_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - ap_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - ap_list = paginator.page(paginator.num_pages) + ap_list = re2o_paginator(request, ap_list, pagination_number) return render(request, 'topologie/index_ap.html', { 'ap_list': ap_list }) @@ -262,10 +232,7 @@ def new_port(request, switchid): port = port.save(commit=False) port.switch = switch try: - with transaction.atomic(), reversion.create_revision(): - port.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + port.save() messages.success(request, "Port ajouté") except IntegrityError: messages.error(request, "Ce port existe déjà") @@ -284,13 +251,9 @@ def edit_port(request, port_object, portid): port = EditPortForm(request.POST or None, instance=port_object) if port.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if port.changed_data: port.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in port.changed_data - )) - messages.success(request, "Le port a bien été modifié") + messages.success(request, "Le port a bien été modifié") return redirect(reverse( 'topologie:index-port', kwargs={'switchid': str(port_object.switch.id)} @@ -304,11 +267,8 @@ def del_port(request, port, portid): """ Supprime le port""" if request.method == "POST": try: - with transaction.atomic(), reversion.create_revision(): - port.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") - messages.success(request, "Le port a été détruit") + port.delete() + messages.success(request, "Le port a été détruit") except ProtectedError: messages.error(request, "Le port %s est affecté à un autre objet,\ impossible de le supprimer" % port) @@ -325,10 +285,7 @@ def new_stack(request): """Ajoute un nouveau stack : stackid_min, max, et nombre de switches""" stack = StackForm(request.POST or None) if stack.is_valid(): - with transaction.atomic(), reversion.create_revision(): - stack.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + stack.save() messages.success(request, "Stack crée") return form({'topoform': stack, 'action_name' : 'Créer'}, 'topologie/topo.html', request) @@ -337,17 +294,10 @@ def new_stack(request): @can_edit(Stack) def edit_stack(request, stack, stackid): """Edition d'un stack (nombre de switches, nom...)""" - stack = StackForm(request.POST or None, instance=stack) if stack.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if stack.changed_data: stack.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in stack.changed_data - ) - ) return redirect(reverse('topologie:index-stack')) return form({'topoform': stack, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -358,11 +308,8 @@ def del_stack(request, stack, stackid): """Supprime un stack""" if request.method == "POST": try: - with transaction.atomic(), reversion.create_revision(): - stack.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") - messages.success(request, "La stack a eté détruite") + stack.delete() + messages.success(request, "La stack a eté détruite") except ProtectedError: messages.error(request, "La stack %s est affectée à un autre\ objet, impossible de la supprimer" % stack) @@ -412,20 +359,11 @@ def new_switch(request): domain.instance.interface_parent = new_interface_instance if domain.is_valid(): new_domain_instance = domain.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - new_switch.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_switch.save() new_interface_instance.machine = new_switch - with transaction.atomic(), reversion.create_revision(): - new_interface_instance.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_interface_instance.save() new_domain_instance.interface_parent = new_interface_instance - with transaction.atomic(), reversion.create_revision(): - new_domain_instance.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_domain_instance.save() messages.success(request, "Le switch a été créé") return redirect(reverse('topologie:index')) i_mbf_param = generate_ipv4_mbf_param(interface, False) @@ -468,7 +406,6 @@ def create_ports(request, switchid): messages.success(request, "Ports créés.") except ValidationError as e: messages.error(request, ''.join(e)) - return redirect(reverse( 'topologie:index-port', kwargs={'switchid':switchid} @@ -500,26 +437,12 @@ def edit_switch(request, switch, switchid): new_switch = switch_form.save(commit=False) new_interface_instance = interface_form.save(commit=False) new_domain = domain_form.save(commit=False) - with transaction.atomic(), reversion.create_revision(): + if switch_form.changed_data: new_switch.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in switch_form.changed_data - ) - ) - with transaction.atomic(), reversion.create_revision(): + if interface_form.changed_data: new_interface_instance.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in interface_form.changed_data) - ) - with transaction.atomic(), reversion.create_revision(): + if domain_form.changed_data: new_domain.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in domain_form.changed_data) - ) messages.success(request, "Le switch a bien été modifié") return redirect(reverse('topologie:index')) i_mbf_param = generate_ipv4_mbf_param(interface_form, False ) @@ -562,20 +485,11 @@ def new_ap(request): domain.instance.interface_parent = new_interface if domain.is_valid(): new_domain_instance = domain.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - new_ap.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_ap.save() new_interface.machine = new_ap - with transaction.atomic(), reversion.create_revision(): - new_interface.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_interface.save() new_domain_instance.interface_parent = new_interface - with transaction.atomic(), reversion.create_revision(): - new_domain_instance.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + new_domain_instance.save() messages.success(request, "La borne a été créé") return redirect(reverse('topologie:index-ap')) i_mbf_param = generate_ipv4_mbf_param(interface, False) @@ -616,26 +530,12 @@ def edit_ap(request, ap, accesspointid): new_ap = ap_form.save(commit=False) new_interface = interface_form.save(commit=False) new_domain = domain_form.save(commit=False) - with transaction.atomic(), reversion.create_revision(): + if ap_form.changed_data: new_ap.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in ap_form.changed_data) - ) - with transaction.atomic(), reversion.create_revision(): + if interface_form.changed_data: new_interface.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in interface_form.changed_data) - ) - reversion.set_comment("Création") - with transaction.atomic(), reversion.create_revision(): + if domain_form.changed_data: new_domain.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in domain_form.changed_data) - ) messages.success(request, "La borne a été modifiée") return redirect(reverse('topologie:index-ap')) i_mbf_param = generate_ipv4_mbf_param(interface_form, False ) @@ -654,10 +554,7 @@ def new_room(request): """Nouvelle chambre """ room = EditRoomForm(request.POST or None) if room.is_valid(): - with transaction.atomic(), reversion.create_revision(): - room.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + room.save() messages.success(request, "La chambre a été créé") return redirect(reverse('topologie:index-room')) return form({'topoform': room, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) @@ -667,16 +564,11 @@ def new_room(request): @can_edit(Room) def edit_room(request, room, roomid): """ Edition numero et details de la chambre""" - room = EditRoomForm(request.POST or None, instance=room) if room.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if room.changed_data: room.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in room.changed_data) - ) - messages.success(request, "La chambre a bien été modifiée") + messages.success(request, "La chambre a bien été modifiée") return redirect(reverse('topologie:index-room')) return form({'topoform': room, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -687,11 +579,8 @@ def del_room(request, room, roomid): """ Suppression d'un chambre""" if request.method == "POST": try: - with transaction.atomic(), reversion.create_revision(): - room.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") - messages.success(request, "La chambre/prise a été détruite") + room.delete() + messages.success(request, "La chambre/prise a été détruite") except ProtectedError: messages.error(request, "La chambre %s est affectée à un autre objet,\ impossible de la supprimer (switch ou user)" % room) @@ -708,10 +597,7 @@ def new_model_switch(request): """Nouveau modèle de switch""" model_switch = EditModelSwitchForm(request.POST or None) if model_switch.is_valid(): - with transaction.atomic(), reversion.create_revision(): - model_switch.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + model_switch.save() messages.success(request, "Le modèle a été créé") return redirect(reverse('topologie:index-model-switch')) return form({'topoform': model_switch, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) @@ -724,13 +610,9 @@ def edit_model_switch(request, model_switch, modelswitchid): model_switch = EditModelSwitchForm(request.POST or None, instance=model_switch) if model_switch.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if model_switch.changed_data: model_switch.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in model_switch.changed_data) - ) - messages.success(request, "Le modèle a bien été modifié") + messages.success(request, "Le modèle a bien été modifié") return redirect(reverse('topologie:index-model-switch')) return form({'topoform': model_switch, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -741,11 +623,8 @@ def del_model_switch(request, model_switch, modelswitchid): """ Suppression d'un modèle de switch""" if request.method == "POST": try: - with transaction.atomic(), reversion.create_revision(): - model_switch.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") - messages.success(request, "Le modèle a été détruit") + model_switch.delete() + messages.success(request, "Le modèle a été détruit") except ProtectedError: messages.error(request, "Le modèle %s est affectée à un autre objet,\ impossible de la supprimer (switch ou user)" % model_switch) @@ -762,10 +641,7 @@ def new_constructor_switch(request): """Nouveau constructeur de switch""" constructor_switch = EditConstructorSwitchForm(request.POST or None) if constructor_switch.is_valid(): - with transaction.atomic(), reversion.create_revision(): - constructor_switch.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + constructor_switch.save() messages.success(request, "Le constructeur a été créé") return redirect(reverse('topologie:index-model-switch')) return form({'topoform': constructor_switch, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) @@ -778,13 +654,9 @@ def edit_constructor_switch(request, constructor_switch, constructorswitchid): constructor_switch = EditConstructorSwitchForm(request.POST or None, instance=constructor_switch) if constructor_switch.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if constructor_switch.changed_data: constructor_switch.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in constructor_switch.changed_data) - ) - messages.success(request, "Le modèle a bien été modifié") + messages.success(request, "Le modèle a bien été modifié") return redirect(reverse('topologie:index-model-switch')) return form({'topoform': constructor_switch, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -795,11 +667,8 @@ def del_constructor_switch(request, constructor_switch, constructorswitchid): """ Suppression d'un constructeur de switch""" if request.method == "POST": try: - with transaction.atomic(), reversion.create_revision(): - constructor_switch.delete() - reversion.set_user(request.user) - reversion.set_comment("Destruction") - messages.success(request, "Le constructeur a été détruit") + constructor_switch.delete() + messages.success(request, "Le constructeur a été détruit") except ProtectedError: messages.error(request, "Le constructeur %s est affecté à un autre objet,\ impossible de la supprimer (switch ou user)" % constructor_switch) diff --git a/users/admin.py b/users/admin.py index f8acb855..a6b70009 100644 --- a/users/admin.py +++ b/users/admin.py @@ -32,11 +32,28 @@ from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from reversion.admin import VersionAdmin -from .models import User, ServiceUser, School, ListRight, ListShell -from .models import Ban, Whitelist, Request, LdapUser, LdapServiceUser -from .models import LdapServiceUserGroup, LdapUserGroup -from .forms import UserChangeForm, UserCreationForm -from .forms import ServiceUserChangeForm, ServiceUserCreationForm +from .models import ( + User, + ServiceUser, + School, + ListRight, + ListShell, + Adherent, + Club, + Ban, + Whitelist, + Request, + LdapUser, + LdapServiceUser, + LdapServiceUserGroup, + LdapUserGroup +) +from .forms import ( + UserChangeForm, + UserCreationForm, + ServiceUserChangeForm, + ServiceUserCreationForm +) class UserAdmin(admin.ModelAdmin): @@ -195,6 +212,8 @@ class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): admin.site.register(User, UserAdmin) +admin.site.register(Adherent, UserAdmin) +admin.site.register(Club, UserAdmin) admin.site.register(ServiceUser, ServiceUserAdmin) admin.site.register(LdapUser, LdapUserAdmin) admin.site.register(LdapUserGroup, LdapUserGroupAdmin) diff --git a/users/forms.py b/users/forms.py index 81f1e343..3617b26f 100644 --- a/users/forms.py +++ b/users/forms.py @@ -53,13 +53,11 @@ from .models import ( Club ) from re2o.utils import remove_user_room - +from re2o.mixins import FormRevMixin from re2o.field_permissions import FieldPermissionFormMixin -NOW = timezone.now() - -class PassForm(FieldPermissionFormMixin, forms.ModelForm): +class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): """Formulaire de changement de mot de passe. Verifie que les 2 nouveaux mots de passe renseignés sont identiques et respectent une norme""" @@ -107,7 +105,7 @@ class PassForm(FieldPermissionFormMixin, forms.ModelForm): user.save() -class UserCreationForm(forms.ModelForm): +class UserCreationForm(FormRevMixin, forms.ModelForm): """A form for creating new users. Includes all the required fields, plus a repeated password. @@ -154,7 +152,7 @@ class UserCreationForm(forms.ModelForm): return user -class ServiceUserCreationForm(forms.ModelForm): +class ServiceUserCreationForm(FormRevMixin, forms.ModelForm): """A form for creating new users. Includes all the required fields, plus a repeated password. @@ -202,7 +200,7 @@ class ServiceUserCreationForm(forms.ModelForm): return user -class UserChangeForm(forms.ModelForm): +class UserChangeForm(FormRevMixin, forms.ModelForm): """A form for updating users. Includes all the fields on the user, but replaces the password field with admin's password hash display field. @@ -238,7 +236,7 @@ class UserChangeForm(forms.ModelForm): return user -class ServiceUserChangeForm(forms.ModelForm): +class ServiceUserChangeForm(FormRevMixin, forms.ModelForm): """A form for updating users. Includes all the fields on the user, but replaces the password field with admin's password hash display field. @@ -281,12 +279,12 @@ class MassArchiveForm(forms.Form): cleaned_data = super(MassArchiveForm, self).clean() date = cleaned_data.get("date") if date: - if date > NOW: + if date > timezone.now(): raise forms.ValidationError("Impossible d'archiver des\ utilisateurs dont la fin d'accès se situe dans le futur !") -class AdherentForm(FieldPermissionFormMixin, ModelForm): +class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Formulaire de base d'edition d'un user. Formulaire de base, utilisé pour l'edition de self par self ou un cableur. On formate les champs avec des label plus jolis""" @@ -339,7 +337,7 @@ class AdherentForm(FieldPermissionFormMixin, ModelForm): return -class ClubForm(FieldPermissionFormMixin, ModelForm): +class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Formulaire de base d'edition d'un user. Formulaire de base, utilisé pour l'edition de self par self ou un cableur. On formate les champs avec des label plus jolis""" @@ -379,7 +377,7 @@ class ClubForm(FieldPermissionFormMixin, ModelForm): return telephone -class ClubAdminandMembersForm(ModelForm): +class ClubAdminandMembersForm(FormRevMixin, ModelForm): """Permet d'éditer la liste des membres et des administrateurs d'un club""" class Meta: @@ -391,7 +389,7 @@ class ClubAdminandMembersForm(ModelForm): super(ClubAdminandMembersForm, self).__init__(*args, prefix=prefix, **kwargs) -class PasswordForm(ModelForm): +class PasswordForm(FormRevMixin, ModelForm): """ Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement""" class Meta: @@ -403,7 +401,7 @@ class PasswordForm(ModelForm): super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs) -class ServiceUserForm(ModelForm): +class ServiceUserForm(FormRevMixin, ModelForm): """ Modification d'un service user""" password = forms.CharField( label=u'Nouveau mot de passe', @@ -429,7 +427,7 @@ class EditServiceUserForm(ServiceUserForm): fields = ['access_group', 'comment'] -class StateForm(ModelForm): +class StateForm(FormRevMixin, ModelForm): """ Changement de l'état d'un user""" class Meta: model = User @@ -440,7 +438,7 @@ class StateForm(ModelForm): super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) -class GroupForm(ModelForm): +class GroupForm(FormRevMixin, ModelForm): """ Gestion des groupes d'un user""" groups = forms.ModelMultipleChoiceField( Group.objects.all(), @@ -457,7 +455,7 @@ class GroupForm(ModelForm): super(GroupForm, self).__init__(*args, prefix=prefix, **kwargs) -class SchoolForm(ModelForm): +class SchoolForm(FormRevMixin, ModelForm): """Edition, creation d'un école""" class Meta: model = School @@ -469,7 +467,7 @@ class SchoolForm(ModelForm): self.fields['name'].label = 'Établissement' -class ShellForm(ModelForm): +class ShellForm(FormRevMixin, ModelForm): """Edition, creation d'un école""" class Meta: model = ListShell @@ -481,7 +479,7 @@ class ShellForm(ModelForm): self.fields['shell'].label = 'Nom du shell' -class ListRightForm(ModelForm): +class ListRightForm(FormRevMixin, ModelForm): """Edition, d'un groupe , équivalent à un droit Ne peremet pas d'editer le gid, car il sert de primary key""" permissions = forms.ModelMultipleChoiceField( @@ -545,7 +543,7 @@ class DelSchoolForm(Form): self.fields['schools'].queryset = School.objects.all() -class BanForm(ModelForm): +class BanForm(FormRevMixin, ModelForm): """Creation, edition d'un objet bannissement""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) @@ -557,7 +555,7 @@ class BanForm(ModelForm): exclude = ['user'] -class WhitelistForm(ModelForm): +class WhitelistForm(FormRevMixin, ModelForm): """Creation, edition d'un objet whitelist""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) diff --git a/users/management/commands/chgpass.py b/users/management/commands/chgpass.py new file mode 100644 index 00000000..c3fabf8a --- /dev/null +++ b/users/management/commands/chgpass.py @@ -0,0 +1,47 @@ +# ⁻*- mode: python; coding: utf-8 -*- +# 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 Lev-Arcady Sellem +# +# 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. + +import os, pwd + +from django.core.management.base import BaseCommand, CommandError +from users.forms import PassForm +from re2o.script_utils import get_user, get_system_user, form_cli + +class Command(BaseCommand): + help = "Changer le mot de passe d'un utilisateur" + + def add_arguments(self, parser): + parser.add_argument('target_username', nargs='?') + + def handle(self, *args, **kwargs): + + current_username = get_system_user() + current_user = get_user(current_username) + target_username = kwargs["target_username"] or current_username + target_user = get_user(target_username) + + ok, msg = target_user.can_change_password(current_user) + if not ok: + raise CommandError(msg) + + self.stdout.write("Changement du mot de passe de %s" % target_user.pseudo) + + form_cli(PassForm,current_user,"Changement du mot de passe",instance=target_user) diff --git a/users/management/commands/chsh.py b/users/management/commands/chsh.py index df4d6c0d..6c5b06f7 100644 --- a/users/management/commands/chsh.py +++ b/users/management/commands/chsh.py @@ -26,6 +26,7 @@ from django.db import transaction from reversion import revisions as reversion from users.models import User, ListShell +from re2o.script_utils import get_user, get_system_user class Command(BaseCommand): help = 'Change the default shell of a user' @@ -35,14 +36,7 @@ class Command(BaseCommand): def handle(self, *args, **options): - def get_user(user_pseudo): - """Return the user queried by pseudo, and exit the script if not found.""" - user = User.objects.filter(pseudo=user_pseudo) - if not user: - raise CommandError("Utilisateur invalide") - return user[0] - - current_username = pwd.getpwuid(int(os.getenv("SUDO_UID") or os.getuid())).pw_name + current_username = get_system_user() current_user = get_user(current_username) target_username = options["target_username"] or current_username diff --git a/users/models.py b/users/models.py index 512259ae..efd46a75 100644 --- a/users/models.py +++ b/users/models.py @@ -76,7 +76,7 @@ import ldapdb.models.fields from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES, UID_RANGES from re2o.login import hashNT from re2o.field_permissions import FieldPermissionModelMixin -from re2o.mixins import AclMixin +from re2o.mixins import AclMixin, RevMixin from cotisations.models import Cotisation, Facture, Paiement, Vente from machines.models import Domain, Interface, Machine, regen @@ -171,7 +171,7 @@ class UserManager(BaseUserManager): """ return self._create_user(pseudo, surname, email, password, True) -class User(FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin, AclMixin): +class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin, AclMixin): """ Definition de l'utilisateur de base. Champs principaux : name, surnname, pseudo, email, room, password Herite du django BaseUser et du système d'auth django""" @@ -892,8 +892,8 @@ def user_post_save(sender, **kwargs): Synchronise le ldap""" is_created = kwargs['created'] user = kwargs['instance'] - if is_created: - user.notif_inscription() + #if is_created: + #user.notif_inscription() user.ldap_sync(base=True, access_refresh=True, mac_refresh=False, group_refresh=True) regen('mailing') @@ -907,7 +907,7 @@ def user_post_delete(sender, **kwargs): user.ldap_del() regen('mailing') -class ServiceUser(AclMixin, AbstractBaseUser): +class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): """ Classe des users daemons, règle leurs accès au ldap""" readonly = 'readonly' ACCESS = ( @@ -991,7 +991,7 @@ def service_user_post_delete(sender, **kwargs): service_user.ldap_del() -class School(AclMixin, models.Model): +class School(RevMixin, AclMixin, models.Model): """ Etablissement d'enseignement""" PRETTY_NAME = "Établissements enregistrés" @@ -1006,7 +1006,7 @@ class School(AclMixin, models.Model): return self.name -class ListRight(AclMixin, Group): +class ListRight(RevMixin, AclMixin, Group): """ Ensemble des droits existants. Chaque droit crée un groupe ldap synchronisé, avec gid. Permet de gérer facilement les accès serveurs et autres @@ -1073,7 +1073,7 @@ def listright_post_delete(sender, **kwargs): right.ldap_del() -class ListShell(AclMixin, models.Model): +class ListShell(RevMixin, AclMixin, models.Model): """Un shell possible. Pas de check si ce shell existe, les admin sont des grands""" PRETTY_NAME = "Liste des shells disponibles" @@ -1093,7 +1093,7 @@ class ListShell(AclMixin, models.Model): return self.shell -class Ban(AclMixin, models.Model): +class Ban(RevMixin, AclMixin, models.Model): """ Bannissement. Actuellement a un effet tout ou rien. Gagnerait à être granulaire""" PRETTY_NAME = "Liste des bannissements" @@ -1140,9 +1140,6 @@ class Ban(AclMixin, models.Model): """Ce ban est-il actif?""" return self.date_end > timezone.now() - def get_instance(banid, *args, **kwargs): - return Ban.objects.get(pk=banid) - def can_view(self, user_request, *args, **kwargs): """Check if an user can view a Ban object. @@ -1189,7 +1186,7 @@ def ban_post_delete(sender, **kwargs): regen('mac_ip_list') -class Whitelist(AclMixin, models.Model): +class Whitelist(RevMixin, AclMixin, models.Model): """Accès à titre gracieux. L'utilisateur ne paye pas; se voit accorder un accès internet pour une durée défini. Moins fort qu'un ban quel qu'il soit""" diff --git a/users/templates/users/aff_shell.html b/users/templates/users/aff_shell.html index ad325ddf..a660f88b 100644 --- a/users/templates/users/aff_shell.html +++ b/users/templates/users/aff_shell.html @@ -39,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_edit shell %} {% include 'buttons/edit.html' with href='users:edit-shell' id=shell.id %} {% acl_end %} - {% include 'buttons/history.html' with href='users:history' name='shell' id=shell.id %} + {% include 'buttons/history.html' with href='users:history' name='listshell' id=shell.id %} {% endfor %} diff --git a/users/views.py b/users/views.py index 8eb23bbf..12e6740e 100644 --- a/users/views.py +++ b/users/views.py @@ -37,7 +37,6 @@ from __future__ import unicode_literals from django.urls import reverse from django.shortcuts import get_object_or_404, render, redirect -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.db.models import ProtectedError, Q @@ -95,6 +94,7 @@ from re2o.views import form from re2o.utils import ( all_has_access, SortTable, + re2o_paginator ) from re2o.acl import ( can_create, @@ -115,10 +115,7 @@ def new_user(request): GTU = GeneralOption.get_cached_value('GTU') if user.is_valid(): user = user.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - user.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + user.save() user.reset_passwd_mail(request) messages.success(request, "L'utilisateur %s a été crée, un mail\ pour l'initialisation du mot de passe a été envoyé" % user.pseudo) @@ -137,10 +134,7 @@ def new_club(request): club = ClubForm(request.POST or None, user=request.user) if club.is_valid(): club = club.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - club.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + club.save() club.reset_passwd_mail(request) messages.success(request, "L'utilisateur %s a été crée, un mail\ pour l'initialisation du mot de passe a été envoyé" % club.pseudo) @@ -158,13 +152,9 @@ def edit_club_admin_members(request, club_instance, clubid): membres d'un club""" club = ClubAdminandMembersForm(request.POST or None, instance=club_instance) if club.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if club.changed_data: club.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in club.changed_data - )) - messages.success(request, "Le club a bien été modifié") + messages.success(request, "Le club a bien été modifié") return redirect(reverse( 'users:profil', kwargs={'userid':str(club_instance.id)} @@ -191,13 +181,9 @@ def edit_info(request, user, userid): user=request.user ) if user.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if user.changed_data: user.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in user.changed_data - )) - messages.success(request, "L'user a bien été modifié") + messages.success(request, "L'user a bien été modifié") return redirect(reverse( 'users:profil', kwargs={'userid':str(userid)} @@ -212,19 +198,15 @@ def state(request, user, userid): need droit bureau """ state = StateForm(request.POST or None, instance=user) if state.is_valid(): - with transaction.atomic(), reversion.create_revision(): - if state.cleaned_data['state'] == User.STATE_ARCHIVE: - user.archive() - elif state.cleaned_data['state'] == User.STATE_ACTIVE: - user.unarchive() - elif state.cleaned_data['state'] == User.STATE_DISABLED: - user.state = User.STATE_DISABLED - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in state.changed_data - )) + if state.cleaned_data['state'] == User.STATE_ARCHIVE: + user.archive() + elif state.cleaned_data['state'] == User.STATE_ACTIVE: + user.unarchive() + elif state.cleaned_data['state'] == User.STATE_DISABLED: + user.state = User.STATE_DISABLED + if user.changed_data: user.save() - messages.success(request, "Etat changé avec succès") + messages.success(request, "Etat changé avec succès") return redirect(reverse( 'users:profil', kwargs={'userid':str(userid)} @@ -237,13 +219,9 @@ def state(request, user, userid): def groups(request, user, userid): group = GroupForm(request.POST or None, instance=user) if group.is_valid(): - with transaction.atomic(), reversion.create_revision(): - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in group.changed_data - )) - group.save() - messages.success(request, "Groupes changés avec succès") + if group.changed_data: + group.save() + messages.success(request, "Groupes changés avec succès") return redirect(reverse( 'users:profil', kwargs={'userid':str(userid)} @@ -259,11 +237,9 @@ def password(request, user, userid): pour tous si droit bureau """ u_form = PassForm(request.POST or None, instance=user, user=request.user) if u_form.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if u_form.changed_data: u_form.save() - reversion.set_user(request.user) - reversion.set_comment("Changement du mot de passe") - messages.success(request, "Le mot de passe a changé") + messages.success(request, "Le mot de passe a changé") return redirect(reverse( 'users:profil', kwargs={'userid':str(user.id)} @@ -274,12 +250,9 @@ def password(request, user, userid): @login_required @can_edit(User, 'groups') def del_group(request, user, userid, listrightid): - with transaction.atomic(), reversion.create_revision(): - user.groups.remove(ListRight.objects.get(id=listrightid)) - user.save() - reversion.set_user(request.user) - reversion.set_comment("Suppression de droit") - messages.success(request, "Droit supprimé à %s" % user) + user.groups.remove(ListRight.objects.get(id=listrightid)) + user.save() + messages.success(request, "Droit supprimé à %s" % user) return HttpResponseRedirect(request.META.get('HTTP_REFERER')) @@ -290,11 +263,8 @@ def new_serviceuser(request): user = ServiceUserForm(request.POST or None) if user.is_valid(): user_object = user.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - user_object.set_password(user.cleaned_data['password']) - user_object.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + user_object.set_password(user.cleaned_data['password']) + user_object.save() messages.success( request, "L'utilisateur %s a été crée" % user_object.pseudo @@ -307,17 +277,13 @@ def new_serviceuser(request): @can_edit(ServiceUser) def edit_serviceuser(request, serviceuser, serviceuserid): """ Edit a ServiceUser """ - serviceuser = EditServiceUserForm(request.POST or None, instance=serviceuser) - if serviceuser.is_valid(): - user_object = serviceuser.save(commit=False) - with transaction.atomic(), reversion.create_revision(): - if serviceuser.cleaned_data['password']: - user_object.set_password(serviceuser.cleaned_data['password']) + user = EditServiceUserForm(request.POST or None, instance=user) + if user.is_valid(): + user_object = user.save(commit=False) + if user.cleaned_data['password']: + user_object.set_password(user.cleaned_data['password']) + if user.changed_data: user_object.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in serviceuser.changed_data - )) messages.success(request, "L'user a bien été modifié") return redirect(reverse('users:index-serviceusers')) return form({'userform': serviceuser, 'action_name':'Editer un serviceuser'}, 'users/user.html', request) @@ -328,9 +294,7 @@ def edit_serviceuser(request, serviceuser, serviceuserid): def del_serviceuser(request, serviceuser, serviceuserid): """Suppression d'un ou plusieurs serviceusers""" if request.method == "POST": - with transaction.atomic(), reversion.create_revision(): - serviceuser.delete() - reversion.set_user(request.user) + user.delete() messages.success(request, "L'user a été détruite") return redirect(reverse('users:index-serviceusers')) return form( @@ -350,10 +314,7 @@ def add_ban(request, user, userid): ban_instance = Ban(user=user) ban = BanForm(request.POST or None, instance=ban_instance) if ban.is_valid(): - with transaction.atomic(), reversion.create_revision(): - _ban_object = ban.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + _ban_object = ban.save() messages.success(request, "Bannissement ajouté") return redirect(reverse( 'users:profil', @@ -374,13 +335,9 @@ def edit_ban(request, ban_instance, banid): Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" ban = BanForm(request.POST or None, instance=ban_instance) if ban.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if ban.changed_data: ban.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in ban.changed_data - )) - messages.success(request, "Bannissement modifié") + messages.success(request, "Bannissement modifié") return redirect(reverse('users:index')) return form({'userform': ban, 'action_name': 'Editer un ban'}, 'users/user.html', request) @@ -399,10 +356,7 @@ def add_whitelist(request, user, userid): instance=whitelist_instance ) if whitelist.is_valid(): - with transaction.atomic(), reversion.create_revision(): - whitelist.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + whitelist.save() messages.success(request, "Accès à titre gracieux accordé") return redirect(reverse( 'users:profil', @@ -428,13 +382,9 @@ def edit_whitelist(request, whitelist_instance, whitelistid): instance=whitelist_instance ) if whitelist.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if whitelist.changed_data: whitelist.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in whitelist.changed_data - )) - messages.success(request, "Whitelist modifiée") + messages.success(request, "Whitelist modifiée") return redirect(reverse('users:index')) return form({'userform': whitelist, 'action_name': 'Editer une whitelist'}, 'users/user.html', request) @@ -446,10 +396,7 @@ def add_school(request): need cableur""" school = SchoolForm(request.POST or None) if school.is_valid(): - with transaction.atomic(), reversion.create_revision(): - school.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + school.save() messages.success(request, "L'établissement a été ajouté") return redirect(reverse('users:index-school')) return form({'userform': school, 'action_name':'Ajouter'}, 'users/user.html', request) @@ -462,13 +409,9 @@ def edit_school(request, school_instance, schoolid): la base de donnée, need cableur""" school = SchoolForm(request.POST or None, instance=school_instance) if school.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if school.changed_data: school.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in school.changed_data - )) - messages.success(request, "Établissement modifié") + messages.success(request, "Établissement modifié") return redirect(reverse('users:index-school')) return form({'userform': school, 'action_name':'Editer'}, 'users/user.html', request) @@ -485,9 +428,7 @@ def del_school(request, instances): school_dels = school.cleaned_data['schools'] for school_del in school_dels: try: - with transaction.atomic(), reversion.create_revision(): - school_del.delete() - reversion.set_comment("Destruction") + school_del.delete() messages.success(request, "L'établissement a été supprimé") except ProtectedError: messages.error( @@ -504,10 +445,7 @@ def add_shell(request): """ Ajouter un shell à la base de donnée""" shell = ShellForm(request.POST or None) if shell.is_valid(): - with transaction.atomic(), reversion.create_revision(): - shell.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + shell.save() messages.success(request, "Le shell a été ajouté") return redirect(reverse('users:index-shell')) return form({'userform': shell, 'action_name':'Ajouter'}, 'users/user.html', request) @@ -519,13 +457,9 @@ def edit_shell(request, shell_instance, listshellid): """ Editer un shell à partir du listshellid""" shell = ShellForm(request.POST or None, instance=shell_instance) if shell.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if shell.changed_data: shell.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in shell.changed_data - )) - messages.success(request, "Le shell a été modifié") + messages.success(request, "Le shell a été modifié") return redirect(reverse('users:index-shell')) return form({'userform': shell, 'action_name':'Editer'}, 'users/user.html', request) @@ -535,9 +469,7 @@ def edit_shell(request, shell_instance, listshellid): def del_shell(request, shell, listshellid): """Destruction d'un shell""" if request.method == "POST": - with transaction.atomic(), reversion.create_revision(): - shell.delete() - reversion.set_user(request.user) + shell.delete() messages.success(request, "Le shell a été détruit") return redirect(reverse('users:index-shell')) return form( @@ -554,10 +486,7 @@ def add_listright(request): Obligation de fournir un gid pour la synchro ldap, unique """ listright = NewListRightForm(request.POST or None) if listright.is_valid(): - with transaction.atomic(), reversion.create_revision(): - listright.save() - reversion.set_user(request.user) - reversion.set_comment("Création") + listright.save() messages.success(request, "Le droit/groupe a été ajouté") return redirect(reverse('users:index-listright')) return form({'userform': listright, 'action_name': 'Ajouter'}, 'users/user.html', request) @@ -573,13 +502,9 @@ def edit_listright(request, listright_instance, listrightid): instance=listright_instance ) if listright.is_valid(): - with transaction.atomic(), reversion.create_revision(): + if listright.changed_data: listright.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in listright.changed_data - )) - messages.success(request, "Droit modifié") + messages.success(request, "Droit modifié") return redirect(reverse('users:index-listright')) return form({'userform': listright, 'action_name': 'Editer'}, 'users/user.html', request) @@ -594,9 +519,7 @@ def del_listright(request, instances): listright_dels = listright.cleaned_data['listrights'] for listright_del in listright_dels: try: - with transaction.atomic(), reversion.create_revision(): - listright_del.delete() - reversion.set_comment("Destruction") + listright_del.delete() messages.success(request, "Le droit/groupe a été supprimé") except ProtectedError: messages.error( @@ -625,7 +548,6 @@ def mass_archive(request): with transaction.atomic(), reversion.create_revision(): user.archive() user.save() - reversion.set_user(request.user) reversion.set_comment("Archivage") messages.success(request, "%s users ont été archivés" % len( to_archive_list @@ -650,16 +572,7 @@ def index(request): request.GET.get('order'), SortTable.USERS_INDEX ) - paginator = Paginator(users_list, pagination_number) - page = request.GET.get('page') - try: - users_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - users_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - users_list = paginator.page(paginator.num_pages) + users_list = re2o_paginator(request, users_list, pagination_number) return render(request, 'users/index.html', {'users_list': users_list}) @@ -675,16 +588,7 @@ def index_clubs(request): request.GET.get('order'), SortTable.USERS_INDEX ) - paginator = Paginator(clubs_list, pagination_number) - page = request.GET.get('page') - try: - clubs_list = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - clubs_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - clubs_list = paginator.page(paginator.num_pages) + clubs_list = re2o_paginator(request, clubs_list, pagination_number) return render(request, 'users/index_clubs.html', {'clubs_list': clubs_list}) @@ -700,16 +604,7 @@ def index_ban(request): request.GET.get('order'), SortTable.USERS_INDEX_BAN ) - paginator = Paginator(ban_list, pagination_number) - page = request.GET.get('page') - try: - ban_list = paginator.page(page) - except PageNotAnInteger: - # If page isn't an integer, deliver first page - ban_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - ban_list = paginator.page(paginator.num_pages) + ban_list = re2o_paginator(request, ban_list, pagination_number) return render(request, 'users/index_ban.html', {'ban_list': ban_list}) @@ -725,16 +620,7 @@ def index_white(request): request.GET.get('order'), SortTable.USERS_INDEX_BAN ) - paginator = Paginator(white_list, pagination_number) - page = request.GET.get('page') - try: - white_list = paginator.page(page) - except PageNotAnInteger: - # If page isn't an integer, deliver first page - white_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - white_list = paginator.page(paginator.num_pages) + white_list = re2o_paginator(request, white_list, pagination_number) return render( request, 'users/index_whitelist.html', @@ -754,16 +640,7 @@ def index_school(request): request.GET.get('order'), SortTable.USERS_INDEX_SCHOOL ) - paginator = Paginator(school_list, pagination_number) - page = request.GET.get('page') - try: - school_list = paginator.page(page) - except PageNotAnInteger: - # If page isn't an integer, deliver first page - school_list = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - school_list = paginator.page(paginator.num_pages) + school_list = re2o_paginator(request, school_list, pagination_number) return render( request, 'users/index_schools.html', @@ -832,6 +709,8 @@ def profil(request, users, userid): request.GET.get('order'), SortTable.MACHINES_INDEX ) + pagination_large_number = GeneralOption.get_cached_value('pagination_large_number') + machines = re2o_paginator(request, machines, pagination_large_number) factures = Facture.objects.filter(user=users) factures = SortTable.sort( factures,