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 90c9e826..76a67975 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -19,74 +19,125 @@ # 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): - super(NewFactureForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['cheque'].required = False 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__' @@ -97,38 +148,67 @@ class TrezEditFactureForm(EditFactureForm): class ArticleForm(ModelForm): + """Creation d'un article. Champs : nom, cotisation, durée""" class Meta: model = Article fields = '__all__' def __init__(self, *args, **kwargs): - super(ArticleForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + 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'] def __init__(self, *args, **kwargs): - super(PaiementForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) 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'] def __init__(self, *args, **kwargs): - super(BanqueForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + 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/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index 2d6663f7..f1af2b8b 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load staticfiles%} +{% load massive_bootstrap_form %} {% block title %}Création et modification de factures{% endblock %} @@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Editer la facture

- {% bootstrap_form factureform %} + {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }}

Articles de la facture

diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index 2dfa52c2..f2586e8b 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -38,18 +38,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ venteform.management_form }}

Articles de la facture

-
+
{% for form in venteform.forms %} -
-

- {{ form.as_table }} -

+
+ Article :   + {% bootstrap_form form label_class='sr-only' %} +   +
{% endfor %}
-

-

Prix total : 0,00

@@ -63,19 +65,23 @@ with this program; if not, write to the Free Software Foundation, Inc., prices[{{ article.id|escapejs }}] = {{ article.prix }}; {% endfor %} - var template = `

{{ venteform.empty_form.as_table }}

`; + var template = `Article :   + {% bootstrap_form venteform.empty_form label_class='sr-only' %} +   + ` function add_article(){ // Index start at 0 => new_index = number of items var new_index = document.getElementsByClassName('product_to_sell').length; - document.getElementById('id_form-TOTAL_FORMS').value = - parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1; + document.getElementById('id_form-TOTAL_FORMS').value ++; var new_article = document.createElement('div'); - new_article.className = 'product_to_sell'; + new_article.className = 'product_to_sell form-inline'; new_article.innerHTML = template.replace(/__prefix__/g, new_index); - document.getElementById('form_set') - .appendChild(new_article); + document.getElementById('form_set').appendChild(new_article); add_listenner_for_id(new_index); } @@ -106,18 +112,28 @@ with this program; if not, write to the Free Software Foundation, Inc., .addEventListener("onkeypress", update_price, true); document.getElementById('id_form-' + i.toString() + '-quantity') .addEventListener("change", update_price, true); + document.getElementById('id_form-' + i.toString() + '-article-remove') + .addEventListener("click", function(event) { + var article = event.target.parentNode; + article.parentNode.removeChild(article); + document.getElementById('id_form-TOTAL_FORMS').value --; + update_price(); + } + ) } - function set_cheque_info_visibility(){ - var visible = document.getElementById("id_paiement").value == document.getElementById("id_paiement").getAttribute('data-cheque'); - p = document.getElementById("id_paiement") - console.log(p); + function set_cheque_info_visibility() { + var paiement = document.getElementById("id_Facture-paiement"); + var visible = paiement.value == paiement.getAttribute('data-cheque'); + p = document.getElementById("id_Facture-paiement"); var display = 'none'; if (visible) { display = 'block'; } - document.getElementById("id_cheque").parentNode.style.display = display; - document.getElementById("id_banque").parentNode.style.display = display; + document.getElementById("id_Facture-cheque") + .parentNode.style.display = display; + document.getElementById("id_Facture-banque") + .parentNode.style.display = display; } // Add events manager when DOM is fully loaded @@ -129,7 +145,7 @@ with this program; if not, write to the Free Software Foundation, Inc., for (i = 0; i < product_count; ++i){ add_listenner_for_id(i); } - document.getElementById("id_paiement") + document.getElementById("id_Facture-paiement") .addEventListener("change", set_cheque_info_visibility, true); set_cheque_info_visibility(); update_price(); 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/logs/urls.py b/logs/urls.py index 3bb41c4a..11009835 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -19,7 +19,10 @@ # 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. - +""" +Urls de l'application logs, pointe vers les fonctions de views. +Inclu dans le re2o.urls +""" from __future__ import unicode_literals from django.conf.urls import url @@ -29,7 +32,9 @@ from . import views urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^stats_logs$', views.stats_logs, name='stats-logs'), - url(r'^revert_action/(?P[0-9]+)$', views.revert_action, name='revert-action'), + url(r'^revert_action/(?P[0-9]+)$', + views.revert_action, + name='revert-action'), url(r'^stats_general/$', views.stats_general, name='stats-general'), url(r'^stats_models/$', views.stats_models, name='stats-models'), url(r'^stats_users/$', views.stats_users, name='stats-users'), diff --git a/logs/views.py b/logs/views.py index d84a2f43..e21d4de6 100644 --- a/logs/views.py +++ b/logs/views.py @@ -23,62 +23,67 @@ # App de gestion des statistiques pour re2o # Gabriel Détraz # Gplv2 +""" +Vues des logs et statistiques générales. + +La vue index générale affiche une selection des dernières actions, +classées selon l'importance, avec date, et user formatés. + +Stats_logs renvoie l'ensemble des logs. + +Les autres vues sont thématiques, ensemble des statistiques et du +nombre d'objets par models, nombre d'actions par user, etc +""" from __future__ import unicode_literals -from django.http import HttpResponse from django.shortcuts import render, redirect -from django.shortcuts import get_object_or_404 -from django.template.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import ProtectedError -from django.forms import ValidationError -from django.db import transaction from django.db.models import Count from reversion.models import Revision from reversion.models import Version, ContentType -from users.models import User, ServiceUser, Right, School, ListRight, ListShell, Ban, Whitelist -from users.models import all_has_access, all_whitelisted, all_baned, all_adherent -from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation -from machines.models import Machine, MachineType, IpType, Extension, Interface, Domain, IpList -from machines.views import all_active_assigned_interfaces_count, all_active_interfaces_count +from users.models import User, ServiceUser, Right, School, ListRight, ListShell +from users.models import Ban, Whitelist +from cotisations.models import Facture, Vente, Article, Banque, Paiement +from cotisations.models import Cotisation +from machines.models import Machine, MachineType, IpType, Extension, Interface +from machines.models import Domain, IpList from topologie.models import Switch, Port, Room from preferences.models import GeneralOption - -from django.utils import timezone -from dateutil.relativedelta import relativedelta +from re2o.views import form +from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent +from re2o.utils import all_active_assigned_interfaces_count +from re2o.utils import all_active_interfaces_count STATS_DICT = { - 0 : ["Tout", 36], - 1 : ["1 mois", 1], - 2 : ["2 mois", 2], - 3 : ["6 mois", 6], - 4 : ["1 an", 12], - 5 : ["2 an", 24], + 0: ["Tout", 36], + 1: ["1 mois", 1], + 2: ["2 mois", 2], + 3: ["6 mois", 6], + 4: ["1 an", 12], + 5: ["2 an", 24], } -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) @login_required @permission_required('cableur') def index(request): - options, created = GeneralOption.objects.get_or_create() + """Affiche les logs affinés, date reformatées, selectionne + les event importants (ajout de droits, ajout de ban/whitelist)""" + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - # The types of content kept for display - content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user'] - + content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user'] # Select only wanted versions - versions = Version.objects.filter(content_type__in=ContentType.objects.filter(model__in=content_type_filter)).order_by('revision__date_created').reverse().select_related('revision') - + versions = Version.objects.filter( + content_type__in=ContentType.objects.filter( + model__in=content_type_filter + ) + ).order_by('revision__date_created').reverse().select_related('revision') paginator = Paginator(versions, pagination_number) page = request.GET.get('page') try: @@ -87,7 +92,7 @@ def index(request): # If page is not an integer, deliver first page. versions = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. + # If page is out of range (e.g. 9999), deliver last page of results. versions = paginator.page(paginator.num_pages) # Force to have a list instead of QuerySet @@ -95,30 +100,38 @@ def index(request): # Items to remove later because invalid to_remove = [] # Parse every item (max = pagination_number) - for i in range( len( versions.object_list ) ): - if versions.object_list[i].object : - v = versions.object_list[i] + for i in range(len(versions.object_list)): + if versions.object_list[i].object: + version = versions.object_list[i] versions.object_list[i] = { - 'rev_id' : v.revision.id, - 'comment': v.revision.comment, - 'datetime': v.revision.date_created.strftime('%d/%m/%y %H:%M:%S'), - 'username': v.revision.user.get_username() if v.revision.user else '?', - 'user_id': v.revision.user_id, - 'version': v } - else : - to_remove.insert(0,i) + 'rev_id': version.revision.id, + 'comment': version.revision.comment, + 'datetime': version.revision.date_created.strftime( + '%d/%m/%y %H:%M:%S' + ), + 'username': + version.revision.user.get_username() + if version.revision.user else '?', + 'user_id': version.revision.user_id, + 'version': version} + else: + to_remove.insert(0, i) # Remove all tagged invalid items - for i in to_remove : + for i in to_remove: versions.object_list.pop(i) - return render(request, 'logs/index.html', {'versions_list': versions}) + @login_required @permission_required('cableur') def stats_logs(request): - options, created = GeneralOption.objects.get_or_create() + """Affiche l'ensemble des logs et des modifications sur les objets, + classés par date croissante, en vrac""" + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - revisions = Revision.objects.all().order_by('date_created').reverse().select_related('user').prefetch_related('version_set__object') + revisions = Revision.objects.all().order_by('date_created')\ + .reverse().select_related('user')\ + .prefetch_related('version_set__object') paginator = Paginator(revisions, pagination_number) page = request.GET.get('page') try: @@ -127,9 +140,12 @@ def stats_logs(request): # If page is not an integer, deliver first page. revisions = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. + # If page is out of range (e.g. 9999), deliver last page of results. revisions = paginator.page(paginator.num_pages) - return render(request, 'logs/stats_logs.html', {'revisions_list': revisions}) + return render(request, 'logs/stats_logs.html', { + 'revisions_list': revisions + }) + @login_required @permission_required('bureau') @@ -138,121 +154,182 @@ def revert_action(request, revision_id): try: revision = Revision.objects.get(id=revision_id) except Revision.DoesNotExist: - messages.error(request, u"Revision inexistante" ) + messages.error(request, u"Revision inexistante") if request.method == "POST": revision.revert() messages.success(request, "L'action a été supprimée") return redirect("/logs/") - return form({'objet': revision, 'objet_name': revision.__class__.__name__ }, 'logs/delete.html', request) + return form({ + 'objet': revision, + 'objet_name': revision.__class__.__name__ + }, 'logs/delete.html', request) + @login_required @permission_required('cableur') def stats_general(request): - all_active_users = User.objects.filter(state=User.STATE_ACTIVE) - ip = dict() + """Statistiques générales affinées sur les ip, activées, utilisées par + range, et les statistiques générales sur les users : users actifs, + cotisants, activés, archivés, etc""" + ip_dict = dict() for ip_range in IpType.objects.all(): all_ip = IpList.objects.filter(ip_type=ip_range) used_ip = Interface.objects.filter(ipv4__in=all_ip).count() - active_ip = all_active_assigned_interfaces_count().filter(ipv4__in=IpList.objects.filter(ip_type=ip_range)).count() - ip[ip_range] = [ip_range, all_ip.count(), used_ip, active_ip, all_ip.count()-used_ip] + active_ip = all_active_assigned_interfaces_count().filter( + ipv4__in=IpList.objects.filter(ip_type=ip_range) + ).count() + ip_dict[ip_range] = [ip_range, all_ip.count(), + used_ip, active_ip, all_ip.count()-used_ip] stats = [ - [["Categorie", "Nombre d'utilisateurs"], { - 'active_users' : ["Users actifs", User.objects.filter(state=User.STATE_ACTIVE).count()], - 'inactive_users' : ["Users désactivés", User.objects.filter(state=User.STATE_DISABLED).count()], - 'archive_users' : ["Users archivés", User.objects.filter(state=User.STATE_ARCHIVE).count()], - 'adherent_users' : ["Adhérents à l'association", all_adherent().count()], - 'connexion_users' : ["Utilisateurs bénéficiant d'une connexion", all_has_access().count()], - 'ban_users' : ["Utilisateurs bannis", all_baned().count()], - 'whitelisted_user' : ["Utilisateurs bénéficiant d'une connexion gracieuse", all_whitelisted().count()], - 'actives_interfaces' : ["Interfaces actives (ayant accès au reseau)", all_active_interfaces_count().count()], - 'actives_assigned_interfaces' : ["Interfaces actives et assignées ipv4", all_active_assigned_interfaces_count().count()] - }], - [["Range d'ip", "Nombre d'ip totales", "Ip assignées", "Ip assignées à une machine active", "Ip non assignées"] ,ip] - ] + [["Categorie", "Nombre d'utilisateurs"], { + 'active_users': [ + "Users actifs", + User.objects.filter(state=User.STATE_ACTIVE).count()], + 'inactive_users': [ + "Users désactivés", + User.objects.filter(state=User.STATE_DISABLED).count()], + 'archive_users': [ + "Users archivés", + User.objects.filter(state=User.STATE_ARCHIVE).count()], + 'adherent_users': [ + "Adhérents à l'association", + all_adherent().count()], + 'connexion_users': [ + "Utilisateurs bénéficiant d'une connexion", + all_has_access().count()], + 'ban_users': [ + "Utilisateurs bannis", + all_baned().count()], + 'whitelisted_user': [ + "Utilisateurs bénéficiant d'une connexion gracieuse", + all_whitelisted().count()], + 'actives_interfaces': [ + "Interfaces actives (ayant accès au reseau)", + all_active_interfaces_count().count()], + 'actives_assigned_interfaces': [ + "Interfaces actives et assignées ipv4", + all_active_assigned_interfaces_count().count()] + }], + [["Range d'ip", "Nombre d'ip totales", "Ip assignées", + "Ip assignées à une machine active", "Ip non assignées"], ip_dict] + ] return render(request, 'logs/stats_general.html', {'stats_list': stats}) @login_required @permission_required('cableur') def stats_models(request): - all_active_users = User.objects.filter(state=User.STATE_ACTIVE) + """Statistiques générales, affiche les comptages par models: + nombre d'users, d'écoles, de droits, de bannissements, + de factures, de ventes, de banque, de machines, etc""" stats = { - 'Users' : { - 'users' : [User.PRETTY_NAME, User.objects.count()], - 'serviceuser' : [ServiceUser.PRETTY_NAME, ServiceUser.objects.count()], - 'right' : [Right.PRETTY_NAME, Right.objects.count()], - 'school' : [School.PRETTY_NAME, School.objects.count()], - 'listright' : [ListRight.PRETTY_NAME, ListRight.objects.count()], - 'listshell' : [ListShell.PRETTY_NAME, ListShell.objects.count()], - 'ban' : [Ban.PRETTY_NAME, Ban.objects.count()], - 'whitelist' : [Whitelist.PRETTY_NAME, Whitelist.objects.count()] - }, - 'Cotisations' : { - 'factures' : [Facture.PRETTY_NAME, Facture.objects.count()], - 'vente' : [Vente.PRETTY_NAME, Vente.objects.count()], - 'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()], - 'article' : [Article.PRETTY_NAME, Article.objects.count()], - 'banque' : [Banque.PRETTY_NAME, Banque.objects.count()], - 'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()], - }, - 'Machines' : { - 'machine' : [Machine.PRETTY_NAME, Machine.objects.count()], - 'typemachine' : [MachineType.PRETTY_NAME, MachineType.objects.count()], - 'typeip' : [IpType.PRETTY_NAME, IpType.objects.count()], - 'extension' : [Extension.PRETTY_NAME, Extension.objects.count()], - 'interface' : [Interface.PRETTY_NAME, Interface.objects.count()], - 'alias' : [Domain.PRETTY_NAME, Domain.objects.exclude(cname=None).count()], - 'iplist' : [IpList.PRETTY_NAME, IpList.objects.count()], - }, - 'Topologie' : { - 'switch' : [Switch.PRETTY_NAME, Switch.objects.count()], - 'port' : [Port.PRETTY_NAME, Port.objects.count()], - 'chambre' : [Room.PRETTY_NAME, Room.objects.count()], - }, - 'Actions effectuées sur la base' : - { - 'revision' : ["Nombre d'actions", Revision.objects.count()], - }, + 'Users': { + 'users': [User.PRETTY_NAME, User.objects.count()], + 'serviceuser': [ServiceUser.PRETTY_NAME, + ServiceUser.objects.count()], + 'right': [Right.PRETTY_NAME, Right.objects.count()], + 'school': [School.PRETTY_NAME, School.objects.count()], + 'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()], + 'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()], + 'ban': [Ban.PRETTY_NAME, Ban.objects.count()], + 'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()] + }, + 'Cotisations': { + 'factures': [Facture.PRETTY_NAME, Facture.objects.count()], + 'vente': [Vente.PRETTY_NAME, Vente.objects.count()], + 'cotisation': [Cotisation.PRETTY_NAME, Cotisation.objects.count()], + 'article': [Article.PRETTY_NAME, Article.objects.count()], + 'banque': [Banque.PRETTY_NAME, Banque.objects.count()], + }, + 'Machines': { + 'machine': [Machine.PRETTY_NAME, Machine.objects.count()], + 'typemachine': [MachineType.PRETTY_NAME, + MachineType.objects.count()], + 'typeip': [IpType.PRETTY_NAME, IpType.objects.count()], + 'extension': [Extension.PRETTY_NAME, Extension.objects.count()], + 'interface': [Interface.PRETTY_NAME, Interface.objects.count()], + 'alias': [Domain.PRETTY_NAME, + Domain.objects.exclude(cname=None).count()], + 'iplist': [IpList.PRETTY_NAME, IpList.objects.count()], + }, + 'Topologie': { + 'switch': [Switch.PRETTY_NAME, Switch.objects.count()], + 'port': [Port.PRETTY_NAME, Port.objects.count()], + 'chambre': [Room.PRETTY_NAME, Room.objects.count()], + }, + 'Actions effectuées sur la base': + { + 'revision': ["Nombre d'actions", Revision.objects.count()], + }, } - return render(request, 'logs/stats_models.html', {'stats_list': stats}) + return render(request, 'logs/stats_models.html', {'stats_list': stats}) + @login_required @permission_required('cableur') def stats_users(request): + """Affiche les statistiques base de données aggrégées par user : + nombre de machines par user, d'etablissements par user, + de moyens de paiements par user, de banque par user, + de bannissement par user, etc""" onglet = request.GET.get('onglet') try: - search_field = STATS_DICT[onglet] - except: - search_field = STATS_DICT[0] + _search_field = STATS_DICT[onglet] + except KeyError: + _search_field = STATS_DICT[0] onglet = 0 - start_date = timezone.now() + relativedelta(months=-search_field[1]) stats = { - 'Utilisateur' : { - 'Machines' : User.objects.annotate(num=Count('machine')).order_by('-num')[:10], - 'Facture' : User.objects.annotate(num=Count('facture')).order_by('-num')[:10], - 'Bannissement' : User.objects.annotate(num=Count('ban')).order_by('-num')[:10], - 'Accès gracieux' : User.objects.annotate(num=Count('whitelist')).order_by('-num')[:10], - 'Droits' : User.objects.annotate(num=Count('right')).order_by('-num')[:10], - }, - 'Etablissement' : { - 'Utilisateur' : School.objects.annotate(num=Count('user')).order_by('-num')[:10], - }, - 'Moyen de paiement' : { - 'Utilisateur' : Paiement.objects.annotate(num=Count('facture')).order_by('-num')[:10], - }, - 'Banque' : { - 'Utilisateur' : Banque.objects.annotate(num=Count('facture')).order_by('-num')[:10], - }, + 'Utilisateur': { + 'Machines': User.objects.annotate( + num=Count('machine') + ).order_by('-num')[:10], + 'Facture': User.objects.annotate( + num=Count('facture') + ).order_by('-num')[:10], + 'Bannissement': User.objects.annotate( + num=Count('ban') + ).order_by('-num')[:10], + 'Accès gracieux': User.objects.annotate( + num=Count('whitelist') + ).order_by('-num')[:10], + 'Droits': User.objects.annotate( + num=Count('right') + ).order_by('-num')[:10], + }, + 'Etablissement': { + 'Utilisateur': School.objects.annotate( + num=Count('user') + ).order_by('-num')[:10], + }, + 'Moyen de paiement': { + 'Utilisateur': Paiement.objects.annotate( + num=Count('facture') + ).order_by('-num')[:10], + }, + 'Banque': { + 'Utilisateur': Banque.objects.annotate( + num=Count('facture') + ).order_by('-num')[:10], + }, } - return render(request, 'logs/stats_users.html', {'stats_list': stats, 'stats_dict' : STATS_DICT, 'active_field': onglet}) + return render(request, 'logs/stats_users.html', { + 'stats_list': stats, + 'stats_dict': STATS_DICT, + 'active_field': onglet + }) + @login_required @permission_required('cableur') def stats_actions(request): - onglet = request.GET.get('onglet') + """Vue qui affiche les statistiques de modifications d'objets par + utilisateurs. + Affiche le nombre de modifications aggrégées par utilisateurs""" stats = { - 'Utilisateur' : { - 'Action' : User.objects.annotate(num=Count('revision')).order_by('-num')[:40], - }, + 'Utilisateur': { + 'Action': User.objects.annotate( + num=Count('revision') + ).order_by('-num')[:40], + }, } return render(request, 'logs/stats_users.html', {'stats_list': stats}) diff --git a/machines/forms.py b/machines/forms.py index 062c4965..eb16e2c9 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -40,7 +40,8 @@ class EditMachineForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditMachineForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditMachineForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Nom de la machine' class NewMachineForm(EditMachineForm): @@ -57,7 +58,8 @@ class EditInterfaceForm(ModelForm): fields = ['machine', 'type', 'ipv4', 'mac_address', 'details'] def __init__(self, *args, **kwargs): - super(EditInterfaceForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['mac_address'].label = 'Adresse mac' self.fields['type'].label = 'Type de machine' self.fields['type'].empty_label = "Séléctionner un type de machine" @@ -110,9 +112,10 @@ class AliasForm(ModelForm): fields = ['name','extension'] def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) if 'infra' in kwargs: infra = kwargs.pop('infra') - super(AliasForm, self).__init__(*args, **kwargs) + super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs) class DomainForm(AliasForm): class Meta(AliasForm.Meta): @@ -125,7 +128,8 @@ class DomainForm(AliasForm): initial = kwargs.get('initial', {}) initial['name'] = user.get_next_domain_name() kwargs['initial'] = initial - super(DomainForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(DomainForm, self).__init__(*args, prefix=prefix, **kwargs) class DelAliasForm(Form): alias = forms.ModelMultipleChoiceField(queryset=Domain.objects.all(), label="Alias actuels", widget=forms.CheckboxSelectMultiple) @@ -141,7 +145,8 @@ class MachineTypeForm(ModelForm): fields = ['type','ip_type'] def __init__(self, *args, **kwargs): - super(MachineTypeForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(MachineTypeForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['type'].label = 'Type de machine à ajouter' self.fields['ip_type'].label = "Type d'ip relié" @@ -153,9 +158,9 @@ class IpTypeForm(ModelForm): model = IpType fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports'] - def __init__(self, *args, **kwargs): - super(IpTypeForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(IpTypeForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['type'].label = 'Type ip à ajouter' class EditIpTypeForm(IpTypeForm): @@ -171,7 +176,8 @@ class ExtensionForm(ModelForm): fields = ['name', 'need_infra', 'origin'] def __init__(self, *args, **kwargs): - super(ExtensionForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ExtensionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Extension à ajouter' self.fields['origin'].label = 'Enregistrement A origin' @@ -184,7 +190,8 @@ class MxForm(ModelForm): fields = ['zone', 'priority', 'name'] def __init__(self, *args, **kwargs): - super(MxForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(MxForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None) class DelMxForm(Form): @@ -196,25 +203,34 @@ class NsForm(ModelForm): fields = ['zone', 'ns'] def __init__(self, *args, **kwargs): - super(NsForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(NsForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None) class DelNsForm(Form): ns = forms.ModelMultipleChoiceField(queryset=Ns.objects.all(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple) -class TextForm(ModelForm): +class TxtForm(ModelForm): class Meta: model = Text fields = '__all__' -class DelTextForm(Form): - text = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Text actuels", widget=forms.CheckboxSelectMultiple) + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(TxtForm, self).__init__(*args, prefix=prefix, **kwargs) + +class DelTxtForm(Form): + txt = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Txt actuels", widget=forms.CheckboxSelectMultiple) class NasForm(ModelForm): class Meta: model = Nas fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(NasForm, self).__init__(*args, prefix=prefix, **kwargs) + class DelNasForm(Form): nas = forms.ModelMultipleChoiceField(queryset=Nas.objects.all(), label="Enregistrements Nas actuels", widget=forms.CheckboxSelectMultiple) @@ -223,6 +239,10 @@ class ServiceForm(ModelForm): model = Service fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) + def save(self, commit=True): instance = super(ServiceForm, self).save(commit=False) if commit: @@ -238,6 +258,10 @@ class VlanForm(ModelForm): model = Vlan fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) + class DelVlanForm(Form): vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple) @@ -246,8 +270,16 @@ class EditOuverturePortConfigForm(ModelForm): model = Interface fields = ['port_lists'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOuverturePortConfigForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditOuverturePortListForm(ModelForm): class Meta: model = OuverturePortList fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOuverturePortListForm, self).__init__(*args, prefix=prefix, **kwargs) + diff --git a/machines/templates/machines/aff_text.html b/machines/templates/machines/aff_txt.html similarity index 84% rename from machines/templates/machines/aff_text.html rename to machines/templates/machines/aff_txt.html index f3ada132..fd7c5ee6 100644 --- a/machines/templates/machines/aff_text.html +++ b/machines/templates/machines/aff_txt.html @@ -25,21 +25,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - - + + + - {% for text in text_list %} + {% for txt in txt_list %} - - + + {% endfor %} diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html index 20587d85..88eb5411 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -47,12 +47,12 @@ with this program; if not, write to the Free Software Foundation, Inc., Supprimer un enregistrement NS {% endif %} {% include "machines/aff_ns.html" with ns_list=ns_list %} -

Liste des enregistrements Text

+

Liste des enregistrements TXT

{% if is_infra %} - Ajouter un enregistrement TXT - Supprimer un enregistrement TXT + Ajouter un enregistrement TXT + Supprimer un enregistrement TXT {% endif %} - {% include "machines/aff_text.html" with text_list=text_list %} + {% include "machines/aff_txt.html" with txt_list=txt_list %}


diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index d34dccb9..86bf7b90 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de machines{% endblock %} @@ -39,6 +39,36 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if domainform %} {% bootstrap_form_errors domainform %} {% endif %} +{% if iptypeform %} +{% bootstrap_form_errors iptypeform %} +{% endif %} +{% if machinetypeform %} +{% bootstrap_form_errors machinetypeform %} +{% endif %} +{% if extensionform %} +{% bootstrap_form_errors extensionform %} +{% endif %} +{% if mxform %} +{% bootstrap_form_errors mxform %} +{% endif %} +{% if nsform %} +{% bootstrap_form_errors nsform %} +{% endif %} +{% if txtform %} +{% bootstrap_form_errors txtform %} +{% endif %} +{% if aliasform %} +{% bootstrap_form_errors aliasform %} +{% endif %} +{% if serviceform %} +{% bootstrap_form_errors serviceform %} +{% endif %} +{% if vlanform %} +{% bootstrap_form_errors vlanform %} +{% endif %} +{% if nasform %} +{% bootstrap_form_errors nasform %} +{% endif %} {% csrf_token %} @@ -48,24 +78,56 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if interfaceform %}

