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/cotisations/admin.py b/cotisations/admin.py index 29e3285d..8186e4e3 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -28,23 +28,37 @@ from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente + class FactureAdmin(VersionAdmin): - list_display = ('user','paiement','date','valid','control') + """Class admin d'une facture, tous les champs""" + pass + class VenteAdmin(VersionAdmin): - list_display = ('facture','name','prix','number','iscotisation','duration') + """Class admin d'une vente, tous les champs (facture related)""" + pass + class ArticleAdmin(VersionAdmin): - list_display = ('name','prix','iscotisation','duration') + """Class admin d'un article en vente""" + pass + class BanqueAdmin(VersionAdmin): - list_display = ('name',) + """Class admin de la liste des banques (facture related)""" + pass + class PaiementAdmin(VersionAdmin): - list_display = ('moyen','type_paiement') + """Class admin d'un moyen de paiement (facture related""" + pass + class CotisationAdmin(VersionAdmin): - list_display = ('vente','date_start','date_end') + """Class admin d'une cotisation (date de debut et de fin), + Vente related""" + pass + admin.site.register(Facture, FactureAdmin) admin.site.register(Article, ArticleAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 97bc82ab..76a67975 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -19,16 +19,33 @@ # 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): + """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 +53,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 +148,7 @@ class TrezEditFactureForm(EditFactureForm): class ArticleForm(ModelForm): + """Creation d'un article. Champs : nom, cotisation, durée""" class Meta: model = Article fields = '__all__' @@ -107,10 +158,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 +182,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 +204,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 + ) diff --git a/cotisations/models.py b/cotisations/models.py index fca6bfa5..54843076 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -20,59 +20,111 @@ # 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 + 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'] + """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'] 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): + """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) + 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 +132,67 @@ 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): + """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) + 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,14 +203,20 @@ 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): + """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 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 +235,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 +245,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 +264,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,15 +282,19 @@ class Cotisation(models.Model): def __str__(self): return str(self.vente) + @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') regen('mailing') + @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') 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'), ] - - diff --git a/cotisations/views.py b/cotisations/views.py index 0a2a7648..e44eee65 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -24,44 +24,45 @@ # 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 -from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, CreditSoldeForm, SelectArticleForm +# Import des models, forms et fonctions re2o from users.models import User -from .tex import render_tex 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 .tex import render_tex -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 @@ -70,50 +71,80 @@ 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 + # 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_instance.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() + new_facture_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") for art_item in articles: 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_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): - 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() + options, _created = AssoOption.objects.get_or_create() tbl = [] article = facture_form.cleaned_data['article'] quantite = facture_form.cleaned_data['number'] @@ -121,71 +152,131 @@ 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', {'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_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]) - 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)}) + 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, + '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") + 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/") if request.method == "POST": with transaction.atomic(), reversion.create_revision(): @@ -193,7 +284,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 +297,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 +306,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 +327,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 +344,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 +384,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 +400,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 +438,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 +466,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 +506,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): - options, created = GeneralOption.objects.get_or_create() + """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 +533,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 +543,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): - options, created = GeneralOption.objects.get_or_create() + """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,41 +597,47 @@ 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): +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/") - 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=object_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=object_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=object_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/") - 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) @@ -470,4 +650,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 + }) 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") 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): diff --git a/static/logo/etherpad.png b/static/logo/etherpad.png deleted file mode 100644 index 4dde5bf3..00000000 Binary files a/static/logo/etherpad.png and /dev/null differ diff --git a/static/logo/federez.png b/static/logo/federez.png deleted file mode 100644 index 439de178..00000000 Binary files a/static/logo/federez.png and /dev/null differ diff --git a/static/logo/gitlab.png b/static/logo/gitlab.png deleted file mode 100644 index b5040adc..00000000 Binary files a/static/logo/gitlab.png and /dev/null differ diff --git a/static/logo/kanboard.png b/static/logo/kanboard.png deleted file mode 100644 index 5c13c937..00000000 Binary files a/static/logo/kanboard.png and /dev/null differ diff --git a/static/logo/wiki.png b/static/logo/wiki.png deleted file mode 100644 index c4437bb0..00000000 Binary files a/static/logo/wiki.png and /dev/null differ diff --git a/static/logo/zerobin.png b/static/logo/zerobin.png deleted file mode 100644 index becbe150..00000000 Binary files a/static/logo/zerobin.png and /dev/null differ diff --git a/static_files/.static b/static_files/.static deleted file mode 100644 index e69de29b..00000000 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) - diff --git a/topologie/models.py b/topologie/models.py index c02c0c51..086e0aff 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -20,24 +20,32 @@ # 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 +67,41 @@ 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 +110,68 @@ 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 +182,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 +190,28 @@ 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: + 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: 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'): @@ -167,8 +220,9 @@ 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""" + """Une chambre/local contenant une prise murale""" PRETTY_NAME = "Chambre/ Prise murale" name = models.CharField(max_length=255, unique=True) @@ -176,10 +230,12 @@ 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) 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 9fe37d6f..ceb06f0a 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,9 +46,9 @@ 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.views import generate_ipv4_bft_param @@ -46,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) @@ -93,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') @@ -104,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') @@ -124,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 @@ -153,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/") @@ -171,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) @@ -193,32 +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à") - else: - return redirect('/topologie/index_stack') - 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: @@ -229,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: @@ -249,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: @@ -267,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) @@ -300,14 +369,14 @@ 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éé") @@ -318,38 +387,59 @@ def new_switch(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/") i_bft_param = generate_ipv4_bft_param( interface_form, False ) @@ -367,7 +457,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') @@ -383,10 +474,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') @@ -395,7 +489,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: @@ -405,6 +499,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) 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