From 58eea72c13167493289ba5d470f9d3c83b3307a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Tue, 10 Oct 2017 21:00:41 +0000 Subject: [PATCH 01/12] Gitignore des logos de la page d'acceuil J'en ai marre d'avoir le logo du RezoMetz pour le lien du wiki --- .gitignore | 1 + static/logo/etherpad.png | Bin 4416 -> 0 bytes static/logo/federez.png | Bin 25231 -> 0 bytes static/logo/gitlab.png | Bin 16989 -> 0 bytes static/logo/kanboard.png | Bin 16495 -> 0 bytes static/logo/wiki.png | Bin 16484 -> 0 bytes static/logo/zerobin.png | Bin 4391 -> 0 bytes static_files/.static | 0 8 files changed, 1 insertion(+) delete mode 100644 static/logo/etherpad.png delete mode 100644 static/logo/federez.png delete mode 100644 static/logo/gitlab.png delete mode 100644 static/logo/kanboard.png delete mode 100644 static/logo/wiki.png delete mode 100644 static/logo/zerobin.png delete mode 100644 static_files/.static diff --git a/.gitignore b/.gitignore index 31d6b3f8..438dfbcd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ settings_local.py re2o.png __pycache__/* static_files/* +static/logo/* diff --git a/static/logo/etherpad.png b/static/logo/etherpad.png deleted file mode 100644 index 4dde5bf3fd7bfd347931f79bbf570a0b5aaabbd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4416 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 diff --git a/static/logo/federez.png b/static/logo/federez.png deleted file mode 100644 index 439de178bf70d6b0d73f1f2c703df6c07610c2b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25231 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 HDWM4fWX0V& diff --git a/static/logo/kanboard.png b/static/logo/kanboard.png deleted file mode 100644 index 5c13c937e47c7f6a50ca295f47d554496696fe73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16495 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 diff --git a/static/logo/wiki.png b/static/logo/wiki.png deleted file mode 100644 index c4437bb070355637cec2664c974f1473402cc552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16484 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 b6(Fy|Ijy_9kLvWtg)!U=R;lL@#l-Y~#Dm6W diff --git a/static_files/.static b/static_files/.static deleted file mode 100644 index e69de29b..00000000 From a03e7400161a0a0daddd49866b6c6671134f4985 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 11 Oct 2017 01:02:50 +0200 Subject: [PATCH 02/12] Vire __pseudo, inutile --- users/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/users/views.py b/users/views.py index 66a5f7ad..aa72517b 100644 --- a/users/views.py +++ b/users/views.py @@ -651,10 +651,10 @@ def profil(request, userid): if not request.user.has_perms(('cableur',)) and users != request.user: messages.error(request, "Vous ne pouvez pas afficher un autre user que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) - machines = Machine.objects.filter(user__pseudo=users).select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type__extension').prefetch_related('interface_set__type').prefetch_related('interface_set__domain__related_domain__extension') - factures = Facture.objects.filter(user__pseudo=users) - bans = Ban.objects.filter(user__pseudo=users) - whitelists = Whitelist.objects.filter(user__pseudo=users) + machines = Machine.objects.filter(user=users).select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type__extension').prefetch_related('interface_set__type').prefetch_related('interface_set__domain__related_domain__extension') + factures = Facture.objects.filter(user=users) + bans = Ban.objects.filter(user=users) + whitelists = Whitelist.objects.filter(user=users) list_droits = Right.objects.filter(user=users) options, created = OptionalUser.objects.get_or_create() user_solde = options.user_solde From 74ca8c8a9ac55cedd1d70668d3472a943bbb829d Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 11 Oct 2017 22:22:31 +0200 Subject: [PATCH 03/12] Rationalise les import + corrige les auteurs --- freeradius_utils/auth.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 5ea4e48c..e3e272c9 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -3,6 +3,7 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # +# Copyirght © 2017 Daniel Stan # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle @@ -30,20 +31,18 @@ moment de l'authentification, en WiFi, filaire, ou par les NAS eux-mêmes. Inspirés d'autres exemples trouvés ici : https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ + +Inspiré du travail de Daniel Stan au Crans """ import logging import netaddr import radiusd # Module magique freeradius (radiusd.py is dummy) -import os import binascii import hashlib - import os, sys -import os, sys - proj_path = "/var/www/re2o/" # This is so Django knows where to find stuff. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") From 993aeb31c81a4ac860bbfbcf004e511cadcd3caf Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 04:07:56 +0200 Subject: [PATCH 04/12] Pep8 + docstrings --- cotisations/models.py | 88 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index fca6bfa5..6388b38c 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -36,43 +36,73 @@ from django.utils import timezone from machines.models import regen + class Facture(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 + valides et controle (trésorerie)""" PRETTY_NAME = "Factures émises" user = models.ForeignKey('users.User', on_delete=models.PROTECT) paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) - banque = models.ForeignKey('Banque', on_delete=models.PROTECT, blank=True, null=True) + banque = models.ForeignKey( + 'Banque', + on_delete=models.PROTECT, + blank=True, + null=True) cheque = models.CharField(max_length=255, blank=True) date = models.DateTimeField(auto_now_add=True) valid = models.BooleanField(default=True) control = models.BooleanField(default=False) def prix(self): - prix = Vente.objects.filter(facture=self).aggregate(models.Sum('prix'))['prix__sum'] + prix = Vente.objects.filter( + facture=self + ).aggregate(models.Sum('prix'))['prix__sum'] return prix def prix_total(self): - return Vente.objects.filter(facture=self).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] + """Prix total : somme des produits prix_unitaire et quantité des + ventes de l'objet""" + return Vente.objects.filter( + facture=self + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] def name(self): - name = ' - '.join(Vente.objects.filter(facture=self).values_list('name', flat=True)) + """String, somme des name des ventes de self""" + name = ' - '.join(Vente.objects.filter( + facture=self + ).values_list('name', flat=True)) return name def __str__(self): return str(self.user) + ' ' + str(self.date) + @receiver(post_save, sender=Facture) def facture_post_save(sender, **kwargs): + """Post save d'une facture, synchronise l'user ldap""" facture = kwargs['instance'] user = facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + @receiver(post_delete, sender=Facture) def facture_post_delete(sender, **kwargs): user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + class Vente(models.Model): + """Objet vente, contient une quantité, une facture parente, un nom, + un prix. Peut-être relié à un objet cotisation, via le boolean + iscotisation""" PRETTY_NAME = "Ventes effectuées" facture = models.ForeignKey('Facture', on_delete=models.CASCADE) @@ -80,44 +110,64 @@ class Vente(models.Model): name = models.CharField(max_length=255) prix = models.DecimalField(max_digits=5, decimal_places=2) iscotisation = models.BooleanField() - duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True) + duration = models.IntegerField( + help_text="Durée exprimée en mois entiers", + blank=True, + null=True) def prix_total(self): + """Renvoie le prix_total de self (nombre*prix)""" return self.prix*self.number def update_cotisation(self): if hasattr(self, 'cotisation'): cotisation = self.cotisation - cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number) + cotisation.date_end = cotisation.date_start + relativedelta( + months=self.duration*self.number) return def create_cotis(self, date_start=False): - """ Update et crée l'objet cotisation associé à une facture, prend en argument l'user, la facture pour la quantitéi, et l'article pour la durée""" + """Update et crée l'objet cotisation associé à une facture, prend + en argument l'user, la facture pour la quantitéi, et l'article pour + la durée""" if not hasattr(self, 'cotisation'): - cotisation=Cotisation(vente=self) + cotisation = Cotisation(vente=self) if date_start: - end_adhesion = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self.facture.user).exclude(valid=False))).filter(date_start__lt=date_start).aggregate(Max('date_end'))['date_end__max'] + end_adhesion = Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self.facture.user + ).exclude(valid=False)) + ).filter( + date_start__lt=date_start + ).aggregate(Max('date_end'))['date_end__max'] else: end_adhesion = self.facture.user.end_adhesion() date_start = date_start or timezone.now() end_adhesion = end_adhesion or date_start date_max = max(end_adhesion, date_start) cotisation.date_start = date_max - cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number) + cotisation.date_end = cotisation.date_start + relativedelta( + months=self.duration*self.number + ) return def save(self, *args, **kwargs): # On verifie que si iscotisation, duration est présent if self.iscotisation and not self.duration: - raise ValidationError("Cotisation et durée doivent être présents ensembles") + raise ValidationError("Cotisation et durée doivent être présents\ + ensembles") self.update_cotisation() super(Vente, self).save(*args, **kwargs) def __str__(self): return str(self.name) + ' ' + str(self.facture) + @receiver(post_save, sender=Vente) def vente_post_save(sender, **kwargs): + """Post save d'une vente, déclencge la création de l'objet cotisation + si il y a lieu(si iscotisation) """ vente = kwargs['instance'] if hasattr(vente, 'cotisation'): vente.cotisation.vente = vente @@ -128,6 +178,7 @@ def vente_post_save(sender, **kwargs): user = vente.facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + @receiver(post_delete, sender=Vente) def vente_post_delete(sender, **kwargs): vente = kwargs['instance'] @@ -135,7 +186,10 @@ def vente_post_delete(sender, **kwargs): user = vente.facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + class Article(models.Model): + """Liste des articles en vente : prix, nom, et attribut iscotisation + et duree si c'est une cotisation""" PRETTY_NAME = "Articles en vente" name = models.CharField(max_length=255, unique=True) @@ -154,7 +208,9 @@ class Article(models.Model): def __str__(self): return self.name + class Banque(models.Model): + """Liste des banques""" PRETTY_NAME = "Banques enregistrées" name = models.CharField(max_length=255) @@ -162,7 +218,9 @@ class Banque(models.Model): def __str__(self): return self.name + class Paiement(models.Model): + """Moyens de paiement""" PRETTY_NAME = "Moyens de paiement" PAYMENT_TYPES = ( (0, 'Autre'), @@ -179,11 +237,15 @@ class Paiement(models.Model): self.moyen = self.moyen.title() def save(self, *args, **kwargs): + """Un seul type de paiement peut-etre cheque...""" if Paiement.objects.filter(type_paiement=1).count() > 1: - raise ValidationError("On ne peut avoir plusieurs mode de paiement chèque") + raise ValidationError("On ne peut avoir plusieurs mode de paiement\ + chèque") super(Paiement, self).save(*args, **kwargs) + class Cotisation(models.Model): + """Objet cotisation, debut et fin, relié en onetoone à une vente""" PRETTY_NAME = "Cotisations" vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True) @@ -193,6 +255,7 @@ class Cotisation(models.Model): def __str__(self): return str(self.vente) + @receiver(post_save, sender=Cotisation) def cotisation_post_save(sender, **kwargs): regen('dns') @@ -200,6 +263,7 @@ def cotisation_post_save(sender, **kwargs): regen('mac_ip_list') regen('mailing') + @receiver(post_delete, sender=Cotisation) def vente_post_delete(sender, **kwargs): cotisation = kwargs['instance'] From 500884458c135be209b73c57ce389c98a278db4d Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 05:08:30 +0200 Subject: [PATCH 05/12] PEP8 et doc strings sur views de cotisations --- cotisations/views.py | 323 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 259 insertions(+), 64 deletions(-) diff --git a/cotisations/views.py b/cotisations/views.py index 0a2a7648..517eafd1 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -40,7 +40,10 @@ from reversion import revisions as reversion from reversion.models import Version from .models import Facture, Article, Vente, Cotisation, Paiement, Banque -from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, CreditSoldeForm, SelectArticleForm +from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm +from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm +from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf +from .forms import SelectArticleForm, CreditSoldeForm from users.models import User from .tex import render_tex from re2o.settings import LOGO_PATH @@ -50,18 +53,27 @@ from preferences.models import OptionalUser, AssoOption, GeneralOption from dateutil.relativedelta import relativedelta from django.utils import timezone + def form(ctx, template, request): c = ctx c.update(csrf(request)) return render(request, template, c) + @login_required @permission_required('cableur') def new_facture(request, userid): + """Creation d'une facture pour un user. Renvoie la liste des articles + et crée des factures dans un formset. Utilise un peu de js coté template + pour ajouter des articles. + Parse les article et boucle dans le formset puis save les ventes, + enfin sauve la facture parente. + TODO : simplifier cette fonction, déplacer l'intelligence coté models + Facture et Vente.""" try: user = User.objects.get(pk=userid) except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) + messages.error(request, u"Utilisateur inexistant") return redirect("/cotisations/") facture = Facture(user=user) # Le template a besoin de connaitre les articles pour le js @@ -77,15 +89,20 @@ def new_facture(request, userid): options, created = OptionalUser.objects.get_or_create() user_solde = options.user_solde solde_negatif = options.solde_negatif - # Si on paye par solde, que l'option est activée, on vérifie que le négatif n'est pas atteint + # Si on paye par solde, que l'option est activée, + # on vérifie que le négatif n'est pas atteint if user_solde: - if new_facture.paiement == Paiement.objects.get_or_create(moyen='solde')[0]: + if new_facture.paiement == Paiement.objects.get_or_create( + moyen='solde' + )[0]: prix_total = 0 for art_item in articles: if art_item.cleaned_data: - prix_total += art_item.cleaned_data['article'].prix*art_item.cleaned_data['quantity'] + prix_total += art_item.cleaned_data['article']\ + .prix*art_item.cleaned_data['quantity'] if float(user.solde) - float(prix_total) < solde_negatif: - messages.error(request, "Le solde est insuffisant pour effectuer l'opération") + messages.error(request, "Le solde est insuffisant pour\ + effectuer l'opération") return redirect("/users/profil/" + userid) with transaction.atomic(), reversion.create_revision(): new_facture.save() @@ -95,22 +112,47 @@ def new_facture(request, userid): if art_item.cleaned_data: article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] - new_vente = Vente.objects.create(facture=new_facture, name=article.name, prix=article.prix, iscotisation=article.iscotisation, duration=article.duration, number=quantity) + new_vente = Vente.objects.create( + facture=new_facture, + name=article.name, + prix=article.prix, + iscotisation=article.iscotisation, + duration=article.duration, + number=quantity + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) reversion.set_comment("Création") - if any(art_item.cleaned_data['article'].iscotisation for art_item in articles if art_item.cleaned_data): - messages.success(request, "La cotisation a été prolongée pour l'adhérent %s jusqu'au %s" % (user.pseudo, user.end_adhesion()) ) + if any(art_item.cleaned_data['article'].iscotisation + for art_item in articles if art_item.cleaned_data): + messages.success( + request, + "La cotisation a été prolongée\ + pour l'adhérent %s jusqu'au %s" % ( + user.pseudo, user.end_adhesion() + ) + ) else: messages.success(request, "La facture a été crée") return redirect("/users/profil/" + userid) - messages.error(request, u"Il faut au moins un article valide pour créer une facture" ) - return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request) + messages.error( + request, + u"Il faut au moins un article valide pour créer une facture" + ) + return form({ + 'factureform': facture_form, + 'venteform': article_formset, + 'articlelist': article_list + }, 'cotisations/new_facture.html', request) + @login_required @permission_required('tresorier') def new_facture_pdf(request): + """Permet de générer un pdf d'une facture. Réservée + au trésorier, permet d'emettre des factures sans objet + Vente ou Facture correspondant en bdd""" facture_form = NewFactureFormPdf(request.POST or None) if facture_form.is_valid(): options, created = AssoOption.objects.get_or_create() @@ -124,68 +166,129 @@ def new_facture_pdf(request): for a in article: tbl.append([a, quantite, a.prix * quantite]) prix_total = sum(a[2] for a in tbl) - user = {'name':destinataire, 'room':chambre} - return render_tex(request, 'cotisations/factures.tex', {'DATE' : timezone.now(),'dest':user,'fid':fid, 'article':tbl, 'total':prix_total, 'paid':paid, 'asso_name':options.name, 'line1':options.adresse1, 'line2':options.adresse2, 'siret':options.siret, 'email':options.contact, 'phone':options.telephone, 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)}) - return form({'factureform': facture_form}, 'cotisations/facture.html', request) + user = {'name': destinataire, 'room': chambre} + return render_tex(request, 'cotisations/factures.tex', { + 'DATE': timezone.now(), + 'dest': user, + 'fid': fid, + 'article': tbl, + 'total': prix_total, + 'paid': paid, + 'asso_name': options.name, + 'line1': options.adresse1, + 'line2': options.adresse2, + 'siret': options.siret, + 'email': options.contact, + 'phone': options.telephone, + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + }) + return form({ + 'factureform': facture_form + }, 'cotisations/facture.html', request) + @login_required def facture_pdf(request, factureid): + """Affiche en pdf une facture. Cree une ligne par Vente de la facture, + et génére une facture avec le total, le moyen de paiement, l'adresse + de l'adhérent, etc. Réservée à self pour un user sans droits, + les droits cableurs permettent d'afficher toute facture""" try: facture = Facture.objects.get(pk=factureid) except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante" ) + messages.error(request, u"Facture inexistante") return redirect("/cotisations/") - if not request.user.has_perms(('cableur',)) and facture.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher une facture ne vous appartenant pas sans droit cableur") + if not request.user.has_perms(('cableur',))\ + and facture.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher une facture ne vous\ + appartenant pas sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) if not facture.valid: - messages.error(request, "Vous ne pouvez pas afficher une facture non valide") + messages.error(request, "Vous ne pouvez pas afficher\ + une facture non valide") return redirect("/users/profil/" + str(request.user.id)) vente = Vente.objects.all().filter(facture=facture) ventes = [] options, created = AssoOption.objects.get_or_create() for v in vente: ventes.append([v, v.number, v.prix_total]) - return render_tex(request, 'cotisations/factures.tex', {'paid':True, 'fid':facture.id, 'DATE':facture.date,'dest':facture.user, 'article':ventes, 'total': facture.prix_total(), 'asso_name':options.name, 'line1': options.adresse1, 'line2':options.adresse2, 'siret':options.siret, 'email':options.contact, 'phone':options.telephone, 'tpl_path':os.path.join(settings.BASE_DIR, LOGO_PATH)}) + return render_tex(request, 'cotisations/factures.tex', { + 'paid': True, + 'fid': facture.id, + 'DATE': facture.date, + 'dest': facture.user, + 'article': ventes, + 'total': facture.prix_total(), + 'asso_name': options.name, + 'line1': options.adresse1, + 'line2': options.adresse2, + 'siret': options.siret, + 'email': options.contact, + 'phone': options.telephone, + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + }) + @login_required @permission_required('cableur') def edit_facture(request, factureid): + """Permet l'édition d'une facture. On peut y éditer les ventes + déjà effectuer, ou rendre une facture invalide (non payées, chèque + en bois etc). Mets à jour les durée de cotisation attenantes""" try: facture = Facture.objects.get(pk=factureid) except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante" ) + messages.error(request, u"Facture inexistante") return redirect("/cotisations/") if request.user.has_perms(['tresorier']): - facture_form = TrezEditFactureForm(request.POST or None, instance=facture) + facture_form = TrezEditFactureForm( + request.POST or None, + instance=facture + ) elif facture.control or not facture.valid: - messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier") + messages.error(request, "Vous ne pouvez pas editer une facture\ + controlée ou invalidée par le trésorier") return redirect("/cotisations/") else: facture_form = EditFactureForm(request.POST or None, instance=facture) ventes_objects = Vente.objects.filter(facture=facture) - vente_form_set = modelformset_factory(Vente, fields=('name','number'), extra=0, max_num=len(ventes_objects)) + vente_form_set = modelformset_factory( + Vente, + fields=('name', 'number'), + extra=0, + max_num=len(ventes_objects) + ) 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(): 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)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for form in vente_form for field + in facture_form.changed_data + form.changed_data) + ) messages.success(request, "La facture a bien été modifiée") return redirect("/cotisations/") - return form({'factureform': facture_form, 'venteform': vente_form}, 'cotisations/edit_facture.html', request) + return form({ + 'factureform': facture_form, + 'venteform': vente_form + }, 'cotisations/edit_facture.html', request) + @login_required @permission_required('cableur') def del_facture(request, factureid): + """Suppression d'une facture. Supprime en cascade les ventes + et cotisations filles""" try: facture = Facture.objects.get(pk=factureid) except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante" ) + messages.error(request, u"Facture inexistante") return redirect("/cotisations/") if (facture.control or not facture.valid): - messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier") + messages.error(request, "Vous ne pouvez pas editer une facture\ + controlée ou invalidée par le trésorier") return redirect("/cotisations/") if request.method == "POST": with transaction.atomic(), reversion.create_revision(): @@ -193,7 +296,11 @@ def del_facture(request, factureid): reversion.set_user(request.user) messages.success(request, "La facture a été détruite") return redirect("/cotisations/") - return form({'objet': facture, 'objet_name': 'facture'}, 'cotisations/delete.html', request) + return form({ + 'objet': facture, + 'objet_name': 'facture' + }, 'cotisations/delete.html', request) + @login_required @permission_required('cableur') @@ -202,7 +309,7 @@ def credit_solde(request, userid): try: user = User.objects.get(pk=userid) except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) + messages.error(request, u"Utilisateur inexistant") return redirect("/cotisations/") facture = CreditSoldeForm(request.POST or None) if facture.is_valid(): @@ -211,8 +318,15 @@ def credit_solde(request, userid): facture_instance.user = user facture_instance.save() reversion.set_user(request.user) - reversion.set_comment("Création") - new_vente = Vente.objects.create(facture=facture_instance, name="solde", prix=facture.cleaned_data['montant'], iscotisation=False, duration=0, number=1) + reversion.set_comment("Création") + new_vente = Vente.objects.create( + facture=facture_instance, + name="solde", + prix=facture.cleaned_data['montant'], + iscotisation=False, + duration=0, + number=1 + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) @@ -225,6 +339,13 @@ def credit_solde(request, userid): @login_required @permission_required('tresorier') def add_article(request): + """Ajoute un article. Champs : désignation, + prix, est-ce une cotisation et si oui sa durée + Réservé au trésorier + Nota bene : les ventes déjà effectuées ne sont pas reliées + aux articles en vente. La désignation, le prix... sont + copiés à la création de la facture. Un changement de prix n'a + 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(): @@ -235,27 +356,36 @@ def add_article(request): return redirect("/cotisations/index_article/") return form({'factureform': article}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def edit_article(request, articleid): + """Edition d'un article (designation, prix, etc) + Réservé au trésorier""" try: article_instance = Article.objects.get(pk=articleid) except Article.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/cotisations/index_article/") article = ArticleForm(request.POST or None, instance=article_instance) if article.is_valid(): with transaction.atomic(), reversion.create_revision(): article.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in article.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in article.changed_data + ) + ) messages.success(request, "Type d'article modifié") return redirect("/cotisations/index_article/") return form({'factureform': article}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def del_article(request): + """Suppression d'un article en vente""" article = DelArticleForm(request.POST or None) if article.is_valid(): article_del = article.cleaned_data['articles'] @@ -266,9 +396,12 @@ def del_article(request): return redirect("/cotisations/index_article") return form({'factureform': article}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def add_paiement(request): + """Ajoute un moyen de paiement. Relié aux factures + via foreign key""" paiement = PaiementForm(request.POST or None) if paiement.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -279,27 +412,35 @@ def add_paiement(request): return redirect("/cotisations/index_paiement/") return form({'factureform': paiement}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def edit_paiement(request, paiementid): + """Edition d'un moyen de paiement""" try: paiement_instance = Paiement.objects.get(pk=paiementid) except Paiement.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/cotisations/index_paiement/") paiement = PaiementForm(request.POST or None, instance=paiement_instance) if paiement.is_valid(): with transaction.atomic(), reversion.create_revision(): paiement.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in paiement.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in paiement.changed_data + ) + ) messages.success(request, "Type de paiement modifié") return redirect("/cotisations/index_paiement/") return form({'factureform': paiement}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def del_paiement(request): + """Suppression d'un moyen de paiement""" paiement = DelPaiementForm(request.POST or None) if paiement.is_valid(): paiement_dels = paiement.cleaned_data['paiements'] @@ -309,15 +450,24 @@ def del_paiement(request): paiement_del.delete() reversion.set_user(request.user) reversion.set_comment("Destruction") - messages.success(request, "Le moyen de paiement a été supprimé") + messages.success( + request, + "Le moyen de paiement a été supprimé" + ) except ProtectedError: - messages.error(request, "Le moyen de paiement %s est affecté à au moins une facture, vous ne pouvez pas le supprimer" % paiement_del) + messages.error( + request, + "Le moyen de paiement %s est affecté à au moins une\ + facture, vous ne pouvez pas le supprimer" % paiement_del + ) return redirect("/cotisations/index_paiement/") return form({'factureform': paiement}, 'cotisations/facture.html', request) + @login_required @permission_required('cableur') 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(): @@ -328,27 +478,35 @@ def add_banque(request): return redirect("/cotisations/index_banque/") return form({'factureform': banque}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def edit_banque(request, banqueid): + """Edite le nom d'une banque""" try: banque_instance = Banque.objects.get(pk=banqueid) except Banque.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/cotisations/index_banque/") banque = BanqueForm(request.POST or None, instance=banque_instance) if banque.is_valid(): with transaction.atomic(), reversion.create_revision(): banque.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in banque.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in banque.changed_data + ) + ) messages.success(request, "Banque modifiée") return redirect("/cotisations/index_banque/") return form({'factureform': banque}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def del_banque(request): + """Supprime une banque""" banque = DelBanqueForm(request.POST or None) if banque.is_valid(): banque_dels = banque.cleaned_data['banques'] @@ -360,17 +518,25 @@ def del_banque(request): reversion.set_comment("Destruction") messages.success(request, "La banque a été supprimée") except ProtectedError: - messages.error(request, "La banque %s est affectée à au moins une facture, vous ne pouvez pas la supprimer" % banque_del) + messages.error(request, "La banque %s est affectée à au moins\ + une facture, vous ne pouvez pas la supprimer" % banque_del) return redirect("/cotisations/index_banque/") return form({'factureform': banque}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def control(request): + """Pour le trésorier, vue pour controler en masse les + factures.Case à cocher, pratique""" options, created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number facture_list = Facture.objects.order_by('date').reverse() - controlform_set = modelformset_factory(Facture, fields=('control','valid'), extra=0) + controlform_set = modelformset_factory( + Facture, + fields=('control', 'valid'), + extra=0 + ) paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -379,7 +545,9 @@ def control(request): facture_list = paginator.page(1) except EmptyPage: facture_list = paginator.page(paginator.num.pages) - page_query = Facture.objects.order_by('date').reverse().filter(id__in=[facture.id for facture in facture_list]) + page_query = Facture.objects.order_by('date').reverse().filter( + id__in=[facture.id for facture in facture_list] + ) controlform = controlform_set(request.POST or None, queryset=page_query) if controlform.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -387,32 +555,50 @@ def control(request): reversion.set_user(request.user) reversion.set_comment("Controle trésorier") return redirect("/cotisations/control/") - return render(request, 'cotisations/control.html', {'facture_list': facture_list, 'controlform': controlform}) + return render(request, 'cotisations/control.html', { + 'facture_list': facture_list, + 'controlform': controlform + }) + @login_required @permission_required('cableur') def index_article(request): + """Affiche l'ensemble des articles en vente""" article_list = Article.objects.order_by('name') - return render(request, 'cotisations/index_article.html', {'article_list':article_list}) + return render(request, 'cotisations/index_article.html', { + 'article_list': article_list + }) + @login_required @permission_required('cableur') def index_paiement(request): + """Affiche l'ensemble des moyens de paiement en vente""" paiement_list = Paiement.objects.order_by('moyen') - return render(request, 'cotisations/index_paiement.html', {'paiement_list':paiement_list}) + return render(request, 'cotisations/index_paiement.html', { + 'paiement_list': paiement_list + }) + @login_required @permission_required('cableur') def index_banque(request): + """Affiche l'ensemble des banques""" banque_list = Banque.objects.order_by('name') - return render(request, 'cotisations/index_banque.html', {'banque_list':banque_list}) + return render(request, 'cotisations/index_banque.html', { + 'banque_list': banque_list + }) + @login_required @permission_required('cableur') def index(request): + """Affiche l'ensemble des factures, pour les cableurs et +""" options, created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - facture_list = Facture.objects.order_by('date').select_related('user').select_related('paiement').prefetch_related('vente_set').reverse() + facture_list = Facture.objects.order_by('date').select_related('user')\ + .select_related('paiement').prefetch_related('vente_set').reverse() paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -423,37 +609,43 @@ def index(request): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. facture_list = paginator.page(paginator.num_pages) - return render(request, 'cotisations/index.html', {'facture_list': facture_list}) + return render(request, 'cotisations/index.html', { + 'facture_list': facture_list + }) + @login_required def history(request, object, id): + """Affiche l'historique de chaque objet""" if object == 'facture': try: - object_instance = Facture.objects.get(pk=id) + object_instance = Facture.objects.get(pk=id) except Facture.DoesNotExist: - messages.error(request, "Facture inexistante") - return redirect("/cotisations/") - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'une facture d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) + messages.error(request, "Facture inexistante") + return redirect("/cotisations/") + if not request.user.has_perms(('cableur',))\ + and object_instance.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher l'historique\ + d'une facture d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) elif object == 'paiement' and request.user.has_perms(('cableur',)): try: - object_instance = Paiement.objects.get(pk=id) + object_instance = Paiement.objects.get(pk=id) except Paiement.DoesNotExist: - messages.error(request, "Paiement inexistant") - return redirect("/cotisations/") + messages.error(request, "Paiement inexistant") + return redirect("/cotisations/") elif object == 'article' and request.user.has_perms(('cableur',)): try: - object_instance = Article.objects.get(pk=id) + object_instance = Article.objects.get(pk=id) except Article.DoesNotExist: - messages.error(request, "Article inexistante") - return redirect("/cotisations/") + messages.error(request, "Article inexistante") + return redirect("/cotisations/") elif object == 'banque' and request.user.has_perms(('cableur',)): try: - object_instance = Banque.objects.get(pk=id) + object_instance = Banque.objects.get(pk=id) except Banque.DoesNotExist: - messages.error(request, "Banque inexistante") - return redirect("/cotisations/") + messages.error(request, "Banque inexistante") + return redirect("/cotisations/") else: messages.error(request, "Objet inconnu") return redirect("/cotisations/") @@ -470,4 +662,7 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render(request, 're2o/history.html', { + 'reversions': reversions, + 'object': object_instance + }) From 7039e7b0914e4ae94083be3574e0111ff1e535e2 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 05:24:57 +0200 Subject: [PATCH 06/12] Pep8 et doc strings --- cotisations/forms.py | 100 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 97bc82ab..b3742088 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -28,7 +28,10 @@ from django import forms from django.core.validators import MinValueValidator from .models import Article, Paiement, Facture, Banque, Vente + class NewFactureForm(ModelForm): + """Creation d'une facture, moyen de paiement, banque et numero + de cheque""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -36,58 +39,91 @@ class NewFactureForm(ModelForm): self.fields['banque'].required = False self.fields['cheque'].label = 'Numero de chèque' self.fields['banque'].empty_label = "Non renseigné" - self.fields['paiement'].empty_label = "Séléctionner un moyen de paiement" - self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects.filter(type_paiement=1).first().id + self.fields['paiement'].empty_label = "Séléctionner\ + un moyen de paiement" + self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects\ + .filter(type_paiement=1).first().id class Meta: model = Facture - fields = ['paiement','banque','cheque'] + fields = ['paiement', 'banque', 'cheque'] def clean(self): - cleaned_data=super(NewFactureForm, self).clean() + cleaned_data = super(NewFactureForm, self).clean() paiement = cleaned_data.get("paiement") cheque = cleaned_data.get("cheque") banque = cleaned_data.get("banque") if not paiement: - raise forms.ValidationError("Le moyen de paiement est obligatoire.") + raise forms.ValidationError("Le moyen de paiement est obligatoire") elif paiement.type_paiement == "check" and not (cheque and banque): - raise forms.ValidationError("Le numéro de chèque et la banque sont obligatoires.") + raise forms.ValidationError("Le numéro de chèque et\ + la banque sont obligatoires.") return cleaned_data + class CreditSoldeForm(NewFactureForm): + """Permet de faire des opérations sur le solde si il est activé""" class Meta(NewFactureForm.Meta): model = Facture - fields = ['paiement','banque','cheque'] + fields = ['paiement', 'banque', 'cheque'] def __init__(self, *args, **kwargs): super(CreditSoldeForm, self).__init__(*args, **kwargs) - self.fields['paiement'].queryset = Paiement.objects.exclude(moyen='solde').exclude(moyen="Solde") - + self.fields['paiement'].queryset = Paiement.objects.exclude( + moyen='solde' + ).exclude(moyen="Solde") montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) + class SelectArticleForm(Form): - article = forms.ModelChoiceField(queryset=Article.objects.all(), label="Article", required=True) - quantity = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)], required=True) + """Selection d'un article lors de la creation d'une facture""" + article = forms.ModelChoiceField( + queryset=Article.objects.all(), + label="Article", + required=True + ) + quantity = forms.IntegerField( + label="Quantité", + validators=[MinValueValidator(1)], + required=True + ) + class NewFactureFormPdf(Form): - article = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Article") - number = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)]) + """Creation d'un pdf facture par le trésorier""" + article = forms.ModelMultipleChoiceField( + queryset=Article.objects.all(), + label="Article" + ) + number = forms.IntegerField( + label="Quantité", + validators=[MinValueValidator(1)] + ) paid = forms.BooleanField(label="Payé", required=False) dest = forms.CharField(required=True, max_length=255, label="Destinataire") chambre = forms.CharField(required=False, max_length=10, label="Adresse") - fid = forms.CharField(required=True, max_length=10, label="Numéro de la facture") + fid = forms.CharField( + required=True, + max_length=10, + label="Numéro de la facture" + ) + class EditFactureForm(NewFactureForm): + """Edition d'une facture : moyen de paiement, banque, user parent""" class Meta(NewFactureForm.Meta): - fields = ['paiement','banque','cheque','user'] + fields = ['paiement', 'banque', 'cheque', 'user'] def __init__(self, *args, **kwargs): super(EditFactureForm, self).__init__(*args, **kwargs) self.fields['user'].label = 'Adherent' - self.fields['user'].empty_label = "Séléctionner l'adhérent propriétaire" + self.fields['user'].empty_label = "Séléctionner\ + l'adhérent propriétaire" + class TrezEditFactureForm(EditFactureForm): + """Vue pour édition controle trésorier""" class Meta(EditFactureForm.Meta): fields = '__all__' @@ -98,6 +134,7 @@ class TrezEditFactureForm(EditFactureForm): class ArticleForm(ModelForm): + """Creation d'un article. Champs : nom, cotisation, durée""" class Meta: model = Article fields = '__all__' @@ -107,10 +144,20 @@ class ArticleForm(ModelForm): super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = "Désignation de l'article" + class DelArticleForm(Form): - articles = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Articles actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs articles en vente. Choix + parmis les modèles""" + articles = forms.ModelMultipleChoiceField( + queryset=Article.objects.all(), + label="Articles actuels", + widget=forms.CheckboxSelectMultiple + ) + class PaiementForm(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: model = Paiement fields = ['moyen', 'type_paiement'] @@ -121,10 +168,19 @@ class PaiementForm(ModelForm): self.fields['moyen'].label = 'Moyen de paiement à ajouter' self.fields['type_paiement'].label = 'Type de paiement à ajouter' + class DelPaiementForm(Form): - paiements = forms.ModelMultipleChoiceField(queryset=Paiement.objects.all(), label="Moyens de paiement actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs moyens de paiements, selection + parmis les models""" + paiements = forms.ModelMultipleChoiceField( + queryset=Paiement.objects.all(), + label="Moyens de paiement actuels", + widget=forms.CheckboxSelectMultiple + ) + class BanqueForm(ModelForm): + """Creation d'une banque, field name""" class Meta: model = Banque fields = ['name'] @@ -134,5 +190,11 @@ class BanqueForm(ModelForm): super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Banque à ajouter' + class DelBanqueForm(Form): - banques = forms.ModelMultipleChoiceField(queryset=Banque.objects.all(), label="Banques actuelles", widget=forms.CheckboxSelectMultiple) + """Selection d'une ou plusieurs banques, pour suppression""" + banques = forms.ModelMultipleChoiceField( + queryset=Banque.objects.all(), + label="Banques actuelles", + widget=forms.CheckboxSelectMultiple + ) From 9ab06207bd8b0f10a95be01c3d0b75d28ccb6246 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 05:30:35 +0200 Subject: [PATCH 07/12] Pep8 --- cotisations/urls.py | 116 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/cotisations/urls.py b/cotisations/urls.py index 2cf86888..f59fd678 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -27,30 +27,96 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^new_facture/(?P[0-9]+)$', views.new_facture, name='new-facture'), - url(r'^edit_facture/(?P[0-9]+)$', views.edit_facture, name='edit-facture'), - url(r'^del_facture/(?P[0-9]+)$', views.del_facture, name='del-facture'), - url(r'^facture_pdf/(?P[0-9]+)$', views.facture_pdf, name='facture-pdf'), - url(r'^new_facture_pdf/$', views.new_facture_pdf, name='new-facture-pdf'), - url(r'^credit_solde/(?P[0-9]+)$', views.credit_solde, name='credit-solde'), - url(r'^add_article/$', views.add_article, name='add-article'), - url(r'^edit_article/(?P[0-9]+)$', views.edit_article, name='edit-article'), - url(r'^del_article/$', views.del_article, name='del-article'), - url(r'^add_paiement/$', views.add_paiement, name='add-paiement'), - url(r'^edit_paiement/(?P[0-9]+)$', views.edit_paiement, name='edit-paiement'), - url(r'^del_paiement/$', views.del_paiement, name='del-paiement'), - url(r'^add_banque/$', views.add_banque, name='add-banque'), - url(r'^edit_banque/(?P[0-9]+)$', views.edit_banque, name='edit-banque'), - url(r'^del_banque/$', views.del_banque, name='del-banque'), - url(r'^index_article/$', views.index_article, name='index-article'), - url(r'^index_banque/$', views.index_banque, name='index-banque'), - url(r'^index_paiement/$', views.index_paiement, name='index-paiement'), - url(r'^history/(?Pfacture)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Particle)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Ppaiement)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pbanque)/(?P[0-9]+)$', views.history, name='history'), - url(r'^control/$', views.control, name='control'), + url(r'^new_facture/(?P[0-9]+)$', + views.new_facture, + name='new-facture' + ), + url(r'^edit_facture/(?P[0-9]+)$', + views.edit_facture, + name='edit-facture' + ), + url(r'^del_facture/(?P[0-9]+)$', + views.del_facture, + name='del-facture' + ), + url(r'^facture_pdf/(?P[0-9]+)$', + views.facture_pdf, + name='facture-pdf' + ), + url(r'^new_facture_pdf/$', + views.new_facture_pdf, + name='new-facture-pdf' + ), + url(r'^credit_solde/(?P[0-9]+)$', + views.credit_solde, + name='credit-solde' + ), + url(r'^add_article/$', + views.add_article, + name='add-article' + ), + url(r'^edit_article/(?P[0-9]+)$', + views.edit_article, + name='edit-article' + ), + url(r'^del_article/$', + views.del_article, + name='del-article' + ), + url(r'^add_paiement/$', + views.add_paiement, + name='add-paiement' + ), + url(r'^edit_paiement/(?P[0-9]+)$', + views.edit_paiement, + name='edit-paiement' + ), + url(r'^del_paiement/$', + views.del_paiement, + name='del-paiement' + ), + url(r'^add_banque/$', + views.add_banque, + name='add-banque' + ), + url(r'^edit_banque/(?P[0-9]+)$', + views.edit_banque, + name='edit-banque' + ), + url(r'^del_banque/$', + views.del_banque, + name='del-banque' + ), + url(r'^index_article/$', + views.index_article, + name='index-article' + ), + url(r'^index_banque/$', + views.index_banque, + name='index-banque' + ), + url(r'^index_paiement/$', + views.index_paiement, + name='index-paiement' + ), + url(r'^history/(?Pfacture)/(?P[0-9]+)$', + views.history, + name='history' + ), + url(r'^history/(?Particle)/(?P[0-9]+)$', + views.history, + name='history' + ), + url(r'^history/(?Ppaiement)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pbanque)/(?P[0-9]+)$', + views.history, + name='history' + ), + url(r'^control/$', + views.control, + name='control' + ), url(r'^$', views.index, name='index'), ] - - From 0f47ab79ebd4cd9220f2ed5b05c1a18a128833f1 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 22:47:32 +0200 Subject: [PATCH 08/12] Passage d'un coup de pylint --- cotisations/admin.py | 19 ++++-- cotisations/views.py | 138 ++++++++++++++++++++----------------------- re2o/views.py | 6 +- 3 files changed, 79 insertions(+), 84 deletions(-) diff --git a/cotisations/admin.py b/cotisations/admin.py index 29e3285d..b3f91854 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -28,23 +28,30 @@ from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente + class FactureAdmin(VersionAdmin): - list_display = ('user','paiement','date','valid','control') + pass + class VenteAdmin(VersionAdmin): - list_display = ('facture','name','prix','number','iscotisation','duration') + pass + class ArticleAdmin(VersionAdmin): - list_display = ('name','prix','iscotisation','duration') + pass + class BanqueAdmin(VersionAdmin): - list_display = ('name',) + pass + class PaiementAdmin(VersionAdmin): - list_display = ('moyen','type_paiement') + pass + class CotisationAdmin(VersionAdmin): - list_display = ('vente','date_start','date_end') + pass + admin.site.register(Facture, FactureAdmin) admin.site.register(Article, ArticleAdmin) diff --git a/cotisations/views.py b/cotisations/views.py index 517eafd1..e44eee65 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -24,40 +24,29 @@ # Goulven Kermarec, Gabriel Détraz # Gplv2 from __future__ import unicode_literals - +import os 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.auth.decorators import login_required, permission_required from django.contrib import messages -from django.db.models import Max, ProtectedError +from django.db.models import ProtectedError from django.db import transaction from django.forms import modelformset_factory, formset_factory -import os +from django.utils import timezone from reversion import revisions as reversion from reversion.models import Version - -from .models import Facture, Article, Vente, Cotisation, Paiement, Banque +# Import des models, forms et fonctions re2o +from users.models import User +from re2o.settings import LOGO_PATH +from re2o import settings +from re2o.views import form +from preferences.models import OptionalUser, AssoOption, GeneralOption +from .models import Facture, Article, Vente, Paiement, Banque from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf from .forms import SelectArticleForm, CreditSoldeForm -from users.models import User from .tex import render_tex -from re2o.settings import LOGO_PATH -from re2o import settings -from preferences.models import OptionalUser, AssoOption, GeneralOption - -from dateutil.relativedelta import relativedelta -from django.utils import timezone - - -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) @login_required @@ -82,18 +71,18 @@ def new_facture(request, userid): facture_form = NewFactureForm(request.POST or None, instance=facture) article_formset = formset_factory(SelectArticleForm)(request.POST or None) if facture_form.is_valid() and article_formset.is_valid(): - new_facture = facture_form.save(commit=False) + new_facture_instance = facture_form.save(commit=False) articles = article_formset # Si au moins un article est rempli if any(art.cleaned_data for art in articles): - options, created = OptionalUser.objects.get_or_create() + options, _created = OptionalUser.objects.get_or_create() user_solde = options.user_solde solde_negatif = options.solde_negatif # Si on paye par solde, que l'option est activée, # on vérifie que le négatif n'est pas atteint if user_solde: - if new_facture.paiement == Paiement.objects.get_or_create( - moyen='solde' + if new_facture_instance.paiement == Paiement.objects.get_or_create( + moyen='solde' )[0]: prix_total = 0 for art_item in articles: @@ -105,7 +94,7 @@ def new_facture(request, userid): effectuer l'opération") return redirect("/users/profil/" + userid) with transaction.atomic(), reversion.create_revision(): - new_facture.save() + new_facture_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") for art_item in articles: @@ -113,19 +102,19 @@ def new_facture(request, userid): article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] new_vente = Vente.objects.create( - facture=new_facture, - name=article.name, - prix=article.prix, - iscotisation=article.iscotisation, - duration=article.duration, - number=quantity - ) + facture=new_facture_instance, + name=article.name, + prix=article.prix, + iscotisation=article.iscotisation, + duration=article.duration, + number=quantity + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) reversion.set_comment("Création") if any(art_item.cleaned_data['article'].iscotisation - for art_item in articles if art_item.cleaned_data): + for art_item in articles if art_item.cleaned_data): messages.success( request, "La cotisation a été prolongée\ @@ -137,8 +126,8 @@ def new_facture(request, userid): messages.success(request, "La facture a été crée") return redirect("/users/profil/" + userid) messages.error( - request, - u"Il faut au moins un article valide pour créer une facture" + request, + u"Il faut au moins un article valide pour créer une facture" ) return form({ 'factureform': facture_form, @@ -155,7 +144,7 @@ def new_facture_pdf(request): Vente ou Facture correspondant en bdd""" facture_form = NewFactureFormPdf(request.POST or None) if facture_form.is_valid(): - options, created = AssoOption.objects.get_or_create() + options, _created = AssoOption.objects.get_or_create() tbl = [] article = facture_form.cleaned_data['article'] quantite = facture_form.cleaned_data['number'] @@ -163,8 +152,8 @@ def new_facture_pdf(request): destinataire = facture_form.cleaned_data['dest'] chambre = facture_form.cleaned_data['chambre'] fid = facture_form.cleaned_data['fid'] - for a in article: - tbl.append([a, quantite, a.prix * quantite]) + for art in article: + tbl.append([art, quantite, art.prix * quantite]) prix_total = sum(a[2] for a in tbl) user = {'name': destinataire, 'room': chambre} return render_tex(request, 'cotisations/factures.tex', { @@ -207,11 +196,11 @@ def facture_pdf(request, factureid): messages.error(request, "Vous ne pouvez pas afficher\ une facture non valide") return redirect("/users/profil/" + str(request.user.id)) - vente = Vente.objects.all().filter(facture=facture) + ventes_objects = Vente.objects.all().filter(facture=facture) ventes = [] - options, created = AssoOption.objects.get_or_create() - for v in vente: - ventes.append([v, v.number, v.prix_total]) + options, _created = AssoOption.objects.get_or_create() + for vente in ventes_objects: + ventes.append([vente, vente.number, vente.prix_total]) return render_tex(request, 'cotisations/factures.tex', { 'paid': True, 'fid': facture.id, @@ -253,11 +242,11 @@ def edit_facture(request, factureid): facture_form = EditFactureForm(request.POST or None, instance=facture) ventes_objects = Vente.objects.filter(facture=facture) vente_form_set = modelformset_factory( - Vente, - fields=('name', 'number'), - extra=0, - max_num=len(ventes_objects) - ) + Vente, + fields=('name', 'number'), + extra=0, + max_num=len(ventes_objects) + ) 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(): @@ -266,8 +255,7 @@ def edit_facture(request, factureid): 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) - ) + in facture_form.changed_data + form.changed_data)) messages.success(request, "La facture a bien été modifiée") return redirect("/cotisations/") return form({ @@ -286,7 +274,7 @@ def del_facture(request, factureid): except Facture.DoesNotExist: messages.error(request, u"Facture inexistante") return redirect("/cotisations/") - if (facture.control or not facture.valid): + if facture.control or not facture.valid: messages.error(request, "Vous ne pouvez pas editer une facture\ controlée ou invalidée par le trésorier") return redirect("/cotisations/") @@ -320,13 +308,13 @@ def credit_solde(request, userid): reversion.set_user(request.user) reversion.set_comment("Création") new_vente = Vente.objects.create( - facture=facture_instance, - name="solde", - prix=facture.cleaned_data['montant'], - iscotisation=False, - duration=0, - number=1 - ) + facture=facture_instance, + name="solde", + prix=facture.cleaned_data['montant'], + iscotisation=False, + duration=0, + number=1 + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) @@ -429,7 +417,7 @@ def edit_paiement(request, paiementid): reversion.set_user(request.user) reversion.set_comment( "Champs modifié(s) : %s" % ', '.join( - field for field in paiement.changed_data + field for field in paiement.changed_data ) ) messages.success(request, "Type de paiement modifié") @@ -451,8 +439,8 @@ def del_paiement(request): reversion.set_user(request.user) reversion.set_comment("Destruction") messages.success( - request, - "Le moyen de paiement a été supprimé" + request, + "Le moyen de paiement a été supprimé" ) except ProtectedError: messages.error( @@ -529,14 +517,14 @@ def del_banque(request): def control(request): """Pour le trésorier, vue pour controler en masse les factures.Case à cocher, pratique""" - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number facture_list = Facture.objects.order_by('date').reverse() controlform_set = modelformset_factory( - Facture, - fields=('control', 'valid'), - extra=0 - ) + Facture, + fields=('control', 'valid'), + extra=0 + ) paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -546,8 +534,8 @@ def control(request): except EmptyPage: facture_list = paginator.page(paginator.num.pages) page_query = Facture.objects.order_by('date').reverse().filter( - id__in=[facture.id for facture in facture_list] - ) + id__in=[facture.id for facture in facture_list] + ) controlform = controlform_set(request.POST or None, queryset=page_query) if controlform.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -595,7 +583,7 @@ def index_banque(request): @permission_required('cableur') def index(request): """Affiche l'ensemble des factures, pour les cableurs et +""" - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number facture_list = Facture.objects.order_by('date').select_related('user')\ .select_related('paiement').prefetch_related('vente_set').reverse() @@ -615,11 +603,11 @@ def index(request): @login_required -def history(request, object, id): +def history(request, object, object_id): """Affiche l'historique de chaque objet""" if object == 'facture': try: - object_instance = Facture.objects.get(pk=id) + object_instance = Facture.objects.get(pk=object_id) except Facture.DoesNotExist: messages.error(request, "Facture inexistante") return redirect("/cotisations/") @@ -630,26 +618,26 @@ def history(request, object, id): return redirect("/users/profil/" + str(request.user.id)) elif object == 'paiement' and request.user.has_perms(('cableur',)): try: - object_instance = Paiement.objects.get(pk=id) + object_instance = Paiement.objects.get(pk=object_id) except Paiement.DoesNotExist: messages.error(request, "Paiement inexistant") return redirect("/cotisations/") elif object == 'article' and request.user.has_perms(('cableur',)): try: - object_instance = Article.objects.get(pk=id) + object_instance = Article.objects.get(pk=object_id) except Article.DoesNotExist: messages.error(request, "Article inexistante") return redirect("/cotisations/") elif object == 'banque' and request.user.has_perms(('cableur',)): try: - object_instance = Banque.objects.get(pk=id) + object_instance = Banque.objects.get(pk=object_id) except Banque.DoesNotExist: messages.error(request, "Banque inexistante") return redirect("/cotisations/") else: messages.error(request, "Objet inconnu") return redirect("/cotisations/") - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) diff --git a/re2o/views.py b/re2o/views.py index bd8077e1..77a36418 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -29,9 +29,9 @@ from django.template import Context, RequestContext, loader from preferences.models import Service def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) + context = ctx + context.update(csrf(request)) + return render(request, template, context) def index(request): From e211957e9ca94e80b97bfcd7b17175040c1e4dc0 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 23:15:07 +0200 Subject: [PATCH 09/12] Pylint et docstring des fichiers --- cotisations/admin.py | 7 ++++ cotisations/forms.py | 18 ++++++++-- cotisations/models.py | 81 +++++++++++++++++++++++++++++-------------- 3 files changed, 78 insertions(+), 28 deletions(-) diff --git a/cotisations/admin.py b/cotisations/admin.py index b3f91854..8186e4e3 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,26 +30,33 @@ from .models import Facture, Article, Banque, Paiement, Cotisation, Vente class FactureAdmin(VersionAdmin): + """Class admin d'une facture, tous les champs""" pass class VenteAdmin(VersionAdmin): + """Class admin d'une vente, tous les champs (facture related)""" pass class ArticleAdmin(VersionAdmin): + """Class admin d'un article en vente""" pass class BanqueAdmin(VersionAdmin): + """Class admin de la liste des banques (facture related)""" pass class PaiementAdmin(VersionAdmin): + """Class admin d'un moyen de paiement (facture related""" pass class CotisationAdmin(VersionAdmin): + """Class admin d'une cotisation (date de debut et de fin), + Vente related""" pass diff --git a/cotisations/forms.py b/cotisations/forms.py index b3742088..76a67975 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -19,14 +19,28 @@ # 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. +""" +Forms de l'application cotisation de re2o. Dépendance avec les models, +importé par les views. +Permet de créer une nouvelle facture pour un user (NewFactureForm), +et de l'editer (soit l'user avec EditFactureForm, +soit le trésorier avec TrezEdit qui a plus de possibilités que self +notamment sur le controle trésorier) + +SelectArticleForm est utilisée lors de la creation d'une facture en +parrallèle de NewFacture pour le choix des articles désirés. +(la vue correspondante est unique) + +ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter, +éditer ou supprimer une banque/moyen de paiement ou un article +""" from __future__ import unicode_literals from django import forms from django.forms import ModelForm, Form -from django import forms from django.core.validators import MinValueValidator -from .models import Article, Paiement, Facture, Banque, Vente +from .models import Article, Paiement, Facture, Banque class NewFactureForm(ModelForm): diff --git a/cotisations/models.py b/cotisations/models.py index 6388b38c..54843076 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -20,20 +20,39 @@ # 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. +""" +Definition des models bdd pour les factures et cotisation. +Pièce maitresse : l'ensemble du code intelligent se trouve ici, +dans les clean et save des models ainsi que de leur methodes supplémentaires. + +Facture : reliée à un user, elle a un moyen de paiement, une banque (option), +une ou plusieurs ventes + +Article : liste des articles en vente, leur prix, etc + +Vente : ensemble des ventes effectuées, reliées à une facture (foreignkey) + +Banque : liste des banques existantes + +Cotisation : objets de cotisation, contenant un début et une fin. Reliées +aux ventes, en onetoone entre une vente et une cotisation. +Crées automatiquement au save des ventes. + +Post_save et Post_delete : sychronisation des services et régénération +des services d'accès réseau (ex dhcp) lors de la vente d'une cotisation +par exemple +""" from __future__ import unicode_literals +from dateutil.relativedelta import relativedelta from django.db import models - from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from dateutil.relativedelta import relativedelta from django.forms import ValidationError from django.core.validators import MinValueValidator - from django.db.models import Max from django.utils import timezone - from machines.models import regen @@ -47,32 +66,34 @@ class Facture(models.Model): user = models.ForeignKey('users.User', on_delete=models.PROTECT) paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) banque = models.ForeignKey( - 'Banque', - on_delete=models.PROTECT, - blank=True, - null=True) + 'Banque', + on_delete=models.PROTECT, + blank=True, + null=True) cheque = models.CharField(max_length=255, blank=True) date = models.DateTimeField(auto_now_add=True) valid = models.BooleanField(default=True) control = models.BooleanField(default=False) def prix(self): + """Renvoie le prix brut sans les quantités. Méthode + dépréciée""" prix = Vente.objects.filter( - facture=self - ).aggregate(models.Sum('prix'))['prix__sum'] + facture=self + ).aggregate(models.Sum('prix'))['prix__sum'] return prix def prix_total(self): """Prix total : somme des produits prix_unitaire et quantité des ventes de l'objet""" return Vente.objects.filter( - facture=self - ).aggregate( + facture=self + ).aggregate( total=models.Sum( models.F('prix')*models.F('number'), output_field=models.FloatField() - ) - )['total'] + ) + )['total'] def name(self): """String, somme des name des ventes de self""" @@ -95,6 +116,7 @@ def facture_post_save(sender, **kwargs): @receiver(post_delete, sender=Facture) def facture_post_delete(sender, **kwargs): + """Après la suppression d'une facture, on synchronise l'user ldap""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -111,19 +133,22 @@ class Vente(models.Model): prix = models.DecimalField(max_digits=5, decimal_places=2) iscotisation = models.BooleanField() duration = models.IntegerField( - help_text="Durée exprimée en mois entiers", - blank=True, - null=True) + help_text="Durée exprimée en mois entiers", + blank=True, + null=True) def prix_total(self): """Renvoie le prix_total de self (nombre*prix)""" return self.prix*self.number def update_cotisation(self): + """Mets à jour l'objet related cotisation de la vente, si + il existe : update la date de fin à partir de la durée de + la vente""" if hasattr(self, 'cotisation'): cotisation = self.cotisation cotisation.date_end = cotisation.date_start + relativedelta( - months=self.duration*self.number) + months=self.duration*self.number) return def create_cotis(self, date_start=False): @@ -134,13 +159,13 @@ class Vente(models.Model): cotisation = Cotisation(vente=self) if date_start: end_adhesion = Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.filter( - user=self.facture.user - ).exclude(valid=False)) - ).filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self.facture.user + ).exclude(valid=False)) + ).filter( date_start__lt=date_start - ).aggregate(Max('date_end'))['date_end__max'] + ).aggregate(Max('date_end'))['date_end__max'] else: end_adhesion = self.facture.user.end_adhesion() date_start = date_start or timezone.now() @@ -148,8 +173,8 @@ class Vente(models.Model): date_max = max(end_adhesion, date_start) cotisation.date_start = date_max cotisation.date_end = cotisation.date_start + relativedelta( - months=self.duration*self.number - ) + months=self.duration*self.number + ) return def save(self, *args, **kwargs): @@ -181,6 +206,8 @@ def vente_post_save(sender, **kwargs): @receiver(post_delete, sender=Vente) def vente_post_delete(sender, **kwargs): + """Après suppression d'une vente, on synchronise l'user ldap (ex + suppression d'une cotisation""" vente = kwargs['instance'] if vente.iscotisation: user = vente.facture.user @@ -258,6 +285,7 @@ class Cotisation(models.Model): @receiver(post_save, sender=Cotisation) def cotisation_post_save(sender, **kwargs): + """Après modification d'une cotisation, regeneration des services""" regen('dns') regen('dhcp') regen('mac_ip_list') @@ -266,6 +294,7 @@ def cotisation_post_save(sender, **kwargs): @receiver(post_delete, sender=Cotisation) def vente_post_delete(sender, **kwargs): + """Après suppression d'une vente, régénération des services""" cotisation = kwargs['instance'] regen('mac_ip_list') regen('mailing') From d0620328e5a2c579104c8a95eacad5def281a8fa Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 23:42:37 +0200 Subject: [PATCH 10/12] Pylint, pep8 et doc sur forms et admin de topologie --- topologie/admin.py | 12 +++++++++++ topologie/forms.py | 53 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/topologie/admin.py b/topologie/admin.py index 8dcce849..bfc2a393 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -20,6 +20,9 @@ # 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. +""" +Fichier définissant les administration des models dans l'interface admin +""" from __future__ import unicode_literals @@ -28,18 +31,27 @@ from reversion.admin import VersionAdmin from .models import Port, Room, Switch, Stack + class StackAdmin(VersionAdmin): + """Administration d'une stack de switches (inclus des switches)""" pass + class SwitchAdmin(VersionAdmin): + """Administration d'un switch""" pass + class PortAdmin(VersionAdmin): + """Administration d'un port de switches""" pass + class RoomAdmin(VersionAdmin): + """Administration d'un chambre""" pass + admin.site.register(Port, PortAdmin) admin.site.register(Room, RoomAdmin) admin.site.register(Switch, SwitchAdmin) diff --git a/topologie/forms.py b/topologie/forms.py index 8c82afba..267d64b0 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -19,14 +19,27 @@ # 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. +""" +Un forms le plus simple possible pour les objets topologie de re2o. + +Permet de créer et supprimer : un Port de switch, relié à un switch. + +Permet de créer des stacks et d'y ajouter des switchs (StackForm) + +Permet de créer, supprimer et editer un switch (EditSwitchForm, +NewSwitchForm) +""" from __future__ import unicode_literals -from .models import Port, Switch, Room, Stack -from django.forms import ModelForm, Form from machines.models import Interface +from django.forms import ModelForm +from .models import Port, Switch, Room, Stack + class PortForm(ModelForm): + """Formulaire pour la création d'un port d'un switch + Relié directement au modèle port""" class Meta: model = Port fields = '__all__' @@ -35,25 +48,45 @@ class PortForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PortForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditPortForm(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 + + Un port est relié à une chambre, un autre port (uplink) ou une machine + (serveur ou borne), mutuellement exclusif + Optimisation sur les queryset pour machines et port_related pour + optimiser le temps de chargement avec select_related (vraiment + lent sans)""" class Meta(PortForm.Meta): - fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details'] + fields = ['room', 'related', 'machine_interface', 'radius', + 'vlan_force', 'details'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['machine_interface'].queryset = Interface.objects.all().select_related('domain__extension') - self.fields['related'].queryset = Port.objects.all().select_related('switch__switch_interface__domain__extension').order_by('switch', 'port') + self.fields['machine_interface'].queryset = Interface.objects.all()\ + .select_related('domain__extension') + self.fields['related'].queryset = Port.objects.all()\ + .select_related('switch__switch_interface__domain__extension')\ + .order_by('switch', 'port') + class AddPortForm(ModelForm): + """Permet d'ajouter un port de switch. Voir EditPortForm pour plus + d'informations""" class Meta(PortForm.Meta): - fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details'] + fields = ['port', 'room', 'machine_interface', 'related', + 'radius', 'vlan_force', 'details'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs) + class StackForm(ModelForm): + """Permet d'edition d'une stack : stack_id, et switches membres + de la stack""" class Meta: model = Stack fields = '__all__' @@ -62,7 +95,9 @@ class StackForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(StackForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditSwitchForm(ModelForm): + """Permet d'éditer un switch : nom et nombre de ports""" class Meta: model = Switch fields = '__all__' @@ -73,7 +108,10 @@ class EditSwitchForm(ModelForm): self.fields['location'].label = 'Localisation' self.fields['number'].label = 'Nombre de ports' + class NewSwitchForm(ModelForm): + """Permet de créer un switch : emplacement, paramètres machine, + membre d'un stack (option), nombre de ports (number)""" class Meta(EditSwitchForm.Meta): fields = ['location', 'number', 'details', 'stack', 'stack_member_id'] @@ -81,7 +119,9 @@ class NewSwitchForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditRoomForm(ModelForm): + """Permet d'éediter le nom et commentaire d'une prise murale""" class Meta: model = Room fields = '__all__' @@ -89,4 +129,3 @@ class EditRoomForm(ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs) - From 9660a3d0177f4dc613f34df428786dda654119ec Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 00:00:16 +0200 Subject: [PATCH 11/12] Documentation et pep8 sur models de topologie --- topologie/models.py | 142 +++++++++++++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 47 deletions(-) diff --git a/topologie/models.py b/topologie/models.py index c02c0c51..4924e31e 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -20,24 +20,31 @@ # 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. +""" +Definition des modèles de l'application topologie. + +On défini les models suivants : + +- stack (id, id_min, id_max et nom) regrouppant les switches +- switch : nom, nombre de port, et interface +machine correspondante (mac, ip, etc) (voir machines.models.interface) +- Port: relié à un switch parent par foreign_key, numero du port, +relié de façon exclusive à un autre port, une machine +(serveur ou borne) ou une prise murale +- room : liste des prises murales, nom et commentaire de l'état de +la prise +""" from __future__ import unicode_literals from django.db import models from django.db.models.signals import post_delete from django.dispatch import receiver -from django.forms import ModelForm, Form -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import ValidationError -import reversion - -from machines.models import Vlan - class Stack(models.Model): - """ Un objet stack. Regrouppe des switchs en foreign key - , contient une id de stack, un switch id min et max dans + """Un objet stack. Regrouppe des switchs en foreign key + ,contient une id de stack, un switch id min et max dans le stack""" PRETTY_NAME = "Stack de switchs" @@ -59,28 +66,40 @@ class Stack(models.Model): def clean(self): """ Verification que l'id_max < id_min""" if self.member_id_max < self.member_id_min: - raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"}) + raise ValidationError({'member_id_max':"L'id maximale est\ + inférieure à l'id minimale"}) class Switch(models.Model): - """ Definition d'un switch. Contient un nombre de ports (number), + """ Definition d'un switch. Contient un nombre de ports (number), un emplacement (location), un stack parent (optionnel, stack) et un id de membre dans le stack (stack_member_id) relié en onetoone à une interface - Pourquoi ne pas avoir fait hériter switch de interface ? + Pourquoi ne pas avoir fait hériter switch de interface ? Principalement par méconnaissance de la puissance de cette façon de faire. Ceci étant entendu, django crée en interne un onetoone, ce qui a un - effet identique avec ce que l'on fait ici""" + effet identique avec ce que l'on fait ici + + Validation au save que l'id du stack est bien dans le range id_min + id_max de la stack parente""" PRETTY_NAME = "Switch / Commutateur" - switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE) + switch_interface = models.OneToOneField( + 'machines.Interface', + on_delete=models.CASCADE + ) location = models.CharField(max_length=255) number = models.IntegerField() details = models.CharField(max_length=255, blank=True) - stack = models.ForeignKey(Stack, blank=True, null=True, on_delete=models.SET_NULL) + stack = models.ForeignKey( + Stack, + blank=True, + null=True, + on_delete=models.SET_NULL + ) stack_member_id = models.IntegerField(blank=True, null=True) class Meta: - unique_together = ('stack','stack_member_id') + unique_together = ('stack', 'stack_member_id') def __str__(self): return str(self.location) + ' ' + str(self.switch_interface) @@ -89,41 +108,65 @@ class Switch(models.Model): """ Verifie que l'id stack est dans le bon range""" if self.stack is not None: if self.stack_member_id is not None: - if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min): - raise ValidationError({'stack_member_id': "L'id de ce switch est en dehors des bornes permises pas la stack"}) + if (self.stack_member_id > self.stack.member_id_max) or\ + (self.stack_member_id < self.stack.member_id_min): + raise ValidationError({'stack_member_id': "L'id de ce\ + switch est en dehors des bornes permises pas la stack"}) else: - raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"}) + raise ValidationError({'stack_member_id': "L'id dans la stack\ + ne peut être nul"}) class Port(models.Model): - """ Definition d'un port. Relié à un switch(foreign_key), + """ Definition d'un port. Relié à un switch(foreign_key), un port peut etre relié de manière exclusive à : - une chambre (room) - une machine (serveur etc) (machine_interface) - un autre port (uplink) (related) - Champs supplémentaires : + Champs supplémentaires : - RADIUS (mode STRICT : connexion sur port uniquement si machine - d'un adhérent à jour de cotisation et que la chambre est également à jour de cotisation + d'un adhérent à jour de cotisation et que la chambre est également à + jour de cotisation mode COMMON : vérification uniquement du statut de la machine mode NO : accepte toute demande venant du port et place sur le vlan normal mode BLOQ : rejet de toute authentification - vlan_force : override la politique générale de placement vlan, permet - de forcer un port sur un vlan particulier. S'additionne à la politique + de forcer un port sur un vlan particulier. S'additionne à la politique RADIUS""" PRETTY_NAME = "Port de switch" STATES = ( - ('NO', 'NO'), - ('STRICT', 'STRICT'), - ('BLOQ', 'BLOQ'), - ('COMMON', 'COMMON'), - ) - + ('NO', 'NO'), + ('STRICT', 'STRICT'), + ('BLOQ', 'BLOQ'), + ('COMMON', 'COMMON'), + ) + switch = models.ForeignKey('Switch', related_name="ports") port = models.IntegerField() - room = models.ForeignKey('Room', on_delete=models.PROTECT, blank=True, null=True) - machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True) - related = models.OneToOneField('self', null=True, blank=True, related_name='related_port') + room = models.ForeignKey( + 'Room', + on_delete=models.PROTECT, + blank=True, + null=True + ) + machine_interface = models.ForeignKey( + 'machines.Interface', + on_delete=models.SET_NULL, + blank=True, + null=True + ) + related = models.OneToOneField( + 'self', + null=True, + blank=True, + related_name='related_port' + ) radius = models.CharField(max_length=32, choices=STATES, default='NO') - vlan_force = models.ForeignKey('machines.Vlan', on_delete=models.SET_NULL, blank=True, null=True) + vlan_force = models.ForeignKey( + 'machines.Vlan', + on_delete=models.SET_NULL, + blank=True, + null=True + ) details = models.CharField(max_length=255, blank=True) class Meta: @@ -134,7 +177,7 @@ class Port(models.Model): related_port = self.related related_port.related = self related_port.save() - + def clean_port_related(self): """ Supprime la relation related sur self""" related_port = self.related_port @@ -142,23 +185,27 @@ class Port(models.Model): related_port.save() def clean(self): - """ Verifie que un seul de chambre, interface_parent et related_port est rempli. - Verifie que le related n'est pas le port lui-même.... - Verifie que le related n'est pas déjà occupé par une machine ou une chambre. Si - ce n'est pas le cas, applique la relation related + """ Verifie que un seul de chambre, interface_parent et related_port + est rempli. Verifie que le related n'est pas le port lui-même.... + Verifie que le related n'est pas déjà occupé par une machine ou une + chambre. Si ce n'est pas le cas, applique la relation related Si un port related point vers self, on nettoie la relation - A priori pas d'autre solution que de faire ça à la main. A priori tout cela est dans - un bloc transaction, donc pas de problème de cohérence""" + A priori pas d'autre solution que de faire ça à la main. A priori + tout cela est dans un bloc transaction, donc pas de problème de + cohérence""" if hasattr(self, 'switch'): if self.port > self.switch.number: raise ValidationError("Ce port ne peut exister, numero trop élevé") - if self.room and self.machine_interface or self.room and self.related or self.machine_interface and self.related: - raise ValidationError("Chambre, interface et related_port sont mutuellement exclusifs") - if self.related==self: + if self.room and self.machine_interface or self.room and\ + self.related or self.machine_interface and self.related: + raise ValidationError("Chambre, interface et related_port sont\ + mutuellement exclusifs") + if self.related == self: raise ValidationError("On ne peut relier un port à lui même") if self.related and not self.related.related: if self.related.machine_interface or self.related.room: - raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation") + raise ValidationError("Le port relié est déjà occupé, veuillez\ + le libérer avant de créer une relation") else: self.make_port_related() elif hasattr(self, 'related_port'): @@ -168,7 +215,7 @@ class Port(models.Model): return str(self.switch) + " - " + str(self.port) class Room(models.Model): - """ Une chambre/local contenant une prise murale""" + """Une chambre/local contenant une prise murale""" PRETTY_NAME = "Chambre/ Prise murale" name = models.CharField(max_length=255, unique=True) @@ -176,10 +223,11 @@ class Room(models.Model): class Meta: ordering = ['name'] - + def __str__(self): return str(self.name) @receiver(post_delete, sender=Stack) def stack_post_delete(sender, **kwargs): - Switch.objects.filter(stack=None).update(stack_member_id = None) + """Vide les id des switches membres d'une stack supprimée""" + Switch.objects.filter(stack=None).update(stack_member_id=None) From dc3096a3bfc50bfd82e8b995a9f13985d4dda086 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 01:43:01 +0200 Subject: [PATCH 12/12] Doc et respect de la pep8 --- topologie/models.py | 20 ++- topologie/urls.py | 39 ++++-- topologie/views.py | 295 ++++++++++++++++++++++++++++++-------------- 3 files changed, 248 insertions(+), 106 deletions(-) diff --git a/topologie/models.py b/topologie/models.py index 4924e31e..086e0aff 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -42,6 +42,7 @@ from django.db.models.signals import post_delete from django.dispatch import receiver from django.core.exceptions import ValidationError + class Stack(models.Model): """Un objet stack. Regrouppe des switchs en foreign key ,contient une id de stack, un switch id min et max dans @@ -66,9 +67,10 @@ class Stack(models.Model): def clean(self): """ Verification que l'id_max < id_min""" if self.member_id_max < self.member_id_min: - raise ValidationError({'member_id_max':"L'id maximale est\ + raise ValidationError({'member_id_max': "L'id maximale est\ inférieure à l'id minimale"}) + class Switch(models.Model): """ Definition d'un switch. Contient un nombre de ports (number), un emplacement (location), un stack parent (optionnel, stack) @@ -109,13 +111,16 @@ class Switch(models.Model): if self.stack is not None: if self.stack_member_id is not None: if (self.stack_member_id > self.stack.member_id_max) or\ - (self.stack_member_id < self.stack.member_id_min): - raise ValidationError({'stack_member_id': "L'id de ce\ - switch est en dehors des bornes permises pas la stack"}) + (self.stack_member_id < self.stack.member_id_min): + raise ValidationError( + {'stack_member_id': "L'id de ce switch est en\ + dehors des bornes permises pas la stack"} + ) else: raise ValidationError({'stack_member_id': "L'id dans la stack\ ne peut être nul"}) + class Port(models.Model): """ Definition d'un port. Relié à un switch(foreign_key), un port peut etre relié de manière exclusive à : @@ -195,9 +200,10 @@ class Port(models.Model): cohérence""" if hasattr(self, 'switch'): if self.port > self.switch.number: - raise ValidationError("Ce port ne peut exister, numero trop élevé") + raise ValidationError("Ce port ne peut exister,\ + numero trop élevé") if self.room and self.machine_interface or self.room and\ - self.related or self.machine_interface and self.related: + self.related or self.machine_interface and self.related: raise ValidationError("Chambre, interface et related_port sont\ mutuellement exclusifs") if self.related == self: @@ -214,6 +220,7 @@ class Port(models.Model): def __str__(self): return str(self.switch) + " - " + str(self.port) + class Room(models.Model): """Une chambre/local contenant une prise murale""" PRETTY_NAME = "Chambre/ Prise murale" @@ -227,6 +234,7 @@ class Room(models.Model): def __str__(self): return str(self.name) + @receiver(post_delete, sender=Stack) def stack_post_delete(sender, **kwargs): """Vide les id des switches membres d'une stack supprimée""" diff --git a/topologie/urls.py b/topologie/urls.py index f4537ac5..4d0a6779 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -19,6 +19,12 @@ # 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. +""" +Definition des urls de l'application topologie. +Inclu dans urls de re2o. + +Fait référence aux fonctions du views +""" from __future__ import unicode_literals @@ -33,18 +39,33 @@ urlpatterns = [ url(r'^new_room/$', views.new_room, name='new-room'), url(r'^edit_room/(?P[0-9]+)$', views.edit_room, name='edit-room'), url(r'^del_room/(?P[0-9]+)$', views.del_room, name='del-room'), - url(r'^switch/(?P[0-9]+)$', views.index_port, name='index-port'), - url(r'^history/(?Pswitch)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pport)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Proom)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pstack)/(?P[0-9]+)$', views.history, name='history'), + url(r'^switch/(?P[0-9]+)$', + views.index_port, + name='index-port'), + url(r'^history/(?Pswitch)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pport)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Proom)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pstack)/(?P[0-9]+)$', + views.history, + name='history'), url(r'^edit_port/(?P[0-9]+)$', views.edit_port, name='edit-port'), url(r'^new_port/(?P[0-9]+)$', views.new_port, name='new-port'), url(r'^del_port/(?P[0-9]+)$', views.del_port, name='del-port'), - url(r'^edit_switch/(?P[0-9]+)$', views.edit_switch, name='edit-switch'), + url(r'^edit_switch/(?P[0-9]+)$', + views.edit_switch, + name='edit-switch'), url(r'^new_stack/$', views.new_stack, name='new-stack'), url(r'^index_stack/$', views.index_stack, name='index-stack'), - url(r'^edit_stack/(?P[0-9]+)$', views.edit_stack, name='edit-stack'), - url(r'^del_stack/(?P[0-9]+)$', views.del_stack, name='del-stack'), + url(r'^edit_stack/(?P[0-9]+)$', + views.edit_stack, + name='edit-stack'), + url(r'^del_stack/(?P[0-9]+)$', + views.del_stack, + name='del-stack'), ] - diff --git a/topologie/views.py b/topologie/views.py index 42cd09e7..08ada8d0 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -19,7 +19,20 @@ # 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. +""" +Page des vues de l'application topologie +Permet de créer, modifier et supprimer : +- un port (add_port, edit_port, del_port) +- un switch : les vues d'ajout et d'édition font appel aux forms de creation +de switch, mais aussi aux forms de machines.forms (domain, interface et +machine). Le views les envoie et les save en même temps. TODO : rationaliser +et faire que la creation de machines (interfaces, domain etc) soit gérée +coté models et forms de topologie +- une chambre (new_room, edit_room, del_room) +- une stack +- l'historique de tous les objets cités +""" from __future__ import unicode_literals from django.shortcuts import render, redirect @@ -33,11 +46,12 @@ from reversion import revisions as reversion from reversion.models import Version from topologie.models import Switch, Port, Room, Stack -from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm, AddPortForm, EditRoomForm, StackForm +from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm +from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form -from users.models import User -from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from machines.forms import AliasForm, NewMachineForm, EditMachineForm +from machines.forms import EditInterfaceForm, AddInterfaceForm from preferences.models import AssoOption, GeneralOption @@ -45,41 +59,52 @@ from preferences.models import AssoOption, GeneralOption @permission_required('cableur') def index(request): """ Vue d'affichage de tous les swicthes""" - switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack') - return render(request, 'topologie/index.html', {'switch_list': switch_list}) + switch_list = Switch.objects.order_by( + 'stack', + 'stack_member_id', + 'location' + )\ + .select_related('switch_interface__domain__extension')\ + .select_related('switch_interface__ipv4')\ + .select_related('switch_interface__domain')\ + .select_related('stack') + return render(request, 'topologie/index.html', { + 'switch_list': switch_list + }) + @login_required @permission_required('cableur') -def history(request, object, id): +def history(request, object_name, object_id): """ Vue générique pour afficher l'historique complet d'un objet""" - if object == 'switch': + if object_name == 'switch': try: - object_instance = Switch.objects.get(pk=id) + object_instance = Switch.objects.get(pk=object_id) except Switch.DoesNotExist: - messages.error(request, "Switch inexistant") - return redirect("/topologie/") - elif object == 'port': + messages.error(request, "Switch inexistant") + return redirect("/topologie/") + elif object_name == 'port': try: - object_instance = Port.objects.get(pk=id) + object_instance = Port.objects.get(pk=object_id) except Port.DoesNotExist: - messages.error(request, "Port inexistant") - return redirect("/topologie/") - elif object == 'room': + messages.error(request, "Port inexistant") + return redirect("/topologie/") + elif object_name == 'room': try: - object_instance = Room.objects.get(pk=id) + object_instance = Room.objects.get(pk=object_id) except Room.DoesNotExist: - messages.error(request, "Chambre inexistante") - return redirect("/topologie/") - elif object == 'stack': + messages.error(request, "Chambre inexistante") + return redirect("/topologie/") + elif object_name == 'stack': try: - object_instance = Stack.objects.get(pk=id) + object_instance = Stack.objects.get(pk=object_id) except Room.DoesNotExist: - messages.error(request, "Stack inexistante") - return redirect("/topologie/") + messages.error(request, "Stack inexistante") + return redirect("/topologie/") else: messages.error(request, "Objet inconnu") return redirect("/topologie/") - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) @@ -92,7 +117,11 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render(request, 're2o/history.html', { + 'reversions': reversions, + 'object': object_instance + }) + @login_required @permission_required('cableur') @@ -103,15 +132,25 @@ def index_port(request, switch_id): except Switch.DoesNotExist: messages.error(request, u"Switch inexistant") return redirect("/topologie/") - port_list = Port.objects.filter(switch = switch).select_related('room').select_related('machine_interface__domain__extension').select_related('related').select_related('switch').order_by('port') - return render(request, 'topologie/index_p.html', {'port_list':port_list, 'id_switch':switch_id, 'nom_switch':switch}) + port_list = Port.objects.filter(switch=switch)\ + .select_related('room')\ + .select_related('machine_interface__domain__extension')\ + .select_related('related')\ + .select_related('switch')\ + .order_by('port') + return render(request, 'topologie/index_p.html', { + 'port_list': port_list, + 'id_switch': switch_id, + 'nom_switch': switch + }) + @login_required @permission_required('cableur') def index_room(request): """ Affichage de l'ensemble des chambres""" room_list = Room.objects.order_by('name') - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number paginator = Paginator(room_list, pagination_number) page = request.GET.get('page') @@ -123,13 +162,20 @@ def index_room(request): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. room_list = paginator.page(paginator.num_pages) - return render(request, 'topologie/index_room.html', {'room_list': room_list}) + return render(request, 'topologie/index_room.html', { + 'room_list': room_list + }) + @login_required @permission_required('infra') def index_stack(request): - stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension') - return render(request, 'topologie/index_stack.html', {'stack_list': stack_list}) + """Affichage de la liste des stacks (affiche l'ensemble des switches)""" + stack_list = Stack.objects.order_by('name')\ + .prefetch_related('switch_set__switch_interface__domain__extension') + return render(request, 'topologie/index_stack.html', { + 'stack_list': stack_list + }) @login_required @@ -152,16 +198,24 @@ def new_port(request, switch_id): reversion.set_comment("Création") messages.success(request, "Port ajouté") except IntegrityError: - messages.error(request,"Ce port existe déjà" ) + messages.error(request, "Ce port existe déjà") return redirect("/topologie/switch/" + switch_id) - return form({'topoform':port}, 'topologie/topo.html', request) + return form({'topoform': port}, 'topologie/topo.html', request) + @login_required @permission_required('infra') def edit_port(request, port_id): - """ Edition d'un port. Permet de changer le switch parent et l'affectation du port""" + """ Edition d'un port. Permet de changer le switch parent et + l'affectation du port""" try: - port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id) + port_object = Port.objects\ + .select_related('switch__switch_interface__domain__extension')\ + .select_related('machine_interface__domain__extension')\ + .select_related('machine_interface__switch')\ + .select_related('room')\ + .select_related('related')\ + .get(pk=port_id) except Port.DoesNotExist: messages.error(request, u"Port inexistant") return redirect("/topologie/") @@ -170,14 +224,17 @@ def edit_port(request, port_id): with transaction.atomic(), reversion.create_revision(): port.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in port.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in port.changed_data + )) messages.success(request, "Le port a bien été modifié") return redirect("/topologie/switch/" + str(port_object.switch.id)) - return form({'topoform':port}, 'topologie/topo.html', request) + return form({'topoform': port}, 'topologie/topo.html', request) + @login_required @permission_required('infra') -def del_port(request,port_id): +def del_port(request, port_id): """ Supprime le port""" try: port = Port.objects.get(pk=port_id) @@ -192,30 +249,30 @@ def del_port(request,port_id): reversion.set_comment("Destruction") messages.success(request, "Le port a eté détruit") except ProtectedError: - messages.error(request, "Le port %s est affecté à un autre objet, impossible de le supprimer" % port) + messages.error(request, "Le port %s est affecté à un autre objet,\ + impossible de le supprimer" % port) return redirect('/topologie/switch/' + str(port.switch.id)) - return form({'objet':port}, 'topologie/delete.html', request) + return form({'objet': port}, 'topologie/delete.html', request) + @login_required @permission_required('infra') def new_stack(request): + """Ajoute un nouveau stack : stack_id_min, max, et nombre de switches""" stack = StackForm(request.POST or None) - #if stack.is_valid(): - if request.POST: - try: - with transaction.atomic(), reversion.create_revision(): - stack.save() - reversion.set_user(request.user) - reversion.set_comment("Création") - messages.success(request, "Stack crée") - except: - messages.error(request, "Cette stack existe déjà") - return form({'topoform':stack}, 'topologie/topo.html', request) + if stack.is_valid(): + with transaction.atomic(), reversion.create_revision(): + stack.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Stack crée") + return form({'topoform': stack}, 'topologie/topo.html', request) @login_required @permission_required('infra') -def edit_stack(request,stack_id): +def edit_stack(request, stack_id): + """Edition d'un stack (nombre de switches, nom...)""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -226,13 +283,19 @@ def edit_stack(request,stack_id): with transaction.atomic(), reversion.create_revision(): stack.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in stack.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in stack.changed_data + ) + ) return redirect('/topologie/index_stack') - return form({'topoform':stack}, 'topologie/topo.html', request) + return form({'topoform': stack}, 'topologie/topo.html', request) + @login_required @permission_required('infra') -def del_stack(request,stack_id): +def del_stack(request, stack_id): + """Supprime un stack""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -246,13 +309,16 @@ def del_stack(request,stack_id): reversion.set_comment("Destruction") 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) + messages.error(request, "La stack %s est affectée à un autre\ + objet, impossible de la supprimer" % stack) return redirect('/topologie/index_stack') - return form({'objet':stack}, 'topologie/delete.html', request) + return form({'objet': stack}, 'topologie/delete.html', request) + @login_required @permission_required('infra') -def edit_switchs_stack(request,stack_id): +def edit_switchs_stack(request, stack_id): + """Permet d'éditer la liste des switches dans une stack et l'ajouter""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -264,30 +330,36 @@ def edit_switchs_stack(request,stack_id): context = {'stack': stack} context['switchs_stack'] = stack.switchs_set.all() context['switchs_autres'] = Switch.object.filter(stack=None) - pass @login_required @permission_required('infra') def new_switch(request): - """ Creation d'un switch. Cree en meme temps l'interface et la machine associée. - Vue complexe. Appelle successivement les 4 models forms adaptés : machine, - interface, domain et switch""" + """ Creation d'un switch. Cree en meme temps l'interface et la machine + associée. Vue complexe. Appelle successivement les 4 models forms + adaptés : machine, interface, domain et switch""" switch = NewSwitchForm(request.POST or None) machine = NewMachineForm(request.POST or None) - interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) - domain = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',))) + interface = AddInterfaceForm( + request.POST or None, + infra=request.user.has_perms(('infra',)) + ) + domain = AliasForm( + request.POST or None, + infra=request.user.has_perms(('infra',)) + ) if switch.is_valid() and machine.is_valid() and interface.is_valid(): - options, created = AssoOption.objects.get_or_create() + options, _created = AssoOption.objects.get_or_create() user = options.utilisateur_asso if not user: - messages.error(request, "L'user association n'existe pas encore, veuillez le créer ou le linker dans preferences") + messages.error(request, "L'user association n'existe pas encore,\ + veuillez le créer ou le linker dans preferences") return redirect("/topologie/") new_machine = machine.save(commit=False) new_machine.user = user new_interface = interface.save(commit=False) - new_switch = switch.save(commit=False) - new_domain = domain.save(commit=False) + new_switch_instance = switch.save(commit=False) + new_domain_instance = domain.save(commit=False) with transaction.atomic(), reversion.create_revision(): new_machine.save() reversion.set_user(request.user) @@ -297,58 +369,91 @@ def new_switch(request): new_interface.save() reversion.set_user(request.user) reversion.set_comment("Création") - new_domain.interface_parent = new_interface + new_domain_instance.interface_parent = new_interface with transaction.atomic(), reversion.create_revision(): - new_domain.save() + new_domain_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") - new_switch.switch_interface = new_interface + new_switch_instance.switch_interface = new_interface with transaction.atomic(), reversion.create_revision(): - new_switch.save() + new_switch_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Le switch a été crée") return redirect("/topologie/") - return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'topologie/switch.html', request) + return form({ + 'topoform': switch, + 'machineform': machine, + 'interfaceform': interface, + 'domainform': domain + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') def edit_switch(request, switch_id): - """ Edition d'un switch. Permet de chambre nombre de ports, place dans le stack, - interface et machine associée""" + """ Edition d'un switch. Permet de chambre nombre de ports, + place dans le stack, interface et machine associée""" try: switch = Switch.objects.get(pk=switch_id) except Switch.DoesNotExist: messages.error(request, u"Switch inexistant") return redirect("/topologie/") switch_form = EditSwitchForm(request.POST or None, instance=switch) - machine_form = EditMachineForm(request.POST or None, instance=switch.switch_interface.machine) - interface_form = EditInterfaceForm(request.POST or None, instance=switch.switch_interface) - domain_form = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',)), instance=switch.switch_interface.domain) - if switch_form.is_valid() and machine_form.is_valid() and interface_form.is_valid(): + machine_form = EditMachineForm( + request.POST or None, + instance=switch.switch_interface.machine + ) + interface_form = EditInterfaceForm( + request.POST or None, + instance=switch.switch_interface + ) + domain_form = AliasForm( + request.POST or None, + infra=request.user.has_perms(('infra',)), + instance=switch.switch_interface.domain + ) + if switch_form.is_valid() and machine_form.is_valid()\ + and interface_form.is_valid(): new_interface = interface_form.save(commit=False) new_machine = machine_form.save(commit=False) - new_switch = switch_form.save(commit=False) + new_switch_instance = switch_form.save(commit=False) new_domain = domain_form.save(commit=False) with transaction.atomic(), reversion.create_revision(): new_machine.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machine_form.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in machine_form.changed_data + ) + ) with transaction.atomic(), reversion.create_revision(): 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("Champs modifié(s) : %s" % ', '.join( + field for field in interface_form.changed_data) + ) with transaction.atomic(), reversion.create_revision(): new_domain.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in domain_form.changed_data) + ) with transaction.atomic(), reversion.create_revision(): - new_switch.save() + new_switch_instance.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in switch_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in switch_form.changed_data) + ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'topologie/switch.html', request) + return form({ + 'topoform': switch_form, + 'machineform': machine_form, + 'interfaceform': interface_form, + 'domainform': domain_form + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') @@ -362,7 +467,8 @@ def new_room(request): reversion.set_comment("Création") messages.success(request, "La chambre a été créé") return redirect("/topologie/index_room/") - return form({'topoform':room}, 'topologie/topo.html', request) + return form({'topoform': room}, 'topologie/topo.html', request) + @login_required @permission_required('infra') @@ -378,10 +484,13 @@ def edit_room(request, room_id): with transaction.atomic(), reversion.create_revision(): room.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in room.changed_data)) + 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") return redirect("/topologie/index_room/") - return form({'topoform':room}, 'topologie/topo.html', request) + return form({'topoform': room}, 'topologie/topo.html', request) + @login_required @permission_required('infra') @@ -390,7 +499,7 @@ def del_room(request, room_id): try: room = Room.objects.get(pk=room_id) except Room.DoesNotExist: - messages.error(request, u"Chambre inexistante" ) + messages.error(request, u"Chambre inexistante") return redirect("/topologie/index_room/") if request.method == "POST": try: @@ -400,6 +509,10 @@ def del_room(request, room_id): reversion.set_comment("Destruction") 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) + messages.error(request, "La chambre %s est affectée à un autre objet,\ + impossible de la supprimer (switch ou user)" % room) return redirect("/topologie/index_room/") - return form({'objet': room, 'objet_name': 'Chambre'}, 'topologie/delete.html', request) + return form({ + 'objet': room, + 'objet_name': 'Chambre' + }, 'topologie/delete.html', request)