Interface

- {% if i_bft_param %} - {% if 'machine' in interfaceform.fields %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} - {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4' bft_param=i_bft_param %} - {% endif %} + {% if i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% else %} - {% if 'machine' in interfaceform.fields %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} - {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4' %} - {% endif %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %}

Domaine

{% bootstrap_form domainform %} {% endif %} + {% if iptypeform %} +

Type d'IP

+ {% bootstrap_form iptypeform %} + {% endif %} + {% if machinetypeform %} +

Type de machine

+ {% bootstrap_form machinetypeform %} + {% endif %} + {% if extensionform %} +

Extension

+ {% massive_bootstrap_form extensionform 'origin' %} + {% endif %} + {% if mxform %} +

Enregistrement MX

+ {% massive_bootstrap_form mxform 'name' %} + {% endif %} + {% if nsform %} +

Enregistrement NS

+ {% massive_bootstrap_form nsform 'ns' %} + {% endif %} + {% if txtform %} +

Enregistrement TXT

+ {% bootstrap_form txtform %} + {% endif %} + {% if aliasform %} +

Alias

+ {% bootstrap_form aliasform %} + {% endif %} + {% if serviceform %} +

Service

+ {% massive_bootstrap_form serviceform 'servers' %} + {% endif %} + {% if vlanform %} +

Vlan

+ {% bootstrap_form vlanform %} + {% endif %} + {% if nasform %} +

NAS

+ {% bootstrap_form nasform %} + {% endif %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/machines/templatetags/bootstrap_form_typeahead.py b/machines/templatetags/bootstrap_form_typeahead.py deleted file mode 100644 index 05dd3147..00000000 --- a/machines/templatetags/bootstrap_form_typeahead.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il -# se veut agnostique au réseau considéré, de manière à être installable en -# quelques clics. -# -# Copyright © 2017 Maël Kervella -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -from django import template -from django.utils.safestring import mark_safe -from django.forms import TextInput -from bootstrap3.templatetags.bootstrap3 import bootstrap_form -from bootstrap3.utils import render_tag -from bootstrap3.forms import render_field - -register = template.Library() - -@register.simple_tag -def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): - """ - Render a form where some specific fields are rendered using Typeahead. - Using Typeahead really improves the performance, the speed and UX when - dealing with very large datasets (select with 50k+ elts for instance). - For convenience, it accepts the same parameters as a standard bootstrap - can accept. - - **Tag name**:: - - bootstrap_form_typeahead - - **Parameters**: - - form - The form that is to be rendered - - typeahead_fields - A list of field names (comma separated) that should be rendered - with typeahead instead of the default bootstrap renderer. - - bft_param - A dict of parameters for the bootstrap_form_typeahead tag. The - possible parameters are the following. - - choices - A dict of strings representing the choices in JS. The keys of - the dict are the names of the concerned fields. The choices - must be an array of objects. Each of those objects must at - least have the fields 'key' (value to send) and 'value' (value - to display). Other fields can be added as desired. - For a more complex structure you should also consider - reimplementing the engine and the match_func. - If not specified, the key is the id of the object and the value - is its string representation as in a normal bootstrap form. - Example : - 'choices' : { - 'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]', - 'field_B':..., - ... - } - - engine - A dict of strings representating the engine used for matching - queries and possible values with typeahead. The keys of the - dict are the names of the concerned fields. The string is valid - JS code. - If not specified, BloodHound with relevant basic properties is - used. - Example : - 'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} - - match_func - A dict of strings representing a valid JS function used in the - dataset to overload the matching engine. The keys of the dict - are the names of the concerned fields. This function is used - the source of the dataset. This function receives 2 parameters, - the query and the synchronize function as specified in - typeahead.js documentation. If needed, the local variables - 'choices_' and 'engine_' contains - respectively the array of all possible values and the engine - to match queries with possible values. - If not specified, the function used display up to the 10 first - elements if the query is empty and else the matching results. - Example : - 'match_func' : { - 'field_A': 'function(q, sync) { engine.search(q, sync); }', - 'field_B': ..., - ... - } - - update_on - A dict of list of ids that the values depends on. The engine - and the typeahead properties are recalculated and reapplied. - Example : - 'addition' : { - 'field_A' : [ 'id0', 'id1', ... ] , - 'field_B' : ... , - ... - } - - See boostrap_form_ for other arguments - - **Usage**:: - - {% bootstrap_form_typeahead - form - [ '[,[,...]]' ] - [ { - [ 'choices': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'engine': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'match_func': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'update_on': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - } ] - [ ] - %} - - **Example**: - - {% bootstrap_form_typeahead form 'ipv4' choices='[...]' %} - """ - - t_fields = typeahead_fields.split(',') - params = kwargs.get('bft_param', {}) - exclude = params.get('exclude', None) - exclude = exclude.split(',') if exclude else [] - t_choices = params.get('choices', {}) - t_engine = params.get('engine', {}) - t_match_func = params.get('match_func', {}) - t_update_on = params.get('update_on', {}) - hidden = [h.name for h in django_form.hidden_fields()] - - form = '' - for f_name, f_value in django_form.fields.items() : - if not f_name in exclude : - if f_name in t_fields and not f_name in hidden : - f_bound = f_value.get_bound_field( django_form, f_name ) - f_value.widget = TextInput( - attrs={ - 'name': 'typeahead_'+f_name, - 'placeholder': f_value.empty_label - } - ) - form += render_field( - f_value.get_bound_field( django_form, f_name ), - *args, - **kwargs - ) - form += render_tag( - 'div', - content = hidden_tag( f_bound, f_name ) + - typeahead_js( - f_name, - f_value, - f_bound, - t_choices, - t_engine, - t_match_func, - t_update_on - ) - ) - else: - form += render_field( - f_value.get_bound_field(django_form, f_name), - *args, - **kwargs - ) - - return mark_safe( form ) - -def input_id( f_name ) : - """ The id of the HTML input element """ - return 'id_'+f_name - -def hidden_id( f_name ): - """ The id of the HTML hidden input element """ - return 'typeahead_hidden_'+f_name - -def hidden_tag( f_bound, f_name ): - """ The HTML hidden input element """ - return render_tag( - 'input', - attrs={ - 'id': hidden_id(f_name), - 'name': f_name, - 'type': 'hidden', - 'value': f_bound.value() or "" - } - ) - -def typeahead_js( f_name, f_value, f_bound, - t_choices, t_engine, t_match_func, t_update_on ) : - """ The whole script to use """ - - choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \ - else default_choices( f_value ) - - engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \ - else default_engine ( f_name ) - - match_func = mark_safe(t_match_func[f_name]) \ - if f_name in t_match_func.keys() else default_match_func( f_name ) - - update_on = t_update_on[f_name] if f_name in t_update_on.keys() else [] - - js_content = ( - 'var choices_{f_name} = {choices};' - 'var engine_{f_name};' - 'var setup_{f_name} = function() {{' - 'engine_{f_name} = {engine};' - '$( "#{input_id}" ).typeahead( "destroy" );' - '$( "#{input_id}" ).typeahead( {datasets} );' - '}};' - '$( "#{input_id}" ).bind( "typeahead:select", {updater} );' - '$( "#{input_id}" ).bind( "typeahead:change", {change} );' - '{updates}' - '$( "#{input_id}" ).ready( function() {{' - 'setup_{f_name}();' - '{init_input}' - '}} );' - ).format( - f_name = f_name, - choices = choices, - engine = engine, - input_id = input_id( f_name ), - datasets = default_datasets( f_name, match_func ), - updater = typeahead_updater( f_name ), - change = typeahead_change( f_name ), - updates = ''.join( [ ( - '$( "#{u_id}" ).change( function() {{' - 'setup_{f_name}();' - '{reset_input}' - '}} );' - ).format( - u_id = u_id, - reset_input = reset_input( f_name ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = init_input( f_name, f_bound ), - ) - - return render_tag( 'script', content=mark_safe( js_content ) ) - -def init_input( f_name, f_bound ) : - """ The JS script to init the fields values """ - init_key = f_bound.value() or '""' - return ( - '$( "#{input_id}" ).typeahead("val", {init_val});' - '$( "#{hidden_id}" ).val( {init_key} );' - ).format( - input_id = input_id( f_name ), - init_val = '""' if init_key == '""' else - 'engine_{f_name}.get( {init_key} )[0].value'.format( - f_name = f_name, - init_key = init_key - ), - init_key = init_key, - hidden_id = hidden_id( f_name ) - ) - -def reset_input( f_name ) : - """ The JS script to reset the fields values """ - return ( - '$( "#{input_id}" ).typeahead("val", "");' - '$( "#{hidden_id}" ).val( "" );' - ).format( - input_id = input_id( f_name ), - hidden_id = hidden_id( f_name ) - ) - -def default_choices( f_value ) : - """ The JS script creating the variable choices_ """ - return '[{objects}]'.format( - objects = ','.join( - [ '{{key:{k},value:"{v}"}}'.format( - k = choice[0] if choice[0] != '' else '""', - v = choice[1] - ) for choice in f_value.choices ] - ) - ) - -def default_engine ( f_name ) : - """ The JS script creating the variable engine_ """ - return ( - 'new Bloodhound({{' - 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' - 'queryTokenizer: Bloodhound.tokenizers.whitespace,' - 'local: choices_{f_name},' - 'identify: function(obj) {{ return obj.key; }}' - '}})' - ).format( - f_name = f_name - ) - -def default_datasets( f_name, match_func ) : - """ The JS script creating the datasets to use with typeahead """ - return ( - '{{' - 'hint: true,' - 'highlight: true,' - 'minLength: 0' - '}},' - '{{' - 'display: "value",' - 'name: "{f_name}",' - 'source: {match_func}' - '}}' - ).format( - f_name = f_name, - match_func = match_func - ) - -def default_match_func ( f_name ) : - """ The JS script creating the matching function to use with typeahed """ - return ( - 'function ( q, sync ) {{' - 'if ( q === "" ) {{' - 'var first = choices_{f_name}.slice( 0, 5 ).map(' - 'function ( obj ) {{ return obj.key; }}' - ');' - 'sync( engine_{f_name}.get( first ) );' - '}} else {{' - 'engine_{f_name}.search( q, sync );' - '}}' - '}}' - ).format( - f_name = f_name - ) - -def typeahead_updater( f_name ): - """ The JS script creating the function triggered when an item is - selected through typeahead """ - return ( - 'function(evt, item) {{' - '$( "#{hidden_id}" ).val( item.key );' - '$( "#{hidden_id}" ).change();' - 'return item;' - '}}' - ).format( - hidden_id = hidden_id( f_name ) - ) - -def typeahead_change( f_name ): - """ The JS script creating the function triggered when an item is changed - (i.e. looses focus and value has changed since the moment it gained focus - """ - return ( - 'function(evt) {{' - 'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{' - '$( "#{hidden_id}" ).val( "" );' - '$( "#{hidden_id}" ).change();' - '}}' - '}}' - ).format( - input_id = input_id( f_name ), - hidden_id = hidden_id( f_name ) - ) - diff --git a/machines/urls.py b/machines/urls.py index dde908ce..fa9b3497 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -47,9 +47,9 @@ urlpatterns = [ url(r'^add_mx/$', views.add_mx, name='add-mx'), url(r'^edit_mx/(?P[0-9]+)$', views.edit_mx, name='edit-mx'), url(r'^del_mx/$', views.del_mx, name='del-mx'), - url(r'^add_text/$', views.add_text, name='add-text'), - url(r'^edit_text/(?P[0-9]+)$', views.edit_text, name='edit-text'), - url(r'^del_text/$', views.del_text, name='del-text'), + url(r'^add_txt/$', views.add_txt, name='add-txt'), + url(r'^edit_txt/(?P[0-9]+)$', views.edit_txt, name='edit-txt'), + url(r'^del_txt/$', views.del_txt, name='del-txt'), url(r'^add_ns/$', views.add_ns, name='add-ns'), url(r'^edit_ns/(?P[0-9]+)$', views.edit_ns, name='edit-ns'), url(r'^del_ns/$', views.del_ns, name='del-ns'), @@ -76,7 +76,7 @@ urlpatterns = [ url(r'^history/(?Pextension)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pmx)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pns)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Ptext)/(?P[0-9]+)$', views.history, name='history'), + url(r'^history/(?Ptxt)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Piptype)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Palias)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pvlan)/(?P[0-9]+)$', views.history, name='history'), diff --git a/machines/views.py b/machines/views.py index 7ac2bb63..48adf44b 100644 --- a/machines/views.py +++ b/machines/views.py @@ -79,8 +79,8 @@ from .forms import ( DelAliasForm, NsForm, DelNsForm, - TextForm, - DelTextForm, + TxtForm, + DelTxtForm, MxForm, DelMxForm, VlanForm, @@ -110,58 +110,24 @@ from .models import ( OuverturePort ) from users.models import User -from users.models import all_has_access from preferences.models import GeneralOption, OptionalMachine -from .templatetags.bootstrap_form_typeahead import hidden_id, input_id -def filter_active_interfaces(q): - """Filtre les machines autorisées à sortir sur internet dans une requête""" - return q.filter( - machine__in=Machine.objects.filter( - user__in=all_has_access() - ).filter(active=True)) \ - .select_related('domain') \ - .select_related('machine') \ - .select_related('type') \ - .select_related('ipv4') \ - .select_related('domain__extension') \ - .select_related('ipv4__ip_type').distinct() - -def all_active_interfaces(): - """Renvoie l'ensemble des machines autorisées à sortir sur internet """ - return filter_active_interfaces(Interface.objects) - -def all_active_assigned_interfaces(): - """ - Renvoie l'ensemble des machines qui ont une ipv4 assignées et disposant de - l'accès internet - """ - return all_active_interfaces().filter(ipv4__isnull=False) - -def all_active_interfaces_count(): - """ Version light seulement pour compter""" - return Interface.objects.filter( - machine__in=Machine.objects.filter(user__in=all_has_access())\ - .filter(active=True) - ) - -def all_active_assigned_interfaces_count(): - """ Version light seulement pour compter""" - return all_active_interfaces_count().filter(ipv4__isnull=False) - -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) +from re2o.templatetags.massive_bootstrap_form import hidden_id, input_id +from re2o.utils import ( + all_active_assigned_interfaces, + all_has_access, + filter_active_interfaces +) +from re2o.views import form def f_type_id( is_type_tt ): """ The id that will be used in HTML to store the value of the field type. Depends on the fact that type is generate using typeahead or not """ - return hidden_id('type') if is_type_tt else input_id('type') + return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type' def generate_ipv4_choices( form ) : - """ Generate the parameter choices for the bootstrap_form_typeahead tag + """ Generate the parameter choices for the massive_bootstrap_form tag """ f_ipv4 = form.fields['ipv4'] used_mtype_id = [] @@ -189,7 +155,7 @@ def generate_ipv4_choices( form ) : return choices def generate_ipv4_engine( is_type_tt ) : - """ Generate the parameter engine for the bootstrap_form_typeahead tag + """ Generate the parameter engine for the massive_bootstrap_form tag """ return ( 'new Bloodhound( {{' @@ -203,7 +169,7 @@ def generate_ipv4_engine( is_type_tt ) : ) def generate_ipv4_match_func( is_type_tt ) : - """ Generate the parameter match_func for the bootstrap_form_typeahead tag + """ Generate the parameter match_func for the massive_bootstrap_form tag """ return ( 'function(q, sync) {{' @@ -219,20 +185,20 @@ def generate_ipv4_match_func( is_type_tt ) : type_id = f_type_id( is_type_tt ) ) -def generate_ipv4_bft_param( form, is_type_tt ): - """ Generate all the parameters to use with the bootstrap_form_typeahead +def generate_ipv4_mbf_param( form, is_type_tt ): + """ Generate all the parameters to use with the massive_bootstrap_form tag """ i_choices = { 'ipv4': generate_ipv4_choices( form ) } i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) } i_update_on = { 'ipv4': [f_type_id( is_type_tt )] } - i_bft_param = { + i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, 'update_on': i_update_on } - return i_bft_param + return i_mbf_param @login_required def new_machine(request, userid): @@ -282,8 +248,8 @@ def new_machine(request, userid): reversion.set_comment("Création") messages.success(request, "La machine a été créée") return redirect("/users/profil/" + str(user.id)) - i_bft_param = generate_ipv4_bft_param( interface, False ) - return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface, False ) + return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def edit_interface(request, interfaceid): @@ -322,8 +288,8 @@ def edit_interface(request, interfaceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) messages.success(request, "La machine a été modifiée") return redirect("/users/profil/" + str(interface.machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_machine(request, machineid): @@ -381,8 +347,8 @@ def new_interface(request, machineid): reversion.set_comment("Création") messages.success(request, "L'interface a été ajoutée") return redirect("/users/profil/" + str(machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_interface(request, interfaceid): @@ -419,7 +385,7 @@ def add_iptype(request): reversion.set_comment("Création") messages.success(request, "Ce type d'ip a été ajouté") return redirect("/machines/index_iptype") - return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'iptypeform': iptype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -438,7 +404,7 @@ def edit_iptype(request, iptypeid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data)) messages.success(request, "Type d'ip modifié") return redirect("/machines/index_iptype/") - return form({'machineform': iptype}, 'machines/machine.html', request) + return form({'iptypeform': iptype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -456,7 +422,7 @@ def del_iptype(request): except ProtectedError: messages.error(request, "Le type d'ip %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % iptype_del) return redirect("/machines/index_iptype") - return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'iptypeform': iptype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -469,7 +435,7 @@ def add_machinetype(request): reversion.set_comment("Création") messages.success(request, "Ce type de machine a été ajouté") return redirect("/machines/index_machinetype") - return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -487,7 +453,7 @@ def edit_machinetype(request, machinetypeid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data)) messages.success(request, "Type de machine modifié") return redirect("/machines/index_machinetype/") - return form({'machineform': machinetype}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -504,7 +470,7 @@ def del_machinetype(request): except ProtectedError: messages.error(request, "Le type de machine %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % machinetype_del) return redirect("/machines/index_machinetype") - return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -517,7 +483,7 @@ def add_extension(request): reversion.set_comment("Création") messages.success(request, "Cette extension a été ajoutée") return redirect("/machines/index_extension") - return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request) + return form({'extensionform': extension}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -535,7 +501,7 @@ def edit_extension(request, extensionid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data)) messages.success(request, "Extension modifiée") return redirect("/machines/index_extension/") - return form({'machineform': extension}, 'machines/machine.html', request) + return form({'extensionform': extension}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -552,7 +518,7 @@ def del_extension(request): except ProtectedError: messages.error(request, "L'extension %s est affectée à au moins un type de machine, vous ne pouvez pas la supprimer" % extension_del) return redirect("/machines/index_extension") - return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request) + return form({'extensionform': extension}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -565,7 +531,7 @@ def add_mx(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement mx a été ajouté") return redirect("/machines/index_extension") - return form({'machineform': mx, 'interfaceform': None}, 'machines/machine.html', request) + return form({'mxform': mx}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -583,7 +549,7 @@ def edit_mx(request, mxid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data)) messages.success(request, "Mx modifié") return redirect("/machines/index_extension/") - return form({'machineform': mx}, 'machines/machine.html', request) + return form({'mxform': mx}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -600,7 +566,7 @@ def del_mx(request): except ProtectedError: messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del) return redirect("/machines/index_extension") - return form({'machineform': mx, 'interfaceform': None}, 'machines/machine.html', request) + return form({'mxform': mx}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -613,7 +579,7 @@ def add_ns(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement ns a été ajouté") return redirect("/machines/index_extension") - return form({'machineform': ns, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nsform': ns}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -631,7 +597,7 @@ def edit_ns(request, nsid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data)) messages.success(request, "Ns modifié") return redirect("/machines/index_extension/") - return form({'machineform': ns}, 'machines/machine.html', request) + return form({'nsform': ns}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -648,55 +614,55 @@ def del_ns(request): except ProtectedError: messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del) return redirect("/machines/index_extension") - return form({'machineform': ns, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nsform': ns}, 'machines/machine.html', request) @login_required @permission_required('infra') -def add_text(request): - text = TextForm(request.POST or None) - if text.is_valid(): +def add_txt(request): + txt = TxtForm(request.POST or None) + if txt.is_valid(): with transaction.atomic(), reversion.create_revision(): - text.save() + txt.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Cet enregistrement text a été ajouté") return redirect("/machines/index_extension") - return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request) + return form({'txtform': txt}, 'machines/machine.html', request) @login_required @permission_required('infra') -def edit_text(request, textid): +def edit_txt(request, txtid): try: - text_instance = Text.objects.get(pk=textid) + txt_instance = Text.objects.get(pk=txtid) except Text.DoesNotExist: messages.error(request, u"Entrée inexistante" ) return redirect("/machines/index_extension/") - text = TextForm(request.POST or None, instance=text_instance) - if text.is_valid(): + txt = TxtForm(request.POST or None, instance=txt_instance) + if txt.is_valid(): with transaction.atomic(), reversion.create_revision(): - text.save() + txt.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in text.changed_data)) - messages.success(request, "Text modifié") + reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data)) + messages.success(request, "Txt modifié") return redirect("/machines/index_extension/") - return form({'machineform': text}, 'machines/machine.html', request) + return form({'txtform': txt}, 'machines/machine.html', request) @login_required @permission_required('infra') -def del_text(request): - text = DelTextForm(request.POST or None) - if text.is_valid(): - text_dels = text.cleaned_data['text'] - for text_del in text_dels: +def del_txt(request): + txt = DelTxtForm(request.POST or None) + if txt.is_valid(): + txt_dels = txt.cleaned_data['txt'] + for txt_del in txt_dels: try: with transaction.atomic(), reversion.create_revision(): - text_del.delete() + txt_del.delete() reversion.set_user(request.user) - messages.success(request, "Le text a été supprimé") + messages.success(request, "Le txt a été supprimé") except ProtectedError: - messages.error(request, "Erreur le Text suivant %s ne peut être supprimé" % text_del) + messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del) return redirect("/machines/index_extension") - return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request) + return form({'txtform': txt}, 'machines/machine.html', request) @login_required def add_alias(request, interfaceid): @@ -724,7 +690,7 @@ def add_alias(request, interfaceid): reversion.set_comment("Création") messages.success(request, "Cet alias a été ajouté") return redirect("/machines/index_alias/" + str(interfaceid)) - return form({'machineform': alias, 'interfaceform': None}, 'machines/machine.html', request) + return form({'aliasform': alias}, 'machines/machine.html', request) @login_required def edit_alias(request, aliasid): @@ -744,7 +710,7 @@ def edit_alias(request, aliasid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data)) messages.success(request, "Alias modifié") return redirect("/machines/index_alias/" + str(alias_instance.cname.interface_parent.id)) - return form({'machineform': alias}, 'machines/machine.html', request) + return form({'aliasform': alias}, 'machines/machine.html', request) @login_required def del_alias(request, interfaceid): @@ -768,7 +734,7 @@ def del_alias(request, interfaceid): except ProtectedError: messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del) return redirect("/machines/index_alias/" + str(interfaceid)) - return form({'machineform': alias, 'interfaceform': None}, 'machines/machine.html', request) + return form({'aliasform': alias}, 'machines/machine.html', request) @login_required @@ -782,7 +748,7 @@ def add_service(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement service a été ajouté") return redirect("/machines/index_service") - return form({'machineform': service}, 'machines/machine.html', request) + return form({'serviceform': service}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -800,7 +766,7 @@ def edit_service(request, serviceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data)) messages.success(request, "Service modifié") return redirect("/machines/index_service/") - return form({'machineform': service}, 'machines/machine.html', request) + return form({'serviceform': service}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -817,7 +783,7 @@ def del_service(request): except ProtectedError: messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del) return redirect("/machines/index_service") - return form({'machineform': service}, 'machines/machine.html', request) + return form({'serviceform': service}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -830,7 +796,7 @@ def add_vlan(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement vlan a été ajouté") return redirect("/machines/index_vlan") - return form({'machineform': vlan, 'interfaceform': None}, 'machines/machine.html', request) + return form({'vlanform': vlan}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -848,7 +814,7 @@ def edit_vlan(request, vlanid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data)) messages.success(request, "Vlan modifié") return redirect("/machines/index_vlan/") - return form({'machineform': vlan}, 'machines/machine.html', request) + return form({'vlanform': vlan}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -865,7 +831,7 @@ def del_vlan(request): except ProtectedError: messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del) return redirect("/machines/index_vlan") - return form({'machineform': vlan, 'interfaceform': None}, 'machines/machine.html', request) + return form({'vlanform': vlan}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -878,7 +844,7 @@ def add_nas(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement nas a été ajouté") return redirect("/machines/index_nas") - return form({'machineform': nas, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nasform': nas}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -896,7 +862,7 @@ def edit_nas(request, nasid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data)) messages.success(request, "Nas modifié") return redirect("/machines/index_nas/") - return form({'machineform': nas}, 'machines/machine.html', request) + return form({'nasform': nas}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -913,7 +879,7 @@ def del_nas(request): except ProtectedError: messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del) return redirect("/machines/index_nas") - return form({'machineform': nas, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nasform': nas}, 'machines/machine.html', request) @login_required @permission_required('cableur') @@ -1039,11 +1005,11 @@ def history(request, object, id): except Mx.DoesNotExist: messages.error(request, "Mx inexistant") return redirect("/machines/") - elif object == 'text' and request.user.has_perms(('cableur',)): + elif object == 'txt' and request.user.has_perms(('cableur',)): try: object_instance = Text.objects.get(pk=id) except Text.DoesNotExist: - messages.error(request, "Text inexistant") + messages.error(request, "Txt inexistant") return redirect("/machines/") elif object == 'ns' and request.user.has_perms(('cableur',)): try: diff --git a/preferences/admin.py b/preferences/admin.py index a8ce9335..96b4d9e1 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -20,35 +20,53 @@ # 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. - +""" +Classes admin pour les models de preferences +""" from __future__ import unicode_literals from django.contrib import admin from reversion.admin import VersionAdmin -from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, Service, AssoOption, MailMessageOption +from .models import OptionalUser, OptionalMachine, OptionalTopologie +from .models import GeneralOption, Service, AssoOption, MailMessageOption + class OptionalUserAdmin(VersionAdmin): + """Class admin options user""" pass + class OptionalTopologieAdmin(VersionAdmin): + """Class admin options topologie""" pass + class OptionalMachineAdmin(VersionAdmin): + """Class admin options machines""" pass + class GeneralOptionAdmin(VersionAdmin): + """Class admin options générales""" pass + class ServiceAdmin(VersionAdmin): + """Class admin gestion des services de la page d'accueil""" pass + class AssoOptionAdmin(VersionAdmin): + """Class admin options de l'asso""" pass + class MailMessageOptionAdmin(VersionAdmin): + """Class admin options mail""" pass + admin.site.register(OptionalUser, OptionalUserAdmin) admin.site.register(OptionalMachine, OptionalMachineAdmin) admin.site.register(OptionalTopologie, OptionalTopologieAdmin) diff --git a/preferences/forms.py b/preferences/forms.py index 20c7ca95..51cbb885 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -19,66 +19,116 @@ # 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. +""" +Formulaire d'edition des réglages : user, machine, topologie, asso... +""" from __future__ import unicode_literals -from django.forms import ModelForm, Form, ValidationError +from django.forms import ModelForm, Form from django import forms -from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, AssoOption, MailMessageOption, Service -from django.db.models import Q +from .models import OptionalUser, OptionalMachine, OptionalTopologie +from .models import GeneralOption, AssoOption, MailMessageOption, Service + class EditOptionalUserForm(ModelForm): + """Formulaire d'édition des options de l'user. (solde, telephone..)""" class Meta: model = OptionalUser fields = '__all__' def __init__(self, *args, **kwargs): - super(EditOptionalUserForm, self).__init__(*args, **kwargs) - self.fields['is_tel_mandatory'].label = 'Exiger un numéro de téléphone' - self.fields['user_solde'].label = 'Activation du solde pour les utilisateurs' + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOptionalUserForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['is_tel_mandatory'].label = 'Exiger un numéro de\ + téléphone' + self.fields['user_solde'].label = 'Activation du solde pour\ + les utilisateurs' + class EditOptionalMachineForm(ModelForm): + """Options machines (max de machines, etc)""" class Meta: model = OptionalMachine fields = '__all__' def __init__(self, *args, **kwargs): - super(EditOptionalMachineForm, self).__init__(*args, **kwargs) - self.fields['password_machine'].label = "Possibilité d'attribuer un mot de passe par interface" - self.fields['max_lambdauser_interfaces'].label = "Maximum d'interfaces autorisées pour un user normal" - self.fields['max_lambdauser_aliases'].label = "Maximum d'alias dns autorisés pour un user normal" + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOptionalMachineForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['password_machine'].label = "Possibilité d'attribuer\ + un mot de passe par interface" + self.fields['max_lambdauser_interfaces'].label = "Maximum\ + d'interfaces autorisées pour un user normal" + self.fields['max_lambdauser_aliases'].label = "Maximum d'alias\ + dns autorisés pour un user normal" + class EditOptionalTopologieForm(ModelForm): + """Options de topologie, formulaire d'edition (vlan par default etc)""" class Meta: model = OptionalTopologie fields = '__all__' def __init__(self, *args, **kwargs): - super(EditOptionalTopologieForm, self).__init__(*args, **kwargs) - self.fields['vlan_decision_ok'].label = "Vlan où placer les machines après acceptation RADIUS" - self.fields['vlan_decision_nok'].label = "Vlan où placer les machines après rejet RADIUS" + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOptionalTopologieForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['vlan_decision_ok'].label = "Vlan où placer les\ + machines après acceptation RADIUS" + self.fields['vlan_decision_nok'].label = "Vlan où placer les\ + machines après rejet RADIUS" + class EditGeneralOptionForm(ModelForm): + """Options générales (affichages de résultats de recherche, etc)""" class Meta: model = GeneralOption fields = '__all__' def __init__(self, *args, **kwargs): - super(EditGeneralOptionForm, self).__init__(*args, **kwargs) - self.fields['search_display_page'].label = 'Resultats affichés dans une recherche' - self.fields['pagination_number'].label = 'Items par page, taille normale (ex users)' - self.fields['pagination_large_number'].label = 'Items par page, taille élevée (machines)' - self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien de reinitialisation de mot de passe (en heures)' + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditGeneralOptionForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['search_display_page'].label = 'Resultats\ + affichés dans une recherche' + self.fields['pagination_number'].label = 'Items par page,\ + taille normale (ex users)' + self.fields['pagination_large_number'].label = 'Items par page,\ + taille élevée (machines)' + self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien\ + de reinitialisation de mot de passe (en heures)' self.fields['site_name'].label = 'Nom du site web' - self.fields['email_from'].label = 'Adresse mail d\'expedition automatique' + self.fields['email_from'].label = "Adresse mail d\ + 'expedition automatique" + class EditAssoOptionForm(ModelForm): + """Options de l'asso (addresse, telephone, etc)""" class Meta: model = AssoOption fields = '__all__' def __init__(self, *args, **kwargs): - super(EditAssoOptionForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditAssoOptionForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) self.fields['name'].label = 'Nom de l\'asso' self.fields['siret'].label = 'SIRET' self.fields['adresse1'].label = 'Adresse (ligne 1)' @@ -86,22 +136,44 @@ class EditAssoOptionForm(ModelForm): self.fields['contact'].label = 'Email de contact' self.fields['telephone'].label = 'Numéro de téléphone' self.fields['pseudo'].label = 'Pseudo d\'usage' - self.fields['utilisateur_asso'].label = 'Compte utilisé pour faire les modifications depuis /admin' + self.fields['utilisateur_asso'].label = 'Compte utilisé pour\ + faire les modifications depuis /admin' + class EditMailMessageOptionForm(ModelForm): + """Formulaire d'edition des messages de bienvenue personnalisés""" class Meta: model = MailMessageOption fields = '__all__' def __init__(self, *args, **kwargs): - super(EditMailMessageOptionForm, self).__init__(*args, **kwargs) - self.fields['welcome_mail_fr'].label = 'Message dans le mail de bienvenue en français' - self.fields['welcome_mail_en'].label = 'Message dans le mail de bienvenue en anglais' + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditMailMessageOptionForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['welcome_mail_fr'].label = 'Message dans le\ + mail de bienvenue en français' + self.fields['welcome_mail_en'].label = 'Message dans le\ + mail de bienvenue en anglais' + class ServiceForm(ModelForm): + """Edition, ajout de services sur la page d'accueil""" class Meta: model = Service fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) + + class DelServiceForm(Form): - services = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), label="Enregistrements service actuels", widget=forms.CheckboxSelectMultiple) + """Suppression de services sur la page d'accueil""" + services = forms.ModelMultipleChoiceField( + queryset=Service.objects.all(), + label="Enregistrements service actuels", + widget=forms.CheckboxSelectMultiple + ) diff --git a/preferences/models.py b/preferences/models.py index 34c4c0b1..dc1412e7 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -20,26 +20,38 @@ # 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. - +""" +Reglages généraux, machines, utilisateurs, mail, general pour l'application. +""" from __future__ import unicode_literals from django.db import models from cotisations.models import Paiement -from machines.models import Vlan + class OptionalUser(models.Model): + """Options pour l'user : obligation ou nom du telephone, + activation ou non du solde, autorisation du negatif, fingerprint etc""" PRETTY_NAME = "Options utilisateur" is_tel_mandatory = models.BooleanField(default=True) user_solde = models.BooleanField(default=False) - solde_negatif = models.DecimalField(max_digits=5, decimal_places=2, default=0) + solde_negatif = models.DecimalField( + max_digits=5, + decimal_places=2, + default=0 + ) gpg_fingerprint = models.BooleanField(default=True) def clean(self): + """Creation du mode de paiement par solde""" if self.user_solde: Paiement.objects.get_or_create(moyen="Solde") + class OptionalMachine(models.Model): + """Options pour les machines : maximum de machines ou d'alias par user + sans droit, activation de l'ipv6""" PRETTY_NAME = "Options machines" password_machine = models.BooleanField(default=False) @@ -47,21 +59,43 @@ class OptionalMachine(models.Model): max_lambdauser_aliases = models.IntegerField(default=10) ipv6 = models.BooleanField(default=False) + class OptionalTopologie(models.Model): + """Reglages pour la topologie : mode d'accès radius, vlan où placer + les machines en accept ou reject""" PRETTY_NAME = "Options topologie" MACHINE = 'MACHINE' DEFINED = 'DEFINED' CHOICE_RADIUS = ( - (MACHINE, 'Sur le vlan de la plage ip machine'), - (DEFINED, 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"'), + (MACHINE, 'Sur le vlan de la plage ip machine'), + (DEFINED, 'Prédéfini dans "Vlan où placer les machines\ + après acceptation RADIUS"'), + ) + + radius_general_policy = models.CharField( + max_length=32, + choices=CHOICE_RADIUS, + default='DEFINED' + ) + vlan_decision_ok = models.OneToOneField( + 'machines.Vlan', + on_delete=models.PROTECT, + related_name='decision_ok', + blank=True, + null=True + ) + vlan_decision_nok = models.OneToOneField( + 'machines.Vlan', + on_delete=models.PROTECT, + related_name='decision_nok', + blank=True, + null=True ) - radius_general_policy = models.CharField(max_length=32, choices=CHOICE_RADIUS, default='DEFINED') - vlan_decision_ok = models.OneToOneField('machines.Vlan', on_delete=models.PROTECT, related_name='decision_ok', blank=True, null=True) - vlan_decision_nok = models.OneToOneField('machines.Vlan', on_delete=models.PROTECT, related_name='decision_nok', blank=True, null=True) - class GeneralOption(models.Model): + """Options générales : nombre de resultats par page, nom du site, + temps où les liens sont valides""" PRETTY_NAME = "Options générales" search_display_page = models.IntegerField(default=15) @@ -71,30 +105,44 @@ class GeneralOption(models.Model): site_name = models.CharField(max_length=32, default="Re2o") email_from = models.EmailField(default="www-data@serveur.net") + class Service(models.Model): + """Liste des services affichés sur la page d'accueil : url, description, + image et nom""" name = models.CharField(max_length=32) url = models.URLField() description = models.TextField() - image = models.ImageField(upload_to='logo', blank=True) + image = models.ImageField(upload_to='logo', blank=True) def __str__(self): return str(self.name) + class AssoOption(models.Model): + """Options générales de l'asso : siret, addresse, nom, etc""" PRETTY_NAME = "Options de l'association" - name = models.CharField(default="Association réseau école machin", max_length=256) + name = models.CharField( + default="Association réseau école machin", + max_length=256 + ) siret = models.CharField(default="00000000000000", max_length=32) adresse1 = models.CharField(default="1 Rue de exemple", max_length=128) adresse2 = models.CharField(default="94230 Cachan", max_length=128) contact = models.EmailField(default="contact@example.org") telephone = models.CharField(max_length=15, default="0000000000") pseudo = models.CharField(default="Asso", max_length=32) - utilisateur_asso = models.OneToOneField('users.User', on_delete=models.PROTECT, blank=True, null=True) + utilisateur_asso = models.OneToOneField( + 'users.User', + on_delete=models.PROTECT, + blank=True, + null=True + ) + class MailMessageOption(models.Model): + """Reglages, mail de bienvenue et autre""" PRETTY_NAME = "Options de corps de mail" welcome_mail_fr = models.TextField(default="") welcome_mail_en = models.TextField(default="") - diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 25fa4c02..02f006c1 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load massive_bootstrap_form %} {% block title %}Création et modification des préférences{% endblock %} @@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} -{% bootstrap_form options %} +{% massive_bootstrap_form options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/preferences/urls.py b/preferences/urls.py index 624d2e75..2169f83c 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -19,6 +19,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. +""" +Urls de l'application preferences, pointant vers les fonctions de views +""" from __future__ import unicode_literals @@ -28,15 +31,47 @@ from . import views urlpatterns = [ - url(r'^edit_options/(?P
OptionalUser)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
OptionalMachine)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
OptionalTopologie)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
GeneralOption)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
AssoOption)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
MailMessageOption)$', views.edit_options, name='edit-options'), + url( + r'^edit_options/(?P
OptionalUser)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
OptionalMachine)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
OptionalTopologie)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
GeneralOption)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
AssoOption)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
MailMessageOption)$', + views.edit_options, + name='edit-options' + ), url(r'^add_services/$', views.add_services, name='add-services'), - url(r'^edit_services/(?P[0-9]+)$', views.edit_services, name='edit-services'), + url( + r'^edit_services/(?P[0-9]+)$', + views.edit_services, + name='edit-services' + ), url(r'^del_services/$', views.del_services, name='del-services'), - url(r'^history/(?Pservice)/(?P[0-9]+)$', views.history, name='history'), + url( + r'^history/(?Pservice)/(?P[0-9]+)$', + views.history, + name='history' + ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 5fe1cff5..1e2c433e 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -23,48 +23,53 @@ # App de gestion des machines pour re2o # Gabriel Détraz, Augustin Lemesle # Gplv2 +""" +Vue d'affichage, et de modification des réglages (réglages machine, +topologie, users, service...) +""" from __future__ import unicode_literals -from django.shortcuts import render -from django.shortcuts import get_object_or_404, render, redirect -from django.template.context_processors import csrf +from django.shortcuts import render, redirect from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import Max, ProtectedError -from django.db import IntegrityError -from django.core.mail import send_mail -from django.utils import timezone -from django.core.urlresolvers import reverse +from django.db.models import ProtectedError from django.db import transaction from reversion.models import Version from reversion import revisions as reversion +from re2o.views import form from .forms import ServiceForm, DelServiceForm -from .models import Service, OptionalUser, OptionalMachine, AssoOption, MailMessageOption, GeneralOption, OptionalTopologie +from .models import Service, OptionalUser, OptionalMachine, AssoOption +from .models import MailMessageOption, GeneralOption, OptionalTopologie from . import models from . import forms -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) - @login_required @permission_required('cableur') def display_options(request): - useroptions, created = OptionalUser.objects.get_or_create() - machineoptions, created = OptionalMachine.objects.get_or_create() - topologieoptions, created = OptionalTopologie.objects.get_or_create() - generaloptions, created = GeneralOption.objects.get_or_create() - assooptions, created = AssoOption.objects.get_or_create() - mailmessageoptions, created = MailMessageOption.objects.get_or_create() + """Vue pour affichage des options (en vrac) classé selon les models + correspondants dans un tableau""" + useroptions, _created = OptionalUser.objects.get_or_create() + machineoptions, _created = OptionalMachine.objects.get_or_create() + topologieoptions, _created = OptionalTopologie.objects.get_or_create() + generaloptions, _created = GeneralOption.objects.get_or_create() + assooptions, _created = AssoOption.objects.get_or_create() + mailmessageoptions, _created = MailMessageOption.objects.get_or_create() service_list = Service.objects.all() - return form({'useroptions': useroptions, 'machineoptions': machineoptions, 'topologieoptions': topologieoptions, 'generaloptions': generaloptions, 'assooptions' : assooptions, 'mailmessageoptions' : mailmessageoptions, 'service_list':service_list}, 'preferences/display_preferences.html', request) + return form({ + 'useroptions': useroptions, + 'machineoptions': machineoptions, + 'topologieoptions': topologieoptions, + 'generaloptions': generaloptions, + 'assooptions': assooptions, + 'mailmessageoptions': mailmessageoptions, + 'service_list': service_list + }, 'preferences/display_preferences.html', request) + @login_required @permission_required('admin') @@ -73,23 +78,36 @@ def edit_options(request, section): model = getattr(models, section, None) form_instance = getattr(forms, 'Edit' + section + 'Form', None) if model and form: - options_instance, created = model.objects.get_or_create() - options = form_instance(request.POST or None, instance=options_instance) + options_instance, _created = model.objects.get_or_create() + options = form_instance( + request.POST or None, + instance=options_instance + ) if options.is_valid(): with transaction.atomic(), reversion.create_revision(): options.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in options.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in options.changed_data + ) + ) messages.success(request, "Préférences modifiées") return redirect("/preferences/") - return form({'options': options}, 'preferences/edit_preferences.html', request) + return form( + {'options': options}, + 'preferences/edit_preferences.html', + request + ) else: messages.error(request, "Objet inconnu") return redirect("/preferences/") + @login_required @permission_required('admin') def add_services(request): + """Ajout d'un service de la page d'accueil""" services = ServiceForm(request.POST or None) if services.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -98,29 +116,45 @@ def add_services(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement ns a été ajouté") return redirect("/preferences/") - return form({'preferenceform': services}, 'preferences/preferences.html', request) + return form( + {'preferenceform': services}, + 'preferences/preferences.html', + request + ) + @login_required @permission_required('admin') def edit_services(request, servicesid): + """Edition des services affichés sur la page d'accueil""" try: services_instance = Service.objects.get(pk=servicesid) except Service.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/preferences/") services = ServiceForm(request.POST or None, instance=services_instance) if services.is_valid(): with transaction.atomic(), reversion.create_revision(): services.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in services.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in services.changed_data + ) + ) messages.success(request, "Service modifié") return redirect("/preferences/") - return form({'preferenceform': services}, 'preferences/preferences.html', request) + return form( + {'preferenceform': services}, + 'preferences/preferences.html', + request + ) + @login_required @permission_required('admin') def del_services(request): + """Suppression d'un service de la page d'accueil""" services = DelServiceForm(request.POST or None) if services.is_valid(): services_dels = services.cleaned_data['services'] @@ -131,20 +165,28 @@ def del_services(request): reversion.set_user(request.user) messages.success(request, "Le services a été supprimée") except ProtectedError: - messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % services_del) + messages.error(request, "Erreur le service\ + suivant %s ne peut être supprimé" % services_del) return redirect("/preferences/") - return form({'preferenceform': services}, 'preferences/preferences.html', request) + return form( + {'preferenceform': services}, + 'preferences/preferences.html', + request + ) + @login_required @permission_required('cableur') -def history(request, object, id): - if object == 'service': +def history(request, object_name, object_id): + """Historique de creation et de modification d'un service affiché sur + la page d'accueil""" + if object_name == 'service': try: - object_instance = Service.objects.get(pk=id) + object_instance = Service.objects.get(pk=object_id) except Service.DoesNotExist: - messages.error(request, "Service inexistant") - return redirect("/preferences/") - options, created = GeneralOption.objects.get_or_create() + messages.error(request, "Service inexistant") + return redirect("/preferences/") + 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) @@ -157,4 +199,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/re2o/context_processors.py b/re2o/context_processors.py index ed4769b5..e562a347 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -19,15 +19,19 @@ # 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. +"""Fonction de context, variables renvoyées à toutes les vues""" + from __future__ import unicode_literals -from machines.models import Interface, Machine from preferences.models import GeneralOption, OptionalMachine + def context_user(request): - general_options, created = GeneralOption.objects.get_or_create() - machine_options, created = OptionalMachine.objects.get_or_create() + """Fonction de context lorsqu'un user est logué (ou non), + renvoie les infos sur l'user, la liste de ses droits, ses machines""" + general_options, _created = GeneralOption.objects.get_or_create() + machine_options, _created = OptionalMachine.objects.get_or_create() user = request.user if user.is_authenticated(): interfaces = user.user_interfaces() @@ -52,8 +56,8 @@ def context_user(request): 'is_bofh': is_bofh, 'is_trez': is_trez, 'is_infra': is_infra, - 'is_admin' : is_admin, + 'is_admin': is_admin, 'interfaces': interfaces, 'site_name': general_options.site_name, - 'ipv6_enabled' : machine_options.ipv6, + 'ipv6_enabled': machine_options.ipv6, } diff --git a/machines/templatetags/__init__.py b/re2o/templatetags/__init__.py similarity index 100% rename from machines/templatetags/__init__.py rename to re2o/templatetags/__init__.py diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py new file mode 100644 index 00000000..df7edc1f --- /dev/null +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -0,0 +1,572 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django import template +from django.utils.safestring import mark_safe +from django.forms import TextInput +from django.forms.widgets import Select +from bootstrap3.templatetags.bootstrap3 import bootstrap_form +from bootstrap3.utils import render_tag +from bootstrap3.forms import render_field + +register = template.Library() + +@register.simple_tag +def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): + """ + Render a form where some specific fields are rendered using Twitter + Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the + speed and UX when dealing with very large datasets (select with 50k+ elts + for instance). + When the fields specified should normally be rendered as a select with + single selectable option, Twitter Typeahead is used for a better display + and the matching query engine. When dealing with multiple selectable + options, sliptree's Bootstrap Tokenfield in addition with Typeahead. + For convenience, it accepts the same parameters as a standard bootstrap + can accept. + + **Tag name**:: + + massive_bootstrap_form + + **Parameters**: + + form (required) + The form that is to be rendered + + mbf_fields (optional) + A list of field names (comma separated) that should be rendered + with Typeahead/Tokenfield instead of the default bootstrap + renderer. + If not specified, all fields will be rendered as a normal bootstrap + field. + + mbf_param (optional) + A dict of parameters for the massive_bootstrap_form tag. The + possible parameters are the following. + + choices (optional) + A dict of strings representing the choices in JS. The keys of + the dict are the names of the concerned fields. The choices + must be an array of objects. Each of those objects must at + least have the fields 'key' (value to send) and 'value' (value + to display). Other fields can be added as desired. + For a more complex structure you should also consider + reimplementing the engine and the match_func. + If not specified, the key is the id of the object and the value + is its string representation as in a normal bootstrap form. + Example : + 'choices' : { + 'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]', + 'field_B':..., + ... + } + + engine (optional) + A dict of strings representating the engine used for matching + queries and possible values with typeahead. The keys of the + dict are the names of the concerned fields. The string is valid + JS code. + If not specified, BloodHound with relevant basic properties is + used. + Example : + 'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} + + match_func (optional) + A dict of strings representing a valid JS function used in the + dataset to overload the matching engine. The keys of the dict + are the names of the concerned fields. This function is used + the source of the dataset. This function receives 2 parameters, + the query and the synchronize function as specified in + typeahead.js documentation. If needed, the local variables + 'choices_' and 'engine_' contains + respectively the array of all possible values and the engine + to match queries with possible values. + If not specified, the function used display up to the 10 first + elements if the query is empty and else the matching results. + Example : + 'match_func' : { + 'field_A': 'function(q, sync) { engine.search(q, sync); }', + 'field_B': ..., + ... + } + + update_on (optional) + A dict of list of ids that the values depends on. The engine + and the typeahead properties are recalculated and reapplied. + Example : + 'addition' : { + 'field_A' : [ 'id0', 'id1', ... ] , + 'field_B' : ... , + ... + } + + See boostrap_form_ for other arguments + + **Usage**:: + + {% massive_bootstrap_form + form + [ '[,[,...]]' ] + [ mbf_param = { + [ 'choices': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'engine': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'match_func': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'update_on': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + } ] + [ ] + %} + + **Example**: + + {% massive_bootstrap_form form 'ipv4' choices='[...]' %} + """ + + fields = mbf_fields.split(',') + param = kwargs.pop('mbf_param', {}) + exclude = param.get('exclude', '').split(',') + choices = param.get('choices', {}) + engine = param.get('engine', {}) + match_func = param.get('match_func', {}) + update_on = param.get('update_on', {}) + hidden_fields = [h.name for h in form.hidden_fields()] + + html = '' + + for f_name, f_value in form.fields.items() : + if not f_name in exclude : + if f_name in fields and not f_name in hidden_fields : + + if not isinstance(f_value.widget, Select) : + raise ValueError( + ('Field named {f_name} from {form} is not a Select and' + 'can\'t be rendered with massive_bootstrap_form.' + ).format( + f_name=f_name, + form=form + ) + ) + + multiple = f_value.widget.allow_multiple_selected + f_bound = f_value.get_bound_field( form, f_name ) + + f_value.widget = TextInput( + attrs = { + 'name': 'mbf_'+f_name, + 'placeholder': f_value.empty_label + } + ) + html += render_field( + f_value.get_bound_field( form, f_name ), + *args, + **kwargs + ) + + if multiple : + content = mbf_js( + f_name, + f_value, + f_bound, + multiple, + choices, + engine, + match_func, + update_on + ) + else : + content = hidden_tag( f_bound, f_name ) + mbf_js( + f_name, + f_value, + f_bound, + multiple, + choices, + engine, + match_func, + update_on + ) + html += render_tag( + 'div', + content = content, + attrs = { 'id': custom_div_id( f_bound ) } + ) + + else: + html += render_field( + f_value.get_bound_field( form, f_name ), + *args, + **kwargs + ) + + return mark_safe( html ) + +def input_id( f_bound ) : + """ The id of the HTML input element """ + return f_bound.auto_id + +def hidden_id( f_bound ): + """ The id of the HTML hidden input element """ + return input_id( f_bound ) + '_hidden' + +def custom_div_id( f_bound ): + """ The id of the HTML div element containing values and script """ + return input_id( f_bound ) + '_div' + +def hidden_tag( f_bound, f_name ): + """ The HTML hidden input element """ + return render_tag( + 'input', + attrs={ + 'id': hidden_id( f_bound ), + 'name': f_bound.html_name, + 'type': 'hidden', + 'value': f_bound.value() or "" + } + ) + +def mbf_js( f_name, f_value, f_bound, multiple, + choices_, engine_, match_func_, update_on_ ) : + """ The whole script to use """ + + choices = ( mark_safe( choices_[f_name] ) if f_name in choices_.keys() + else default_choices( f_value ) ) + + engine = ( mark_safe( engine_[f_name] ) if f_name in engine_.keys() + else default_engine ( f_name ) ) + + match_func = ( mark_safe( match_func_[f_name] ) + if f_name in match_func_.keys() else default_match_func( f_name ) ) + + update_on = update_on_[f_name] if f_name in update_on_.keys() else [] + + if multiple : + js_content = ( + 'var choices_{f_name} = {choices};' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).tokenfield( "destroy" );' + '$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});' + '}};' + '$( "#{input_id}" ).bind( "tokenfield:createtoken", {create} );' + '$( "#{input_id}" ).bind( "tokenfield:edittoken", {edit} );' + '$( "#{input_id}" ).bind( "tokenfield:removetoken", {remove} );' + '{updates}' + '$( "#{input_id}" ).ready( function() {{' + 'setup_{f_name}();' + '{init_input}' + '}} );' + ).format( + f_name = f_name, + choices = choices, + engine = engine, + input_id = input_id( f_bound ), + datasets = default_datasets( f_name, match_func ), + create = tokenfield_create( f_name, f_bound ), + edit = tokenfield_edit( f_name, f_bound ), + remove = tokenfield_remove( f_name, f_bound ), + updates = ''.join( [ ( + '$( "#{u_id}" ).change( function() {{' + 'setup_{f_name}();' + '{reset_input}' + '}} );' + ).format( + u_id = u_id, + reset_input = tokenfield_reset_input( f_bound ), + f_name = f_name + ) for u_id in update_on ] + ), + init_input = tokenfield_init_input( f_name, f_bound ), + ) + else : + js_content = ( + 'var choices_{f_name} = {choices};' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).typeahead( "destroy" );' + '$( "#{input_id}" ).typeahead( {datasets} );' + '}};' + '$( "#{input_id}" ).bind( "typeahead:select", {select} );' + '$( "#{input_id}" ).bind( "typeahead:change", {change} );' + '{updates}' + '$( "#{input_id}" ).ready( function() {{' + 'setup_{f_name}();' + '{init_input}' + '}} );' + ).format( + f_name = f_name, + choices = choices, + engine = engine, + input_id = input_id( f_bound ), + datasets = default_datasets( f_name, match_func ), + select = typeahead_select( f_bound ), + change = typeahead_change( f_bound ), + updates = ''.join( [ ( + '$( "#{u_id}" ).change( function() {{' + 'setup_{f_name}();' + '{reset_input}' + '}} );' + ).format( + u_id = u_id, + reset_input = typeahead_reset_input( f_bound ), + f_name = f_name + ) for u_id in update_on ] + ), + init_input = typeahead_init_input( f_name, f_bound ), + ) + + return render_tag( 'script', content=mark_safe( js_content ) ) + +def typeahead_init_input( f_name, f_bound ) : + """ The JS script to init the fields values """ + init_key = f_bound.value() or '""' + return ( + '$( "#{input_id}" ).typeahead("val", {init_val});' + '$( "#{hidden_id}" ).val( {init_key} );' + ).format( + input_id = input_id( f_bound ), + init_val = '""' if init_key == '""' else + 'engine_{f_name}.get( {init_key} )[0].value'.format( + f_name = f_name, + init_key = init_key + ), + init_key = init_key, + hidden_id = hidden_id( f_bound ) + ) + +def typeahead_reset_input( f_bound ) : + """ The JS script to reset the fields values """ + return ( + '$( "#{input_id}" ).typeahead("val", "");' + '$( "#{hidden_id}" ).val( "" );' + ).format( + input_id = input_id( f_bound ), + hidden_id = hidden_id( f_bound ) + ) + +def tokenfield_init_input( f_name, f_bound ) : + """ The JS script to init the fields values """ + init_key = f_bound.value() or '""' + return ( + '$( "#{input_id}" ).tokenfield("setTokens", {init_val});' + ).format( + input_id = input_id( f_bound ), + init_val = '""' if init_key == '""' else ( + 'engine_{f_name}.get( {init_key} ).map(' + 'function(o) {{ return o.value; }}' + ')').format( + f_name = f_name, + init_key = init_key + ), + init_key = init_key, + ) + +def tokenfield_reset_input( f_bound ) : + """ The JS script to reset the fields values """ + return ( + '$( "#{input_id}" ).tokenfield("setTokens", "");' + ).format( + input_id = input_id( f_bound ), + ) + +def default_choices( f_value ) : + """ The JS script creating the variable choices_ """ + return '[{objects}]'.format( + objects = ','.join( + [ '{{key:{k},value:"{v}"}}'.format( + k = choice[0] if choice[0] != '' else '""', + v = choice[1] + ) for choice in f_value.choices ] + ) + ) + +def default_engine ( f_name ) : + """ The JS script creating the variable engine_ """ + return ( + 'new Bloodhound({{' + 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' + 'queryTokenizer: Bloodhound.tokenizers.whitespace,' + 'local: choices_{f_name},' + 'identify: function(obj) {{ return obj.key; }}' + '}})' + ).format( + f_name = f_name + ) + +def default_datasets( f_name, match_func ) : + """ The JS script creating the datasets to use with typeahead """ + return ( + '{{' + 'hint: true,' + 'highlight: true,' + 'minLength: 0' + '}},' + '{{' + 'display: "value",' + 'name: "{f_name}",' + 'source: {match_func}' + '}}' + ).format( + f_name = f_name, + match_func = match_func + ) + +def default_match_func ( f_name ) : + """ The JS script creating the matching function to use with typeahed """ + return ( + 'function ( q, sync ) {{' + 'if ( q === "" ) {{' + 'var first = choices_{f_name}.slice( 0, 5 ).map(' + 'function ( obj ) {{ return obj.key; }}' + ');' + 'sync( engine_{f_name}.get( first ) );' + '}} else {{' + 'engine_{f_name}.search( q, sync );' + '}}' + '}}' + ).format( + f_name = f_name + ) + +def typeahead_select( f_bound ): + """ The JS script creating the function triggered when an item is + selected through typeahead """ + return ( + 'function(evt, item) {{' + '$( "#{hidden_id}" ).val( item.key );' + '$( "#{hidden_id}" ).change();' + 'return item;' + '}}' + ).format( + hidden_id = hidden_id( f_bound ) + ) + +def typeahead_change( f_bound ): + """ The JS script creating the function triggered when an item is changed + (i.e. looses focus and value has changed since the moment it gained focus + """ + return ( + 'function(evt) {{' + 'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{' + '$( "#{hidden_id}" ).val( "" );' + '$( "#{hidden_id}" ).change();' + '}}' + '}}' + ).format( + input_id = input_id( f_bound ), + hidden_id = hidden_id( f_bound ) + ) + +def tokenfield_create( f_name, f_bound ): + """ The JS script triggered when a new token is created in tokenfield. """ + return ( + 'function(evt) {{' + 'var k = evt.attrs.key;' + 'if (!k) {{' + 'var data = evt.attrs.value;' + 'var i = 0;' + 'while ( i= 0) + this._delimiters[whitespace] = '\\s' + + if (dash >= 0) { + delete this._delimiters[dash] + this._delimiters.unshift('-') + } + + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] + $.each(this._delimiters, function (index, character) { + var pos = $.inArray(character, specialCharacters) + if (pos >= 0) _self._delimiters[index] = '\\' + character; + }); + + // Store original input width + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null + , elStyleWidth = element.style.width + , elCSSWidth + , elWidth = this.$element.width() + + if (elRules) { + $.each( elRules, function (i, rule) { + if (rule.style.width) { + elCSSWidth = rule.style.width; + } + }); + } + + // Move original input out of the way + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', + originalStyles = { position: this.$element.css('position') }; + originalStyles[hidingPosition] = this.$element.css(hidingPosition); + + this.$element + .data('original-styles', originalStyles) + .data('original-tabindex', this.$element.prop('tabindex')) + .css('position', 'absolute') + .css(hidingPosition, '-10000px') + .prop('tabindex', -1) + + // Create a wrapper + this.$wrapper = $('
') + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') + + // Create a new input + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) + this.$input = $('') + .appendTo( this.$wrapper ) + .prop( 'placeholder', this.$element.prop('placeholder') ) + .prop( 'id', id + '-tokenfield' ) + .prop( 'tabindex', this.$element.data('original-tabindex') ) + + // Re-route original input label to new input + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$input.prop('id') ) + } + + // Set up a copy helper to handle copy & paste + this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) + + // Set wrapper width + if (elStyleWidth) { + this.$wrapper.css('width', elStyleWidth); + } + else if (elCSSWidth) { + this.$wrapper.css('width', elCSSWidth); + } + // If input is inside inline-form with no width set, set fixed width + else if (this.$element.parents('.form-inline').length) { + this.$wrapper.width( elWidth ) + } + + // Set tokenfield disabled, if original or fieldset input is disabled + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { + this.disable(); + } + + // Set tokenfield readonly, if original input is readonly + if (this.$element.prop('readonly')) { + this.readonly(); + } + + // Set up mirror for input auto-sizing + this.$mirror = $(''); + this.$input.css('min-width', this.options.minWidth + 'px') + $.each([ + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'letterSpacing', + 'textTransform', + 'wordSpacing', + 'textIndent' + ], function (i, val) { + _self.$mirror[0].style[val] = _self.$input.css(val); + }); + this.$mirror.appendTo( 'body' ) + + // Insert tokenfield to HTML + this.$wrapper.insertBefore( this.$element ) + this.$element.prependTo( this.$wrapper ) + + // Calculate inner input width + this.update() + + // Create initial tokens, if any + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) + + // Start listening to events + this.listen() + + // Initialize autocomplete, if necessary + if ( ! $.isEmptyObject( this.options.autocomplete ) ) { + var side = this.textDirection === 'rtl' ? 'right' : 'left' + , autocompleteOptions = $.extend({ + minLength: this.options.showAutocompleteOnFocus ? 0 : null, + position: { my: side + " top", at: side + " bottom", of: this.$wrapper } + }, this.options.autocomplete ) + + this.$input.autocomplete( autocompleteOptions ) + } + + // Initialize typeahead, if necessary + if ( ! $.isEmptyObject( this.options.typeahead ) ) { + + var typeaheadOptions = this.options.typeahead + , defaults = { + minLength: this.options.showAutocompleteOnFocus ? 0 : null + } + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] + + args[0] = $.extend( {}, defaults, args[0] ) + + this.$input.typeahead.apply( this.$input, args ) + this.typeahead = true + } + } + + Tokenfield.prototype = { + + constructor: Tokenfield + + , createToken: function (attrs, triggerChange) { + var _self = this + + if (typeof attrs === 'string') { + attrs = { value: attrs, label: attrs } + } else { + // Copy objects to prevent contamination of data sources. + attrs = $.extend( {}, attrs ) + } + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + // Normalize label and value + attrs.value = $.trim(attrs.value.toString()); + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value + + // Bail out if has no value or label, or label is too short + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return + + // Bail out if maximum number of tokens is reached + if (this.options.limit && this.getTokens().length >= this.options.limit) return + + // Allow changing token data before creating it + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) + this.$element.trigger(createEvent) + + // Bail out if there if attributes are empty or event was defaultPrevented + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return + + var $token = $('
') + .append('') + .append('×') + .data('attrs', attrs) + + // Insert token into HTML + if (this.$input.hasClass('tt-input')) { + // If the input has typeahead enabled, insert token before it's parent + this.$input.parent().before( $token ) + } else { + this.$input.before( $token ) + } + + // Temporarily set input width to minimum + this.$input.css('width', this.options.minWidth + 'px') + + var $tokenLabel = $token.find('.token-label') + , $closeButton = $token.find('.close') + + // Determine maximum possible token label width + if (!this.maxTokenWidth) { + this.maxTokenWidth = + this.$wrapper.width() - $closeButton.outerWidth() - + parseInt($closeButton.css('margin-left'), 10) - + parseInt($closeButton.css('margin-right'), 10) - + parseInt($token.css('border-left-width'), 10) - + parseInt($token.css('border-right-width'), 10) - + parseInt($token.css('padding-left'), 10) - + parseInt($token.css('padding-right'), 10) + parseInt($tokenLabel.css('border-left-width'), 10) - + parseInt($tokenLabel.css('border-right-width'), 10) - + parseInt($tokenLabel.css('padding-left'), 10) - + parseInt($tokenLabel.css('padding-right'), 10) + parseInt($tokenLabel.css('margin-left'), 10) - + parseInt($tokenLabel.css('margin-right'), 10) + } + + $tokenLabel.css('max-width', this.maxTokenWidth) + if (this.options.html) + $tokenLabel.html(attrs.label) + else + $tokenLabel.text(attrs.label) + + // Listen to events on token + $token + .on('mousedown', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = true + }) + .on('click', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = false + + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + return _self.toggle( $token ) + } + + _self.activate( $token, e.shiftKey, e.shiftKey ) + }) + .on('dblclick', function (e) { + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false + _self.edit( $token ) + }) + + $closeButton + .on('click', $.proxy(this.remove, this)) + + // Trigger createdtoken event on the original field + // indicating that the token is now in the DOM + this.$element.trigger($.Event('tokenfield:createdtoken', { + attrs: attrs, + relatedTarget: $token.get(0) + })) + + // Trigger change event on the original field + if (triggerChange) { + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) + } + + // Update tokenfield dimensions + var _self = this + setTimeout(function () { + _self.update() + }, 0) + + // Return original element + return this.$element.get(0) + } + + , setTokens: function (tokens, add, triggerChange) { + if (!add) this.$wrapper.find('.token').remove() + + if (!tokens) return + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + if (typeof tokens === 'string') { + if (this._delimiters.length) { + // Split based on delimiters + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) + } else { + tokens = [tokens]; + } + } + + var _self = this + $.each(tokens, function (i, attrs) { + _self.createToken(attrs, triggerChange) + }) + + return this.$element.get(0) + } + + , getTokenData: function($token) { + var data = $token.map(function() { + var $token = $(this); + return $token.data('attrs') + }).get(); + + if (data.length == 1) { + data = data[0]; + } + + return data; + } + + , getTokens: function(active) { + var self = this + , tokens = [] + , activeClass = active ? '.active' : '' // get active tokens only + this.$wrapper.find( '.token' + activeClass ).each( function() { + tokens.push( self.getTokenData( $(this) ) ) + }) + return tokens + } + + , getTokensList: function(delimiter, beautify, active) { + delimiter = delimiter || this._firstDelimiter + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify + + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') + return $.map( this.getTokens(active), function (token) { + return token.value + }).join(separator) + } + + , getInput: function() { + return this.$input.val() + } + + , setInput: function (val) { + if (this.$input.hasClass('tt-input')) { + // Typeahead acts weird when simply setting input value to empty, + // so we set the query to empty instead + this.$input.typeahead('val', val) + } else { + this.$input.val(val) + } + } + + , listen: function () { + var _self = this + + this.$element + .on('change', $.proxy(this.change, this)) + + this.$wrapper + .on('mousedown',$.proxy(this.focusInput, this)) + + this.$input + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('paste', $.proxy(this.paste, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + this.$copyHelper + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keyup', $.proxy(this.keyup, this)) + + // Secondary listeners for input width calculation + this.$input + .on('keypress', $.proxy(this.update, this)) + .on('keyup', $.proxy(this.update, this)) + + this.$input + .on('autocompletecreate', function() { + // Set minimum autocomplete menu width + var $_menuElement = $(this).data('ui-autocomplete').menu.element + + var minWidth = _self.$wrapper.outerWidth() - + parseInt( $_menuElement.css('border-left-width'), 10 ) - + parseInt( $_menuElement.css('border-right-width'), 10 ) + + $_menuElement.css( 'min-width', minWidth + 'px' ) + }) + .on('autocompleteselect', function (e, ui) { + if (_self.createToken( ui.item )) { + _self.$input.val('') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + return false + }) + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { + // Create token + if (_self.createToken( datum )) { + _self.$input.typeahead('val', '') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + }) + + // Listen to window resize + $(window).on('resize', $.proxy(this.update, this )) + + } + + , keydown: function (e) { + + if (!this.focused) return + + var _self = this + + switch(e.keyCode) { + case 8: // backspace + if (!this.$input.is(document.activeElement)) break + this.lastInputValue = this.$input.val() + break + + case 37: // left arrow + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) + break + + case 38: // up arrow + upDown('prev') + break + + case 39: // right arrow + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) + break + + case 40: // down arrow + upDown('next') + break + + case 65: // a (to handle ctrl + a) + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break + this.activateAll() + e.preventDefault() + break + + case 9: // tab + case 13: // enter + + // We will handle creating tokens from autocomplete in autocomplete events + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break + + // We will handle creating tokens from typeahead in typeahead events + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break + + // Create token + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { + return this.createTokensFromInput(e, this.$input.data('edit')); + } + + // Edit token + if (e.keyCode === 13) { + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break + if (!_self.options.allowEditing) break + this.edit( this.$wrapper.find('.token.active') ) + } + } + + function leftRight(direction) { + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + direction += 'All' + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') + if (!$token.length) return + + _self.preventInputFocus = true + _self.preventDeactivation = true + + _self.activate( $token ) + e.preventDefault() + + } else { + _self[direction]( e.shiftKey ) + e.preventDefault() + } + } + + function upDown(direction) { + if (!e.shiftKey) return + + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') + if (!$token.length) return + + _self.activate( $token ) + } + + var opposite = direction === 'prev' ? 'next' : 'prev' + , position = direction === 'prev' ? 'first' : 'last' + + _self.$firstActiveToken[opposite + 'All']('.token').each(function() { + _self.deactivate( $(this) ) + }) + + _self.activate( _self.$wrapper.find('.token:' + position), true, true ) + e.preventDefault() + } + + this.lastKeyDown = e.keyCode + } + + , keypress: function(e) { + + // Comma + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { + if (this.$input.val()) { + this.createTokensFromInput(e) + } + return false; + } + } + + , keyup: function (e) { + this.preventInputFocus = false + + if (!this.focused) return + + switch(e.keyCode) { + case 8: // backspace + if (this.$input.is(document.activeElement)) { + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break + + this.preventDeactivation = true + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') + + if (!$prevToken.length) break + + this.activate( $prevToken ) + } else { + this.remove(e) + } + break + + case 46: // delete + this.remove(e, 'next') + break + } + this.lastKeyUp = e.keyCode + } + + , focus: function (e) { + this.focused = true + this.$wrapper.addClass('focus') + + if (this.$input.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + + if (this.options.showAutocompleteOnFocus) { + this.search() + } + } + } + + , blur: function (e) { + + this.focused = false + this.$wrapper.removeClass('focus') + + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + } + + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { + this.createTokensFromInput(e) + } + + this.preventDeactivation = false + this.preventCreateTokens = false + } + + , paste: function (e) { + var _self = this + + // Add tokens to existing ones + if (_self.options.allowPasting) { + setTimeout(function () { + _self.createTokensFromInput(e) + }, 1) + } + } + + , change: function (e) { + if ( e.initiator === 'tokenfield' ) return // Prevent loops + + this.setTokens( this.$element.val() ) + } + + , createTokensFromInput: function (e, focus) { + if (this.$input.val().length < this.options.minLength) + return // No input, simply return + + var tokensBefore = this.getTokensList() + this.setTokens( this.$input.val(), true ) + + if (tokensBefore == this.getTokensList() && this.$input.val().length) + return false // No tokens were added, do nothing (prevent form submit) + + this.setInput('') + + if (this.$input.data( 'edit' )) { + this.unedit(focus) + } + + return false // Prevent form being submitted + } + + , next: function (add) { + if (add) { + var $firstActiveToken = this.$wrapper.find('.active:first') + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $firstActiveToken ) + } + + var $lastActiveToken = this.$wrapper.find('.active:last') + , $nextToken = $lastActiveToken.nextAll('.token:first') + + if (!$nextToken.length) { + this.$input.focus() + return + } + + this.activate($nextToken, add) + } + + , prev: function (add) { + + if (add) { + var $lastActiveToken = this.$wrapper.find('.active:last') + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $lastActiveToken ) + } + + var $firstActiveToken = this.$wrapper.find('.active:first') + , $prevToken = $firstActiveToken.prevAll('.token:first') + + if (!$prevToken.length) { + $prevToken = this.$wrapper.find('.token:first') + } + + if (!$prevToken.length && !add) { + this.$input.focus() + return + } + + this.activate( $prevToken, add ) + } + + , activate: function ($token, add, multi, remember) { + + if (!$token) return + + if (typeof remember === 'undefined') var remember = true + + if (multi) var add = true + + this.$copyHelper.focus() + + if (!add) { + this.$wrapper.find('.active').removeClass('active') + if (remember) { + this.$firstActiveToken = $token + } else { + delete this.$firstActiveToken + } + } + + if (multi && this.$firstActiveToken) { + // Determine first active token and the current tokens indicies + // Account for the 1 hidden textarea by subtracting 1 from both + var i = this.$firstActiveToken.index() - 2 + , a = $token.index() - 2 + , _self = this + + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { + _self.activate( $(this), true ) + }) + } + + $token.addClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , activateAll: function() { + var _self = this + + this.$wrapper.find('.token').each( function (i) { + _self.activate($(this), i !== 0, false, false) + }) + } + + , deactivate: function($token) { + if (!$token) return + + $token.removeClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , toggle: function($token) { + if (!$token) return + + $token.toggleClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , edit: function ($token) { + if (!$token) return + + var attrs = $token.data('attrs') + + // Allow changing input value before editing + var options = { attrs: attrs, relatedTarget: $token.get(0) } + var editEvent = $.Event('tokenfield:edittoken', options) + this.$element.trigger( editEvent ) + + // Edit event can be cancelled if default is prevented + if (editEvent.isDefaultPrevented()) return + + $token.find('.token-label').text(attrs.value) + var tokenWidth = $token.outerWidth() + + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + + $token.replaceWith( $_input ) + + this.preventCreateTokens = true + + this.$input.val( attrs.value ) + .select() + .data( 'edit', true ) + .width( tokenWidth ) + + this.update(); + + // Indicate that token is now being edited, and is replaced with an input field in the DOM + this.$element.trigger($.Event('tokenfield:editedtoken', options )) + } + + , unedit: function (focus) { + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + $_input.appendTo( this.$wrapper ) + + this.$input.data('edit', false) + this.$mirror.text('') + + this.update() + + // Because moving the input element around in DOM + // will cause it to lose focus, we provide an option + // to re-focus the input after appending it to the wrapper + if (focus) { + var _self = this + setTimeout(function () { + _self.$input.focus() + }, 1) + } + } + + , remove: function (e, direction) { + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return + + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') + + if (e.type !== 'click') { + if (!direction) var direction = 'prev' + this[direction]() + + // Was it the first token? + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 + } + + // Prepare events and their options + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } + , removeEvent = $.Event('tokenfield:removetoken', options) + + this.$element.trigger(removeEvent); + + // Remove event can be intercepted and cancelled + if (removeEvent.isDefaultPrevented()) return + + var removedEvent = $.Event('tokenfield:removedtoken', options) + , changeEvent = $.Event('change', { initiator: 'tokenfield' }) + + // Remove token from DOM + $token.remove() + + // Trigger events + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) + + // Focus, when necessary: + // When there are no more tokens, or if this was the first token + // and it was removed with backspace or it was clicked on + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() + + // Adjust input width + this.$input.css('width', this.options.minWidth + 'px') + this.update() + + // Cancel original event handlers + e.preventDefault() + e.stopPropagation() + } + + /** + * Update tokenfield dimensions + */ + , update: function (e) { + var value = this.$input.val() + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) + , inputPadding = inputPaddingLeft + inputPaddingRight + + if (this.$input.data('edit')) { + + if (!value) { + value = this.$input.prop("placeholder") + } + if (value === this.$mirror.text()) return + + this.$mirror.text(value) + + var mirrorWidth = this.$mirror.width() + 10; + if ( mirrorWidth > this.$wrapper.width() ) { + return this.$input.width( this.$wrapper.width() ) + } + + this.$input.width( mirrorWidth ) + } + else { + //temporary reset width to minimal value to get proper results + this.$input.width(this.options.minWidth); + + var w = (this.textDirection === 'rtl') + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; + // + // some usecases pre-render widget before attaching to DOM, + // dimensions returned by jquery will be NaN -> we default to 100% + // so placeholder won't be cut off. + isNaN(w) ? this.$input.width('100%') : this.$input.width(w); + } + } + + , focusInput: function (e) { + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return + // Focus only after the current call stack has cleared, + // otherwise has no effect. + // Reason: mousedown is too early - input will lose focus + // after mousedown. However, since the input may be moved + // in DOM, there may be no click or mouseup event triggered. + var _self = this + setTimeout(function() { + _self.$input.focus() + }, 0) + } + + , search: function () { + if ( this.$input.data('ui-autocomplete') ) { + this.$input.autocomplete('search') + } + } + + , disable: function () { + this.setProperty('disabled', true); + } + + , enable: function () { + this.setProperty('disabled', false); + } + + , readonly: function () { + this.setProperty('readonly', true); + } + + , writeable: function () { + this.setProperty('readonly', false); + } + + , setProperty: function(property, value) { + this['_' + property] = value; + this.$input.prop(property, value); + this.$element.prop(property, value); + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); + } + + , destroy: function() { + // Set field value + this.$element.val( this.getTokensList() ); + // Restore styles and properties + this.$element.css( this.$element.data('original-styles') ); + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); + + // Re-route tokenfield label to original input + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$element.prop('id') ) + } + + // Move original element outside of tokenfield wrapper + this.$element.insertBefore( this.$wrapper ); + + // Remove tokenfield-related data + this.$element.removeData('original-styles') + .removeData('original-tabindex') + .removeData('bs.tokenfield'); + + // Remove tokenfield from DOM + this.$wrapper.remove(); + this.$mirror.remove(); + + var $_element = this.$element; + + return $_element; + } + + } + + + /* TOKENFIELD PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.tokenfield + + $.fn.tokenfield = function (option, param) { + var value + , args = [] + + Array.prototype.push.apply( args, arguments ); + + var elements = this.each(function () { + var $this = $(this) + , data = $this.data('bs.tokenfield') + , options = typeof option == 'object' && option + + if (typeof option === 'string' && data && data[option]) { + args.shift() + value = data[option].apply(data, args) + } else { + if (!data && typeof option !== 'string' && !param) { + $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) + $this.trigger('tokenfield:initialize') + } + } + }) + + return typeof value !== 'undefined' ? value : elements; + } + + $.fn.tokenfield.defaults = { + minWidth: 60, + minLength: 0, + html: true, + allowEditing: true, + allowPasting: true, + limit: 0, + autocomplete: {}, + typeahead: {}, + showAutocompleteOnFocus: false, + createTokensOnBlur: false, + delimiter: ',', + beautify: true, + inputType: 'text' + } + + $.fn.tokenfield.Constructor = Tokenfield + + + /* TOKENFIELD NO CONFLICT + * ================== */ + + $.fn.tokenfield.noConflict = function () { + $.fn.tokenfield = old + return this + } + + return Tokenfield; + +})); diff --git a/static/js/handlebars/LICENSE b/static/js/handlebars/LICENSE new file mode 100644 index 00000000..b802d14e --- /dev/null +++ b/static/js/handlebars/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2011-2017 by Yehuda Katz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/static/js/handlebars.js b/static/js/handlebars/handlebars.js similarity index 100% rename from static/js/handlebars.js rename to static/js/handlebars/handlebars.js diff --git a/static/js/konami/LICENSE.md b/static/js/konami/LICENSE.md new file mode 100644 index 00000000..ee46fc71 --- /dev/null +++ b/static/js/konami/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Snaptortoise + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/static/js/konami/konami.js b/static/js/konami/konami.js new file mode 100644 index 00000000..0fcaab2c --- /dev/null +++ b/static/js/konami/konami.js @@ -0,0 +1,139 @@ +/* + * Konami-JS ~ + * :: Now with support for touch events and multiple instances for + * :: those situations that call for multiple easter eggs! + * Code: https://github.com/snaptortoise/konami-js + * Examples: http://www.snaptortoise.com/konami-js + * Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com) + * Version: 1.5.1 (9/4/2017) + * Licensed under the MIT License (http://opensource.org/licenses/MIT) + * Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android + */ + +var Konami = function (callback) { + var konami = { + addEvent: function (obj, type, fn, ref_obj) { + if (obj.addEventListener) + obj.addEventListener(type, fn, false); + else if (obj.attachEvent) { + // IE + obj["e" + type + fn] = fn; + obj[type + fn] = function () { + obj["e" + type + fn](window.event, ref_obj); + } + obj.attachEvent("on" + type, obj[type + fn]); + } + }, + removeEvent: function (obj, eventName, eventCallback) { + if (obj.removeEventListener) { + obj.removeEventListener(eventName, eventCallback); + } else if (obj.attachEvent) { + obj.detachEvent(eventName); + } + }, + input: "", + pattern: "38384040373937396665", + keydownHandler: function (e, ref_obj) { + if (ref_obj) { + konami = ref_obj; + } // IE + konami.input += e ? e.keyCode : event.keyCode; + if (konami.input.length > konami.pattern.length) { + konami.input = konami.input.substr((konami.input.length - konami.pattern.length)); + } + if (konami.input === konami.pattern) { + konami.code(this._currentlink); + konami.input = ''; + e.preventDefault(); + return false; + } + }, + load: function (link) { + this.addEvent(document, "keydown", this.keydownHandler, this); + this.iphone.load(link); + }, + unload: function () { + this.removeEvent(document, 'keydown', this.keydownHandler); + this.iphone.unload(); + }, + code: function (link) { + window.location = link + }, + iphone: { + start_x: 0, + start_y: 0, + stop_x: 0, + stop_y: 0, + tap: false, + capture: false, + orig_keys: "", + keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"], + input: [], + code: function (link) { + konami.code(link); + }, + touchmoveHandler: function (e) { + if (e.touches.length === 1 && konami.iphone.capture === true) { + var touch = e.touches[0]; + konami.iphone.stop_x = touch.pageX; + konami.iphone.stop_y = touch.pageY; + konami.iphone.tap = false; + konami.iphone.capture = false; + konami.iphone.check_direction(); + } + }, + toucheendHandler: function () { + if (konami.iphone.tap === true) { + konami.iphone.check_direction(this._currentLink); + } + }, + touchstartHandler: function (e) { + konami.iphone.start_x = e.changedTouches[0].pageX; + konami.iphone.start_y = e.changedTouches[0].pageY; + konami.iphone.tap = true; + konami.iphone.capture = true; + }, + load: function (link) { + this.orig_keys = this.keys; + konami.addEvent(document, "touchmove", this.touchmoveHandler); + konami.addEvent(document, "touchend", this.toucheendHandler, false); + konami.addEvent(document, "touchstart", this.touchstartHandler); + }, + unload: function () { + konami.removeEvent(document, 'touchmove', this.touchmoveHandler); + konami.removeEvent(document, 'touchend', this.toucheendHandler); + konami.removeEvent(document, 'touchstart', this.touchstartHandler); + }, + check_direction: function () { + x_magnitude = Math.abs(this.start_x - this.stop_x); + y_magnitude = Math.abs(this.start_y - this.stop_y); + x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT"; + y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP"; + result = (x_magnitude > y_magnitude) ? x : y; + result = (this.tap === true) ? "TAP" : result; + return result; + } + } + } + + typeof callback === "string" && konami.load(callback); + if (typeof callback === "function") { + konami.code = callback; + konami.load(); + } + + return konami; +}; + + +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = Konami; +} else { + if (typeof define === 'function' && define.amd) { + define([], function() { + return Konami; + }); + } else { + window.Konami = Konami; + } +} diff --git a/static/js/sapphire.js b/static/js/sapphire.js new file mode 100644 index 00000000..db65fdd1 --- /dev/null +++ b/static/js/sapphire.js @@ -0,0 +1,316 @@ +// Re2o est un logiciel d'administration développé initiallement au rezometz. Il +// se veut agnostique au réseau considéré, de manière à être installable en +// quelques clics. +// +// Copyright © 2017 Maël Kervella +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +// General options +//===================================== +// Times the canvas is refreshed a second +var FPS = 30; +// Determine the length of the trail (0=instant disappear, maximum=window.innerHeight=no disappear) +var TRAIL_TIME = 5; +// The color of the characters +var RAIN_COLOR = "#00F"; +// The characters displayed +var CHARACTERS = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split(""); +// The font size used to display the characters +var FONT_SIZE = 10; +// The maximum number of characters displayed by column +var MAX_CHAR = 7; + +var Sapphire = function () { + var sapphire = { + triggerHandle: undefined, + activated: false, + runOnce: false, + + getClass: function(elt, main, name) { elt.obj = main.getElementsByClassName(name); }, + getTag: function(elt, main, name) { elt.obj = main.getElementsByTagName(name); }, + + getProp: function(elt) { + for (var i=0 ; i sapphire.canvas.height && Math.random() > 0.975) + sapphire.drops[i][j] = 0; + sapphire.drops[i][j]++; + } + } + } + + function drawEverything() { + attenuateBackground(); + drawMatrixRainDrop(); + } + + sapphire.resize(); + window.addEventListener('resize', sapphire.resize); + sapphire.triggerHandle = setInterval(drawEverything, 1000/FPS); + }, + + stop: function() { + window.removeEventListener('resize', sapphire.resize); + clearInterval(sapphire.triggerHandle); + sapphire.canvas.parentNode.removeChild(sapphire.canvas); + }, + + alterElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].alter(main); } }, + revertElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].revert(main); } }, + + activate: function() { + if (!sapphire.runOnce) { + sapphire.runOnce = true; + sapphire.init(); + } + if (!sapphire.activated) { + sapphire.activated = true; + sapphire.alterElts(); + sapphire.run() + } + else { + sapphire.activated = false; + sapphire.stop(); + sapphire.revertElts(); + } + } + } + + return sapphire; +} + +var s = Sapphire(); +Konami(s.activate); + diff --git a/static/js/typeahead/LICENSE b/static/js/typeahead/LICENSE new file mode 100644 index 00000000..83817bac --- /dev/null +++ b/static/js/typeahead/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-2014 Twitter, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/static/js/typeahead.js b/static/js/typeahead/typeahead.js similarity index 100% rename from static/js/typeahead.js rename to static/js/typeahead/typeahead.js 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/templates/base.html b/templates/base.html index bd1a4bb1..dfaca5eb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -33,10 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Load CSS and JavaScript #} {% bootstrap_css %} + + {% comment %}{% endcomment %} {% bootstrap_javascript %} - - + + + + + + {% comment %}{% endcomment %} {{ site_name }} : {% block title %}Accueil{% endblock %} 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 87a3917d..267d64b0 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -19,52 +19,113 @@ # 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__' + def __init__(self, *args, **kwargs): + 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): - super(EditPortForm, self).__init__(*args, **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') + 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') + 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__' + def __init__(self, *args, **kwargs): + 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__' def __init__(self, *args, **kwargs): - super(EditSwitchForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) 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'] + def __init__(self, *args, **kwargs): + 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__' + 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/templates/topologie/aff_stacks.html b/topologie/templates/topologie/aff_stacks.html index 34e7b959..586fd90a 100644 --- a/topologie/templates/topologie/aff_stacks.html +++ b/topologie/templates/topologie/aff_stacks.html @@ -30,32 +30,53 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - {% for stack in stack_list %} - {% for switch in stack.switch_set.all %} - - {% if forloop.first %} - - - - {% endif %} - - {% if forloop.first %} - - {% endif %} - - {% endfor %} - {% endfor %} + + {% for stack in stack_list %} + {% for switch in stack.switch_set.all %} + + + {% if forloop.first %} + + + + {% endif %} + + {% if forloop.first %} + + {% endif %} + + {% empty %} + + + + + + + {% endfor %} + + {% endfor %}
Zone concernéeEnregistrementZone concernéeEnregistrement
{{ text.zone }}{{ text.dns_entry }}{{ txt.zone }}{{ txt.dns_entry }} {% if is_infra %} - {% include 'buttons/edit.html' with href='machines:edit-text' id=text.id %} + {% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %} {% endif %} - {% include 'buttons/history.html' with href='machines:history' name='text' id=text.id %} + {% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %}
Details Membres
{{stack.name}}{{stack.stack_id}}{{stack.details}}{{switch}} - - - - {% if is_infra %} - - - - - - - {% endif %} -
{{stack.name}}{{stack.stack_id}}{{stack.details}}{{switch}} + + + + {% if is_infra %} + + + + + + + {% endif %} +
{{stack.name}}{{stack.stack_id}}{{stack.details}}Aucun + + + + {% if is_infra %} + + + + + + + {% endif %} +
diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index 51cec7f6..249097e7 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'un switch{% endblock %} @@ -46,13 +47,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if topoform %} - {% bootstrap_form topoform %} + {% massive_bootstrap_form topoform 'switch_interface' %} {% endif %} {% if machineform %} - {% bootstrap_form machineform %} + {% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %} - {% bootstrap_form interfaceform %} + {% if i_bft_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} + {% else %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} + {% endif %} {% endif %} {% if domainform %} {% bootstrap_form domainform %} diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index fea858e4..e14b72a7 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load massive_bootstrap_form %} {% block title %}Création et modificationd 'utilisateur{% endblock %} @@ -32,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} - {% bootstrap_form topoform %} + {% massive_bootstrap_form topoform 'room,related,machine_interface' %} {%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %}

diff --git a/topologie/urls.py b/topologie/urls.py index f4537ac5..4d0a6779 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -19,6 +19,12 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Definition des urls de l'application topologie. +Inclu dans urls de re2o. + +Fait référence aux fonctions du views +""" from __future__ import unicode_literals @@ -33,18 +39,33 @@ urlpatterns = [ url(r'^new_room/$', views.new_room, name='new-room'), url(r'^edit_room/(?P[0-9]+)$', views.edit_room, name='edit-room'), url(r'^del_room/(?P[0-9]+)$', views.del_room, name='del-room'), - url(r'^switch/(?P[0-9]+)$', views.index_port, name='index-port'), - url(r'^history/(?Pswitch)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pport)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Proom)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pstack)/(?P[0-9]+)$', views.history, name='history'), + url(r'^switch/(?P[0-9]+)$', + views.index_port, + name='index-port'), + url(r'^history/(?Pswitch)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pport)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Proom)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pstack)/(?P[0-9]+)$', + views.history, + name='history'), url(r'^edit_port/(?P[0-9]+)$', views.edit_port, name='edit-port'), url(r'^new_port/(?P[0-9]+)$', views.new_port, name='new-port'), url(r'^del_port/(?P[0-9]+)$', views.del_port, name='del-port'), - url(r'^edit_switch/(?P[0-9]+)$', views.edit_switch, name='edit-switch'), + url(r'^edit_switch/(?P[0-9]+)$', + views.edit_switch, + name='edit-switch'), url(r'^new_stack/$', views.new_stack, name='new-stack'), url(r'^index_stack/$', views.index_stack, name='index-stack'), - url(r'^edit_stack/(?P[0-9]+)$', views.edit_stack, name='edit-stack'), - url(r'^del_stack/(?P[0-9]+)$', views.del_stack, name='del-stack'), + url(r'^edit_stack/(?P[0-9]+)$', + views.edit_stack, + name='edit-stack'), + url(r'^del_stack/(?P[0-9]+)$', + views.del_stack, + name='del-stack'), ] - diff --git a/topologie/views.py b/topologie/views.py index 42cd09e7..f1e8740c 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -19,7 +19,20 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Page des vues de l'application topologie +Permet de créer, modifier et supprimer : +- un port (add_port, edit_port, del_port) +- un switch : les vues d'ajout et d'édition font appel aux forms de creation +de switch, mais aussi aux forms de machines.forms (domain, interface et +machine). Le views les envoie et les save en même temps. TODO : rationaliser +et faire que la creation de machines (interfaces, domain etc) soit gérée +coté models et forms de topologie +- une chambre (new_room, edit_room, del_room) +- une stack +- l'historique de tous les objets cités +""" from __future__ import unicode_literals from django.shortcuts import render, redirect @@ -33,11 +46,12 @@ from reversion import revisions as reversion from reversion.models import Version from topologie.models import Switch, Port, Room, Stack -from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm, AddPortForm, EditRoomForm, StackForm +from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm +from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form -from users.models import User from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from machines.views import generate_ipv4_mbf_param from preferences.models import AssoOption, GeneralOption @@ -45,41 +59,52 @@ from preferences.models import AssoOption, GeneralOption @permission_required('cableur') def index(request): """ Vue d'affichage de tous les swicthes""" - switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack') - return render(request, 'topologie/index.html', {'switch_list': switch_list}) + switch_list = Switch.objects.order_by( + 'stack', + 'stack_member_id', + 'location' + )\ + .select_related('switch_interface__domain__extension')\ + .select_related('switch_interface__ipv4')\ + .select_related('switch_interface__domain')\ + .select_related('stack') + return render(request, 'topologie/index.html', { + 'switch_list': switch_list + }) + @login_required @permission_required('cableur') -def history(request, object, id): +def history(request, object_name, object_id): """ Vue générique pour afficher l'historique complet d'un objet""" - if object == 'switch': + if object_name == 'switch': try: - object_instance = Switch.objects.get(pk=id) + object_instance = Switch.objects.get(pk=object_id) except Switch.DoesNotExist: - messages.error(request, "Switch inexistant") - return redirect("/topologie/") - elif object == 'port': + messages.error(request, "Switch inexistant") + return redirect("/topologie/") + elif object_name == 'port': try: - object_instance = Port.objects.get(pk=id) + object_instance = Port.objects.get(pk=object_id) except Port.DoesNotExist: - messages.error(request, "Port inexistant") - return redirect("/topologie/") - elif object == 'room': + messages.error(request, "Port inexistant") + return redirect("/topologie/") + elif object_name == 'room': try: - object_instance = Room.objects.get(pk=id) + object_instance = Room.objects.get(pk=object_id) except Room.DoesNotExist: - messages.error(request, "Chambre inexistante") - return redirect("/topologie/") - elif object == 'stack': + messages.error(request, "Chambre inexistante") + return redirect("/topologie/") + elif object_name == 'stack': try: - object_instance = Stack.objects.get(pk=id) + object_instance = Stack.objects.get(pk=object_id) except Room.DoesNotExist: - messages.error(request, "Stack inexistante") - return redirect("/topologie/") + messages.error(request, "Stack inexistante") + return redirect("/topologie/") else: messages.error(request, "Objet inconnu") return redirect("/topologie/") - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) @@ -92,7 +117,11 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render(request, 're2o/history.html', { + 'reversions': reversions, + 'object': object_instance + }) + @login_required @permission_required('cableur') @@ -103,15 +132,25 @@ def index_port(request, switch_id): except Switch.DoesNotExist: messages.error(request, u"Switch inexistant") return redirect("/topologie/") - port_list = Port.objects.filter(switch = switch).select_related('room').select_related('machine_interface__domain__extension').select_related('related').select_related('switch').order_by('port') - return render(request, 'topologie/index_p.html', {'port_list':port_list, 'id_switch':switch_id, 'nom_switch':switch}) + port_list = Port.objects.filter(switch=switch)\ + .select_related('room')\ + .select_related('machine_interface__domain__extension')\ + .select_related('related')\ + .select_related('switch')\ + .order_by('port') + return render(request, 'topologie/index_p.html', { + 'port_list': port_list, + 'id_switch': switch_id, + 'nom_switch': switch + }) + @login_required @permission_required('cableur') def index_room(request): """ Affichage de l'ensemble des chambres""" room_list = Room.objects.order_by('name') - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number paginator = Paginator(room_list, pagination_number) page = request.GET.get('page') @@ -123,13 +162,20 @@ def index_room(request): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. room_list = paginator.page(paginator.num_pages) - return render(request, 'topologie/index_room.html', {'room_list': room_list}) + return render(request, 'topologie/index_room.html', { + 'room_list': room_list + }) + @login_required @permission_required('infra') def index_stack(request): - stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension') - return render(request, 'topologie/index_stack.html', {'stack_list': stack_list}) + """Affichage de la liste des stacks (affiche l'ensemble des switches)""" + stack_list = Stack.objects.order_by('name')\ + .prefetch_related('switch_set__switch_interface__domain__extension') + return render(request, 'topologie/index_stack.html', { + 'stack_list': stack_list + }) @login_required @@ -152,16 +198,24 @@ def new_port(request, switch_id): reversion.set_comment("Création") messages.success(request, "Port ajouté") except IntegrityError: - messages.error(request,"Ce port existe déjà" ) + messages.error(request, "Ce port existe déjà") return redirect("/topologie/switch/" + switch_id) - return form({'topoform':port}, 'topologie/topo.html', request) + return form({'topoform': port}, 'topologie/topo.html', request) + @login_required @permission_required('infra') def edit_port(request, port_id): - """ Edition d'un port. Permet de changer le switch parent et l'affectation du port""" + """ Edition d'un port. Permet de changer le switch parent et + l'affectation du port""" try: - port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id) + port_object = Port.objects\ + .select_related('switch__switch_interface__domain__extension')\ + .select_related('machine_interface__domain__extension')\ + .select_related('machine_interface__switch')\ + .select_related('room')\ + .select_related('related')\ + .get(pk=port_id) except Port.DoesNotExist: messages.error(request, u"Port inexistant") return redirect("/topologie/") @@ -170,14 +224,17 @@ def edit_port(request, port_id): with transaction.atomic(), reversion.create_revision(): port.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in port.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in port.changed_data + )) messages.success(request, "Le port a bien été modifié") return redirect("/topologie/switch/" + str(port_object.switch.id)) - return form({'topoform':port}, 'topologie/topo.html', request) + return form({'topoform': port}, 'topologie/topo.html', request) + @login_required @permission_required('infra') -def del_port(request,port_id): +def del_port(request, port_id): """ Supprime le port""" try: port = Port.objects.get(pk=port_id) @@ -192,30 +249,30 @@ def del_port(request,port_id): reversion.set_comment("Destruction") messages.success(request, "Le port a eté détruit") except ProtectedError: - messages.error(request, "Le port %s est affecté à un autre objet, impossible de le supprimer" % port) + messages.error(request, "Le port %s est affecté à un autre objet,\ + impossible de le supprimer" % port) return redirect('/topologie/switch/' + str(port.switch.id)) - return form({'objet':port}, 'topologie/delete.html', request) + return form({'objet': port}, 'topologie/delete.html', request) + @login_required @permission_required('infra') def new_stack(request): + """Ajoute un nouveau stack : stack_id_min, max, et nombre de switches""" stack = StackForm(request.POST or None) - #if stack.is_valid(): - if request.POST: - try: - with transaction.atomic(), reversion.create_revision(): - stack.save() - reversion.set_user(request.user) - reversion.set_comment("Création") - messages.success(request, "Stack crée") - except: - messages.error(request, "Cette stack existe déjà") - return form({'topoform':stack}, 'topologie/topo.html', request) + if stack.is_valid(): + with transaction.atomic(), reversion.create_revision(): + stack.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Stack crée") + return form({'topoform': stack}, 'topologie/topo.html', request) @login_required @permission_required('infra') -def edit_stack(request,stack_id): +def edit_stack(request, stack_id): + """Edition d'un stack (nombre de switches, nom...)""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -226,13 +283,19 @@ def edit_stack(request,stack_id): with transaction.atomic(), reversion.create_revision(): stack.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in stack.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in stack.changed_data + ) + ) return redirect('/topologie/index_stack') - return form({'topoform':stack}, 'topologie/topo.html', request) + return form({'topoform': stack}, 'topologie/topo.html', request) + @login_required @permission_required('infra') -def del_stack(request,stack_id): +def del_stack(request, stack_id): + """Supprime un stack""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -246,13 +309,16 @@ def del_stack(request,stack_id): reversion.set_comment("Destruction") messages.success(request, "La stack a eté détruite") except ProtectedError: - messages.error(request, "La stack %s est affectée à un autre objet, impossible de la supprimer" % stack) + messages.error(request, "La stack %s est affectée à un autre\ + objet, impossible de la supprimer" % stack) return redirect('/topologie/index_stack') - return form({'objet':stack}, 'topologie/delete.html', request) + return form({'objet': stack}, 'topologie/delete.html', request) + @login_required @permission_required('infra') -def edit_switchs_stack(request,stack_id): +def edit_switchs_stack(request, stack_id): + """Permet d'éditer la liste des switches dans une stack et l'ajouter""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -264,30 +330,36 @@ def edit_switchs_stack(request,stack_id): context = {'stack': stack} context['switchs_stack'] = stack.switchs_set.all() context['switchs_autres'] = Switch.object.filter(stack=None) - pass @login_required @permission_required('infra') def new_switch(request): - """ Creation d'un switch. Cree en meme temps l'interface et la machine associée. - Vue complexe. Appelle successivement les 4 models forms adaptés : machine, - interface, domain et switch""" + """ Creation d'un switch. Cree en meme temps l'interface et la machine + associée. Vue complexe. Appelle successivement les 4 models forms + adaptés : machine, interface, domain et switch""" switch = NewSwitchForm(request.POST or None) machine = NewMachineForm(request.POST or None) - interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) - domain = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',))) + interface = AddInterfaceForm( + request.POST or None, + infra=request.user.has_perms(('infra',)) + ) + domain = AliasForm( + request.POST or None, + infra=request.user.has_perms(('infra',)) + ) if switch.is_valid() and machine.is_valid() and interface.is_valid(): - options, created = AssoOption.objects.get_or_create() + options, _created = AssoOption.objects.get_or_create() user = options.utilisateur_asso if not user: - messages.error(request, "L'user association n'existe pas encore, veuillez le créer ou le linker dans preferences") + messages.error(request, "L'user association n'existe pas encore,\ + veuillez le créer ou le linker dans preferences") return redirect("/topologie/") new_machine = machine.save(commit=False) new_machine.user = user new_interface = interface.save(commit=False) - new_switch = switch.save(commit=False) - new_domain = domain.save(commit=False) + new_switch_instance = switch.save(commit=False) + new_domain_instance = domain.save(commit=False) with transaction.atomic(), reversion.create_revision(): new_machine.save() reversion.set_user(request.user) @@ -297,58 +369,95 @@ def new_switch(request): new_interface.save() reversion.set_user(request.user) reversion.set_comment("Création") - new_domain.interface_parent = new_interface + new_domain_instance.interface_parent = new_interface with transaction.atomic(), reversion.create_revision(): - new_domain.save() + new_domain_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") - new_switch.switch_interface = new_interface + new_switch_instance.switch_interface = new_interface with transaction.atomic(), reversion.create_revision(): - new_switch.save() + new_switch_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") - messages.success(request, "Le switch a été crée") + messages.success(request, "Le switch a été créé") return redirect("/topologie/") - return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'topologie/switch.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface, False ) + return form({ + 'topoform': switch, + 'machineform': machine, + 'interfaceform': interface, + 'domainform': domain, + 'i_mbf_param': i_mbf_param + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') def edit_switch(request, switch_id): - """ Edition d'un switch. Permet de chambre nombre de ports, place dans le stack, - interface et machine associée""" + """ Edition d'un switch. Permet de chambre nombre de ports, + place dans le stack, interface et machine associée""" try: switch = Switch.objects.get(pk=switch_id) except Switch.DoesNotExist: messages.error(request, u"Switch inexistant") return redirect("/topologie/") switch_form = EditSwitchForm(request.POST or None, instance=switch) - machine_form = EditMachineForm(request.POST or None, instance=switch.switch_interface.machine) - interface_form = EditInterfaceForm(request.POST or None, instance=switch.switch_interface) - domain_form = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',)), instance=switch.switch_interface.domain) - if switch_form.is_valid() and machine_form.is_valid() and interface_form.is_valid(): + machine_form = EditMachineForm( + request.POST or None, + instance=switch.switch_interface.machine + ) + interface_form = EditInterfaceForm( + request.POST or None, + instance=switch.switch_interface + ) + domain_form = AliasForm( + request.POST or None, + infra=request.user.has_perms(('infra',)), + instance=switch.switch_interface.domain + ) + if switch_form.is_valid() and machine_form.is_valid()\ + and interface_form.is_valid(): new_interface = interface_form.save(commit=False) new_machine = machine_form.save(commit=False) - new_switch = switch_form.save(commit=False) + new_switch_instance = switch_form.save(commit=False) new_domain = domain_form.save(commit=False) with transaction.atomic(), reversion.create_revision(): new_machine.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machine_form.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in machine_form.changed_data + ) + ) with transaction.atomic(), reversion.create_revision(): new_interface.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in interface_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in interface_form.changed_data) + ) with transaction.atomic(), reversion.create_revision(): new_domain.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in domain_form.changed_data) + ) with transaction.atomic(), reversion.create_revision(): - new_switch.save() + new_switch_instance.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in switch_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in switch_form.changed_data) + ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'topologie/switch.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({ + 'topoform': switch_form, + 'machineform': machine_form, + 'interfaceform': interface_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') @@ -362,7 +471,8 @@ def new_room(request): reversion.set_comment("Création") messages.success(request, "La chambre a été créé") return redirect("/topologie/index_room/") - return form({'topoform':room}, 'topologie/topo.html', request) + return form({'topoform': room}, 'topologie/topo.html', request) + @login_required @permission_required('infra') @@ -378,10 +488,13 @@ def edit_room(request, room_id): with transaction.atomic(), reversion.create_revision(): room.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in room.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in room.changed_data) + ) messages.success(request, "La chambre a bien été modifiée") return redirect("/topologie/index_room/") - return form({'topoform':room}, 'topologie/topo.html', request) + return form({'topoform': room}, 'topologie/topo.html', request) + @login_required @permission_required('infra') @@ -390,7 +503,7 @@ def del_room(request, room_id): try: room = Room.objects.get(pk=room_id) except Room.DoesNotExist: - messages.error(request, u"Chambre inexistante" ) + messages.error(request, u"Chambre inexistante") return redirect("/topologie/index_room/") if request.method == "POST": try: @@ -400,6 +513,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/admin.py b/users/admin.py index 6a1e0e74..0c71064a 100644 --- a/users/admin.py +++ b/users/admin.py @@ -20,6 +20,10 @@ # 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 vues pour les admin. Classique, sauf pour users, +où on fait appel à UserChange et ServiceUserChange, forms custom +""" from __future__ import unicode_literals @@ -28,11 +32,15 @@ from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from reversion.admin import VersionAdmin -from .models import User, ServiceUser, School, Right, ListRight, ListShell, Ban, Whitelist, Request, LdapUser, LdapServiceUser, LdapServiceUserGroup, LdapUserGroup -from .forms import UserChangeForm, UserCreationForm, ServiceUserChangeForm, ServiceUserCreationForm +from .models import User, ServiceUser, School, Right, ListRight, ListShell +from .models import Ban, Whitelist, Request, LdapUser, LdapServiceUser +from .models import LdapServiceUserGroup, LdapUserGroup +from .forms import UserChangeForm, UserCreationForm +from .forms import ServiceUserChangeForm, ServiceUserCreationForm class UserAdmin(admin.ModelAdmin): + """Administration d'un user""" list_display = ( 'name', 'surname', @@ -43,51 +51,73 @@ class UserAdmin(admin.ModelAdmin): 'shell', 'state' ) - search_fields = ('name','surname','pseudo','room') + search_fields = ('name', 'surname', 'pseudo', 'room') class LdapUserAdmin(admin.ModelAdmin): - list_display = ('name','uidNumber','login_shell') - exclude = ('user_password','sambat_nt_password') + """Administration du ldapuser""" + list_display = ('name', 'uidNumber', 'login_shell') + exclude = ('user_password', 'sambat_nt_password') search_fields = ('name',) + class LdapServiceUserAdmin(admin.ModelAdmin): + """Administration du ldapserviceuser""" list_display = ('name',) exclude = ('user_password',) search_fields = ('name',) + class LdapUserGroupAdmin(admin.ModelAdmin): - list_display = ('name','members','gid') + """Administration du ldapusergroupe""" + list_display = ('name', 'members', 'gid') search_fields = ('name',) + class LdapServiceUserGroupAdmin(admin.ModelAdmin): + """Administration du ldap serviceusergroup""" list_display = ('name',) search_fields = ('name',) + class SchoolAdmin(VersionAdmin): - list_display = ('name',) + """Administration, gestion des écoles""" + pass + class ListRightAdmin(VersionAdmin): + """Gestion de la liste des droits existants + Ne permet pas l'edition du gid (primarykey pour ldap)""" list_display = ('listright',) + class ListShellAdmin(VersionAdmin): - list_display = ('shell',) + """Gestion de la liste des shells coté admin""" + pass + class RightAdmin(VersionAdmin): - list_display = ('user', 'right') + """Gestion de la liste des droits affectés""" + pass + class RequestAdmin(admin.ModelAdmin): + """Gestion des request objet, ticket pour lien de reinit mot de passe""" list_display = ('user', 'type', 'created_at', 'expires_at') + class BanAdmin(VersionAdmin): - list_display = ('user', 'raison', 'date_start', 'date_end') + """Gestion des bannissements""" + pass class WhitelistAdmin(VersionAdmin): - list_display = ('user', 'raison', 'date_start', 'date_end') + """Gestion des whitelist""" + pass class UserAdmin(VersionAdmin, BaseUserAdmin): + """Gestion d'un user : modification des champs perso, mot de passe, etc""" # The forms to add and change user instances form = UserChangeForm add_form = UserCreationForm @@ -95,27 +125,56 @@ class UserAdmin(VersionAdmin, BaseUserAdmin): # The fields to be used in displaying the User model. # These override the definitions on the base UserAdmin # that reference specific fields on auth.User. - list_display = ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'shell') + list_display = ( + 'pseudo', + 'name', + 'surname', + 'email', + 'school', + 'is_admin', + 'shell' + ) list_display = ('pseudo',) list_filter = () fieldsets = ( (None, {'fields': ('pseudo', 'password')}), - ('Personal info', {'fields': ('name', 'surname', 'email', 'school','shell', 'uid_number')}), + ( + 'Personal info', + { + 'fields': + ('name', 'surname', 'email', 'school', 'shell', 'uid_number') + } + ), ('Permissions', {'fields': ('is_admin', )}), ) # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin # overrides get_fieldsets to use this attribute when creating a user. add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'password1', 'password2')} + ( + None, + { + 'classes': ('wide',), + 'fields': ( + 'pseudo', + 'name', + 'surname', + 'email', + 'school', + 'is_admin', + 'password1', + 'password2' + ) + } ), ) search_fields = ('pseudo',) ordering = ('pseudo',) filter_horizontal = () + class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): + """Gestion d'un service user admin : champs personnels, + mot de passe; etc""" # The forms to add and change user instances form = ServiceUserChangeForm add_form = ServiceUserCreationForm @@ -131,15 +190,19 @@ class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin # overrides get_fieldsets to use this attribute when creating a user. add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('pseudo', 'password1', 'password2')} + ( + None, + { + 'classes': ('wide',), + 'fields': ('pseudo', 'password1', 'password2') + } ), ) search_fields = ('pseudo',) ordering = ('pseudo',) filter_horizontal = () + admin.site.register(User, UserAdmin) admin.site.register(ServiceUser, ServiceUserAdmin) admin.site.register(LdapUser, LdapUserAdmin) diff --git a/users/forms.py b/users/forms.py index 0099176f..fd81b426 100644 --- a/users/forms.py +++ b/users/forms.py @@ -20,8 +20,16 @@ # 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 forms pour l'application users. -# -*- coding: utf-8 -*- +Modification, creation de : + - un user (informations personnelles) + - un bannissement + - le mot de passe d'un user + - une whiteliste + - un user de service +""" from __future__ import unicode_literals @@ -29,17 +37,34 @@ from django import forms from django.forms import ModelForm, Form from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.core.validators import MinLengthValidator -from preferences.models import OptionalUser from django.utils import timezone -from .models import User, ServiceUser, Right, School, ListRight, Whitelist, Ban, Request, remove_user_room -from .models import get_admin_right +from preferences.models import OptionalUser +from .models import User, ServiceUser, Right, School, ListRight, Whitelist +from .models import Ban, remove_user_room + +NOW = timezone.now() + class PassForm(forms.Form): - passwd1 = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput) - passwd2 = forms.CharField(label=u'Saisir à nouveau le mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput) + """Formulaire de changement de mot de passe. Verifie que les 2 + nouveaux mots de passe renseignés sont identiques et respectent + une norme""" + passwd1 = forms.CharField( + label=u'Nouveau mot de passe', + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput + ) + passwd2 = forms.CharField( + label=u'Saisir à nouveau le mot de passe', + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput + ) def clean_passwd2(self): + """Verifie que passwd1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("passwd1") password2 = self.cleaned_data.get("passwd2") @@ -47,18 +72,38 @@ class PassForm(forms.Form): raise forms.ValidationError("Passwords don't match") return password2 + class UserCreationForm(forms.ModelForm): """A form for creating new users. Includes all the required - fields, plus a repeated password.""" - password1 = forms.CharField(label='Password', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255) - password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255) + fields, plus a repeated password. + + Formulaire pour la création d'un user. N'est utilisé que pour + l'admin, lors de la creation d'un user par admin. Inclu tous les + champs obligatoires""" + password1 = forms.CharField( + label='Password', + widget=forms.PasswordInput, + validators=[MinLengthValidator(8)], + max_length=255 + ) + password2 = forms.CharField( + label='Password confirmation', + widget=forms.PasswordInput, + validators=[MinLengthValidator(8)], + max_length=255 + ) is_admin = forms.BooleanField(label='is admin') + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(UserCreationForm, self).__init__(*args, prefix=prefix, **kwargs) + class Meta: model = User fields = ('pseudo', 'name', 'surname', 'email') def clean_password2(self): + """Verifie que password1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") @@ -74,17 +119,40 @@ class UserCreationForm(forms.ModelForm): user.is_admin = self.cleaned_data.get("is_admin") return user + class ServiceUserCreationForm(forms.ModelForm): """A form for creating new users. Includes all the required - fields, plus a repeated password.""" - password1 = forms.CharField(label='Password', widget=forms.PasswordInput, min_length=8, max_length=255) - password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, min_length=8, max_length=255) + fields, plus a repeated password. + + Formulaire pour la creation de nouveaux serviceusers. + Requiert seulement un mot de passe; et un pseudo""" + password1 = forms.CharField( + label='Password', + widget=forms.PasswordInput, + min_length=8, + max_length=255 + ) + password2 = forms.CharField( + label='Password confirmation', + widget=forms.PasswordInput, + min_length=8, + max_length=255 + ) + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ServiceUserCreationForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) class Meta: model = ServiceUser fields = ('pseudo',) def clean_password2(self): + """Verifie que password1 et 2 sont indentiques""" # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") @@ -99,10 +167,13 @@ class ServiceUserCreationForm(forms.ModelForm): user.save() return user + class UserChangeForm(forms.ModelForm): """A form for updating users. Includes all the fields on the user, but replaces the password field with admin's password hash display field. + + Formulaire pour la modification d'un user coté admin """ password = ReadOnlyPasswordHashField() is_admin = forms.BooleanField(label='is admin', required=False) @@ -112,11 +183,13 @@ class UserChangeForm(forms.ModelForm): fields = ('pseudo', 'password', 'name', 'surname', 'email') def __init__(self, *args, **kwargs): - super(UserChangeForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(UserChangeForm, self).__init__(*args, prefix=prefix, **kwargs) print("User is admin : %s" % kwargs['instance'].is_admin) self.initial['is_admin'] = kwargs['instance'].is_admin def clean_password(self): + """Dummy fun""" # Regardless of what the user provides, return the initial value. # This is done here, rather than on the field, because the # field does not have access to the initial value @@ -130,40 +203,62 @@ class UserChangeForm(forms.ModelForm): user.save() return user + class ServiceUserChangeForm(forms.ModelForm): """A form for updating users. Includes all the fields on the user, but replaces the password field with admin's password hash display field. + + Formulaire pour l'edition des service users coté admin """ password = ReadOnlyPasswordHashField() + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ServiceUserChangeForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + class Meta: model = ServiceUser fields = ('pseudo',) def clean_password(self): - # Regardless of what the user provides, return the initial value. - # This is done here, rather than on the field, because the - # field does not have access to the initial value + """Dummy fun""" return self.initial["password"] + class ResetPasswordForm(forms.Form): + """Formulaire de demande de reinitialisation de mot de passe, + mdp oublié""" pseudo = forms.CharField(label=u'Pseudo', max_length=255) email = forms.EmailField(max_length=255) + class MassArchiveForm(forms.Form): + """Formulaire d'archivage des users inactif. Prend en argument + du formulaire la date de depart avant laquelle archiver les + users""" date = forms.DateTimeField(help_text='%d/%m/%y') def clean(self): - cleaned_data=super(MassArchiveForm, self).clean() + cleaned_data = super(MassArchiveForm, self).clean() date = cleaned_data.get("date") if date: - if date>timezone.now(): - raise forms.ValidationError("Impossible d'archiver des utilisateurs dont la fin d'accès se situe dans le futur !") + if date > NOW: + raise forms.ValidationError("Impossible d'archiver des\ + utilisateurs dont la fin d'accès se situe dans le futur !") + class BaseInfoForm(ModelForm): + """Formulaire de base d'edition d'un user. Formulaire de base, utilisé + pour l'edition de self par self ou un cableur. On formate les champs + avec des label plus jolis""" def __init__(self, *args, **kwargs): - super(BaseInfoForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(BaseInfoForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Prénom' self.fields['surname'].label = 'Nom' self.fields['school'].label = 'Établissement' @@ -186,13 +281,21 @@ class BaseInfoForm(ModelForm): ] def clean_telephone(self): + """Verifie que le tel est présent si 'option est validée + dans preferences""" telephone = self.cleaned_data['telephone'] - preferences, created = OptionalUser.objects.get_or_create() + preferences, _created = OptionalUser.objects.get_or_create() if not telephone and preferences.is_tel_mandatory: - raise forms.ValidationError("Un numéro de téléphone valide est requis") + raise forms.ValidationError( + "Un numéro de téléphone valide est requis" + ) return telephone + class EditInfoForm(BaseInfoForm): + """Edition complète d'un user. Utilisé par admin, + permet d'editer normalement la chambre, ou le shell + Herite de la base""" class Meta(BaseInfoForm.Meta): fields = [ 'name', @@ -206,37 +309,67 @@ class EditInfoForm(BaseInfoForm): 'telephone', ] + class InfoForm(EditInfoForm): - """ Utile pour forcer un déménagement quand il y a déjà un user en place""" - force = forms.BooleanField(label="Forcer le déménagement ?", initial=False, required=False) + """ Utile pour forcer un déménagement quand il y a déjà un user en place + Formuaire utilisé pour la creation initiale""" + force = forms.BooleanField( + label="Forcer le déménagement ?", + initial=False, + required=False + ) def clean_force(self): + """On supprime l'ancien user de la chambre si et seulement si la + case est cochée""" if self.cleaned_data.get('force', False): remove_user_room(self.cleaned_data.get('room')) return + class UserForm(InfoForm): """ Model form general""" class Meta(InfoForm.Meta): fields = '__all__' + class PasswordForm(ModelForm): - """ Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement""" + """ Formulaire de changement brut de mot de passe. + Ne pas utiliser sans traitement""" class Meta: model = User fields = ['password', 'pwd_ntlm'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs) + + class ServiceUserForm(ModelForm): """ Modification d'un service user""" - password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False) + password = forms.CharField( + label=u'Nouveau mot de passe', + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput, + required=False + ) class Meta: model = ServiceUser - fields = ('pseudo','access_group') + fields = ('pseudo', 'access_group') + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditServiceUserForm(ServiceUserForm): + """Formulaire d'edition de base d'un service user. Ne permet + d'editer que son group d'acl et son commentaire""" class Meta(ServiceUserForm.Meta): - fields = ['access_group','comment'] + fields = ['access_group', 'comment'] + class StateForm(ModelForm): """ Changement de l'état d'un user""" @@ -244,42 +377,70 @@ class StateForm(ModelForm): model = User fields = ['state'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) + class SchoolForm(ModelForm): + """Edition, creation d'un école""" class Meta: model = School fields = ['name'] def __init__(self, *args, **kwargs): - super(SchoolForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(SchoolForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Établissement' + class ListRightForm(ModelForm): + """Edition, d'un groupe , équivalent à un droit + Ne peremet pas d'editer le gid, car il sert de primary key""" class Meta: model = ListRight fields = ['listright', 'details'] def __init__(self, *args, **kwargs): - super(ListRightForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['listright'].label = 'Nom du droit/groupe' + class NewListRightForm(ListRightForm): + """Ajout d'un groupe/list de droit """ class Meta(ListRightForm.Meta): fields = '__all__' def __init__(self, *args, **kwargs): super(NewListRightForm, self).__init__(*args, **kwargs) - self.fields['gid'].label = 'Gid, attention, cet attribut ne doit pas être modifié après création' + self.fields['gid'].label = 'Gid, attention, cet attribut ne doit\ + pas être modifié après création' + class DelListRightForm(Form): - listrights = forms.ModelMultipleChoiceField(queryset=ListRight.objects.all(), label="Droits actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs groupes""" + listrights = forms.ModelMultipleChoiceField( + queryset=ListRight.objects.all(), + label="Droits actuels", + widget=forms.CheckboxSelectMultiple + ) + class DelSchoolForm(Form): - schools = forms.ModelMultipleChoiceField(queryset=School.objects.all(), label="Etablissements actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'une ou plusieurs écoles""" + schools = forms.ModelMultipleChoiceField( + queryset=School.objects.all(), + label="Etablissements actuels", + widget=forms.CheckboxSelectMultiple + ) + class RightForm(ModelForm): + """Assignation d'un droit à un user""" def __init__(self, *args, **kwargs): - super(RightForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(RightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['right'].label = 'Droit' self.fields['right'].empty_label = "Choisir un nouveau droit" @@ -289,15 +450,22 @@ class RightForm(ModelForm): class DelRightForm(Form): - rights = forms.ModelMultipleChoiceField(queryset=Right.objects.all(), widget=forms.CheckboxSelectMultiple) + """Suppression d'un droit d'un user""" + rights = forms.ModelMultipleChoiceField( + queryset=Right.objects.all(), + widget=forms.CheckboxSelectMultiple + ) def __init__(self, right, *args, **kwargs): super(DelRightForm, self).__init__(*args, **kwargs) self.fields['rights'].queryset = Right.objects.filter(right=right) + class BanForm(ModelForm): + """Creation, edition d'un objet bannissement""" def __init__(self, *args, **kwargs): - super(BanForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(BanForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' class Meta: @@ -305,15 +473,19 @@ class BanForm(ModelForm): exclude = ['user'] def clean_date_end(self): + """Verification que date_end est après now""" date_end = self.cleaned_data['date_end'] - if date_end < timezone.now(): - raise forms.ValidationError("Triple buse, la date de fin ne peut pas être avant maintenant... Re2o ne voyage pas dans le temps") + if date_end < NOW: + raise forms.ValidationError("Triple buse, la date de fin ne peut\ + pas être avant maintenant... Re2o ne voyage pas dans le temps") return date_end class WhitelistForm(ModelForm): + """Creation, edition d'un objet whitelist""" def __init__(self, *args, **kwargs): - super(WhitelistForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' class Meta: @@ -321,7 +493,9 @@ class WhitelistForm(ModelForm): exclude = ['user'] def clean_date_end(self): + """Verification que la date_end est posterieur à now""" date_end = self.cleaned_data['date_end'] - if date_end < timezone.now(): - raise forms.ValidationError("Triple buse, la date de fin ne peut pas être avant maintenant... Re2o ne voyage pas dans le temps") + if date_end < NOW: + raise forms.ValidationError("Triple buse, la date de fin ne peut pas\ + être avant maintenant... Re2o ne voyage pas dans le temps") return date_end diff --git a/users/models.py b/users/models.py index 78b76156..2f8f888f 100644 --- a/users/models.py +++ b/users/models.py @@ -1,7 +1,7 @@ # -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il -# se veut agnostique au réseau considéré, de manière à être installable en -# quelques clics. +# Re2o est un logiciel d'administration développé initiallement au rezometz. +# Il se veut agnostique au réseau considéré, de manière à être installable +# en quelques clics. # # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec @@ -20,44 +20,66 @@ # 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. +""" +Models de l'application users. + +On défini ici des models django classiques: +- users, qui hérite de l'abstract base user de django. Permet de définit +un utilisateur du site (login, passwd, chambre, adresse, etc) +- les whiteslist +- les bannissements +- les établissements d'enseignement (school) +- les droits (right et listright) +- les utilisateurs de service (pour connexion automatique) + +On défini aussi des models qui héritent de django-ldapdb : +- ldapuser +- ldapgroup +- ldapserviceuser + +Ces utilisateurs ldap sont synchronisés à partir des objets +models sql classiques. Seuls certains champs essentiels sont +dupliqués. +""" + from __future__ import unicode_literals +import re +import uuid +import datetime + from django.db import models from django.db.models import Q from django import forms from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.utils.functional import cached_property -from django.template import Context, RequestContext, loader +from django.template import Context, loader from django.core.mail import send_mail from django.core.urlresolvers import reverse +from django.db import transaction +from django.utils import timezone +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.core.validators import RegexValidator from reversion import revisions as reversion -from django.db import transaction import ldapdb.models import ldapdb.models.fields -from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES,UID_RANGES -import re, uuid -import datetime +from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES, UID_RANGES from re2o.login import hashNT -from django.utils import timezone -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager - -from django.core.validators import MinLengthValidator -from django.core.validators import RegexValidator -from topologie.models import Room from cotisations.models import Cotisation, Facture, Paiement, Vente -from machines.models import Domain, Interface, MachineType, Machine, Nas, MachineType, Extension, regen -from preferences.models import GeneralOption, AssoOption, OptionalUser, OptionalMachine, MailMessageOption +from machines.models import Domain, Interface, Machine, regen +from preferences.models import GeneralOption, AssoOption, OptionalUser +from preferences.models import OptionalMachine, MailMessageOption -now = timezone.now() +DT_NOW = timezone.now() -#### Utilitaires généraux +# Utilitaires généraux def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ @@ -76,33 +98,42 @@ def linux_user_check(login): def linux_user_validator(login): - """ Retourne une erreur de validation si le login ne respecte + """ Retourne une erreur de validation si le login ne respecte pas les contraintes unix (maj, min, chiffres ou tiret)""" if not linux_user_check(login): raise forms.ValidationError( - ", ce pseudo ('%(label)s') contient des carractères interdits", - params={'label': login}, + ", ce pseudo ('%(label)s') contient des carractères interdits", + params={'label': login}, ) + def get_fresh_user_uid(): """ Renvoie le plus petit uid non pris. Fonction très paresseuse """ - uids = list(range(int(min(UID_RANGES['users'])),int(max(UID_RANGES['users'])))) + uids = list(range( + int(min(UID_RANGES['users'])), + int(max(UID_RANGES['users'])) + )) try: used_uids = list(User.objects.values_list('uid_number', flat=True)) except: used_uids = [] - free_uids = [ id for id in uids if id not in used_uids] + free_uids = [id for id in uids if id not in used_uids] return min(free_uids) + def get_fresh_gid(): """ Renvoie le plus petit gid libre """ - gids = list(range(int(min(GID_RANGES['posix'])),int(max(GID_RANGES['posix'])))) + gids = list(range( + int(min(GID_RANGES['posix'])), + int(max(GID_RANGES['posix'])) + )) used_gids = list(ListRight.objects.values_list('gid', flat=True)) - free_gids = [ id for id in gids if id not in used_gids] + free_gids = [id for id in gids if id not in used_gids] return min(free_gids) + def get_admin_right(): - """ Renvoie l'instance droit admin. La crée si elle n'existe pas + """ Renvoie l'instance droit admin. La crée si elle n'existe pas Lui attribue un gid libre""" try: admin_right = ListRight.objects.get(listright="admin") @@ -112,25 +143,18 @@ def get_admin_right(): admin_right.save() return admin_right -def all_adherent(search_time=now): - """ Fonction renvoyant tous les users adherents. Optimisee pour n'est qu'une seule requete sql - Inspecte les factures de l'user et ses cotisation, regarde si elles sont posterieur à now (end_time)""" - return User.objects.filter(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))).distinct() - -def all_baned(search_time=now): - """ Fonction renvoyant tous les users bannis """ - return User.objects.filter(ban__in=Ban.objects.filter(date_end__gt=search_time)).distinct() - -def all_whitelisted(search_time=now): - """ Fonction renvoyant tous les users whitelistes """ - return User.objects.filter(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)).distinct() - -def all_has_access(search_time=now): - """ Renvoie tous les users beneficiant d'une connexion : user adherent ou whiteliste et non banni """ - return User.objects.filter(Q(state=User.STATE_ACTIVE) & ~Q(ban__in=Ban.objects.filter(date_end__gt=timezone.now())) & (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=timezone.now())) | Q(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))))).distinct() class UserManager(BaseUserManager): - def _create_user(self, pseudo, name, surname, email, password=None, su=False): + """User manager basique de django""" + def _create_user( + self, + pseudo, + name, + surname, + email, + password=None, + su=False + ): if not pseudo: raise ValueError('Users must have an username') @@ -174,28 +198,53 @@ class User(AbstractBaseUser): STATE_DISABLED = 1 STATE_ARCHIVE = 2 STATES = ( - (0, 'STATE_ACTIVE'), - (1, 'STATE_DISABLED'), - (2, 'STATE_ARCHIVE'), - ) + (0, 'STATE_ACTIVE'), + (1, 'STATE_DISABLED'), + (2, 'STATE_ARCHIVE'), + ) def auto_uid(): + """Renvoie un uid libre""" return get_fresh_user_uid() name = models.CharField(max_length=255) surname = models.CharField(max_length=255) - pseudo = models.CharField(max_length=32, unique=True, help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", validators=[linux_user_validator]) + pseudo = models.CharField( + max_length=32, + unique=True, + help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", + validators=[linux_user_validator] + ) email = models.EmailField() - school = models.ForeignKey('School', on_delete=models.PROTECT, null=True, blank=True) - shell = models.ForeignKey('ListShell', on_delete=models.PROTECT, null=True, blank=True) - comment = models.CharField(help_text="Commentaire, promo", max_length=255, blank=True) - room = models.OneToOneField('topologie.Room', on_delete=models.PROTECT, blank=True, null=True) + school = models.ForeignKey( + 'School', + on_delete=models.PROTECT, + null=True, + blank=True + ) + shell = models.ForeignKey( + 'ListShell', + on_delete=models.PROTECT, + null=True, + blank=True + ) + comment = models.CharField( + help_text="Commentaire, promo", + max_length=255, + blank=True + ) + room = models.OneToOneField( + 'topologie.Room', + on_delete=models.PROTECT, + blank=True, + null=True + ) pwd_ntlm = models.CharField(max_length=255) state = models.IntegerField(choices=STATES, default=STATE_ACTIVE) registered = models.DateTimeField(auto_now_add=True) telephone = models.CharField(max_length=15, blank=True, null=True) uid_number = models.IntegerField(default=auto_uid, unique=True) - rezo_rez_uid = models.IntegerField(unique=True, blank=True, null=True) + rezo_rez_uid = models.IntegerField(unique=True, blank=True, null=True) USERNAME_FIELD = 'pseudo' REQUIRED_FIELDS = ['name', 'surname', 'email'] @@ -223,7 +272,8 @@ class User(AbstractBaseUser): @is_admin.setter def is_admin(self, value): - """ Change la valeur de admin à true ou false suivant la valeur de value""" + """ Change la valeur de admin à true ou false suivant la valeur de + value""" if value and not self.is_admin: self.make_admin() elif not value and self.is_admin: @@ -247,7 +297,7 @@ class User(AbstractBaseUser): for right in RIGHTS_LINK[perm]: query = query | Q(right__listright=right) if Right.objects.filter(Q(user=self) & query): - return True + return True try: Right.objects.get(user=self, right__listright=perm) except Right.DoesNotExist: @@ -255,17 +305,20 @@ class User(AbstractBaseUser): return True def has_perm(self, perm, obj=None): + """Ne sert à rien""" return True - def has_right(self, right): - """ Renvoie si un user a un right donné. Crée le right si il n'existe pas""" + """ Renvoie si un user a un right donné. Crée le right si il n'existe + pas""" try: list_right = ListRight.objects.get(listright=right) except: list_right = ListRight(listright=right, gid=get_fresh_gid()) list_right.save() - return Right.objects.filter(user=self).filter(right=list_right).exists() + return Right.objects.filter(user=self).filter( + right=list_right + ).exists() @cached_property def is_bureau(self): @@ -279,9 +332,10 @@ class User(AbstractBaseUser): @cached_property def is_cableur(self): - """ True si l'user a les droits cableur + """ True si l'user a les droits cableur (également true si bureau, infra ou bofh)""" - return self.has_right('cableur') or self.has_right('bureau') or self.has_right('infra') or self.has_right('bofh') + return self.has_right('cableur') or self.has_right('bureau') or\ + self.has_right('infra') or self.has_right('bofh') @cached_property def is_trez(self): @@ -296,15 +350,22 @@ class User(AbstractBaseUser): def end_adhesion(self): """ Renvoie la date de fin d'adhésion d'un user. Examine les objets cotisation""" - date_max = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self).exclude(valid=False))).aggregate(models.Max('date_end'))['date_end__max'] + date_max = Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self + ).exclude(valid=False) + ) + ).aggregate(models.Max('date_end'))['date_end__max'] return date_max def is_adherent(self): - """ Renvoie True si l'user est adhérent : si self.end_adhesion()>now""" + """ Renvoie True si l'user est adhérent : si + self.end_adhesion()>now""" end = self.end_adhesion() if not end: return False - elif end < timezone.now(): + elif end < DT_NOW: return False else: return True @@ -312,13 +373,17 @@ class User(AbstractBaseUser): @cached_property def end_ban(self): """ Renvoie la date de fin de ban d'un user, False sinon """ - date_max = Ban.objects.filter(user=self).aggregate(models.Max('date_end'))['date_end__max'] + date_max = Ban.objects.filter( + user=self + ).aggregate(models.Max('date_end'))['date_end__max'] return date_max @cached_property def end_whitelist(self): """ Renvoie la date de fin de whitelist d'un user, False sinon """ - date_max = Whitelist.objects.filter(user=self).aggregate(models.Max('date_end'))['date_end__max'] + date_max = Whitelist.objects.filter( + user=self + ).aggregate(models.Max('date_end'))['date_end__max'] return date_max @cached_property @@ -327,7 +392,7 @@ class User(AbstractBaseUser): end = self.end_ban if not end: return False - elif end < timezone.now(): + elif end < DT_NOW: return False else: return True @@ -338,14 +403,14 @@ class User(AbstractBaseUser): end = self.end_whitelist if not end: return False - elif end < timezone.now(): + elif end < DT_NOW: return False else: return True def has_access(self): """ Renvoie si un utilisateur a accès à internet """ - return self.state == User.STATE_ACTIVE \ + return self.state == User.STATE_ACTIVE\ and not self.is_ban and (self.is_adherent() or self.is_whitelisted) def end_access(self): @@ -358,27 +423,50 @@ class User(AbstractBaseUser): else: if not self.end_whitelist: return self.end_adhesion() - else: + else: return max(self.end_adhesion(), self.end_whitelist) @cached_property def solde(self): - """ Renvoie le solde d'un user. Vérifie que l'option solde est activé, retourne 0 sinon. + """ Renvoie le solde d'un user. Vérifie que l'option solde est + activé, retourne 0 sinon. Somme les crédits de solde et retire les débit payés par solde""" - options, created = OptionalUser.objects.get_or_create() + options, _created = OptionalUser.objects.get_or_create() user_solde = options.user_solde if user_solde: - solde_object, created=Paiement.objects.get_or_create(moyen='Solde') - somme_debit = Vente.objects.filter(facture__in=Facture.objects.filter(user=self, paiement=solde_object)).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] or 0 - somme_credit =Vente.objects.filter(facture__in=Facture.objects.filter(user=self), name="solde").aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] or 0 + solde_object, _created = Paiement.objects.get_or_create( + moyen='Solde' + ) + somme_debit = Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self, + paiement=solde_object + ) + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + somme_credit = Vente.objects.filter( + facture__in=Facture.objects.filter(user=self), + name="solde" + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 return somme_credit - somme_debit else: return 0 def user_interfaces(self, active=True): - """ Renvoie toutes les interfaces dont les machines appartiennent à self - Par defaut ne prend que les interfaces actives""" - return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active)).select_related('domain__extension') + """ Renvoie toutes les interfaces dont les machines appartiennent à + self. Par defaut ne prend que les interfaces actives""" + return Interface.objects.filter( + machine__in=Machine.objects.filter(user=self, active=active) + ).select_related('domain__extension') def assign_ips(self): """ Assign une ipv4 aux machines d'un user """ @@ -400,17 +488,19 @@ class User(AbstractBaseUser): interface.save() def archive(self): - """ Archive l'user : appelle unassign_ips() puis passe state à ARCHIVE""" + """ Archive l'user : appelle unassign_ips() puis passe state à + ARCHIVE""" self.unassign_ips() - self.state = User.STATE_ARCHIVE + self.state = User.STATE_ARCHIVE def unarchive(self): - """ Désarchive l'user : réassigne ses ip et le passe en state ACTIVE""" + """ Désarchive l'user : réassigne ses ip et le passe en state + ACTIVE""" self.assign_ips() self.state = User.STATE_ACTIVE def has_module_perms(self, app_label): - # Simplest version again + """True, a toutes les permissions de module""" return True def make_admin(self): @@ -419,16 +509,20 @@ class User(AbstractBaseUser): user_admin_right.save() def un_admin(self): + """Supprime les droits admin d'un user""" try: - user_right = Right.objects.get(user=self,right=get_admin_right()) + user_right = Right.objects.get(user=self, right=get_admin_right()) except Right.DoesNotExist: return user_right.delete() def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True): - """ Synchronisation du ldap. Synchronise dans le ldap les attributs de self - Options : base : synchronise tous les attributs de base - nom, prenom, mail, password, shell, home - access_refresh : synchronise le dialup_access notant si l'user a accès aux services + """ Synchronisation du ldap. Synchronise dans le ldap les attributs de + self + Options : base : synchronise tous les attributs de base - nom, prenom, + mail, password, shell, home + access_refresh : synchronise le dialup_access notant si l'user a accès + aux services mac_refresh : synchronise les machines de l'user""" self.refresh_from_db() try: @@ -441,7 +535,8 @@ class User(AbstractBaseUser): user_ldap.dialupAccess = str(self.has_access()) user_ldap.home_directory = '/home/' + self.pseudo user_ldap.mail = self.email - user_ldap.given_name = self.surname.lower() + '_' + self.name.lower()[:3] + user_ldap.given_name = self.surname.lower() + '_'\ + + self.name.lower()[:3] user_ldap.gid = LDAP['user_gid'] user_ldap.user_password = self.password[:6] + self.password[7:] user_ldap.sambat_nt_password = self.pwd_ntlm.upper() @@ -454,7 +549,10 @@ class User(AbstractBaseUser): if access_refresh: user_ldap.dialupAccess = str(self.has_access()) if mac_refresh: - user_ldap.macs = [inter.mac_bare() for inter in Interface.objects.filter(machine__in=Machine.objects.filter(user=self))] + user_ldap.macs = [inter.mac_bare() for inter in + Interface.objects.filter( + machine__in=Machine.objects.filter(user=self) + )] user_ldap.save() def ldap_del(self): @@ -467,53 +565,69 @@ class User(AbstractBaseUser): def notif_inscription(self): """ Prend en argument un objet user, envoie un mail de bienvenue """ - t = loader.get_template('users/email_welcome') - assooptions, created = AssoOption.objects.get_or_create() - mailmessageoptions, created = MailMessageOption.objects.get_or_create() - general_options, created = GeneralOption.objects.get_or_create() - c = Context({ + template = loader.get_template('users/email_welcome') + assooptions, _created = AssoOption.objects.get_or_create() + mailmessageoptions, _created = MailMessageOption\ + .objects.get_or_create() + general_options, _created = GeneralOption.objects.get_or_create() + context = Context({ 'nom': str(self.name) + ' ' + str(self.surname), 'asso_name': assooptions.name, 'asso_email': assooptions.contact, - 'welcome_mail_fr' : mailmessageoptions.welcome_mail_fr, - 'welcome_mail_en' : mailmessageoptions.welcome_mail_en, - 'pseudo':self.pseudo, + 'welcome_mail_fr': mailmessageoptions.welcome_mail_fr, + 'welcome_mail_en': mailmessageoptions.welcome_mail_en, + 'pseudo': self.pseudo, }) - send_mail('Bienvenue au %(name)s / Welcome to %(name)s' % {'name': assooptions.name }, '', - general_options.email_from, [self.email], html_message=t.render(c)) + send_mail( + 'Bienvenue au %(name)s / Welcome to %(name)s' % { + 'name': assooptions.name + }, + '', + general_options.email_from, + [self.email], + html_message=template.render(context) + ) return def reset_passwd_mail(self, request): - """ Prend en argument un request, envoie un mail de réinitialisation de mot de pass """ + """ Prend en argument un request, envoie un mail de + réinitialisation de mot de pass """ req = Request() req.type = Request.PASSWD req.user = self req.save() - t = loader.get_template('users/email_passwd_request') - options, created = AssoOption.objects.get_or_create() - general_options, created = GeneralOption.objects.get_or_create() - c = { + template = loader.get_template('users/email_passwd_request') + options, _created = AssoOption.objects.get_or_create() + general_options, _created = GeneralOption.objects.get_or_create() + context = { 'name': str(req.user.name) + ' ' + str(req.user.surname), 'asso': options.name, 'asso_mail': options.contact, 'site_name': general_options.site_name, 'url': request.build_absolute_uri( - reverse('users:process', kwargs={'token': req.token})), + reverse('users:process', kwargs={'token': req.token})), 'expire_in': str(general_options.req_expire_hrs) + ' heures', } - send_mail('Changement de mot de passe du %(name)s / Password renewal for %(name)s' % {'name': options.name }, t.render(c), - general_options.email_from, [req.user.email], fail_silently=False) + send_mail( + 'Changement de mot de passe du %(name)s / Password\ + renewal for %(name)s' % {'name': options.name}, + template.render(context), + general_options.email_from, + [req.user.email], + fail_silently=False + ) return def autoregister_machine(self, mac_address, nas_type): - """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue - sur le compte de l'user""" + """ Fonction appellée par freeradius. Enregistre la mac pour + une machine inconnue sur le compte de l'user""" all_interfaces = self.user_interfaces(active=False) - options, created = OptionalMachine.objects.get_or_create() + options, _created = OptionalMachine.objects.get_or_create() if all_interfaces.count() > options.max_lambdauser_interfaces: return False, "Maximum de machines enregistrees atteinte" if not nas_type: - return False, "Re2o ne sait pas à quel machinetype affecter cette machine" + return False, "Re2o ne sait pas à quel machinetype affecter cette\ + machine" machine_type_cible = nas_type.machine_type try: machine_parent = Machine() @@ -533,12 +647,12 @@ class User(AbstractBaseUser): domain.interface_parent = interface_cible domain.clean() domain.save() - except Exception as e: - return False, e + except Exception as error: + return False, error return True, "Ok" def set_user_password(self, password): - """ A utiliser de préférence, set le password en hash courrant et + """ A utiliser de préférence, set le password en hash courrant et dans la version ntlm""" self.set_password(password) self.pwd_ntlm = hashNT(password) @@ -547,23 +661,28 @@ class User(AbstractBaseUser): def get_next_domain_name(self): """Look for an available name for a new interface for this user by trying "pseudo0", "pseudo1", "pseudo2", ... + + Recherche un nom disponible, pour une machine. Doit-être + unique, concatène le nom, le pseudo et le numero de machine """ def simple_pseudo(): + """Renvoie le pseudo sans underscore (compat dns)""" return self.pseudo.replace('_', '-').lower() - def composed_pseudo( n ): - return simple_pseudo() + str(n) + def composed_pseudo(name): + """Renvoie le resultat de simplepseudo et rajoute le nom""" + return simple_pseudo() + str(name) num = 0 - while Domain.objects.filter(name=composed_pseudo(num)) : + while Domain.objects.filter(name=composed_pseudo(num)): num += 1 return composed_pseudo(num) - def __str__(self): return self.pseudo + @receiver(post_save, sender=User) def user_post_save(sender, **kwargs): """ Synchronisation post_save : envoie le mail de bienvenue si creation @@ -575,29 +694,44 @@ def user_post_save(sender, **kwargs): user.ldap_sync(base=True, access_refresh=True, mac_refresh=False) regen('mailing') + @receiver(post_delete, sender=User) def user_post_delete(sender, **kwargs): + """Post delete d'un user, on supprime son instance ldap""" user = kwargs['instance'] user.ldap_del() regen('mailing') + class ServiceUser(AbstractBaseUser): """ Classe des users daemons, règle leurs accès au ldap""" readonly = 'readonly' ACCESS = ( - ('auth', 'auth'), - ('readonly', 'readonly'), - ('usermgmt', 'usermgmt'), - ) + ('auth', 'auth'), + ('readonly', 'readonly'), + ('usermgmt', 'usermgmt'), + ) PRETTY_NAME = "Utilisateurs de service" - pseudo = models.CharField(max_length=32, unique=True, help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", validators=[linux_user_validator]) - access_group = models.CharField(choices=ACCESS, default=readonly, max_length=32) - comment = models.CharField(help_text="Commentaire", max_length=255, blank=True) + pseudo = models.CharField( + max_length=32, + unique=True, + help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", + validators=[linux_user_validator] + ) + access_group = models.CharField( + choices=ACCESS, + default=readonly, + max_length=32 + ) + comment = models.CharField( + help_text="Commentaire", + max_length=255, + blank=True + ) USERNAME_FIELD = 'pseudo' - objects = UserManager() def ldap_sync(self): @@ -611,6 +745,7 @@ class ServiceUser(AbstractBaseUser): self.serviceuser_group_sync() def ldap_del(self): + """Suppression de l'instance ldap d'un service user""" try: user_ldap = LdapServiceUser.objects.get(name=self.pseudo) user_ldap.delete() @@ -619,30 +754,38 @@ class ServiceUser(AbstractBaseUser): self.serviceuser_group_sync() def serviceuser_group_sync(self): + """Synchronise le groupe et les droits de groupe dans le ldap""" try: group = LdapServiceUserGroup.objects.get(name=self.access_group) except: group = LdapServiceUserGroup(name=self.access_group) - group.members = list(LdapServiceUser.objects.filter(name__in=[user.pseudo for user in ServiceUser.objects.filter(access_group=self.access_group)]).values_list('dn', flat=True)) + group.members = list(LdapServiceUser.objects.filter( + name__in=[user.pseudo for user in ServiceUser.objects.filter( + access_group=self.access_group + )]).values_list('dn', flat=True)) group.save() def __str__(self): return self.pseudo + @receiver(post_save, sender=ServiceUser) def service_user_post_save(sender, **kwargs): """ Synchronise un service user ldap après modification django""" service_user = kwargs['instance'] service_user.ldap_sync() + @receiver(post_delete, sender=ServiceUser) def service_user_post_delete(sender, **kwargs): """ Supprime un service user ldap après suppression django""" service_user = kwargs['instance'] service_user.ldap_del() + class Right(models.Model): - """ Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un manytomany + """ Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un + manytomany Ceci dit le résultat aurait été le même avec une table intermediaire""" PRETTY_NAME = "Droits affectés à des users" @@ -655,18 +798,21 @@ class Right(models.Model): def __str__(self): return str(self.user) + @receiver(post_save, sender=Right) def right_post_save(sender, **kwargs): """ Synchronise les users ldap groups avec les groupes de droits""" right = kwargs['instance'].right right.ldap_sync() + @receiver(post_delete, sender=Right) def right_post_delete(sender, **kwargs): """ Supprime l'user du groupe""" right = kwargs['instance'].right right.ldap_sync() + class School(models.Model): """ Etablissement d'enseignement""" PRETTY_NAME = "Etablissements enregistrés" @@ -678,46 +824,69 @@ class School(models.Model): class ListRight(models.Model): - """ Ensemble des droits existants. Chaque droit crée un groupe ldap synchronisé, avec gid. + """ Ensemble des droits existants. Chaque droit crée un groupe + ldap synchronisé, avec gid. Permet de gérer facilement les accès serveurs et autres - La clef de recherche est le gid, pour cette raison là il n'est plus modifiable après creation""" + La clef de recherche est le gid, pour cette raison là + il n'est plus modifiable après creation""" PRETTY_NAME = "Liste des droits existants" - listright = models.CharField(max_length=255, unique=True, validators=[RegexValidator('^[a-z]+$', message="Les groupes unix ne peuvent contenir que des lettres minuscules")]) + listright = models.CharField( + max_length=255, + unique=True, + validators=[RegexValidator( + '^[a-z]+$', + message="Les groupes unix ne peuvent contenir\ + que des lettres minuscules" + )] + ) gid = models.IntegerField(unique=True, null=True) - details = models.CharField(help_text="Description", max_length=255, blank=True) + details = models.CharField( + help_text="Description", + max_length=255, + blank=True + ) def __str__(self): return self.listright def ldap_sync(self): + """Sychronise les groups ldap avec le model listright coté django""" try: group_ldap = LdapUserGroup.objects.get(gid=self.gid) except LdapUserGroup.DoesNotExist: group_ldap = LdapUserGroup(gid=self.gid) group_ldap.name = self.listright - group_ldap.members = [right.user.pseudo for right in Right.objects.filter(right=self)] + group_ldap.members = [right.user.pseudo for right + in Right.objects.filter(right=self)] group_ldap.save() def ldap_del(self): + """Supprime un groupe ldap""" try: group_ldap = LdapUserGroup.objects.get(gid=self.gid) group_ldap.delete() except LdapUserGroup.DoesNotExist: pass + @receiver(post_save, sender=ListRight) def listright_post_save(sender, **kwargs): """ Synchronise le droit ldap quand il est modifié""" right = kwargs['instance'] right.ldap_sync() + @receiver(post_delete, sender=ListRight) def listright_post_delete(sender, **kwargs): + """Suppression d'un groupe ldap après suppression coté django""" right = kwargs['instance'] right.ldap_del() + class ListShell(models.Model): + """Un shell possible. Pas de check si ce shell existe, les + admin sont des grands""" PRETTY_NAME = "Liste des shells disponibles" shell = models.CharField(max_length=255, unique=True) @@ -725,6 +894,7 @@ class ListShell(models.Model): def __str__(self): return self.shell + class Ban(models.Model): """ Bannissement. Actuellement a un effet tout ou rien. Gagnerait à être granulaire""" @@ -734,38 +904,45 @@ class Ban(models.Model): STATE_SOFT = 1 STATE_BRIDAGE = 2 STATES = ( - (0, 'HARD (aucun accès)'), - (1, 'SOFT (accès local seulement)'), - (2, 'BRIDAGE (bridage du débit)'), - ) + (0, 'HARD (aucun accès)'), + (1, 'SOFT (accès local seulement)'), + (2, 'BRIDAGE (bridage du débit)'), + ) user = models.ForeignKey('User', on_delete=models.PROTECT) raison = models.CharField(max_length=255) date_start = models.DateTimeField(auto_now_add=True) date_end = models.DateTimeField(help_text='%d/%m/%y %H:%M:%S') - state = models.IntegerField(choices=STATES, default=STATE_HARD) + state = models.IntegerField(choices=STATES, default=STATE_HARD) def notif_ban(self): """ Prend en argument un objet ban, envoie un mail de notification """ - general_options, created = GeneralOption.objects.get_or_create() - t = loader.get_template('users/email_ban_notif') - options, created = AssoOption.objects.get_or_create() - c = Context({ + general_options, _created = GeneralOption.objects.get_or_create() + template = loader.get_template('users/email_ban_notif') + options, _created = AssoOption.objects.get_or_create() + context = Context({ 'name': str(self.user.name) + ' ' + str(self.user.surname), 'raison': self.raison, 'date_end': self.date_end, - 'asso_name' : options.name, + 'asso_name': options.name, }) - send_mail('Deconnexion disciplinaire', t.render(c), - general_options.email_from, [self.user.email], fail_silently=False) + send_mail( + 'Deconnexion disciplinaire', + template.render(context), + general_options.email_from, + [self.user.email], + fail_silently=False + ) return def is_active(self): - return self.date_end > now + """Ce ban est-il actif?""" + return self.date_end > DT_NOW def __str__(self): return str(self.user) + ' ' + str(self.raison) + @receiver(post_save, sender=Ban) def ban_post_save(sender, **kwargs): """ Regeneration de tous les services après modification d'un ban""" @@ -782,6 +959,7 @@ def ban_post_save(sender, **kwargs): regen('dhcp') regen('mac_ip_list') + @receiver(post_delete, sender=Ban) def ban_post_delete(sender, **kwargs): """ Regen de tous les services après suppression d'un ban""" @@ -791,7 +969,11 @@ def ban_post_delete(sender, **kwargs): regen('dhcp') regen('mac_ip_list') + class Whitelist(models.Model): + """Accès à titre gracieux. L'utilisateur ne paye pas; se voit + accorder un accès internet pour une durée défini. Moins + fort qu'un ban quel qu'il soit""" PRETTY_NAME = "Liste des accès gracieux" user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -800,13 +982,16 @@ class Whitelist(models.Model): date_end = models.DateTimeField(help_text='%d/%m/%y %H:%M:%S') def is_active(self): - return self.date_end > now + return self.date_end > DT_NOW def __str__(self): return str(self.user) + ' ' + str(self.raison) + @receiver(post_save, sender=Whitelist) def whitelist_post_save(sender, **kwargs): + """Après modification d'une whitelist, on synchronise les services + et on lui permet d'avoir internet""" whitelist = kwargs['instance'] user = whitelist.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -819,17 +1004,21 @@ def whitelist_post_save(sender, **kwargs): regen('dhcp') regen('mac_ip_list') + @receiver(post_delete, sender=Whitelist) def whitelist_post_delete(sender, **kwargs): + """Après suppression d'une whitelist, on supprime l'accès internet + en forçant la régénration""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) regen('mailing') regen('dhcp') regen('mac_ip_list') + class Request(models.Model): """ Objet request, générant une url unique de validation. - Utilisé par exemple pour la generation du mot de passe et + Utilisé par exemple pour la generation du mot de passe et sa réinitialisation""" PASSWD = 'PW' EMAIL = 'EM' @@ -845,38 +1034,86 @@ class Request(models.Model): def save(self): if not self.expires_at: - options, created = GeneralOption.objects.get_or_create() - self.expires_at = timezone.now() \ + options, _created = GeneralOption.objects.get_or_create() + self.expires_at = DT_NOW \ + datetime.timedelta(hours=options.req_expire_hrs) if not self.token: self.token = str(uuid.uuid4()).replace('-', '') # remove hyphens super(Request, self).save() + class LdapUser(ldapdb.models.Model): """ Class for representing an LDAP user entry. """ # LDAP meta-data base_dn = LDAP['base_user_dn'] - object_classes = ['inetOrgPerson','top','posixAccount','sambaSamAccount','radiusprofile', 'shadowAccount'] + object_classes = ['inetOrgPerson', 'top', 'posixAccount', + 'sambaSamAccount', 'radiusprofile', + 'shadowAccount'] # attributes gid = ldapdb.models.fields.IntegerField(db_column='gidNumber') - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) uid = ldapdb.models.fields.CharField(db_column='uid', max_length=200) - uidNumber = ldapdb.models.fields.IntegerField(db_column='uidNumber', unique=True) + uidNumber = ldapdb.models.fields.IntegerField( + db_column='uidNumber', + unique=True + ) sn = ldapdb.models.fields.CharField(db_column='sn', max_length=200) - login_shell = ldapdb.models.fields.CharField(db_column='loginShell', max_length=200, blank=True, null=True) - mail = ldapdb.models.fields.CharField(db_column='mail', max_length=200) - given_name = ldapdb.models.fields.CharField(db_column='givenName', max_length=200) - home_directory = ldapdb.models.fields.CharField(db_column='homeDirectory', max_length=200) - display_name = ldapdb.models.fields.CharField(db_column='displayName', max_length=200, blank=True, null=True) + login_shell = ldapdb.models.fields.CharField( + db_column='loginShell', + max_length=200, + blank=True, + null=True + ) + mail = ldapdb.models.fields.CharField(db_column='mail', max_length=200) + given_name = ldapdb.models.fields.CharField( + db_column='givenName', + max_length=200 + ) + home_directory = ldapdb.models.fields.CharField( + db_column='homeDirectory', + max_length=200 + ) + display_name = ldapdb.models.fields.CharField( + db_column='displayName', + max_length=200, + blank=True, + null=True + ) dialupAccess = ldapdb.models.fields.CharField(db_column='dialupAccess') - sambaSID = ldapdb.models.fields.IntegerField(db_column='sambaSID', unique=True) - user_password = ldapdb.models.fields.CharField(db_column='userPassword', max_length=200, blank=True, null=True) - sambat_nt_password = ldapdb.models.fields.CharField(db_column='sambaNTPassword', max_length=200, blank=True, null=True) - macs = ldapdb.models.fields.ListField(db_column='radiusCallingStationId', max_length=200, blank=True, null=True) - shadowexpire = ldapdb.models.fields.CharField(db_column='shadowExpire', blank=True, null=True) + sambaSID = ldapdb.models.fields.IntegerField( + db_column='sambaSID', + unique=True + ) + user_password = ldapdb.models.fields.CharField( + db_column='userPassword', + max_length=200, + blank=True, + null=True + ) + sambat_nt_password = ldapdb.models.fields.CharField( + db_column='sambaNTPassword', + max_length=200, + blank=True, + null=True + ) + macs = ldapdb.models.fields.ListField( + db_column='radiusCallingStationId', + max_length=200, + blank=True, + null=True + ) + shadowexpire = ldapdb.models.fields.CharField( + db_column='shadowExpire', + blank=True, + null=True + ) def __str__(self): return self.name @@ -890,9 +1127,12 @@ class LdapUser(ldapdb.models.Model): self.sambaSID = self.uidNumber super(LdapUser, self).save(*args, **kwargs) + class LdapUserGroup(ldapdb.models.Model): """ - Class for representing an LDAP user entry. + Class for representing an LDAP group entry. + + Un groupe ldap """ # LDAP meta-data base_dn = LDAP['base_usergroup_dn'] @@ -901,38 +1141,64 @@ class LdapUserGroup(ldapdb.models.Model): # attributes gid = ldapdb.models.fields.IntegerField(db_column='gidNumber') members = ldapdb.models.fields.ListField(db_column='memberUid', blank=True) - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) def __str__(self): return self.name + class LdapServiceUser(ldapdb.models.Model): """ Class for representing an LDAP userservice entry. + + Un user de service coté ldap """ # LDAP meta-data base_dn = LDAP['base_userservice_dn'] - object_classes = ['applicationProcess','simpleSecurityObject'] + object_classes = ['applicationProcess', 'simpleSecurityObject'] # attributes - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) - user_password = ldapdb.models.fields.CharField(db_column='userPassword', max_length=200, blank=True, null=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) + user_password = ldapdb.models.fields.CharField( + db_column='userPassword', + max_length=200, + blank=True, + null=True + ) def __str__(self): return self.name + class LdapServiceUserGroup(ldapdb.models.Model): """ Class for representing an LDAP userservice entry. + + Un group user de service coté ldap. Dans userservicegroupdn + (voir dans settings_local.py) """ # LDAP meta-data base_dn = LDAP['base_userservicegroup_dn'] object_classes = ['groupOfNames'] # attributes - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) - members = ldapdb.models.fields.ListField(db_column='member', blank=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) + members = ldapdb.models.fields.ListField( + db_column='member', + blank=True + ) def __str__(self): return self.name - diff --git a/users/templates/users/del_right.html b/users/templates/users/del_right.html index 740778a2..30edf666 100644 --- a/users/templates/users/del_right.html +++ b/users/templates/users/del_right.html @@ -35,19 +35,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - - - {% for key, values in userform.items %} - - {% endfor %} - - - + {% for key, values in userform.items %} - {% bootstrap_form_errors values %} - - {% endfor %} + + + + + + {% endfor %} +
{{ key }}
{{ values.rights }}
+ +
+
+
    + {% for user in values.rights %} +
  • {{ user }}
  • + {% endfor %} +
+
+
{% bootstrap_button "Modifier" button_type="submit" icon="star" %}
diff --git a/users/templates/users/user.html b/users/templates/users/user.html index bbdc7fc5..756b4153 100644 --- a/users/templates/users/user.html +++ b/users/templates/users/user.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'utilisateur{% endblock %} @@ -32,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% bootstrap_form userform %} + {% massive_bootstrap_form userform 'room' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}

diff --git a/users/urls.py b/users/urls.py index 43054fe5..531e0826 100644 --- a/users/urls.py +++ b/users/urls.py @@ -19,6 +19,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. +""" +Definition des urls, pointant vers les views +""" from __future__ import unicode_literals @@ -32,39 +35,88 @@ urlpatterns = [ url(r'^state/(?P[0-9]+)$', views.state, name='state'), url(r'^password/(?P[0-9]+)$', views.password, name='password'), url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'), - url(r'^edit_serviceuser/(?P[0-9]+)$', views.edit_serviceuser, name='edit-serviceuser'), - url(r'^del_serviceuser/(?P[0-9]+)$', views.del_serviceuser, name='del-serviceuser'), + url( + r'^edit_serviceuser/(?P[0-9]+)$', + views.edit_serviceuser, + name='edit-serviceuser' + ), + url( + r'^del_serviceuser/(?P[0-9]+)$', + views.del_serviceuser, + name='del-serviceuser' + ), url(r'^add_ban/(?P[0-9]+)$', views.add_ban, name='add-ban'), url(r'^edit_ban/(?P[0-9]+)$', views.edit_ban, name='edit-ban'), - url(r'^add_whitelist/(?P[0-9]+)$', views.add_whitelist, name='add-whitelist'), - url(r'^edit_whitelist/(?P[0-9]+)$', views.edit_whitelist, name='edit-whitelist'), + url( + r'^add_whitelist/(?P[0-9]+)$', + views.add_whitelist, + name='add-whitelist' + ), + url( + r'^edit_whitelist/(?P[0-9]+)$', + views.edit_whitelist, + name='edit-whitelist' + ), url(r'^add_right/(?P[0-9]+)$', views.add_right, name='add-right'), url(r'^del_right/$', views.del_right, name='del-right'), url(r'^add_school/$', views.add_school, name='add-school'), - url(r'^edit_school/(?P[0-9]+)$', views.edit_school, name='edit-school'), + url( + r'^edit_school/(?P[0-9]+)$', + views.edit_school, + name='edit-school' + ), url(r'^del_school/$', views.del_school, name='del-school'), url(r'^add_listright/$', views.add_listright, name='add-listright'), - url(r'^edit_listright/(?P[0-9]+)$', views.edit_listright, name='edit-listright'), + url( + r'^edit_listright/(?P[0-9]+)$', + views.edit_listright, + name='edit-listright' + ), url(r'^del_listright/$', views.del_listright, name='del-listright'), url(r'^profil/(?P[0-9]+)$', views.profil, name='profil'), url(r'^index_ban/$', views.index_ban, name='index-ban'), url(r'^index_white/$', views.index_white, name='index-white'), url(r'^index_school/$', views.index_school, name='index-school'), url(r'^index_listright/$', views.index_listright, name='index-listright'), - url(r'^index_serviceusers/$', views.index_serviceusers, name='index-serviceusers'), + url( + r'^index_serviceusers/$', + views.index_serviceusers, + name='index-serviceusers' + ), url(r'^mon_profil/$', views.mon_profil, name='mon-profil'), url(r'^process/(?P[a-z0-9]{32})/$', views.process, name='process'), url(r'^reset_password/$', views.reset_password, name='reset-password'), url(r'^mass_archive/$', views.mass_archive, name='mass-archive'), - url(r'^history/(?Puser)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pban)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pwhitelist)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pschool)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Plistright)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pserviceuser)/(?P[0-9]+)$', views.history, name='history'), + url( + r'^history/(?Puser)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pban)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pwhitelist)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pschool)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Plistright)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pserviceuser)/(?P[0-9]+)$', + views.history, + name='history' + ), url(r'^$', views.index, name='index'), url(r'^rest/mailing/$', views.mailing, name='mailing'), - ] - - diff --git a/users/views.py b/users/views.py index 66a5f7ad..5b0b3910 100644 --- a/users/views.py +++ b/users/views.py @@ -23,20 +23,25 @@ # App de gestion des users pour re2o # Goulven Kermarec, Gabriel Détraz, Lemesle Augustin # Gplv2 +""" +Module des views. + +On définit les vues pour l'ajout, l'edition des users : infos personnelles, +mot de passe, etc + +Permet aussi l'ajout, edition et suppression des droits, des bannissements, +des whitelist, des services users et des écoles +""" from __future__ import unicode_literals from django.shortcuts import get_object_or_404, render, redirect -from django.template.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import Max, ProtectedError +from django.db.models import ProtectedError from django.db import IntegrityError -from django.core.mail import send_mail from django.utils import timezone -from django.core.urlresolvers import reverse from django.db import transaction from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt @@ -47,21 +52,20 @@ from rest_framework.renderers import JSONRenderer from reversion.models import Version from reversion import revisions as reversion from users.serializers import MailSerializer -from users.models import User, Right, Ban, Whitelist, School, ListRight, Request, ServiceUser, all_has_access -from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm, DelListRightForm, NewListRightForm -from users.forms import EditInfoForm, InfoForm, BaseInfoForm, StateForm, RightForm, SchoolForm, EditServiceUserForm, ServiceUserForm, ListRightForm -from cotisations.models import Facture -from machines.models import Machine, Interface +from users.models import User, Right, Ban, Whitelist, School, ListRight +from users.models import Request, ServiceUser +from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm +from users.forms import DelListRightForm, NewListRightForm +from users.forms import InfoForm, BaseInfoForm, StateForm +from users.forms import RightForm, SchoolForm, EditServiceUserForm +from users.forms import ServiceUserForm, ListRightForm from users.forms import MassArchiveForm, PassForm, ResetPasswordForm -from preferences.models import OptionalUser, AssoOption, GeneralOption +from cotisations.models import Facture +from machines.models import Machine +from preferences.models import OptionalUser, GeneralOption -from re2o.login import hashNT - - -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) +from re2o.views import form +from re2o.utils import all_has_access def password_change_action(u_form, user, request, req=False): """ Fonction qui effectue le changeemnt de mdp bdd""" @@ -75,10 +79,12 @@ def password_change_action(u_form, user, request, req=False): return redirect("/") return redirect("/users/profil/" + str(user.id)) + @login_required @permission_required('cableur') def new_user(request): - """ Vue de création d'un nouvel utilisateur, envoie un mail pour le mot de passe""" + """ Vue de création d'un nouvel utilisateur, + envoie un mail pour le mot de passe""" user = InfoForm(request.POST or None) if user.is_valid(): user = user.save(commit=False) @@ -87,21 +93,25 @@ def new_user(request): reversion.set_user(request.user) reversion.set_comment("Création") user.reset_passwd_mail(request) - messages.success(request, "L'utilisateur %s a été crée, un mail pour l'initialisation du mot de passe a été envoyé" % user.pseudo) + messages.success(request, "L'utilisateur %s a été crée, un mail\ + pour l'initialisation du mot de passe a été envoyé" % user.pseudo) return redirect("/users/profil/" + str(user.id)) return form({'userform': user}, 'users/user.html', request) + @login_required def edit_info(request, userid): - """ Edite un utilisateur à partir de son id, - si l'id est différent de request.user, vérifie la possession du droit cableur """ + """ Edite un utilisateur à partir de son id, + si l'id est différent de request.user, vérifie la + possession du droit cableur """ try: user = User.objects.get(pk=userid) except User.DoesNotExist: messages.error(request, "Utilisateur inexistant") return redirect("/users/") if not request.user.has_perms(('cableur',)) and user != request.user: - messages.error(request, "Vous ne pouvez pas modifier un autre user que vous sans droit cableur") + messages.error(request, "Vous ne pouvez pas modifier un autre\ + user que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) if not request.user.has_perms(('cableur',)): user = BaseInfoForm(request.POST or None, instance=user) @@ -111,15 +121,19 @@ def edit_info(request, userid): with transaction.atomic(), reversion.create_revision(): user.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in user.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in user.changed_data + )) messages.success(request, "L'user a bien été modifié") return redirect("/users/profil/" + userid) return form({'userform': user}, 'users/user.html', request) + @login_required @permission_required('bureau') def state(request, userid): - """ Changer l'etat actif/desactivé/archivé d'un user, need droit bureau """ + """ Changer l'etat actif/desactivé/archivé d'un user, + need droit bureau """ try: user = User.objects.get(pk=userid) except User.DoesNotExist: @@ -135,12 +149,15 @@ def state(request, userid): elif state.cleaned_data['state'] == User.STATE_DISABLED: user.state = User.STATE_DISABLED reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in state.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in state.changed_data + )) user.save() messages.success(request, "Etat changé avec succès") return redirect("/users/profil/" + userid) return form({'userform': state}, 'users/user.html', request) + @login_required def password(request, userid): """ Reinitialisation d'un mot de passe à partir de l'userid, @@ -152,16 +169,20 @@ def password(request, userid): messages.error(request, "Utilisateur inexistant") return redirect("/users/") if not request.user.has_perms(('cableur',)) and user != request.user: - messages.error(request, "Vous ne pouvez pas modifier un autre user que vous sans droit cableur") + messages.error(request, "Vous ne pouvez pas modifier un\ + autre user que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) - if not request.user.has_perms(('bureau',)) and user != request.user and Right.objects.filter(user=user): - messages.error(request, "Il faut les droits bureau pour modifier le mot de passe d'un membre actif") + if not request.user.has_perms(('bureau',)) and user != request.user\ + and Right.objects.filter(user=user): + messages.error(request, "Il faut les droits bureau pour modifier le\ + mot de passe d'un membre actif") return redirect("/users/profil/" + str(request.user.id)) u_form = PassForm(request.POST or None) if u_form.is_valid(): return password_change_action(u_form, user, request) return form({'userform': u_form}, 'users/user.html', request) + @login_required @permission_required('infra') def new_serviceuser(request): @@ -174,15 +195,20 @@ def new_serviceuser(request): user_object.save() reversion.set_user(request.user) reversion.set_comment("Création") - messages.success(request, "L'utilisateur %s a été crée" % user_object.pseudo) + messages.success( + request, + "L'utilisateur %s a été crée" % user_object.pseudo + ) return redirect("/users/index_serviceusers/") return form({'userform': user}, 'users/user.html', request) + @login_required @permission_required('infra') def edit_serviceuser(request, userid): - """ Edite un utilisateur à partir de son id, - si l'id est différent de request.user, vérifie la possession du droit cableur """ + """ Edite un utilisateur à partir de son id, + si l'id est différent de request.user, + vérifie la possession du droit cableur """ try: user = ServiceUser.objects.get(pk=userid) except ServiceUser.DoesNotExist: @@ -196,18 +222,22 @@ def edit_serviceuser(request, userid): user_object.set_password(user.cleaned_data['password']) user_object.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in user.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in user.changed_data + )) messages.success(request, "L'user a bien été modifié") return redirect("/users/index_serviceusers") return form({'userform': user}, 'users/user.html', request) + @login_required @permission_required('infra') def del_serviceuser(request, userid): + """Suppression d'un ou plusieurs serviceusers""" try: user = ServiceUser.objects.get(pk=userid) except ServiceUser.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) + messages.error(request, u"Utilisateur inexistant") return redirect("/users/") if request.method == "POST": with transaction.atomic(), reversion.create_revision(): @@ -215,7 +245,12 @@ def del_serviceuser(request, userid): reversion.set_user(request.user) messages.success(request, "L'user a été détruite") return redirect("/users/index_serviceusers/") - return form({'objet': user, 'objet_name': 'serviceuser'}, 'users/delete.html', request) + return form( + {'objet': user, 'objet_name': 'serviceuser'}, + 'users/delete.html', + request + ) + @login_required @permission_required('bureau') @@ -241,28 +276,33 @@ def add_right(request, userid): return redirect("/users/profil/" + userid) return form({'userform': right}, 'users/user.html', request) + @login_required @permission_required('bureau') def del_right(request): """ Supprimer un droit à un user, need droit bureau """ user_right_list = dict() for right in ListRight.objects.all(): - user_right_list[right]= DelRightForm(right, request.POST or None) - for keys, right_item in user_right_list.items(): + user_right_list[right] = DelRightForm(right, request.POST or None) + for _keys, right_item in user_right_list.items(): if right_item.is_valid(): right_del = right_item.cleaned_data['rights'] with transaction.atomic(), reversion.create_revision(): reversion.set_user(request.user) - reversion.set_comment("Retrait des droit %s" % ','.join(str(deleted_right) for deleted_right in right_del)) + reversion.set_comment("Retrait des droit %s" % ','.join( + str(deleted_right) for deleted_right in right_del + )) right_del.delete() messages.success(request, "Droit retiré avec succès") return redirect("/users/") return form({'userform': user_right_list}, 'users/del_right.html', request) + @login_required @permission_required('bofh') def add_ban(request, userid): - """ Ajouter un banissement, nécessite au moins le droit bofh (a fortiori bureau) + """ Ajouter un banissement, nécessite au moins le droit bofh + (a fortiori bureau) Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" try: user = User.objects.get(pk=userid) @@ -273,7 +313,7 @@ def add_ban(request, userid): ban = BanForm(request.POST or None, instance=ban_instance) if ban.is_valid(): with transaction.atomic(), reversion.create_revision(): - ban_object = ban.save() + _ban_object = ban.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Bannissement ajouté") @@ -285,10 +325,12 @@ def add_ban(request, userid): ) return form({'userform': ban}, 'users/user.html', request) + @login_required @permission_required('bofh') def edit_ban(request, banid): - """ Editer un bannissement, nécessite au moins le droit bofh (a fortiori bureau) + """ Editer un bannissement, nécessite au moins le droit bofh + (a fortiori bureau) Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" try: ban_instance = Ban.objects.get(pk=banid) @@ -300,23 +342,31 @@ def edit_ban(request, banid): with transaction.atomic(), reversion.create_revision(): ban.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ban.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in ban.changed_data + )) messages.success(request, "Bannissement modifié") return redirect("/users/") return form({'userform': ban}, 'users/user.html', request) + @login_required @permission_required('cableur') def add_whitelist(request, userid): - """ Accorder un accès gracieux, temporaire ou permanent. Need droit cableur - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, raison obligatoire""" + """ Accorder un accès gracieux, temporaire ou permanent. + Need droit cableur + Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, + raison obligatoire""" try: user = User.objects.get(pk=userid) except User.DoesNotExist: messages.error(request, "Utilisateur inexistant") return redirect("/users/") whitelist_instance = Whitelist(user=user) - whitelist = WhitelistForm(request.POST or None, instance=whitelist_instance) + whitelist = WhitelistForm( + request.POST or None, + instance=whitelist_instance + ) if whitelist.is_valid(): with transaction.atomic(), reversion.create_revision(): whitelist.save() @@ -331,30 +381,40 @@ def add_whitelist(request, userid): ) return form({'userform': whitelist}, 'users/user.html', request) + @login_required @permission_required('cableur') def edit_whitelist(request, whitelistid): - """ Editer un accès gracieux, temporaire ou permanent. Need droit cableur - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, raison obligatoire""" + """ Editer un accès gracieux, temporaire ou permanent. + Need droit cableur + Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, + raison obligatoire""" try: whitelist_instance = Whitelist.objects.get(pk=whitelistid) except Whitelist.DoesNotExist: messages.error(request, "Entrée inexistante") return redirect("/users/") - whitelist = WhitelistForm(request.POST or None, instance=whitelist_instance) + whitelist = WhitelistForm( + request.POST or None, + instance=whitelist_instance + ) if whitelist.is_valid(): with transaction.atomic(), reversion.create_revision(): whitelist.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in whitelist.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in whitelist.changed_data + )) messages.success(request, "Whitelist modifiée") return redirect("/users/") return form({'userform': whitelist}, 'users/user.html', request) + @login_required @permission_required('cableur') def add_school(request): - """ Ajouter un établissement d'enseignement à la base de donnée, need cableur""" + """ Ajouter un établissement d'enseignement à la base de donnée, + need cableur""" school = SchoolForm(request.POST or None) if school.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -365,30 +425,37 @@ def add_school(request): return redirect("/users/index_school/") return form({'userform': school}, 'users/user.html', request) + @login_required @permission_required('cableur') def edit_school(request, schoolid): - """ Editer un établissement d'enseignement à partir du schoolid dans la base de donnée, need cableur""" + """ Editer un établissement d'enseignement à partir du schoolid dans + la base de donnée, need cableur""" try: school_instance = School.objects.get(pk=schoolid) except School.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/users/") school = SchoolForm(request.POST or None, instance=school_instance) if school.is_valid(): with transaction.atomic(), reversion.create_revision(): school.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in school.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in school.changed_data + )) messages.success(request, "Établissement modifié") return redirect("/users/index_school/") return form({'userform': school}, 'users/user.html', request) + @login_required @permission_required('cableur') def del_school(request): - """ Supprimer un établissement d'enseignement à la base de donnée, need cableur - Objet protégé, possible seulement si aucun user n'est affecté à l'établissement """ + """ Supprimer un établissement d'enseignement à la base de donnée, + need cableur + Objet protégé, possible seulement si aucun user n'est affecté à + l'établissement """ school = DelSchoolForm(request.POST or None) if school.is_valid(): school_dels = school.cleaned_data['schools'] @@ -406,6 +473,7 @@ def del_school(request): return redirect("/users/index_school/") return form({'userform': school}, 'users/user.html', request) + @login_required @permission_required('bureau') def add_listright(request): @@ -421,29 +489,38 @@ def add_listright(request): return redirect("/users/index_listright/") return form({'userform': listright}, 'users/user.html', request) + @login_required @permission_required('bureau') def edit_listright(request, listrightid): - """ Editer un groupe/droit, necessite droit bureau, à partir du listright id """ + """ Editer un groupe/droit, necessite droit bureau, + à partir du listright id """ try: listright_instance = ListRight.objects.get(pk=listrightid) except ListRight.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/users/") - listright = ListRightForm(request.POST or None, instance=listright_instance) + listright = ListRightForm( + request.POST or None, + instance=listright_instance + ) if listright.is_valid(): with transaction.atomic(), reversion.create_revision(): listright.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in listright.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in listright.changed_data + )) messages.success(request, "Droit modifié") return redirect("/users/index_listright/") return form({'userform': listright}, 'users/user.html', request) + @login_required @permission_required('bureau') def del_listright(request): - """ Supprimer un ou plusieurs groupe, possible si il est vide, need droit bureau """ + """ Supprimer un ou plusieurs groupe, possible si il est vide, need droit + bureau """ listright = DelListRightForm(request.POST or None) if listright.is_valid(): listright_dels = listright.cleaned_data['listrights'] @@ -461,6 +538,7 @@ def del_listright(request): return redirect("/users/index_listright/") return form({'userform': listright}, 'users/user.html', request) + @login_required @permission_required('bureau') def mass_archive(request): @@ -469,7 +547,10 @@ def mass_archive(request): to_archive_list = [] if to_archive_date.is_valid(): date = to_archive_date.cleaned_data['date'] - to_archive_list = [user for user in User.objects.exclude(state=User.STATE_ARCHIVE) if not user.end_access() or user.end_access() < date] + to_archive_list = [user for user in + User.objects.exclude(state=User.STATE_ARCHIVE) + if not user.end_access() + or user.end_access() < date] if "valider" in request.POST: for user in to_archive_list: with transaction.atomic(), reversion.create_revision(): @@ -477,15 +558,22 @@ def mass_archive(request): user.save() reversion.set_user(request.user) reversion.set_comment("Archivage") - messages.success(request, "%s users ont été archivés" % len(to_archive_list)) - return redirect("/users/") - return form({'userform': to_archive_date, 'to_archive_list': to_archive_list}, 'users/mass_archive.html', request) + messages.success(request, "%s users ont été archivés" % len( + to_archive_list + )) + return redirect("/users/") + return form( + {'userform': to_archive_date, 'to_archive_list': to_archive_list}, + 'users/mass_archive.html', + request + ) + @login_required @permission_required('cableur') def index(request): """ Affiche l'ensemble des users, need droit cableur """ - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number users_list = User.objects.select_related('room').order_by('state', 'name') paginator = Paginator(users_list, pagination_number) @@ -500,13 +588,15 @@ def index(request): users_list = paginator.page(paginator.num_pages) return render(request, 'users/index.html', {'users_list': users_list}) + @login_required @permission_required('cableur') def index_ban(request): """ Affiche l'ensemble des ban, need droit cableur """ - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - ban_list = Ban.objects.order_by('date_start').select_related('user').reverse() + ban_list = Ban.objects.order_by('date_start')\ + .select_related('user').reverse() paginator = Paginator(ban_list, pagination_number) page = request.GET.get('page') try: @@ -515,17 +605,19 @@ def index_ban(request): # If page isn't an integer, deliver first page ban_list = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - ban_list = paginator.page(paginator.num_pages) + # If page is out of range (e.g. 9999), deliver last page of results. + ban_list = paginator.page(paginator.num_pages) return render(request, 'users/index_ban.html', {'ban_list': ban_list}) + @login_required @permission_required('cableur') def index_white(request): """ Affiche l'ensemble des whitelist, need droit cableur """ - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - white_list = Whitelist.objects.select_related('user').order_by('date_start') + white_list = Whitelist.objects.select_related('user')\ + .order_by('date_start') paginator = Paginator(white_list, pagination_number) page = request.GET.get('page') try: @@ -534,92 +626,114 @@ def index_white(request): # If page isn't an integer, deliver first page white_list = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - white_list = paginator.page(paginator.num_pages) + # If page is out of range (e.g. 9999), deliver last page of results. + white_list = paginator.page(paginator.num_pages) return render( request, 'users/index_whitelist.html', {'white_list': white_list} ) + @login_required @permission_required('cableur') def index_school(request): """ Affiche l'ensemble des établissement, need droit cableur """ school_list = School.objects.order_by('name') - return render(request, 'users/index_schools.html', {'school_list':school_list}) + return render( + request, + 'users/index_schools.html', + {'school_list': school_list} + ) + @login_required @permission_required('cableur') def index_listright(request): """ Affiche l'ensemble des droits , need droit cableur """ listright_list = ListRight.objects.order_by('listright') - return render(request, 'users/index_listright.html', {'listright_list':listright_list}) + return render( + request, + 'users/index_listright.html', + {'listright_list': listright_list} + ) + @login_required @permission_required('cableur') def index_serviceusers(request): """ Affiche les users de services (pour les accès ldap)""" serviceusers_list = ServiceUser.objects.order_by('pseudo') - return render(request, 'users/index_serviceusers.html', {'serviceusers_list':serviceusers_list}) + return render( + request, + 'users/index_serviceusers.html', + {'serviceusers_list': serviceusers_list} + ) + @login_required -def history(request, object, id): +def history(request, object_name, object_id): """ Affichage de l'historique : (acl, argument) user : self or cableur, userid, ban : self or cableur, banid, whitelist : self or cableur, whitelistid, school : cableur, schoolid, listright : cableur, listrightid """ - if object == 'user': + if object_name == 'user': try: - object_instance = User.objects.get(pk=id) + object_instance = User.objects.get(pk=object_id) except User.DoesNotExist: - messages.error(request, "Utilisateur inexistant") - return redirect("/users/") - if not request.user.has_perms(('cableur',)) and object_instance != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) - elif object == 'serviceuser' and request.user.has_perms(('cableur',)): + messages.error(request, "Utilisateur inexistant") + return redirect("/users/") + if not request.user.has_perms(('cableur',)) and\ + object_instance != request.user: + messages.error(request, "Vous ne pouvez pas afficher\ + l'historique d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) + elif object_name == 'serviceuser' and request.user.has_perms(('cableur',)): try: - object_instance = ServiceUser.objects.get(pk=id) + object_instance = ServiceUser.objects.get(pk=object_id) except ServiceUser.DoesNotExist: - messages.error(request, "User service inexistant") - return redirect("/users/") - elif object == 'ban': + messages.error(request, "User service inexistant") + return redirect("/users/") + elif object_name == 'ban': try: - object_instance = Ban.objects.get(pk=id) + object_instance = Ban.objects.get(pk=object_id) except Ban.DoesNotExist: - messages.error(request, "Bannissement inexistant") - return redirect("/users/") - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher les bans d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) - elif object == 'whitelist': + messages.error(request, "Bannissement inexistant") + return redirect("/users/") + if not request.user.has_perms(('cableur',)) and\ + object_instance.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher les bans\ + d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) + elif object_name == 'whitelist': try: - object_instance = Whitelist.objects.get(pk=id) - except Whiltelist.DoesNotExist: - messages.error(request, "Whitelist inexistant") - return redirect("/users/") - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher les whitelist d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) - elif object == 'school' and request.user.has_perms(('cableur',)): + object_instance = Whitelist.objects.get(pk=object_id) + except Whitelist.DoesNotExist: + messages.error(request, "Whitelist inexistant") + return redirect("/users/") + if not request.user.has_perms(('cableur',)) and\ + object_instance.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher les\ + whitelist d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) + elif object_name == 'school' and request.user.has_perms(('cableur',)): try: - object_instance = School.objects.get(pk=id) + object_instance = School.objects.get(pk=object_id) except School.DoesNotExist: - messages.error(request, "Ecole inexistante") - return redirect("/users/") - elif object == 'listright' and request.user.has_perms(('cableur',)): + messages.error(request, "Ecole inexistante") + return redirect("/users/") + elif object_name == 'listright' and request.user.has_perms(('cableur',)): try: - object_instance = ListRight.objects.get(pk=id) + object_instance = ListRight.objects.get(pk=object_id) except ListRight.DoesNotExist: - messages.error(request, "Droit inexistant") - return redirect("/users/") + messages.error(request, "Droit inexistant") + return redirect("/users/") else: messages.error(request, "Objet inconnu") return redirect("/users/") - 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) @@ -632,7 +746,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 @@ -640,6 +758,7 @@ def mon_profil(request): """ Lien vers profil, renvoie request.id à la fonction """ return redirect("/users/profil/" + str(request.user.id)) + @login_required def profil(request, userid): """ Affiche un profil, self or cableur, prend un userid en argument """ @@ -649,14 +768,19 @@ def profil(request, userid): messages.error(request, "Utilisateur inexistant") return redirect("/users/") 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") + 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() + options, _created = OptionalUser.objects.get_or_create() user_solde = options.user_solde return render( request, @@ -672,46 +796,56 @@ def profil(request, userid): } ) + def reset_password(request): """ Reintialisation du mot de passe si mdp oublié """ userform = ResetPasswordForm(request.POST or None) if userform.is_valid(): try: - user = User.objects.get(pseudo=userform.cleaned_data['pseudo'],email=userform.cleaned_data['email']) + user = User.objects.get( + pseudo=userform.cleaned_data['pseudo'], + email=userform.cleaned_data['email'] + ) except User.DoesNotExist: messages.error(request, "Cet utilisateur n'existe pas") return form({'userform': userform}, 'users/user.html', request) user.reset_passwd_mail(request) - messages.success(request, "Un mail pour l'initialisation du mot de passe a été envoyé") - redirect("/") + messages.success(request, "Un mail pour l'initialisation du mot\ + de passe a été envoyé") + redirect("/") return form({'userform': userform}, 'users/user.html', request) + def process(request, token): + """Process, lien pour la reinitialisation du mot de passe""" valid_reqs = Request.objects.filter(expires_at__gt=timezone.now()) req = get_object_or_404(valid_reqs, token=token) if req.type == Request.PASSWD: return process_passwd(request, req) - elif req.type == Request.EMAIL: - return process_email(request, req=req) else: messages.error(request, "Entrée incorrecte, contactez un admin") redirect("/") + def process_passwd(request, req): + """Process le changeemnt de mot de passe, renvoie le formulaire + demandant le nouveau password""" u_form = PassForm(request.POST or None) user = req.user if u_form.is_valid(): return password_change_action(u_form, user, request, req=req) return form({'userform': u_form}, 'users/user.html', request) -""" Framework Rest """ + class JSONResponse(HttpResponse): + """ Framework Rest """ def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' super(JSONResponse, self).__init__(content, **kwargs) + @csrf_exempt @login_required @permission_required('serveur') @@ -721,4 +855,3 @@ def mailing(request): mails = all_has_access().values('email').distinct() seria = MailSerializer(mails, many=True) return JSONResponse(seria.data) -