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..354da1f1 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -19,74 +19,140 @@ # 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.db.models import Q 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) + +class SelectUserArticleForm(Form): + """Selection d'un article lors de la creation d'une facture""" + article = forms.ModelChoiceField( + queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')), + label="Article", + required=True + ) + quantity = forms.IntegerField( + label="Quantité", + validators=[MinValueValidator(1)], + required=True + ) + + +class SelectClubArticleForm(Form): + """Selection d'un article lors de la creation d'une facture""" + article = forms.ModelChoiceField( + queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')), + 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 +163,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/migrations/0024_auto_20171015_2033.py b/cotisations/migrations/0024_auto_20171015_2033.py new file mode 100644 index 00000000..b52dad62 --- /dev/null +++ b/cotisations/migrations/0024_auto_20171015_2033.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0023_auto_20170902_1303'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='duration', + field=models.PositiveIntegerField(blank=True, help_text='Durée exprimée en mois entiers', null=True, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name='vente', + name='duration', + field=models.PositiveIntegerField(blank=True, help_text='Durée exprimée en mois entiers', null=True), + ), + ] diff --git a/cotisations/migrations/0025_article_type_user.py b/cotisations/migrations/0025_article_type_user.py new file mode 100644 index 00000000..5ad329fa --- /dev/null +++ b/cotisations/migrations/0025_article_type_user.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-27 03:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0024_auto_20171015_2033'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='type_user', + field=models.CharField(choices=[('Adherent', 'Adherent'), ('Club', 'Club'), ('All', 'All')], default='All', max_length=255), + ), + ] diff --git a/cotisations/migrations/0026_auto_20171028_0126.py b/cotisations/migrations/0026_auto_20171028_0126.py new file mode 100644 index 00000000..436e0574 --- /dev/null +++ b/cotisations/migrations/0026_auto_20171028_0126.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-27 23:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def create_type(apps, schema_editor): + Cotisation = apps.get_model('cotisations', 'Cotisation') + Vente = apps.get_model('cotisations', 'Vente') + Article = apps.get_model('cotisations', 'Article') + db_alias = schema_editor.connection.alias + articles = Article.objects.using(db_alias).all() + ventes = Vente.objects.using(db_alias).all() + cotisations = Cotisation.objects.using(db_alias).all() + for article in articles: + if article.iscotisation: + article.type_cotisation='All' + article.save(using=db_alias) + for vente in ventes: + if vente.iscotisation: + vente.type_cotisation='All' + vente.save(using=db_alias) + for cotisation in cotisations: + cotisation.type_cotisation='All' + cotisation.save(using=db_alias) + +def delete_type(apps, schema_editor): + Vente = apps.get_model('cotisations', 'Vente') + Article = apps.get_model('cotisations', 'Article') + db_alias = schema_editor.connection.alias + articles = Articles.objects.using(db_alias).all() + ventes = Vente.objects.using(db_alias).all() + for article in articles: + if article.type_cotisation: + article.iscotisation=True + else: + article.iscotisation=False + article.save(using=db_alias) + for vente in ventes: + if vente.iscotisation: + vente.iscotisation=True + else: + vente.iscotisation=False + vente.save(using=db_alias) + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0025_article_type_user'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='type_cotisation', + field=models.CharField(blank=True, choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], default=None, max_length=255, null=True), + ), + migrations.AddField( + model_name='cotisation', + name='type_cotisation', + field=models.CharField(choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], max_length=255), + ), + migrations.AddField( + model_name='vente', + name='type_cotisation', + field=models.CharField(blank=True, choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], max_length=255, null=True), + ), + migrations.RunPython(create_type, delete_type), + migrations.RemoveField( + model_name='article', + name='iscotisation', + ), + migrations.RemoveField( + model_name='vente', + name='iscotisation', + ), + ] diff --git a/cotisations/migrations/0027_auto_20171029_1156.py b/cotisations/migrations/0027_auto_20171029_1156.py new file mode 100644 index 00000000..8a9a4f0c --- /dev/null +++ b/cotisations/migrations/0027_auto_20171029_1156.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-29 10:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0026_auto_20171028_0126'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='name', + field=models.CharField(max_length=255), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index fca6bfa5..090636be 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -20,141 +20,269 @@ # 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 import Q 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" + COTISATION_TYPE = ( + ('Connexion', 'Connexion'), + ('Adhesion', 'Adhesion'), + ('All', 'All'), + ) + facture = models.ForeignKey('Facture', on_delete=models.CASCADE) number = models.IntegerField(validators=[MinValueValidator(1)]) 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.PositiveIntegerField( + help_text="Durée exprimée en mois entiers", + blank=True, + null=True) + type_cotisation = models.CharField( + choices=COTISATION_TYPE, + blank=True, + null=True, + max_length=255 + ) 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""" - if not hasattr(self, 'cotisation'): - cotisation=Cotisation(vente=self) + """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') and self.type_cotisation: + cotisation = Cotisation(vente=self) + cotisation.type_cotisation = self.type_cotisation 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_cotisation = Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self.facture.user + ).exclude(valid=False)) + ).filter(Q(type_cotisation='All') | Q(type_cotisation=self.type_cotisation) + ).filter( + date_start__lt=date_start + ).aggregate(Max('date_end'))['date_end__max'] + elif self.type_cotisation=="Adhesion": + end_cotisation = self.facture.user.end_adhesion() else: - end_adhesion = self.facture.user.end_adhesion() + end_cotisation = self.facture.user.end_connexion() date_start = date_start or timezone.now() - end_adhesion = end_adhesion or date_start - date_max = max(end_adhesion, date_start) + end_cotisation = end_cotisation or date_start + date_max = max(end_cotisation, 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") + if self.type_cotisation and not self.duration: + 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 vente.cotisation.save() - if vente.iscotisation: + if vente.type_cotisation: vente.create_cotis() vente.cotisation.save() 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: + if vente.type_cotisation: 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) + USER_TYPES = ( + ('Adherent', 'Adherent'), + ('Club', 'Club'), + ('All', 'All'), + ) + + COTISATION_TYPE = ( + ('Connexion', 'Connexion'), + ('Adhesion', 'Adhesion'), + ('All', 'All'), + ) + + name = models.CharField(max_length=255) prix = models.DecimalField(max_digits=5, decimal_places=2) - iscotisation = models.BooleanField() - duration = models.IntegerField( + duration = models.PositiveIntegerField( help_text="Durée exprimée en mois entiers", blank=True, null=True, validators=[MinValueValidator(0)]) + type_user = models.CharField( + choices=USER_TYPES, + default='All', + max_length=255 + ) + type_cotisation = models.CharField( + choices=COTISATION_TYPE, + default=None, + blank=True, + null=True, + max_length=255 + ) + + unique_together = ('name', 'type_user') def clean(self): if self.name.lower() == "solde": raise ValidationError("Solde est un nom d'article invalide") + if self.type_cotisation and not self.duration: + raise ValidationError( + "La durée est obligatoire si il s'agit d'une cotisation" + ) 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 +290,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,29 +309,47 @@ 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" + COTISATION_TYPE = ( + ('Connexion', 'Connexion'), + ('Adhesion', 'Adhesion'), + ('All', 'All'), + ) + vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True) + type_cotisation = models.CharField( + choices=COTISATION_TYPE, + max_length=255, + ) date_start = models.DateTimeField() date_end = models.DateTimeField() 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/aff_article.html b/cotisations/templates/cotisations/aff_article.html index b756f746..3a0b21f6 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -27,8 +27,9 @@ with this program; if not, write to the Free Software Foundation, Inc., Article Prix - Cotisation + Type Cotisation Durée (mois) + Article pour @@ -36,8 +37,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ article.name }} {{ article.prix }} - {{ article.iscotisation }} + {{ article.type_cotisation }} {{ article.duration }} + {{ article.type_user }} {% if is_trez %} diff --git a/cotisations/templates/cotisations/aff_cotisations.html b/cotisations/templates/cotisations/aff_cotisations.html index e1ce251e..81feb0bf 100644 --- a/cotisations/templates/cotisations/aff_cotisations.html +++ b/cotisations/templates/cotisations/aff_cotisations.html @@ -29,12 +29,12 @@ with this program; if not, write to the Free Software Foundation, Inc., - + - - - + + + @@ -46,17 +46,19 @@ with this program; if not, write to the Free Software Foundation, Inc., + {% if is_cableur %} - {% endfor %}
Utilisateur{% include "buttons/sort.html" with prefix='cotis' col='user' text='Utilisateur' %} Designation Prix totalMoyen de paiementDate{% include "buttons/sort.html" with prefix='cotis' col='paiement' text='Moyen de paiement' %}{% include "buttons/sort.html" with prefix='cotis' col='date' text='Date' %}{% include "buttons/sort.html" with prefix='cotis' col='id' text='Id facture' %}
{{ facture.prix_total }} {{ facture.paiement }} {{ facture.date }}{{ facture.id }} - - - -
diff --git a/cotisations/templates/cotisations/control.html b/cotisations/templates/cotisations/control.html index 1ccb804f..6e9ccbc5 100644 --- a/cotisations/templates/cotisations/control.html +++ b/cotisations/templates/cotisations/control.html @@ -40,14 +40,16 @@ with this program; if not, write to the Free Software Foundation, Inc., Profil - Nom - Prénom - Designation + {% include "buttons/sort.html" with prefix='control' col='name' text='Nom' %} + {% include "buttons/sort.html" with prefix='control' col='surname' text='Prénom' %} + {% include "buttons/sort.html" with prefix='control' col='id' text='Id facture' %} + {% include "buttons/sort.html" with prefix='control' col='user-id' text='Id user' %} + Designation Prix total - Moyen de paiement - Date - Validité - Controle trésorier + {% include "buttons/sort.html" with prefix='control' col='paiement' text='Moyen de paiement' %} + {% include "buttons/sort.html" with prefix='control' col='date' text='Date' %} + {% include "buttons/sort.html" with prefix='control' col='valid' text='Valide' %} + {% include "buttons/sort.html" with prefix='control' col='control' text='Contrôlée' %} {% for form in controlform.forms %} @@ -58,7 +60,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ form.instance.user.name }} {{ form.instance.user.surname }} - {{ form.instance.name }} + {{ form.instance.id }} + {{ form.instance.user.id }} + {{ form.instance.name }} {{ form.instance.prix_total }} {{ form.instance.paiement }} {{ form.instance.date }} 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..d3e56f36 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..943367ae 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -24,96 +24,145 @@ # 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.db.models import Q 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 re2o.utils import SortTable from preferences.models import OptionalUser, AssoOption, GeneralOption +from .models import Facture, Article, Vente, Paiement, Banque +from .forms import ( + NewFactureForm, + TrezEditFactureForm, + EditFactureForm, + ArticleForm, + DelArticleForm, + PaiementForm, + DelPaiementForm, + BanqueForm, + DelBanqueForm, + NewFactureFormPdf, + SelectUserArticleForm, + SelectClubArticleForm, + 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 - article_list = Article.objects.all() + article_list = Article.objects.filter( + Q(type_user='All') | Q(type_user=request.user.class_name) + ) # On envoie la form fature et un formset d'articles facture_form = NewFactureForm(request.POST or None, instance=facture) - article_formset = formset_factory(SelectArticleForm)(request.POST or None) + if request.user.is_class_club: + article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) + else: + article_formset = formset_factory(SelectUserArticleForm)(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, + type_cotisation=article.type_cotisation, + 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'].type_cotisation + 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 +170,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 +302,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 +315,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 +324,13 @@ 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'], + number=1 + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) @@ -225,6 +343,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 +360,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 +400,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 +416,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 +454,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 +482,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 +522,31 @@ 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) + facture_list = Facture.objects.select_related('user').select_related('paiement') + facture_list = SortTable.sort( + facture_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_CONTROL + ) + controlform_set = modelformset_factory( + Facture, + fields=('control', 'valid'), + extra=0 + ) paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -379,40 +555,63 @@ 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]) - controlform = controlform_set(request.POST or None, queryset=page_query) + controlform = controlform_set(request.POST or None, queryset=facture_list.object_list) if controlform.is_valid(): with transaction.atomic(), reversion.create_revision(): controlform.save() reversion.set_user(request.user) reversion.set_comment("Controle trésorier") 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.select_related('user')\ + .select_related('paiement').prefetch_related('vente_set') + facture_list = SortTable.sort( + facture_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_INDEX + ) paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -423,41 +622,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): - if object == 'facture': +def history(request, object_name, object_id): + """Affiche l'historique de chaque objet""" + if object_name == '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)) - elif object == 'paiement' and request.user.has_perms(('cableur',)): + 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_name == 'paiement' and request.user.has_perms(('cableur',)): try: - object_instance = Paiement.objects.get(pk=id) + object_instance = Paiement.objects.get(pk=object_id) except Paiement.DoesNotExist: - messages.error(request, "Paiement inexistant") - return redirect("/cotisations/") - elif object == 'article' and request.user.has_perms(('cableur',)): + messages.error(request, "Paiement inexistant") + return redirect("/cotisations/") + elif object_name == 'article' and request.user.has_perms(('cableur',)): try: - object_instance = Article.objects.get(pk=id) + object_instance = Article.objects.get(pk=object_id) except Article.DoesNotExist: - messages.error(request, "Article inexistante") - return redirect("/cotisations/") - elif object == 'banque' and request.user.has_perms(('cableur',)): + messages.error(request, "Article inexistante") + return redirect("/cotisations/") + elif object_name == '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 +675,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/docs_utils/re2o-archi.dia b/docs_utils/re2o-archi.dia new file mode 100644 index 00000000..1137480e Binary files /dev/null and b/docs_utils/re2o-archi.dia differ diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 5ea4e48c..16f3f084 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") @@ -248,6 +247,9 @@ def check_user_machine_and_register(nas_type, username, mac_address): return (False, u"Machine enregistrée sur le compte d'un autre user...", '') elif not interface.is_active: return (False, u"Machine desactivée", '') + elif not interface.ipv4: + interface.assign_ipv4() + return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm) else: return (True, u"Access ok", user.pwd_ntlm) elif nas_type: @@ -293,11 +295,12 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): if not port.room: return (sw_name, u'Chambre inconnue', VLAN_NOK) - room_user = User.objects.filter(room=Room.objects.filter(name=port.room)) + room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room)) if not room_user: return (sw_name, u'Chambre non cotisante', VLAN_NOK) - elif not room_user.first().has_access(): - return (sw_name, u'Chambre resident desactive', VLAN_NOK) + for user in room_user: + if not user.has_access(): + return (sw_name, u'Chambre resident desactive', VLAN_NOK) # else: user OK, on passe à la verif MAC if port.radius == 'COMMON' or port.radius == 'STRICT': @@ -310,9 +313,12 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): elif not port.room: return (sw_name, u'Chambre et machine inconnues', VLAN_NOK) else: - room_user = User.objects.filter(room=Room.objects.filter(name=port.room)) + if not room_user: + room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room)) if not room_user: return (sw_name, u'Machine et propriétaire de la chambre inconnus', VLAN_NOK) + elif room_user.count() > 1: + return (sw_name, u'Machine inconnue, il y a au moins 2 users dans la chambre/local -> ajout de mac automatique impossible', VLAN_NOK) elif not room_user.first().has_access(): return (sw_name, u'Machine inconnue et adhérent non cotisant', VLAN_NOK) else: @@ -321,9 +327,14 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN) else: return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) - elif not interface.first().is_active: - return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) else: - return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN) + interface = interface.first() + if not interface.is_active: + return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) + elif not interface.ipv4: + interface.assign_ipv4() + return (sw_name, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN) + else: + return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN) diff --git a/logs/templates/logs/aff_stats_logs.html b/logs/templates/logs/aff_stats_logs.html index 35504144..71efb147 100644 --- a/logs/templates/logs/aff_stats_logs.html +++ b/logs/templates/logs/aff_stats_logs.html @@ -33,8 +33,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - + + diff --git a/logs/templates/logs/aff_summary.html b/logs/templates/logs/aff_summary.html index 9b791dfd..4fb5c8b1 100644 --- a/logs/templates/logs/aff_summary.html +++ b/logs/templates/logs/aff_summary.html @@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Objet modifié Type de l'objetModification parDate de modification{% include "buttons/sort.html" with prefix='logs' col='author' text='Modification par' %}{% include "buttons/sort.html" with prefix='logs' col='date' text='Date de modification' %} Commentaire
- + 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..df5ca681 100644 --- a/logs/views.py +++ b/logs/views.py @@ -23,62 +23,110 @@ # 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 topologie.models import Switch, Port, Room +from users.models import ( + User, + ServiceUser, + Right, + School, + ListRight, + ListShell, + Ban, + Whitelist, + Adherent, + Club +) +from cotisations.models import ( + Facture, + Vente, + Article, + Banque, + Paiement, + Cotisation +) +from machines.models import ( + Machine, + MachineType, + IpType, + Extension, + Interface, + Domain, + IpList, + OuverturePortList, + Service, + Vlan, + Nas, + SOA, + Mx, + Ns +) +from topologie.models import ( + Switch, + Port, + Room, + Stack, + ModelSwitch, + ConstructorSwitch +) 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, SortTable 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 + ) + ).select_related('revision') + versions = SortTable.sort( + versions, + request.GET.get('col'), + request.GET.get('order'), + SortTable.LOGS_INDEX + ) paginator = Paginator(versions, pagination_number) page = request.GET.get('page') try: @@ -87,7 +135,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 +143,43 @@ 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().select_related('user')\ + .prefetch_related('version_set__object') + revisions = SortTable.sort( + revisions, + request.GET.get('col'), + request.GET.get('order'), + SortTable.LOGS_STATS_LOGS + ) paginator = Paginator(revisions, pagination_number) page = request.GET.get('page') try: @@ -127,9 +188,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 +202,235 @@ 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() - for ip_range in IpType.objects.all(): + """Statistiques générales affinées sur les ip, activées, utilisées par + range, et les statistiques générales sur les users : users actifs, + cotisants, activés, archivés, etc""" + ip_dict = dict() + for ip_range in IpType.objects.select_related('vlan').all(): 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, ip_range.vlan, all_ip.count(), + used_ip, active_ip, all_ip.count()-used_ip] + _all_adherent = all_adherent() + _all_has_access = all_has_access() + _all_baned = all_baned() + _all_whitelisted = all_whitelisted() + _all_active_interfaces_count = all_active_interfaces_count() + _all_active_assigned_interfaces_count = all_active_assigned_interfaces_count() 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 (total club et adhérents)", "Nombre d'adhérents", "Nombre de clubs"], { + 'active_users': [ + "Users actifs", + User.objects.filter(state=User.STATE_ACTIVE).count(), + Adherent.objects.filter(state=Adherent.STATE_ACTIVE).count(), + Club.objects.filter(state=Club.STATE_ACTIVE).count()], + 'inactive_users': [ + "Users désactivés", + User.objects.filter(state=User.STATE_DISABLED).count(), + Adherent.objects.filter(state=Adherent.STATE_DISABLED).count(), + Club.objects.filter(state=Club.STATE_DISABLED).count()], + 'archive_users': [ + "Users archivés", + User.objects.filter(state=User.STATE_ARCHIVE).count(), + Adherent.objects.filter(state=Adherent.STATE_ARCHIVE).count(), + Club.objects.filter(state=Club.STATE_ARCHIVE).count()], + 'adherent_users': [ + "Cotisant à l'association", + _all_adherent.count(), + _all_adherent.exclude(adherent__isnull=True).count(), + _all_adherent.exclude(club__isnull=True).count()], + 'connexion_users': [ + "Utilisateurs bénéficiant d'une connexion", + _all_has_access.count(), + _all_has_access.exclude(adherent__isnull=True).count(), + _all_has_access.exclude(club__isnull=True).count()], + 'ban_users': [ + "Utilisateurs bannis", + _all_baned.count(), + _all_baned.exclude(adherent__isnull=True).count(), + _all_baned.exclude(club__isnull=True).count()], + 'whitelisted_user': [ + "Utilisateurs bénéficiant d'une connexion gracieuse", + _all_whitelisted.count(), + _all_whitelisted.exclude(adherent__isnull=True).count(), + _all_whitelisted.exclude(club__isnull=True).count()], + 'actives_interfaces': [ + "Interfaces actives (ayant accès au reseau)", + _all_active_interfaces_count.count(), + _all_active_interfaces_count.exclude( + machine__user__adherent__isnull=True + ).count(), + _all_active_interfaces_count.exclude( + machine__user__club__isnull=True + ).count()], + 'actives_assigned_interfaces': [ + "Interfaces actives et assignées ipv4", + _all_active_assigned_interfaces_count.count(), + _all_active_assigned_interfaces_count.exclude( + machine__user__adherent__isnull=True + ).count(), + _all_active_assigned_interfaces_count.exclude( + machine__user__club__isnull=True + ).count()] + }], + [["Range d'ip", "Vlan", "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()], + 'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()], + 'clubs': [Club.PRETTY_NAME, Club.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()], + 'service': [Service.PRETTY_NAME, Service.objects.count()], + 'ouvertureportlist': [ + OuverturePortList.PRETTY_NAME, + OuverturePortList.objects.count() + ], + 'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()], + 'SOA': [Mx.PRETTY_NAME, Mx.objects.count()], + 'Mx': [Mx.PRETTY_NAME, Mx.objects.count()], + 'Ns': [Ns.PRETTY_NAME, Ns.objects.count()], + 'nas': [Nas.PRETTY_NAME, Nas.objects.count()], + }, + 'Topologie': { + 'switch': [Switch.PRETTY_NAME, Switch.objects.count()], + 'port': [Port.PRETTY_NAME, Port.objects.count()], + 'chambre': [Room.PRETTY_NAME, Room.objects.count()], + 'stack': [Stack.PRETTY_NAME, Stack.objects.count()], + 'modelswitch': [ + ModelSwitch.PRETTY_NAME, + ModelSwitch.objects.count() + ], + 'constructorswitch': [ + ConstructorSwitch.PRETTY_NAME, + ConstructorSwitch.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/admin.py b/machines/admin.py index 49b02a7e..93e936e6 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -27,58 +27,79 @@ from django.contrib import admin from reversion.admin import VersionAdmin from .models import IpType, Machine, MachineType, Domain, IpList, Interface -from .models import Extension, Mx, Ns, Vlan, Text, Nas, Service, OuverturePort -from .models import OuverturePortList +from .models import Extension, SOA, Mx, Ns, Vlan, Text, Nas, Service +from .models import OuverturePort, OuverturePortList + class MachineAdmin(VersionAdmin): pass + class IpTypeAdmin(VersionAdmin): pass + class MachineTypeAdmin(VersionAdmin): pass + class VlanAdmin(VersionAdmin): pass + class ExtensionAdmin(VersionAdmin): pass + +class SOAAdmin(VersionAdmin): + pass + + class MxAdmin(VersionAdmin): pass + class NsAdmin(VersionAdmin): pass + class TextAdmin(VersionAdmin): pass + class NasAdmin(VersionAdmin): pass + class IpListAdmin(VersionAdmin): pass + class OuverturePortAdmin(VersionAdmin): pass + class OuverturePortListAdmin(VersionAdmin): pass + class InterfaceAdmin(VersionAdmin): list_display = ('machine','type','mac_address','ipv4','details') + class DomainAdmin(VersionAdmin): list_display = ('interface_parent', 'name', 'extension', 'cname') + class ServiceAdmin(VersionAdmin): list_display = ('service_type', 'min_time_regen', 'regular_time_regen') + admin.site.register(Machine, MachineAdmin) admin.site.register(MachineType, MachineTypeAdmin) admin.site.register(IpType, IpTypeAdmin) admin.site.register(Extension, ExtensionAdmin) +admin.site.register(SOA, SOAAdmin) admin.site.register(Mx, MxAdmin) admin.site.register(Ns, NsAdmin) admin.site.register(Text, TextAdmin) diff --git a/machines/forms.py b/machines/forms.py index 63539888..2c1c53c7 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -21,208 +21,399 @@ # 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. +""" +Formulaires d'ajout, edition et suppressions de : + - machines + - interfaces + - domain (noms de machine) + - alias (cname) + - service (dhcp, dns..) + - ns (serveur dns) + - mx (serveur mail) + - ports ouverts et profils d'ouverture par interface +""" from __future__ import unicode_literals -import re - -from django.forms import ModelForm, Form, ValidationError +from django.forms import ModelForm, Form from django import forms -from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType, OuverturePortList, OuverturePort -from django.db.models import Q -from django.core.validators import validate_email -from users.models import User +from .models import ( + Domain, + Machine, + Interface, + IpList, + MachineType, + Extension, + SOA, + Mx, + Text, + Ns, + Service, + Vlan, + Nas, + IpType, + OuverturePortList, +) + class EditMachineForm(ModelForm): + """Formulaire d'édition d'une machine""" class Meta: model = Machine 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): + """Creation d'une machine, ne renseigne que le nom""" class Meta(EditMachineForm.Meta): fields = ['name'] + class BaseEditMachineForm(EditMachineForm): + """Edition basique, ne permet que de changer le nom et le statut. + Réservé aux users sans droits spécifiques""" class Meta(EditMachineForm.Meta): - fields = ['name','active'] + fields = ['name', 'active'] + class EditInterfaceForm(ModelForm): + """Edition d'une interface. Edition complète""" class Meta: model = Interface 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" if "ipv4" in self.fields: - self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" - self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) + self.fields['ipv4'].empty_label = "Assignation automatique\ + de l'ipv4" + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ) # Add it's own address - self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance) + self.fields['ipv4'].queryset |= IpList.objects.filter( + interface=self.instance + ) if "machine" in self.fields: - self.fields['machine'].queryset = Machine.objects.all().select_related('user') + self.fields['machine'].queryset = Machine.objects.all()\ + .select_related('user') + class AddInterfaceForm(EditInterfaceForm): + """Ajout d'une interface à une machine. En fonction des droits, + affiche ou non l'ensemble des ip disponibles""" class Meta(EditInterfaceForm.Meta): - fields = ['type','ipv4','mac_address','details'] + fields = ['type', 'ipv4', 'mac_address', 'details'] def __init__(self, *args, **kwargs): infra = kwargs.pop('infra') super(AddInterfaceForm, self).__init__(*args, **kwargs) self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" if not infra: - self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False)) - self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)) + self.fields['type'].queryset = MachineType.objects.filter( + ip_type__in=IpType.objects.filter(need_infra=False) + ) + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ).filter(ip_type__in=IpType.objects.filter(need_infra=False)) else: - self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ) + class NewInterfaceForm(EditInterfaceForm): + """Formulaire light, sans choix de l'ipv4; d'ajout d'une interface""" class Meta(EditInterfaceForm.Meta): - fields = ['type','mac_address','details'] + fields = ['type', 'mac_address', 'details'] + class BaseEditInterfaceForm(EditInterfaceForm): + """Edition basique d'une interface. En fonction des droits, + ajoute ou non l'ensemble des ipv4 disponibles (infra)""" class Meta(EditInterfaceForm.Meta): - fields = ['type','ipv4','mac_address','details'] + fields = ['type', 'ipv4', 'mac_address', 'details'] def __init__(self, *args, **kwargs): infra = kwargs.pop('infra') super(BaseEditInterfaceForm, self).__init__(*args, **kwargs) self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" if not infra: - self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False)) - self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)) + self.fields['type'].queryset = MachineType.objects.filter( + ip_type__in=IpType.objects.filter(need_infra=False) + ) + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ).filter(ip_type__in=IpType.objects.filter(need_infra=False)) # Add it's own address - self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance) + self.fields['ipv4'].queryset |= IpList.objects.filter( + interface=self.instance + ) else: - self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) - self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance) + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ) + self.fields['ipv4'].queryset |= IpList.objects.filter( + interface=self.instance + ) + class AliasForm(ModelForm): + """Ajout d'un alias (et edition), CNAME, contenant nom et extension""" class Meta: model = Domain - fields = ['name','extension'] + fields = ['name', 'extension'] def __init__(self, *args, **kwargs): - if 'infra' in kwargs: - infra = kwargs.pop('infra') - super(AliasForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + infra = kwargs.pop('infra') + super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs) + if not infra: + self.fields['extension'].queryset = Extension.objects.filter( + need_infra=False + ) -class DomainForm(AliasForm): - class Meta(AliasForm.Meta): + +class DomainForm(ModelForm): + """Ajout et edition d'un enregistrement de nom, relié à interface""" + class Meta: + model = Domain fields = ['name'] def __init__(self, *args, **kwargs): if 'user' in kwargs: user = kwargs.pop('user') - nb_machine = kwargs.pop('nb_machine') initial = kwargs.get('initial', {}) initial['name'] = user.get_next_domain_name() - kwargs['initial'] = initial - super(DomainForm, self).__init__(*args, **kwargs) - + kwargs['initial'] = initial + 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) + """Suppression d'un ou plusieurs objets alias""" + alias = forms.ModelMultipleChoiceField( + queryset=Domain.objects.all(), + label="Alias actuels", + widget=forms.CheckboxSelectMultiple + ) def __init__(self, *args, **kwargs): interface = kwargs.pop('interface') super(DelAliasForm, self).__init__(*args, **kwargs) - self.fields['alias'].queryset = Domain.objects.filter(cname__in=Domain.objects.filter(interface_parent=interface)) + self.fields['alias'].queryset = Domain.objects.filter( + cname__in=Domain.objects.filter(interface_parent=interface) + ) + class MachineTypeForm(ModelForm): + """Ajout et edition d'un machinetype, relié à un iptype""" class Meta: model = MachineType - fields = ['type','ip_type'] + 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é" + class DelMachineTypeForm(Form): - machinetypes = forms.ModelMultipleChoiceField(queryset=MachineType.objects.all(), label="Types de machines actuelles", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs machinetype""" + machinetypes = forms.ModelMultipleChoiceField( + queryset=MachineType.objects.all(), + label="Types de machines actuelles", + widget=forms.CheckboxSelectMultiple + ) + class IpTypeForm(ModelForm): + """Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de + stop après creation""" class Meta: model = IpType - fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan'] - + 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): + """Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait + synchroniser les objets iplist""" class Meta(IpTypeForm.Meta): - fields = ['extension','type','need_infra', 'prefix_v6', 'vlan'] + fields = ['extension', 'type', 'need_infra', 'prefix_v6', 'vlan', + 'ouverture_ports'] + class DelIpTypeForm(Form): - iptypes = forms.ModelMultipleChoiceField(queryset=IpType.objects.all(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs iptype""" + iptypes = forms.ModelMultipleChoiceField( + queryset=IpType.objects.all(), + label="Types d'ip actuelles", + widget=forms.CheckboxSelectMultiple + ) + class ExtensionForm(ModelForm): + """Formulaire d'ajout et edition d'une extension""" class Meta: model = Extension - fields = ['name', 'need_infra', 'origin'] + fields = '__all__' 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' + self.fields['origin_v6'].label = 'Enregistrement AAAA origin' + self.fields['soa'].label = 'En-tête SOA à utiliser' + class DelExtensionForm(Form): - extensions = forms.ModelMultipleChoiceField(queryset=Extension.objects.all(), label="Extensions actuelles", widget=forms.CheckboxSelectMultiple) + """Suppression d'une ou plusieurs extensions""" + extensions = forms.ModelMultipleChoiceField( + queryset=Extension.objects.all(), + label="Extensions actuelles", + widget=forms.CheckboxSelectMultiple + ) + + +class SOAForm(ModelForm): + """Ajout et edition d'un SOA""" + class Meta: + model = SOA + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(SOAForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class DelSOAForm(Form): + """Suppression d'un ou plusieurs SOA""" + soa = forms.ModelMultipleChoiceField( + queryset=SOA.objects.all(), + label="SOA actuels", + widget=forms.CheckboxSelectMultiple + ) + class MxForm(ModelForm): + """Ajout et edition d'un MX""" class Meta: model = Mx fields = ['zone', 'priority', 'name'] - + def __init__(self, *args, **kwargs): - super(MxForm, self).__init__(*args, **kwargs) - self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None) - + 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 + ).select_related('extension') + + class DelMxForm(Form): - mx = forms.ModelMultipleChoiceField(queryset=Mx.objects.all(), label="MX actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs MX""" + mx = forms.ModelMultipleChoiceField( + queryset=Mx.objects.all(), + label="MX actuels", + widget=forms.CheckboxSelectMultiple + ) + class NsForm(ModelForm): + """Ajout d'un NS pour une zone + On exclue les CNAME dans les objets domain (interdit par la rfc) + donc on prend uniquemet """ class Meta: model = Ns fields = ['zone', 'ns'] def __init__(self, *args, **kwargs): - super(NsForm, self).__init__(*args, **kwargs) - self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None) + 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 + ).select_related('extension') + class DelNsForm(Form): - ns = forms.ModelMultipleChoiceField(queryset=Ns.objects.all(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple) + """Suppresion d'un ou plusieurs NS""" + ns = forms.ModelMultipleChoiceField( + queryset=Ns.objects.all(), + label="Enregistrements NS actuels", + widget=forms.CheckboxSelectMultiple + ) -class TextForm(ModelForm): + +class TxtForm(ModelForm): + """Ajout d'un txt pour une zone""" 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): + """Suppression d'un ou plusieurs TXT""" + txt = forms.ModelMultipleChoiceField( + queryset=Text.objects.all(), + label="Enregistrements Txt actuels", + widget=forms.CheckboxSelectMultiple + ) + class NasForm(ModelForm): + """Ajout d'un type de nas (machine d'authentification, + swicths, bornes...)""" 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) + """Suppression d'un ou plusieurs nas""" + nas = forms.ModelMultipleChoiceField( + queryset=Nas.objects.all(), + label="Enregistrements Nas actuels", + widget=forms.CheckboxSelectMultiple + ) + class ServiceForm(ModelForm): + """Ajout et edition d'une classe de service : dns, dhcp, etc""" 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) + self.fields['servers'].queryset = Interface.objects.all()\ + .select_related('domain__extension') + def save(self, commit=True): instance = super(ServiceForm, self).save(commit=False) if commit: @@ -230,24 +421,63 @@ class ServiceForm(ModelForm): instance.process_link(self.cleaned_data.get('servers')) return instance + class DelServiceForm(Form): - service = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), label="Services actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs service""" + service = forms.ModelMultipleChoiceField( + queryset=Service.objects.all(), + label="Services actuels", + widget=forms.CheckboxSelectMultiple + ) + class VlanForm(ModelForm): + """Ajout d'un vlan : id, nom""" class Meta: 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) + """Suppression d'un ou plusieurs vlans""" + vlan = forms.ModelMultipleChoiceField( + queryset=Vlan.objects.all(), + label="Vlan actuels", + widget=forms.CheckboxSelectMultiple + ) + class EditOuverturePortConfigForm(ModelForm): + """Edition de la liste des profils d'ouverture de ports + pour l'interface""" class Meta: 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): + """Edition de la liste des ports et profils d'ouverture + des ports""" 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/migrations/0060_iptype_ouverture_ports.py b/machines/migrations/0060_iptype_ouverture_ports.py new file mode 100644 index 00000000..e35f398f --- /dev/null +++ b/machines/migrations/0060_iptype_ouverture_ports.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-03 16:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0059_iptype_prefix_v6'), + ] + + operations = [ + migrations.AddField( + model_name='iptype', + name='ouverture_ports', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='machines.OuverturePortList'), + ), + ] diff --git a/machines/migrations/0061_auto_20171015_2033.py b/machines/migrations/0061_auto_20171015_2033.py new file mode 100644 index 00000000..6153bbd0 --- /dev/null +++ b/machines/migrations/0061_auto_20171015_2033.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0060_iptype_ouverture_ports'), + ] + + operations = [ + migrations.AlterField( + model_name='mx', + name='priority', + field=models.PositiveIntegerField(unique=True), + ), + migrations.AlterField( + model_name='ouvertureport', + name='begin', + field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(65535)]), + ), + migrations.AlterField( + model_name='ouvertureport', + name='end', + field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(65535)]), + ), + migrations.AlterField( + model_name='vlan', + name='vlan_id', + field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(4095)]), + ), + ] diff --git a/machines/migrations/0062_extension_origin_v6.py b/machines/migrations/0062_extension_origin_v6.py new file mode 100644 index 00000000..1c3d869a --- /dev/null +++ b/machines/migrations/0062_extension_origin_v6.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-18 14:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0061_auto_20171015_2033'), + ] + + operations = [ + migrations.AddField( + model_name='extension', + name='origin_v6', + field=models.GenericIPAddressField(blank=True, null=True, protocol='IPv6'), + ), + ] diff --git a/machines/migrations/0063_auto_20171020_0040.py b/machines/migrations/0063_auto_20171020_0040.py new file mode 100644 index 00000000..d2f9afd9 --- /dev/null +++ b/machines/migrations/0063_auto_20171020_0040.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-19 22:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import machines.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0062_extension_origin_v6'), + ] + + operations = [ + migrations.CreateModel( + name='SOA', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('mail', models.EmailField(help_text='Email du contact pour la zone', max_length=254)), + ('refresh', models.PositiveIntegerField(default=86400, help_text='Secondes avant que les DNS secondaires doivent demander le serial du DNS primaire pour détecter une modification')), + ('retry', models.PositiveIntegerField(default=7200, help_text='Secondes avant que les DNS secondaires fassent une nouvelle demande de serial en cas de timeout du DNS primaire')), + ('expire', models.PositiveIntegerField(default=3600000, help_text='Secondes après lesquelles les DNS secondaires arrêtent de de répondre aux requêtes en cas de timeout du DNS primaire')), + ('ttl', models.PositiveIntegerField(default=172800, help_text='Time To Live')), + ], + ), + migrations.AddField( + model_name='extension', + name='soa', + field=models.ForeignKey(default=machines.models.SOA.new_default_soa, on_delete=django.db.models.deletion.CASCADE, to='machines.SOA'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 698fdb90..c2cd2143 100644 --- a/machines/models.py +++ b/machines/models.py @@ -23,44 +23,59 @@ from __future__ import unicode_literals +from datetime import timedelta +import re +from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress + from django.db import models -from django.db.models.signals import post_save, pre_delete, post_delete +from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.forms import ValidationError from django.utils.functional import cached_property from django.utils import timezone +from django.core.validators import MaxValueValidator + from macaddress.fields import MACAddressField -from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress -from django.core.validators import MinValueValidator,MaxValueValidator -import re -from reversion import revisions as reversion -from datetime import timedelta class Machine(models.Model): - """ Class définissant une machine, object parent user, objets fils interfaces""" + """ Class définissant une machine, object parent user, objets fils + interfaces""" PRETTY_NAME = "Machine" - + user = models.ForeignKey('users.User', on_delete=models.PROTECT) - name = models.CharField(max_length=255, help_text="Optionnel", blank=True, null=True) + name = models.CharField( + max_length=255, + help_text="Optionnel", + blank=True, + null=True + ) active = models.BooleanField(default=True) def __str__(self): - return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name) + return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name) + class MachineType(models.Model): """ Type de machine, relié à un type d'ip, affecté aux interfaces""" PRETTY_NAME = "Type de machine" type = models.CharField(max_length=255) - ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True) + ip_type = models.ForeignKey( + 'IpType', + on_delete=models.PROTECT, + blank=True, + null=True + ) def all_interfaces(self): - """ Renvoie toutes les interfaces (cartes réseaux) de type machinetype""" + """ Renvoie toutes les interfaces (cartes réseaux) de type + machinetype""" return Interface.objects.filter(type=self) def __str__(self): - return self.type + return self.type + class IpType(models.Model): """ Type d'ip, définissant un range d'ip, affecté aux machine types""" @@ -71,13 +86,27 @@ class IpType(models.Model): need_infra = models.BooleanField(default=False) domaine_ip_start = models.GenericIPAddressField(protocol='IPv4') domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4') - prefix_v6 = models.GenericIPAddressField(protocol='IPv6', null=True, blank=True) - vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True) + prefix_v6 = models.GenericIPAddressField( + protocol='IPv6', + null=True, + blank=True + ) + vlan = models.ForeignKey( + 'Vlan', + on_delete=models.PROTECT, + blank=True, + null=True + ) + ouverture_ports = models.ForeignKey( + 'OuverturePortList', + blank=True, + null=True + ) @cached_property def ip_range(self): - """ Renvoie un objet IPRange à partir de l'objet IpType""" - return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) + """ Renvoie un objet IPRange à partir de l'objet IpType""" + return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) @cached_property def ip_set(self): @@ -95,18 +124,22 @@ class IpType(models.Model): def free_ip(self): """ Renvoie toutes les ip libres associées au type donné (self)""" - return IpList.objects.filter(interface__isnull=True).filter(ip_type=self) + return IpList.objects.filter( + interface__isnull=True + ).filter(ip_type=self) def gen_ip_range(self): - """ Cree les IpList associées au type self. Parcours pédestrement et crée - les ip une par une. Si elles existent déjà, met à jour le type associé - à l'ip""" + """ Cree les IpList associées au type self. Parcours pédestrement et + crée les ip une par une. Si elles existent déjà, met à jour le type + associé à l'ip""" # Creation du range d'ip dans les objets iplist networks = [] for net in self.ip_range.cidrs(): networks += net.iter_hosts() ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks] - listes_ip = IpList.objects.filter(ipv4__in=[str(ip) for ip in networks]) + listes_ip = IpList.objects.filter( + ipv4__in=[str(ip) for ip in networks] + ) # Si il n'y a pas d'ip, on les crée if not listes_ip: IpList.objects.bulk_create(ip_obj) @@ -116,9 +149,11 @@ class IpType(models.Model): return def del_ip_range(self): - """ Methode dépréciée, IpList est en mode cascade et supprimé automatiquement""" + """ Methode dépréciée, IpList est en mode cascade et supprimé + automatiquement""" if Interface.objects.filter(ipv4__in=self.ip_objects()): - raise ValidationError("Une ou plusieurs ip du range sont affectées, impossible de supprimer le range") + raise ValidationError("Une ou plusieurs ip du range sont\ + affectées, impossible de supprimer le range") for ip in self.ip_objects(): ip.delete() @@ -132,11 +167,13 @@ class IpType(models.Model): raise ValidationError("Domaine end doit être après start...") # On ne crée pas plus grand qu'un /16 if self.ip_range.size > 65536: - raise ValidationError("Le range est trop gros, vous ne devez pas créer plus grand qu'un /16") + raise ValidationError("Le range est trop gros, vous ne devez\ + pas créer plus grand qu'un /16") # On check que les / ne se recoupent pas for element in IpType.objects.all().exclude(pk=self.pk): if not self.ip_set.isdisjoint(element.ip_set): - raise ValidationError("Le range indiqué n'est pas disjoint des ranges existants") + raise ValidationError("Le range indiqué n'est pas disjoint\ + des ranges existants") # On formate le prefix v6 if self.prefix_v6: self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network) @@ -149,19 +186,22 @@ class IpType(models.Model): def __str__(self): return self.type + class Vlan(models.Model): - """ Un vlan : vlan_id et nom""" + """ Un vlan : vlan_id et nom + On limite le vlan id entre 0 et 4096, comme défini par la norme""" PRETTY_NAME = "Vlans" - vlan_id = models.IntegerField() + vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)]) name = models.CharField(max_length=256) comment = models.CharField(max_length=256, blank=True) def __str__(self): return self.name + class Nas(models.Model): - """ Les nas. Associé à un machine_type. + """ Les nas. Associé à un machine_type. Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour le radius. Champ autocapture de la mac à true ou false""" PRETTY_NAME = "Correspondance entre les nas et les machines connectées" @@ -173,48 +213,161 @@ class Nas(models.Model): ) name = models.CharField(max_length=255, unique=True) - nas_type = models.ForeignKey('MachineType', on_delete=models.PROTECT, related_name='nas_type') - machine_type = models.ForeignKey('MachineType', on_delete=models.PROTECT, related_name='machinetype_on_nas') - port_access_mode = models.CharField(choices=AUTH, default=default_mode, max_length=32) + nas_type = models.ForeignKey( + 'MachineType', + on_delete=models.PROTECT, + related_name='nas_type' + ) + machine_type = models.ForeignKey( + 'MachineType', + on_delete=models.PROTECT, + related_name='machinetype_on_nas' + ) + port_access_mode = models.CharField( + choices=AUTH, + default=default_mode, + max_length=32 + ) autocapture_mac = models.BooleanField(default=False) def __str__(self): return self.name + +class SOA(models.Model): + """ + Un enregistrement SOA associé à une extension + Les valeurs par défault viennent des recommandations RIPE : + https://www.ripe.net/publications/docs/ripe-203 + """ + PRETTY_NAME = "Enregistrement SOA" + + name = models.CharField(max_length=255) + mail = models.EmailField( + help_text='Email du contact pour la zone' + ) + refresh = models.PositiveIntegerField( + default=86400, # 24 hours + help_text='Secondes avant que les DNS secondaires doivent demander le\ + serial du DNS primaire pour détecter une modification' + ) + retry = models.PositiveIntegerField( + default=7200, # 2 hours + help_text='Secondes avant que les DNS secondaires fassent une nouvelle\ + demande de serial en cas de timeout du DNS primaire' + ) + expire = models.PositiveIntegerField( + default=3600000, # 1000 hours + help_text='Secondes après lesquelles les DNS secondaires arrêtent de\ + de répondre aux requêtes en cas de timeout du DNS primaire' + ) + ttl = models.PositiveIntegerField( + default=172800, # 2 days + help_text='Time To Live' + ) + + def __str__(self): + return str(self.name) + + @cached_property + def dns_soa_param(self): + """ + Renvoie la partie de l'enregistrement SOA correspondant aux champs : + ; refresh + ; retry + ; expire + ; TTL + """ + return ( + ' {refresh}; refresh\n' + ' {retry}; retry\n' + ' {expire}; expire\n' + ' {ttl}; TTL' + ).format( + refresh=str(self.refresh).ljust(12), + retry=str(self.retry).ljust(12), + expire=str(self.expire).ljust(12), + ttl=str(self.ttl).ljust(12) + ) + + @cached_property + def dns_soa_mail(self): + """ Renvoie le mail dans l'enregistrement SOA """ + mail_fields = str(self.mail).split('@') + return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.' + + @classmethod + def new_default_soa(cls): + """ Fonction pour créer un SOA par défaut, utile pour les nouvelles + extensions . + /!\ Ne jamais supprimer ou renommer cette fonction car elle est + utilisée dans les migrations de la BDD. """ + return cls.objects.get_or_create(name="SOA to edit", mail="postmaser@example.com")[0].pk + + + class Extension(models.Model): - """ Extension dns type example.org. Précise si tout le monde peut l'utiliser, - associé à un origin (ip d'origine)""" + """ Extension dns type example.org. Précise si tout le monde peut + l'utiliser, associé à un origin (ip d'origine)""" PRETTY_NAME = "Extensions dns" name = models.CharField(max_length=255, unique=True) need_infra = models.BooleanField(default=False) - origin = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) + origin = models.OneToOneField( + 'IpList', + on_delete=models.PROTECT, + blank=True, + null=True + ) + origin_v6 = models.GenericIPAddressField( + protocol='IPv6', + null=True, + blank=True + ) + soa = models.ForeignKey( + 'SOA', + on_delete=models.CASCADE, + default=SOA.new_default_soa + ) @cached_property def dns_entry(self): - """ Une entrée DNS A""" - return "@ IN A " + str(self.origin) + """ Une entrée DNS A et AAAA sur origin (zone self)""" + entry = "" + if self.origin: + entry += "@ IN A " + str(self.origin) + if self.origin_v6: + if entry: + entry += "\n" + entry += "@ IN AAAA " + str(self.origin_v6) + return entry def __str__(self): return self.name + class Mx(models.Model): - """ Entrées des MX. Enregistre la zone (extension) associée et la priorité + """ Entrées des MX. Enregistre la zone (extension) associée et la + priorité Todo : pouvoir associer un MX à une interface """ PRETTY_NAME = "Enregistrements MX" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) - priority = models.IntegerField(unique=True) + priority = models.PositiveIntegerField(unique=True) name = models.OneToOneField('Domain', on_delete=models.PROTECT) @cached_property def dns_entry(self): - return "@ IN MX " + str(self.priority) + " " + str(self.name) + """Renvoie l'entrée DNS complète pour un MX à mettre dans les + fichiers de zones""" + return "@ IN MX " + str(self.priority).ljust(3) + " " + str(self.name) def __str__(self): return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) + class Ns(models.Model): + """Liste des enregistrements name servers par zone considéérée""" PRETTY_NAME = "Enregistrements NS" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) @@ -222,36 +375,47 @@ class Ns(models.Model): @cached_property def dns_entry(self): - return "@ IN NS " + str(self.ns) + """Renvoie un enregistrement NS complet pour les filezones""" + return "@ IN NS " + str(self.ns) def __str__(self): return str(self.zone) + ' ' + str(self.ns) + class Text(models.Model): """ Un enregistrement TXT associé à une extension""" - PRETTY_NAME = "Enregistrement text" + PRETTY_NAME = "Enregistrement TXT" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) field1 = models.CharField(max_length=255) field2 = models.CharField(max_length=255) - + def __str__(self): - return str(self.zone) + " : " + str(self.field1) + " " + str(self.field2) + return str(self.zone) + " : " + str(self.field1) + " " +\ + str(self.field2) @cached_property def dns_entry(self): - return str(self.field1) + " IN TXT " + str(self.field2) + """Renvoie l'enregistrement TXT complet pour le fichier de zone""" + return str(self.field1).ljust(15) + " IN TXT " + str(self.field2) + class Interface(models.Model): - """ Une interface. Objet clef de l'application machine : - - une address mac unique. Possibilité de la rendre unique avec le typemachine + """ Une interface. Objet clef de l'application machine : + - une address mac unique. Possibilité de la rendre unique avec le + typemachine - une onetoone vers IpList pour attribution ipv4 - le type parent associé au range ip et à l'extension - un objet domain associé contenant son nom - la liste des ports oiuvert""" PRETTY_NAME = "Interface" - ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) + ipv4 = models.OneToOneField( + 'IpList', + on_delete=models.PROTECT, + blank=True, + null=True + ) mac_address = MACAddressField(integer=False, unique=True) machine = models.ForeignKey('Machine', on_delete=models.CASCADE) type = models.ForeignKey('MachineType', on_delete=models.PROTECT) @@ -265,12 +429,14 @@ class Interface(models.Model): user = self.machine.user return machine.active and user.has_access() - @cached_property def ipv6_object(self): - """ Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent""" + """ Renvoie un objet type ipv6 à partir du prefix associé à + l'iptype parent""" if self.type.ip_type.prefix_v6: - return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network) + return EUI(self.mac_address).ipv6( + IPNetwork(self.type.ip_type.prefix_v6).network + ) else: return None @@ -284,15 +450,25 @@ class Interface(models.Model): return str(EUI(self.mac_address, dialect=mac_bare)).lower() def filter_macaddress(self): - """ Tente un formatage mac_bare, si échoue, lève une erreur de validation""" + """ Tente un formatage mac_bare, si échoue, lève une erreur de + validation""" try: self.mac_address = str(EUI(self.mac_address)) - except : + except: raise ValidationError("La mac donnée est invalide") def clean(self, *args, **kwargs): """ Formate l'addresse mac en mac_bare (fonction filter_mac) et assigne une ipv4 dans le bon range si inexistante ou incohérente""" + # If type was an invalid value, django won't create an attribute type + # but try clean() as we may be able to create it from another value + # so even if the error as yet been detected at this point, django + # continues because the error might not prevent us from creating the + # instance. + # But in our case, it's impossible to create a type value so we raise + # the error. + if not hasattr(self, 'type') : + raise ValidationError("Le type d'ip choisi n'est pas valide") self.filter_macaddress() self.mac_address = str(EUI(self.mac_address)) or None if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: @@ -305,7 +481,8 @@ class Interface(models.Model): if free_ips: self.ipv4 = free_ips[0] else: - raise ValidationError("Il n'y a plus d'ip disponibles dans le slash") + raise ValidationError("Il n'y a plus d'ip disponibles\ + dans le slash") return def unassign_ipv4(self): @@ -320,8 +497,10 @@ class Interface(models.Model): def save(self, *args, **kwargs): self.filter_macaddress() # On verifie la cohérence en forçant l'extension par la méthode - if self.type.ip_type != self.ipv4.ip_type: - raise ValidationError("L'ipv4 et le type de la machine ne correspondent pas") + if self.ipv4: + if self.type.ip_type != self.ipv4.ip_type: + raise ValidationError("L'ipv4 et le type de la machine ne\ + correspondent pas") super(Interface, self).save(*args, **kwargs) def __str__(self): @@ -340,18 +519,34 @@ class Interface(models.Model): def may_have_port_open(self): """ True si l'interface a une ip et une ip publique. - Permet de ne pas exporter des ouvertures sur des ip privées (useless)""" + Permet de ne pas exporter des ouvertures sur des ip privées + (useless)""" return self.ipv4 and not self.has_private_ip() + class Domain(models.Model): - """ Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les - alias et les nom de machines, suivant si interface_parent ou cname sont remplis""" + """ Objet domain. Enregistrement A et CNAME en même temps : permet de + stocker les alias et les nom de machines, suivant si interface_parent + ou cname sont remplis""" PRETTY_NAME = "Domaine dns" - interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True) - name = models.CharField(help_text="Obligatoire et unique, ne doit pas comporter de points", max_length=255) + interface_parent = models.OneToOneField( + 'Interface', + on_delete=models.CASCADE, + blank=True, + null=True + ) + name = models.CharField( + help_text="Obligatoire et unique, ne doit pas comporter de points", + max_length=255 + ) extension = models.ForeignKey('Extension', on_delete=models.PROTECT) - cname = models.ForeignKey('self', null=True, blank=True, related_name='related_domain') + cname = models.ForeignKey( + 'self', + null=True, + blank=True, + related_name='related_domain' + ) class Meta: unique_together = (("name", "extension"),) @@ -361,30 +556,35 @@ class Domain(models.Model): Retourne l'extension propre si c'est un cname, renvoie None sinon""" if self.interface_parent: return self.interface_parent.type.ip_type.extension - elif hasattr(self,'extension'): + elif hasattr(self, 'extension'): return self.extension else: return None def clean(self): - """ Validation : + """ Validation : - l'objet est bien soit A soit CNAME - le cname est pas pointé sur lui-même - - le nom contient bien les caractères autorisés par la norme dns et moins de 63 caractères au total + - le nom contient bien les caractères autorisés par la norme + dns et moins de 63 caractères au total - le couple nom/extension est bien unique""" if self.get_extension(): - self.extension=self.get_extension() - """ Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """ + self.extension = self.get_extension() if self.interface_parent and self.cname: raise ValidationError("On ne peut créer à la fois A et CNAME") - if self.cname==self: + if self.cname == self: raise ValidationError("On ne peut créer un cname sur lui même") - HOSTNAME_LABEL_PATTERN = re.compile("(?!-)[A-Z\d-]+(? 63: - raise ValidationError("Le nom de domaine %s est trop long (maximum de 63 caractères)." % dns) + raise ValidationError("Le nom de domaine %s est trop long\ + (maximum de 63 caractères)." % dns) if not HOSTNAME_LABEL_PATTERN.match(dns): - raise ValidationError("Ce nom de domaine %s contient des carractères interdits." % dns) + raise ValidationError("Ce nom de domaine %s contient des\ + carractères interdits." % dns) self.validate_unique() super(Domain, self).clean() @@ -392,10 +592,11 @@ class Domain(models.Model): def dns_entry(self): """ Une entrée DNS""" if self.cname: - return str(self.name) + " IN CNAME " + str(self.cname) + "." + return str(self.name).ljust(15) + " IN CNAME " + str(self.cname) + "." def save(self, *args, **kwargs): - """ Empèche le save sans extension valide. Force à avoir appellé clean avant""" + """ Empèche le save sans extension valide. + Force à avoir appellé clean avant""" if not self.get_extension(): raise ValidationError("Extension invalide") self.full_clean() @@ -404,6 +605,7 @@ class Domain(models.Model): def __str__(self): return str(self.name) + str(self.extension) + class IpList(models.Model): PRETTY_NAME = "Addresses ipv4" @@ -412,13 +614,15 @@ class IpList(models.Model): @cached_property def need_infra(self): - """ Permet de savoir si un user basique peut assigner cette ip ou non""" + """ Permet de savoir si un user basique peut assigner cette ip ou + non""" return self.ip_type.need_infra def clean(self): """ Erreur si l'ip_type est incorrect""" if not str(self.ipv4) in self.ip_type.ip_set_as_str: - raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!") + raise ValidationError("L'ipv4 et le range de l'iptype ne\ + correspondent pas!") return def save(self, *args, **kwargs): @@ -428,24 +632,38 @@ class IpList(models.Model): def __str__(self): return self.ipv4 + class Service(models.Model): """ Definition d'un service (dhcp, dns, etc)""" + PRETTY_NAME = "Services à générer (dhcp, dns, etc)" + service_type = models.CharField(max_length=255, blank=True, unique=True) - min_time_regen = models.DurationField(default=timedelta(minutes=1), help_text="Temps minimal avant nouvelle génération du service") - regular_time_regen = models.DurationField(default=timedelta(hours=1), help_text="Temps maximal avant nouvelle génération du service") + min_time_regen = models.DurationField( + default=timedelta(minutes=1), + help_text="Temps minimal avant nouvelle génération du service" + ) + regular_time_regen = models.DurationField( + default=timedelta(hours=1), + help_text="Temps maximal avant nouvelle génération du service" + ) servers = models.ManyToManyField('Interface', through='Service_link') def ask_regen(self): """ Marque à True la demande de régénération pour un service x """ - Service_link.objects.filter(service=self).exclude(asked_regen=True).update(asked_regen=True) + Service_link.objects.filter(service=self).exclude(asked_regen=True)\ + .update(asked_regen=True) return def process_link(self, servers): - """ Django ne peut créer lui meme les relations manytomany avec table intermediaire explicite""" - for serv in servers.exclude(pk__in=Interface.objects.filter(service=self)): + """ Django ne peut créer lui meme les relations manytomany avec table + intermediaire explicite""" + for serv in servers.exclude( + pk__in=Interface.objects.filter(service=self) + ): link = Service_link(service=self, server=serv) link.save() - Service_link.objects.filter(service=self).exclude(server__in=servers).delete() + Service_link.objects.filter(service=self).exclude(server__in=servers)\ + .delete() return def save(self, *args, **kwargs): @@ -454,15 +672,20 @@ class Service(models.Model): def __str__(self): return str(self.service_type) + def regen(service): - """ Fonction externe pour régérération d'un service, prend un objet service en arg""" + """ Fonction externe pour régérération d'un service, prend un objet service + en arg""" obj = Service.objects.filter(service_type=service) if obj: obj[0].ask_regen() return + class Service_link(models.Model): """ Definition du lien entre serveurs et services""" + PRETTY_NAME = "Relation entre service et serveur" + service = models.ForeignKey('Service', on_delete=models.CASCADE) server = models.ForeignKey('Interface', on_delete=models.CASCADE) last_regen = models.DateTimeField(auto_now_add=True) @@ -475,11 +698,16 @@ class Service_link(models.Model): self.save() def need_regen(self): - """ Décide si le temps minimal écoulé est suffisant pour provoquer une régénération de service""" - if (self.asked_regen and (self.last_regen + self.service.min_time_regen) < timezone.now()) or (self.last_regen + self.service.regular_time_regen) < timezone.now(): - return True - else: - return False + """ Décide si le temps minimal écoulé est suffisant pour provoquer une + régénération de service""" + return bool( + (self.asked_regen and ( + self.last_regen + self.service.min_time_regen + ) < timezone.now() + ) or ( + self.last_regen + self.service.regular_time_regen + ) < timezone.now() + ) def __str__(self): return str(self.server) + " " + str(self.service) @@ -487,143 +715,218 @@ class Service_link(models.Model): class OuverturePortList(models.Model): """Liste des ports ouverts sur une interface.""" - name = models.CharField(help_text="Nom de la configuration des ports.", max_length=255) + PRETTY_NAME = "Profil d'ouverture de ports" + + name = models.CharField( + help_text="Nom de la configuration des ports.", + max_length=255 + ) def __str__(self): return self.name def tcp_ports_in(self): - return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN) - + """Renvoie la liste des ports ouverts en TCP IN pour ce profil""" + return self.ouvertureport_set.filter( + protocole=OuverturePort.TCP, + io=OuverturePort.IN + ) + def udp_ports_in(self): - return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN) + """Renvoie la liste des ports ouverts en UDP IN pour ce profil""" + return self.ouvertureport_set.filter( + protocole=OuverturePort.UDP, + io=OuverturePort.IN + ) def tcp_ports_out(self): - return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT) - + """Renvoie la liste des ports ouverts en TCP OUT pour ce profil""" + return self.ouvertureport_set.filter( + protocole=OuverturePort.TCP, + io=OuverturePort.OUT + ) + def udp_ports_out(self): - return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT) + """Renvoie la liste des ports ouverts en UDP OUT pour ce profil""" + return self.ouvertureport_set.filter( + protocole=OuverturePort.UDP, + io=OuverturePort.OUT + ) class OuverturePort(models.Model): """ Représente un simple port ou une plage de ports. - - Les ports de la plage sont compris entre begin et en inclus. + + Les ports de la plage sont compris entre begin et en inclus. Si begin == end alors on ne représente qu'un seul port. + + On limite les ports entre 0 et 65535, tels que défini par la RFC """ + PRETTY_NAME = "Plage de port ouverte" + TCP = 'T' UDP = 'U' IN = 'I' OUT = 'O' - begin = models.IntegerField() - end = models.IntegerField() - port_list = models.ForeignKey('OuverturePortList', on_delete=models.CASCADE) + begin = models.PositiveIntegerField(validators=[MaxValueValidator(65535)]) + end = models.PositiveIntegerField(validators=[MaxValueValidator(65535)]) + port_list = models.ForeignKey( + 'OuverturePortList', + on_delete=models.CASCADE + ) protocole = models.CharField( - max_length=1, - choices=( - (TCP, 'TCP'), - (UDP, 'UDP'), - ), - default=TCP, + max_length=1, + choices=( + (TCP, 'TCP'), + (UDP, 'UDP'), + ), + default=TCP, ) io = models.CharField( - max_length=1, - choices=( - (IN, 'IN'), - (OUT, 'OUT'), - ), - default=OUT, + max_length=1, + choices=( + (IN, 'IN'), + (OUT, 'OUT'), + ), + default=OUT, ) def __str__(self): - if self.begin == self.end : + if self.begin == self.end: return str(self.begin) return '-'.join([str(self.begin), str(self.end)]) def show_port(self): + """Formatage plus joli, alias pour str""" return str(self) @receiver(post_save, sender=Machine) def machine_post_save(sender, **kwargs): + """Synchronisation ldap et régen parefeu/dhcp lors de la modification + d'une machine""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) regen('dhcp') regen('mac_ip_list') + @receiver(post_delete, sender=Machine) def machine_post_delete(sender, **kwargs): + """Synchronisation ldap et régen parefeu/dhcp lors de la suppression + d'une machine""" machine = kwargs['instance'] user = machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) regen('dhcp') regen('mac_ip_list') + @receiver(post_save, sender=Interface) def interface_post_save(sender, **kwargs): + """Synchronisation ldap et régen parefeu/dhcp lors de la modification + d'une interface""" interface = kwargs['instance'] user = interface.machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) - if not interface.may_have_port_open() and interface.port_lists.all(): - interface.port_lists.clear() # Regen services regen('dhcp') regen('mac_ip_list') + @receiver(post_delete, sender=Interface) def interface_post_delete(sender, **kwargs): + """Synchronisation ldap et régen parefeu/dhcp lors de la suppression + d'une interface""" interface = kwargs['instance'] user = interface.machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) + @receiver(post_save, sender=IpType) def iptype_post_save(sender, **kwargs): + """Generation des objets ip après modification d'un range ip""" iptype = kwargs['instance'] iptype.gen_ip_range() + @receiver(post_save, sender=MachineType) def machine_post_save(sender, **kwargs): + """Mise à jour des interfaces lorsque changement d'attribution + d'une machinetype (changement iptype parent)""" machinetype = kwargs['instance'] for interface in machinetype.all_interfaces(): interface.update_type() + @receiver(post_save, sender=Domain) def domain_post_save(sender, **kwargs): + """Regeneration dns après modification d'un domain object""" regen('dns') + @receiver(post_delete, sender=Domain) def domain_post_delete(sender, **kwargs): + """Regeneration dns après suppression d'un domain object""" regen('dns') + @receiver(post_save, sender=Extension) def extension_post_save(sender, **kwargs): + """Regeneration dns après modification d'une extension""" regen('dns') + @receiver(post_delete, sender=Extension) def extension_post_selete(sender, **kwargs): + """Regeneration dns après suppression d'une extension""" regen('dns') + +@receiver(post_save, sender=SOA) +def soa_post_save(sender, **kwargs): + """Regeneration dns après modification d'un SOA""" + regen('dns') + + +@receiver(post_delete, sender=SOA) +def soa_post_delete(sender, **kwargs): + """Regeneration dns après suppresson d'un SOA""" + regen('dns') + + @receiver(post_save, sender=Mx) def mx_post_save(sender, **kwargs): + """Regeneration dns après modification d'un MX""" regen('dns') + @receiver(post_delete, sender=Mx) def mx_post_delete(sender, **kwargs): + """Regeneration dns après suppresson d'un MX""" regen('dns') + @receiver(post_save, sender=Ns) def ns_post_save(sender, **kwargs): + """Regeneration dns après modification d'un NS""" regen('dns') + @receiver(post_delete, sender=Ns) def ns_post_delete(sender, **kwargs): + """Regeneration dns après modification d'un NS""" regen('dns') + @receiver(post_save, sender=Text) def text_post_save(sender, **kwargs): + """Regeneration dns après modification d'un TXT""" regen('dns') + @receiver(post_delete, sender=Text) def text_post_delete(sender, **kwargs): + """Regeneration dns après modification d'un TX""" regen('dns') diff --git a/machines/serializers.py b/machines/serializers.py index 2cf3d3e8..7d78ebc3 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -24,20 +24,42 @@ #Augustin Lemesle from rest_framework import serializers -from machines.models import Interface, IpType, Extension, IpList, MachineType, Domain, Text, Mx, Service_link, Ns +from machines.models import ( + Interface, + IpType, + Extension, + IpList, + MachineType, + Domain, + Text, + Mx, + Service_link, + Ns, + OuverturePortList, + OuverturePort +) + class IpTypeField(serializers.RelatedField): + """Serialisation d'une iptype, renvoie son evaluation str""" def to_representation(self, value): return value.type + class IpListSerializer(serializers.ModelSerializer): + """Serialisation d'une iplist, ip_type etant une foreign_key, + on evalue sa methode str""" ip_type = IpTypeField(read_only=True) class Meta: model = IpList fields = ('ipv4', 'ip_type') + class InterfaceSerializer(serializers.ModelSerializer): + """Serialisation d'une interface, ipv4, domain et extension sont + des foreign_key, on les override et on les evalue avec des fonctions + get_...""" ipv4 = IpListSerializer(read_only=True) mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') @@ -56,7 +78,9 @@ class InterfaceSerializer(serializers.ModelSerializer): def get_macaddress(self, obj): return str(obj.mac_address) + class FullInterfaceSerializer(serializers.ModelSerializer): + """Serialisation complete d'une interface avec l'ipv6 en plus""" ipv4 = IpListSerializer(read_only=True) mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') @@ -75,24 +99,70 @@ class FullInterfaceSerializer(serializers.ModelSerializer): def get_macaddress(self, obj): return str(obj.mac_address) + class ExtensionNameField(serializers.RelatedField): + """Evaluation str d'un objet extension (.example.org)""" def to_representation(self, value): return value.name + class TypeSerializer(serializers.ModelSerializer): + """Serialisation d'un iptype : extension et la liste des + ouvertures de port son evalués en get_... etant des + foreign_key ou des relations manytomany""" extension = ExtensionNameField(read_only=True) + ouverture_ports_tcp_in = serializers\ + .SerializerMethodField('get_port_policy_input_tcp') + ouverture_ports_tcp_out = serializers\ + .SerializerMethodField('get_port_policy_output_tcp') + ouverture_ports_udp_in = serializers\ + .SerializerMethodField('get_port_policy_input_udp') + ouverture_ports_udp_out = serializers\ + .SerializerMethodField('get_port_policy_output_udp') class Meta: model = IpType - fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop') + fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', + 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', + 'ouverture_ports_udp_in', 'ouverture_ports_udp_out',) + + def get_port_policy(self, obj, protocole, io): + if obj.ouverture_ports is None: + return [] + return map( + str, + obj.ouverture_ports.ouvertureport_set.filter( + protocole=protocole + ).filter(io=io) + ) + + def get_port_policy_input_tcp(self, obj): + """Renvoie la liste des ports ouverts en entrée tcp""" + return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.IN) + + def get_port_policy_output_tcp(self, obj): + """Renvoie la liste des ports ouverts en sortie tcp""" + return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.OUT) + + def get_port_policy_input_udp(self, obj): + """Renvoie la liste des ports ouverts en entrée udp""" + return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.IN) + + def get_port_policy_output_udp(self, obj): + """Renvoie la liste des ports ouverts en sortie udp""" + return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.OUT) + class ExtensionSerializer(serializers.ModelSerializer): + """Serialisation d'une extension : origin_ip et la zone sont + des foreign_key donc evalués en get_...""" origin = serializers.SerializerMethodField('get_origin_ip') zone_entry = serializers.SerializerMethodField('get_zone_name') + soa = serializers.SerializerMethodField('get_soa_data') class Meta: model = Extension - fields = ('name', 'origin', 'zone_entry') + fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa') def get_origin_ip(self, obj): return obj.origin.ipv4 @@ -100,7 +170,13 @@ class ExtensionSerializer(serializers.ModelSerializer): def get_zone_name(self, obj): return str(obj.dns_entry) + def get_soa_data(self, obj): + return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param } + + class MxSerializer(serializers.ModelSerializer): + """Serialisation d'un MX, evaluation du nom, de la zone + et du serveur cible, etant des foreign_key""" name = serializers.SerializerMethodField('get_entry_name') zone = serializers.SerializerMethodField('get_zone_name') mx_entry = serializers.SerializerMethodField('get_mx_name') @@ -118,13 +194,16 @@ class MxSerializer(serializers.ModelSerializer): def get_mx_name(self, obj): return str(obj.dns_entry) + class TextSerializer(serializers.ModelSerializer): + """Serialisation d'un txt : zone cible et l'entrée txt + sont evaluées à part""" zone = serializers.SerializerMethodField('get_zone_name') text_entry = serializers.SerializerMethodField('get_text_name') class Meta: model = Text - fields = ('zone','text_entry','field1', 'field2') + fields = ('zone', 'text_entry', 'field1', 'field2') def get_zone_name(self, obj): return str(obj.zone.name) @@ -132,10 +211,13 @@ class TextSerializer(serializers.ModelSerializer): def get_text_name(self, obj): return str(obj.dns_entry) + class NsSerializer(serializers.ModelSerializer): + """Serialisation d'un NS : la zone, l'entrée ns complète et le serveur + ns sont évalués à part""" zone = serializers.SerializerMethodField('get_zone_name') ns = serializers.SerializerMethodField('get_domain_name') - ns_entry = serializers.SerializerMethodField('get_text_name') + ns_entry = serializers.SerializerMethodField('get_text_name') class Meta: model = Ns @@ -150,10 +232,13 @@ class NsSerializer(serializers.ModelSerializer): def get_text_name(self, obj): return str(obj.dns_entry) + class DomainSerializer(serializers.ModelSerializer): + """Serialisation d'un domain, extension, cname sont des foreign_key, + et l'entrée complète, sont évalués à part""" extension = serializers.SerializerMethodField('get_zone_name') cname = serializers.SerializerMethodField('get_alias_name') - cname_entry = serializers.SerializerMethodField('get_cname_name') + cname_entry = serializers.SerializerMethodField('get_cname_name') class Meta: model = Domain @@ -168,7 +253,9 @@ class DomainSerializer(serializers.ModelSerializer): def get_cname_name(self, obj): return str(obj.dns_entry) + class ServiceServersSerializer(serializers.ModelSerializer): + """Evaluation d'un Service, et serialisation""" server = serializers.SerializerMethodField('get_server_name') service = serializers.SerializerMethodField('get_service_name') need_regen = serializers.SerializerMethodField('get_regen_status') @@ -185,3 +272,31 @@ class ServiceServersSerializer(serializers.ModelSerializer): def get_regen_status(self, obj): return obj.need_regen() + + +class OuverturePortsSerializer(serializers.Serializer): + """Serialisation de l'ouverture des ports""" + ipv4 = serializers.SerializerMethodField() + ipv6 = serializers.SerializerMethodField() + + def get_ipv4(): + return {i.ipv4.ipv4: + { + "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], + "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + } + for i in Interface.objects.all() if i.ipv4 + } + + def get_ipv6(): + return {i.ipv6: + { + "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], + "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + } + for i in Interface.objects.all() if i.ipv6 + } diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index 18fd7c4b..15a4c637 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -26,17 +26,25 @@ with this program; if not, write to the Free Software Foundation, Inc., - + + - + {% if ipv6_enabled %} + + {% endif %} + {% for extension in extension_list %} - + + - + {% endif %} + + @@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., + - + @@ -44,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% for machine in machines_list %}
Date{% include "buttons/sort.html" with prefix='sum' col='date' text='Date' %} Modification
ExtensionAutorisation infra pour utiliser l'extensionDroit infra pour utiliser ?Enregistrement SOA Enregistrement A originEnregistrement AAAA origin
{{ extension.name }}{{ extension.need_infra }}{{ extension.need_infra }}{{ extension.soa}} {{ extension.origin }} + {% if ipv6_enabled %} + {{ extension.origin_v6 }} {% if is_infra %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% endif %} diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index aafc4c1d..454b169d 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Fin Préfixe v6 Sur vlanOuverture ports par défault
{{ type.domaine_ip_stop }} {{ type.prefix_v6 }} {{ type.vlan }}{{ type.ouverture_ports }} {% if is_infra %} {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index 80e887d2..b59bdc23 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Nom DNS{% include "buttons/sort.html" with prefix='machine' col='name' text='Nom DNS' %} Type MAC IP
- {{ machine.name }} + {{ machine.name|default:'Pas de nom' }} {{ machine.user }} diff --git a/machines/templates/machines/aff_soa.html b/machines/templates/machines/aff_soa.html new file mode 100644 index 00000000..3dad11c7 --- /dev/null +++ b/machines/templates/machines/aff_soa.html @@ -0,0 +1,56 @@ +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + + + + + + + + + + + + + + + {% for soa in soa_list %} + + + + + + + + + + {% endfor %} +
NomMailRefreshRetryExpireTTL
{{ soa.name }}{{ soa.mail }}{{ soa.refresh }}{{ soa.retry }}{{ soa.expire }}{{ soa.ttl }} + {% if is_infra %} + {% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %} + {% endif %} + {% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %} +
+ + 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..ceae84c6 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -35,6 +35,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% include "machines/aff_extension.html" with extension_list=extension_list %} +

Liste des enregistrements SOA

+ {% if is_infra %} + Ajouter un enregistrement SOA + Supprimer un enregistrement SOA + {% endif %} + {% include "machines/aff_soa.html" with soa_list=soa_list %}

Liste des enregistrements MX

{% if is_infra %} Ajouter un enregistrement MX @@ -47,12 +53,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/index_nas.html b/machines/templates/machines/index_nas.html index 16fb29e2..ac1fe8b4 100644 --- a/machines/templates/machines/index_nas.html +++ b/machines/templates/machines/index_nas.html @@ -31,8 +31,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Liste des nas

La correpondance nas-machinetype relie le type de nas à un type de machine. Elle est utile pour l'autoenregistrement des macs par radius, et permet de choisir le type de machine à affecter aux machines en fonction du type de nas
+ {% if is_infra %} Ajouter un type de nas Supprimer un ou plusieurs types nas + {% endif %} {% include "machines/aff_nas.html" with nas_list=nas_list %}

diff --git a/machines/templates/machines/index_vlan.html b/machines/templates/machines/index_vlan.html index ccbfa753..ec00b0bf 100644 --- a/machines/templates/machines/index_vlan.html +++ b/machines/templates/machines/index_vlan.html @@ -29,8 +29,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

Liste des vlans

+ {% if is_infra %} Ajouter un vlan Supprimer un ou plusieurs vlan + {% endif %} {% include "machines/aff_vlan.html" with vlan_list=vlan_list %}

diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index d34dccb9..636c8e2f 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,60 @@ 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 soaform %} +

Enregistrement SOA

+ {% bootstrap_form soaform %} + {% 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/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html index e635d69a..6ca3a07f 100644 --- a/machines/templates/machines/sidebar.html +++ b/machines/templates/machines/sidebar.html @@ -58,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if is_cableur %} - Configuration de ports + Ouverture de ports {%endif%} {% endblock %} 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 886c7c0a..3669b5b9 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -44,12 +44,15 @@ urlpatterns = [ url(r'^add_extension/$', views.add_extension, name='add-extension'), url(r'^edit_extension/(?P[0-9]+)$', views.edit_extension, name='edit-extension'), url(r'^del_extension/$', views.del_extension, name='del-extension'), + url(r'^add_soa/$', views.add_soa, name='add-soa'), + url(r'^edit_soa/(?P[0-9]+)$', views.edit_soa, name='edit-soa'), + url(r'^del_soa/$', views.del_soa, name='del-soa'), 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'), @@ -74,9 +77,10 @@ urlpatterns = [ url(r'^history/(?Pinterface)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pmachinetype)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pextension)/(?P[0-9]+)$', views.history, name='history'), + url(r'^history/(?Psoa)/(?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'), @@ -93,6 +97,7 @@ urlpatterns = [ url(r'^rest/text/$', views.text, name='text'), url(r'^rest/zones/$', views.zones, name='zones'), url(r'^rest/service_servers/$', views.service_servers, name='service-servers'), + url(r'^rest/ouverture_ports/$', views.ouverture_ports, name='ouverture-ports'), url(r'index_portlist/$', views.index_portlist, name='index-portlist'), url(r'^edit_portlist/(?P[0-9]+)$', views.edit_portlist, name='edit-portlist'), url(r'^del_portlist/(?P[0-9]+)$', views.del_portlist, name='del-portlist'), diff --git a/machines/views.py b/machines/views.py index ac37d8c6..83cad204 100644 --- a/machines/views.py +++ b/machines/views.py @@ -43,56 +43,101 @@ from django.contrib.auth import authenticate, login from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer -from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer +from machines.serializers import ( FullInterfaceSerializer, + InterfaceSerializer, + TypeSerializer, + DomainSerializer, + TextSerializer, + MxSerializer, + ExtensionSerializer, + ServiceServersSerializer, + NsSerializer, + OuverturePortsSerializer +) from reversion import revisions as reversion from reversion.models import Version import re -from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm -from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm +from .forms import ( + NewMachineForm, + EditMachineForm, + EditInterfaceForm, + AddInterfaceForm, + MachineTypeForm, + DelMachineTypeForm, + ExtensionForm, + DelExtensionForm, + BaseEditInterfaceForm, + BaseEditMachineForm +) +from .forms import ( + EditIpTypeForm, + IpTypeForm, + DelIpTypeForm, + DomainForm, + AliasForm, + DelAliasForm, + SOAForm, + DelSOAForm, + NsForm, + DelNsForm, + TxtForm, + DelTxtForm, + MxForm, + DelMxForm, + VlanForm, + DelVlanForm, + ServiceForm, + DelServiceForm, + NasForm, + DelNasForm +) from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm -from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort +from .models import ( + IpType, + Machine, + Interface, + IpList, + MachineType, + Extension, + SOA, + Mx, + Ns, + Domain, + Service, + Service_link, + Vlan, + Nas, + Text, + OuverturePortList, + 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 all_active_interfaces(): - """Renvoie l'ensemble des machines autorisées à sortir sur internet """ - return Interface.objects.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_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.utils import ( + all_active_assigned_interfaces, + all_has_access, + filter_active_interfaces, + SortTable +) +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 = [] choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},' mtype_id = -1 - for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id')).order_by('mtype_id', 'id') : + for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id'))\ + .order_by('mtype_id', 'id') : if mtype_id != ip.mtype_id : mtype_id = ip.mtype_id used_mtype_id.append(mtype_id) @@ -112,7 +157,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( {{' @@ -126,7 +171,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) {{' @@ -142,25 +187,27 @@ 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_gen_select = { 'ipv4': False } + i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, - 'update_on': i_update_on + 'update_on': i_update_on, + 'gen_select': i_gen_select } - return i_bft_param + return i_mbf_param @login_required def new_machine(request, userid): - """ Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain - à partir de model forms. + """ Fonction de creation d'une machine. Cree l'objet machine, + le sous objet interface et l'objet domain à partir de model forms. Trop complexe, devrait être simplifié""" try: user = User.objects.get(pk=userid) @@ -171,15 +218,16 @@ def new_machine(request, userid): max_lambdauser_interfaces = options.max_lambdauser_interfaces if not request.user.has_perms(('cableur',)): if user != request.user: - messages.error(request, "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit") + messages.error( + request, + "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit") return redirect("/users/profil/" + str(request.user.id)) if user.user_interfaces().count() >= max_lambdauser_interfaces: messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces) return redirect("/users/profil/" + str(request.user.id)) machine = NewMachineForm(request.POST or None) interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) - nb_machine = Interface.objects.filter(machine__user=userid).count() - domain = DomainForm(request.POST or None, user=user, nb_machine=nb_machine) + domain = DomainForm(request.POST or None, user=user) if machine.is_valid() and interface.is_valid(): new_machine = machine.save(commit=False) new_machine.user = user @@ -203,8 +251,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): @@ -243,8 +291,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): @@ -302,8 +350,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): @@ -340,7 +388,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') @@ -359,7 +407,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') @@ -377,7 +425,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') @@ -390,7 +438,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') @@ -408,7 +456,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') @@ -425,7 +473,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') @@ -438,7 +486,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') @@ -456,7 +504,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') @@ -473,7 +521,55 @@ 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') +def add_soa(request): + soa = SOAForm(request.POST or None) + if soa.is_valid(): + with transaction.atomic(), reversion.create_revision(): + soa.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Cet enregistrement SOA a été ajouté") + return redirect("/machines/index_extension") + return form({'soaform': soa}, 'machines/machine.html', request) + +@login_required +@permission_required('infra') +def edit_soa(request, soaid): + try: + soa_instance = SOA.objects.get(pk=soaid) + except SOA.DoesNotExist: + messages.error(request, u"Entrée inexistante" ) + return redirect("/machines/index_extension/") + soa = SOAForm(request.POST or None, instance=soa_instance) + if soa.is_valid(): + with transaction.atomic(), reversion.create_revision(): + soa.save() + reversion.set_user(request.user) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in soa.changed_data)) + messages.success(request, "SOA modifié") + return redirect("/machines/index_extension/") + return form({'soaform': soa}, 'machines/machine.html', request) + +@login_required +@permission_required('infra') +def del_soa(request): + soa = DelSOAForm(request.POST or None) + if soa.is_valid(): + soa_dels = soa.cleaned_data['soa'] + for soa_del in soa_dels: + try: + with transaction.atomic(), reversion.create_revision(): + soa_del.delete() + reversion.set_user(request.user) + messages.success(request, "Le SOA a été supprimée") + except ProtectedError: + messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del) + return redirect("/machines/index_extension") + return form({'soaform': soa}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -486,7 +582,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') @@ -504,7 +600,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') @@ -521,7 +617,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') @@ -534,7 +630,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') @@ -552,7 +648,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') @@ -569,55 +665,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): @@ -645,7 +741,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): @@ -665,7 +761,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): @@ -689,7 +785,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 @@ -703,7 +799,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') @@ -721,7 +817,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') @@ -738,7 +834,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') @@ -751,7 +847,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') @@ -769,7 +865,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') @@ -786,7 +882,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') @@ -799,7 +895,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') @@ -817,7 +913,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') @@ -834,14 +930,20 @@ 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') def index(request): options, created = GeneralOption.objects.get_or_create() pagination_large_number = options.pagination_large_number - machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension').order_by('pk') + machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension') + machines_list = SortTable.sort( + machines_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.MACHINES_INDEX + ) paginator = Paginator(machines_list, pagination_large_number) page = request.GET.get('page') try: @@ -857,13 +959,13 @@ def index(request): @login_required @permission_required('cableur') def index_iptype(request): - iptype_list = IpType.objects.select_related('extension').order_by('type') + iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type') return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list}) @login_required @permission_required('cableur') def index_vlan(request): - vlan_list = Vlan.objects.order_by('vlan_id') + vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id') return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list}) @login_required @@ -875,17 +977,18 @@ def index_machinetype(request): @login_required @permission_required('cableur') def index_nas(request): - nas_list = Nas.objects.select_related('machine_type').order_by('name') + nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name') return render(request, 'machines/index_nas.html', {'nas_list':nas_list}) @login_required @permission_required('cableur') def index_extension(request): - extension_list = Extension.objects.select_related('origin').order_by('name') + extension_list = Extension.objects.select_related('origin').select_related('soa').order_by('name') + soa_list = SOA.objects.order_by('name') mx_list = Mx.objects.order_by('zone').select_related('zone').select_related('name__extension') ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension') text_list = Text.objects.all().select_related('zone') - return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list}) + return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'soa_list': soa_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list}) @login_required def index_alias(request, interfaceid): @@ -903,8 +1006,8 @@ def index_alias(request, interfaceid): @login_required @permission_required('cableur') def index_service(request): - service_list = Service.objects.all() - servers_list = Service_link.objects.all() + service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all() + servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all() return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list}) @login_required @@ -954,17 +1057,23 @@ def history(request, object, id): except Extension.DoesNotExist: messages.error(request, "Extension inexistante") return redirect("/machines/") + elif object == 'soa' and request.user.has_perms(('cableur',)): + try: + object_instance = SOA.objects.get(pk=id) + except SOA.DoesNotExist: + messages.error(request, "SOA inexistant") + return redirect("/machines/") elif object == 'mx' and request.user.has_perms(('cableur',)): try: object_instance = Mx.objects.get(pk=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: @@ -1012,7 +1121,9 @@ def history(request, object, id): @login_required @permission_required('cableur') def index_portlist(request): - port_list = OuverturePortList.objects.all().order_by('name') + port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set')\ + .prefetch_related('interface_set__domain__extension')\ + .prefetch_related('interface_set__machine__user').order_by('name') return render(request, "machines/index_portlist.html", {'port_list':port_list}) @login_required @@ -1203,6 +1314,34 @@ def service_servers(request): @csrf_exempt @login_required @permission_required('serveur') +def ouverture_ports(request): + r = {'ipv4':{}, 'ipv6':{}} + for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'): + pl = { + "tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))), + "tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))), + "udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))), + "udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))), + } + for i in filter_active_interfaces(o.interface_set): + if i.may_have_port_open(): + d = r['ipv4'].get(i.ipv4.ipv4, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv4'][i.ipv4.ipv4] = d + if i.ipv6_object: + d = r['ipv6'].get(i.ipv6, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv6'][i.ipv6] = d + return JSONResponse(r) +@csrf_exempt +@login_required +@permission_required('serveur') def regen_achieved(request): obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server']))) if obj: 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/migrations/0021_auto_20171015_1741.py b/preferences/migrations/0021_auto_20171015_1741.py new file mode 100644 index 00000000..cc94720a --- /dev/null +++ b/preferences/migrations/0021_auto_20171015_1741.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 15:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0020_optionalmachine_ipv6'), + ] + + operations = [ + migrations.AlterField( + model_name='optionaltopologie', + name='radius_general_policy', + field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32), + ), + ] diff --git a/preferences/migrations/0022_auto_20171015_1758.py b/preferences/migrations/0022_auto_20171015_1758.py new file mode 100644 index 00000000..ea389a32 --- /dev/null +++ b/preferences/migrations/0022_auto_20171015_1758.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 15:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0021_auto_20171015_1741'), + ] + + operations = [ + migrations.AlterField( + model_name='optionaltopologie', + name='radius_general_policy', + field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32), + ), + ] diff --git a/preferences/migrations/0023_auto_20171015_2033.py b/preferences/migrations/0023_auto_20171015_2033.py new file mode 100644 index 00000000..3235e49f --- /dev/null +++ b/preferences/migrations/0023_auto_20171015_2033.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0022_auto_20171015_1758'), + ] + + operations = [ + migrations.AlterField( + model_name='optionaltopologie', + name='radius_general_policy', + field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32), + ), + ] 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..f10d25a0 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..26a9bcc8 --- /dev/null +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -0,0 +1,809 @@ +# -*- 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. + +""" Templatetag used to render massive django form selects into bootstrap +forms that can still be manipulating even if there is multiple tens of +thousands of elements in the select. It's made possible using JS libaries +Twitter Typeahead and Splitree's Tokenfield. +See docstring of massive_bootstrap_form for a detailed explaantion on how +to use this templatetag. +""" + +from django import template +from django.utils.safestring import mark_safe +from django.forms import TextInput +from django.forms.widgets import Select +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 : + 'update_on' : { + 'field_A' : [ 'id0', 'id1', ... ] , + 'field_B' : ... , + ... + } + + gen_select (optional) + A dict of boolean telling if the form should either generate + the normal select (set to true) and then use it to generate + the possible choices and then remove it or either (set to + false) generate the choices variable in this tag and do not + send any select. + Sending the select before can be usefull to permit the use + without any JS enabled but it will execute more code locally + for the client so the loading might be slower. + If not specified, this variable is set to true for each field + Example : + 'gen_select' : { + 'field_A': True , + 'field_B': ... , + ... + } + + See boostrap_form_ for other arguments + + **Usage**:: + + {% massive_bootstrap_form + form + [ '[,[,...]]' ] + [ mbf_param = { + [ 'choices': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'engine': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'match_func': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'update_on': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ], + [, 'gen_select': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + } ] + [ ] + %} + + **Example**: + + {% massive_bootstrap_form form 'ipv4' choices='[...]' %} + """ + + mbf_form = MBFForm(form, mbf_fields.split(','), *args, **kwargs) + return mbf_form.render() + + + + +class MBFForm(): + """ An object to hold all the information and useful methods needed to + create and render a massive django form into an actual HTML and JS + code able to handle it correctly. + Every field that is not listed is rendered as a normal bootstrap_field. + """ + + + def __init__(self, form, mbf_fields, *args, **kwargs): + # The django form object + self.form = form + # The fields on which to use JS + self.fields = mbf_fields + + # Other bootstrap_form arguments to render the fields + self.args = args + self.kwargs = kwargs + + # Fields to exclude form the form rendering + self.exclude = self.kwargs.get('exclude', '').split(',') + + # All the mbf parameters specified byt the user + param = kwargs.pop('mbf_param', {}) + self.choices = param.get('choices', {}) + self.engine = param.get('engine', {}) + self.match_func = param.get('match_func', {}) + self.update_on = param.get('update_on', {}) + self.gen_select = param.get('gen_select', {}) + self.hidden_fields = [h.name for h in self.form.hidden_fields()] + + # HTML code to insert inside a template + self.html = "" + + + def render(self): + """ HTML code for the fully rendered form with all the necessary form + """ + for name, field in self.form.fields.items(): + if not name in self.exclude: + + if name in self.fields and not name in self.hidden_fields: + mbf_field = MBFField( + name, + field, + field.get_bound_field(self.form, name), + self.choices.get(name, None), + self.engine.get(name, None), + self.match_func.get(name, None), + self.update_on.get(name, None), + self.gen_select.get(name, True), + *self.args, + **self.kwargs + ) + self.html += mbf_field.render() + + else: + self.html += render_field( + field.get_bound_field(self.form, name), + *self.args, + **self.kwargs + ) + + return mark_safe(self.html) + + + + + +class MBFField(): + """ An object to hold all the information and useful methods needed to + create and render a massive django form field into an actual HTML and JS + code able to handle it correctly. + Twitter Typeahead is used for the display and the matching of queries and + in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage + multiple values. + A div with only non visible elements is created after the div containing + the displayed input. It's used to store the actual data that will be sent + to the server """ + + + def __init__(self, name_, field_, bound_, choices_, engine_, match_func_, + update_on_, gen_select_, *args_, **kwargs_): + + # Verify this field is a Select (or MultipleSelect) (only supported) + if not isinstance(field_.widget, Select): + raise ValueError( + ('Field named {f_name} is not a Select and' + 'can\'t be rendered with massive_bootstrap_form.' + ).format( + f_name=name_ + ) + ) + + # Name of the field + self.name = name_ + # Django field object + self.field = field_ + # Bound Django field associated with field + self.bound = bound_ + + # Id for the main visible input + self.input_id = self.bound.auto_id + # Id for a hidden input used to store the value + self.hidden_id = self.input_id + '_hidden' + # Id for another div containing hidden inputs and script + self.div2_id = self.input_id + '_div' + + # Should the standard select should be generated + self.gen_select = gen_select_ + # Is it select with multiple values possible (use of tokenfield) + self.multiple = self.field.widget.allow_multiple_selected + # JS for the choices variable (user specified or default) + self.choices = choices_ or self.default_choices() + # JS for the engine variable (typeahead) (user specified or default) + self.engine = engine_ or self.default_engine() + # JS for the matching function (typeahead) (user specified or default) + self.match_func = match_func_ or self.default_match_func() + # JS for the datasets variable (typeahead) (user specified or default) + self.datasets = self.default_datasets() + # Ids of other fields to bind a reset/reload with when changed + self.update_on = update_on_ or [] + + # Whole HTML code to insert in the template + self.html = "" + # JS code in the script tag + self.js_script = "" + # Input tag to display instead of select + self.replace_input = None + + # Other bootstrap_form arguments to render the fields + self.args = args_ + self.kwargs = kwargs_ + + + def default_choices(self): + """ JS code of the variable choices_ """ + + if self.gen_select: + return ( + 'function plop(o) {{' + 'var c = [];' + 'for( let i=0 ; i """ + return ( + 'new Bloodhound({{' + ' datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' + ' queryTokenizer: Bloodhound.tokenizers.whitespace,' + ' local: choices_{name},' + ' identify: function(obj) {{ return obj.key; }}' + '}})' + ).format( + name=self.name + ) + + + def default_datasets(self): + """ Default JS script of the datasets to use with typeahead """ + return ( + '{{' + ' hint: true,' + ' highlight: true,' + ' minLength: 0' + '}},' + '{{' + ' display: "value",' + ' name: "{name}",' + ' source: {match_func}' + '}}' + ).format( + name=self.name, + match_func=self.match_func + ) + + + def default_match_func(self): + """ Default JS code of the matching function to use with typeahed """ + return ( + 'function ( q, sync ) {{' + ' if ( q === "" ) {{' + ' var first = choices_{name}.slice( 0, 5 ).map(' + ' function ( obj ) {{ return obj.key; }}' + ' );' + ' sync( engine_{name}.get( first ) );' + ' }} else {{' + ' engine_{name}.search( q, sync );' + ' }}' + '}}' + ).format( + name=self.name + ) + + + def render(self): + """ HTML code for the fully rendered field """ + self.gen_displayed_div() + self.gen_hidden_div() + return mark_safe(self.html) + + + def gen_displayed_div(self): + """ Generate HTML code for the div that contains displayed tags """ + if self.gen_select: + self.html += render_field( + self.bound, + *self.args, + **self.kwargs + ) + + self.field.widget = TextInput( + attrs={ + 'name': 'mbf_'+self.name, + 'placeholder': self.field.empty_label + } + ) + self.replace_input = render_field( + self.bound, + *self.args, + **self.kwargs + ) + + if not self.gen_select: + self.html += self.replace_input + + + def gen_hidden_div(self): + """ Generate HTML code for the div that contains hidden tags """ + self.gen_full_js() + + content = self.js_script + if not self.multiple and not self.gen_select: + content += self.hidden_input() + + self.html += render_tag( + 'div', + content=content, + attrs={'id': self.div2_id} + ) + + + def hidden_input(self): + """ HTML for the hidden input element """ + return render_tag( + 'input', + attrs={ + 'id': self.hidden_id, + 'name': self.bound.html_name, + 'type': 'hidden', + 'value': self.bound.value() or "" + } + ) + + + def gen_full_js(self): + """ Generate the full script tag containing the JS code """ + self.create_js() + self.fill_js() + self.get_script() + + + def create_js(self): + """ Generate a template for the whole script to use depending on + gen_select and multiple """ + if self.gen_select: + if self.multiple: + self.js_script = ( + '$( "#{input_id}" ).ready( function() {{' + ' var choices_{f_name} = {choices};' + ' {del_select}' + ' 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", {tok_create} );' + ' $( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );' + ' $( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );' + ' {tok_updates}' + ' setup_{f_name}();' + ' {tok_init_input}' + '}} );' + ) + else: + self.js_script = ( + '$( "#{input_id}" ).ready( function() {{' + ' var choices_{f_name} = {choices};' + ' {del_select}' + ' {gen_hidden}' + ' 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", {typ_select} );' + ' $( "#{input_id}" ).bind( "typeahead:change", {typ_change} );' + ' {typ_updates}' + ' setup_{f_name}();' + ' {typ_init_input}' + '}} );' + ) + else: + if self.multiple: + self.js_script = ( + '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", {tok_create} );' + '$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );' + '$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );' + '{tok_updates}' + '$( "#{input_id}" ).ready( function() {{' + ' setup_{f_name}();' + ' {tok_init_input}' + '}} );' + ) + else: + self.js_script = ( + '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", {typ_select} );' + '$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );' + '{typ_updates}' + '$( "#{input_id}" ).ready( function() {{' + ' setup_{f_name}();' + ' {typ_init_input}' + '}} );' + ) + + + def fill_js(self): + """ Fill the template with the correct values """ + self.js_script = self.js_script.format( + f_name=self.name, + choices=self.choices, + del_select=self.del_select(), + gen_hidden=self.gen_hidden(), + engine=self.engine, + input_id=self.input_id, + datasets=self.datasets, + typ_select=self.typeahead_select(), + typ_change=self.typeahead_change(), + tok_create=self.tokenfield_create(), + tok_edit=self.tokenfield_edit(), + tok_remove=self.tokenfield_remove(), + typ_updates=self.typeahead_updates(), + tok_updates=self.tokenfield_updates(), + tok_init_input=self.tokenfield_init_input(), + typ_init_input=self.typeahead_init_input() + ) + + + def get_script(self): + """ Insert the JS code inside a script tag """ + self.js_script = render_tag('script', content=mark_safe(self.js_script)) + + + def del_select(self): + """ JS code to delete the select if it has been generated and replace + it with an input. """ + return ( + 'var p = $("#{select_id}").parent()[0];' + 'var new_input = `{replace_input}`;' + 'p.innerHTML = new_input;' + ).format( + select_id=self.input_id, + replace_input=self.replace_input + ) + + + def gen_hidden(self): + """ JS code to add a hidden tag to store the value. """ + return ( + 'var d = $("#{div2_id}")[0];' + 'var i = document.createElement("input");' + 'i.id = "{hidden_id}";' + 'i.name = "{html_name}";' + 'i.value = "";' + 'i.type = "hidden";' + 'd.appendChild(i);' + ).format( + div2_id=self.div2_id, + hidden_id=self.hidden_id, + html_name=self.bound.html_name + ) + + + def typeahead_init_input(self): + """ JS code to init the fields values """ + init_key = self.bound.value() or '""' + return ( + '$( "#{input_id}" ).typeahead("val", {init_val});' + '$( "#{hidden_id}" ).val( {init_key} );' + ).format( + input_id=self.input_id, + init_val='""' if init_key == '""' else + 'engine_{name}.get( {init_key} )[0].value'.format( + name=self.name, + init_key=init_key + ), + init_key=init_key, + hidden_id=self.hidden_id + ) + + + def typeahead_reset_input(self): + """ JS code to reset the fields values """ + return ( + '$( "#{input_id}" ).typeahead("val", "");' + '$( "#{hidden_id}" ).val( "" );' + ).format( + input_id=self.input_id, + hidden_id=self.hidden_id + ) + + + def typeahead_select(self): + """ JS code to create 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=self.hidden_id + ) + + + def typeahead_change(self): + """ JS code of 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=self.input_id, + hidden_id=self.hidden_id + ) + + + def typeahead_updates(self): + """ JS code for binding external fields changes with a reset """ + reset_input = self.typeahead_reset_input() + updates = [ + ( + '$( "#{u_id}" ).change( function() {{' + ' setup_{name}();' + ' {reset_input}' + '}} );' + ).format( + u_id=u_id, + name=self.name, + reset_input=reset_input + ) for u_id in self.update_on] + return ''.join(updates) + + + def tokenfield_init_input(self): + """ JS code to init the fields values """ + init_key = self.bound.value() or '""' + return ( + '$( "#{input_id}" ).tokenfield("setTokens", {init_val});' + ).format( + input_id=self.input_id, + init_val='""' if init_key == '""' else ( + 'engine_{name}.get( {init_key} ).map(' + ' function(o) {{ return o.value; }}' + ')').format( + name=self.name, + init_key=init_key + ) + ) + + + def tokenfield_reset_input(self): + """ JS code to reset the fields values """ + return ( + '$( "#{input_id}" ).tokenfield("setTokens", "");' + ).format( + input_id=self.input_id + ) + + + def tokenfield_create(self): + """ JS code 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&q=foo&search=bar&name=johnDoe + + **Usage**:: + + {% url_insert_param [URL] [param1=val1 [param2=val2 [...]]] %} + + **Example**:: + + {% url_insert_param a=0 b="bar" %} + return "?a=0&b=bar" + + {% url_insert_param "url.net/foo.html" a=0 b="bar" %} + return "url.net/foo.html?a=0&b=bar" + + {% url_insert_param "url.net/foo.html?c=keep" a=0 b="bar" %} + return "url.net/foo.html?c=keep&a=0&b=bar" + + {% url_insert_param "url.net/foo.html?a=del" a=0 b="bar" %} + return "url.net/foo.html?a=0&b=bar" + + {% url_insert_param "url.net/foo.html?a=del&c=keep" a=0 b="bar" %} + return "url.net/foo.hmtl?a=0&c=keep&b=bar" + """ + + # Get existing parameters in the url + params = {} + if '?' in url: + url, parameters = url.split('?', maxsplit=1) + for parameter in parameters.split('&'): + p_name, p_value = parameter.split('=', maxsplit=1) + if p_name not in params: + params[p_name] = [] + params[p_name].append(p_value) + + # Add the request parameters to the list of parameters + for key, value in kwargs.items(): + params[key] = [value] + + # Write the url + url += '?' + for param, value_list in params.items(): + for value in value_list: + url += str(param) + '=' + str(value) + '&' + + # Remove the last '&' (or '?' if no parameters) + return url[:-1] diff --git a/re2o/urls.py b/re2o/urls.py index 5fd45f85..775b87ec 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -49,10 +49,16 @@ urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^users/', include('users.urls', namespace='users')), url(r'^search/', include('search.urls', namespace='search')), - url(r'^cotisations/', include('cotisations.urls', namespace='cotisations')), + url( + r'^cotisations/', + include('cotisations.urls', namespace='cotisations') + ), url(r'^machines/', include('machines.urls', namespace='machines')), url(r'^topologie/', include('topologie.urls', namespace='topologie')), url(r'^logs/', include('logs.urls', namespace='logs')), - url(r'^preferences/', include('preferences.urls', namespace='preferences')), + url( + r'^preferences/', + include('preferences.urls', namespace='preferences') + ), ] diff --git a/re2o/utils.py b/re2o/utils.py new file mode 100644 index 00000000..a6e5c851 --- /dev/null +++ b/re2o/utils.py @@ -0,0 +1,264 @@ +# -*- 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 Gabriel Détraz +# Copyright © 2017 Goulven Kermarec +# Copyright © 2017 Augustin Lemesle +# +# 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. + +# -*- coding: utf-8 -*- +# David Sinquin, Gabriel Détraz, Goulven Kermarec +""" +Regroupe les fonctions transversales utiles + +Fonction : + - récupérer tous les utilisateurs actifs + - récupérer toutes les machines + - récupérer tous les bans + etc +""" + + +from __future__ import unicode_literals + + +from django.utils import timezone +from django.db.models import Q + +from cotisations.models import Cotisation, Facture, Paiement, Vente +from machines.models import Domain, Interface, Machine +from users.models import Adherent, User, Ban, Whitelist +from preferences.models import Service + +DT_NOW = timezone.now() + + +def all_adherent(search_time=DT_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( + Q(type_cotisation='All') | Q(type_cotisation='Adhesion'), + 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=DT_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=DT_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=DT_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=search_time)) & + (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) | + Q(facture__in=Facture.objects.filter( + vente__in=Vente.objects.filter( + cotisation__in=Cotisation.objects.filter( + Q(type_cotisation='All') | Q(type_cotisation='Connexion'), + vente__in=Vente.objects.filter( + facture__in=Facture.objects.all() + .exclude(valid=False) + ) + ).filter(date_end__gt=search_time) + ) + ))) + ).distinct() + + +def filter_active_interfaces(interface_set): + """Filtre les machines autorisées à sortir sur internet dans une requête""" + return interface_set.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) + +class SortTable: + """ Class gathering uselful stuff to sort the colums of a table, according + to the column and order requested. It's used with a dict of possible + values and associated model_fields """ + + # All the possible possible values + # The naming convention is based on the URL or the views function + # The syntax to describe the sort to apply is a dict where the keys are + # the url value and the values are a list of model field name to use to + # order the request. They are applied in the order they are given. + # A 'default' might be provided to specify what to do if the requested col + # doesn't match any keys. + USERS_INDEX = { + 'user_name': ['name'], + 'user_surname': ['surname'], + 'user_pseudo': ['pseudo'], + 'user_room': ['room'], + 'default': ['state', 'pseudo'] + } + USERS_INDEX_BAN = { + 'ban_user': ['user__pseudo'], + 'ban_start': ['date_start'], + 'ban_end': ['date_end'], + 'default': ['-date_end'] + } + USERS_INDEX_WHITE = { + 'white_user': ['user__pseudo'], + 'white_start': ['date_start'], + 'white_end': ['date_end'], + 'default': ['-date_end'] + } + MACHINES_INDEX = { + 'machine_name': ['name'], + 'default': ['pk'] + } + COTISATIONS_INDEX = { + 'cotis_user': ['user__pseudo'], + 'cotis_paiement': ['paiement__moyen'], + 'cotis_date': ['date'], + 'cotis_id': ['id'], + 'default': ['-date'] + } + COTISATIONS_CONTROL = { + 'control_name': ['user__adherent__name'], + 'control_surname': ['user__surname'], + 'control_paiement': ['paiement'], + 'control_date': ['date'], + 'control_valid': ['valid'], + 'control_control': ['control'], + 'control_id': ['id'], + 'control_user-id': ['user__id'], + 'default': ['-date'] + } + TOPOLOGIE_INDEX = { + 'switch_dns': ['switch_interface__domain__name'], + 'switch_ip': ['switch_interface__ipv4__ipv4'], + 'switch_loc': ['location'], + 'switch_ports': ['number'], + 'switch_stack': ['stack__name'], + 'default': ['location', 'stack', 'stack_member_id'] + } + TOPOLOGIE_INDEX_PORT = { + 'port_port': ['port'], + 'port_room': ['room__name'], + 'port_interface': ['machine_interface__domain__name'], + 'port_related': ['related__switch__name'], + 'port_radius': ['radius'], + 'port_vlan': ['vlan_force__name'], + 'default': ['port'] + } + TOPOLOGIE_INDEX_ROOM = { + 'room_name': ['name'], + 'default': ['name'] + } + TOPOLOGIE_INDEX_STACK = { + 'stack_name': ['name'], + 'stack_id': ['stack_id'], + 'default': ['stack_id'], + } + TOPOLOGIE_INDEX_MODEL_SWITCH = { + 'model_switch_name': ['reference'], + 'model_switch__contructor' : ['constructor__name'], + 'default': ['reference'], + } + TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = { + 'room_name': ['name'], + 'default': ['name'], + } + LOGS_INDEX = { + 'sum_date': ['revision__date_created'], + 'default': ['-revision__date_created'], + } + LOGS_STATS_LOGS = { + 'logs_author': ['user__name'], + 'logs_date': ['date_created'], + 'default': ['-date_created'] + } + + @staticmethod + def sort(request, col, order, values): + """ Check if the given values are possible and add .order_by() and + a .reverse() as specified according to those values """ + fields = values.get(col, None) + if not fields: + fields = values.get('default', []) + request = request.order_by(*fields) + if values.get(col, None) and order == 'desc': + return request.reverse() + else: + return request + + +def remove_user_room(room): + """ Déménage de force l'ancien locataire de la chambre """ + try: + user = Adherent.objects.get(room=room) + except Adherent.DoesNotExist: + return + user.room = None + user.save() diff --git a/re2o/views.py b/re2o/views.py index bd8077e1..9cab6273 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -19,25 +19,28 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Fonctions de la page d'accueil et diverses fonctions utiles pour tous +les views +""" from __future__ import unicode_literals from django.shortcuts import render -from django.shortcuts import get_object_or_404 from django.template.context_processors import csrf -from django.template import Context, RequestContext, loader from preferences.models import Service + def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) + """Form générique, raccourci importé par les fonctions views du site""" + context = ctx + context.update(csrf(request)) + return render(request, template, context) def index(request): - i = 0 + """Affiche la liste des services sur la page d'accueil de re2o""" services = [[], [], []] for indice, serv in enumerate(Service.objects.all()): services[indice % 3].append(serv) - return form({'services_urls': services}, 're2o/index.html', request) diff --git a/re2o/wsgi.py b/re2o/wsgi.py index 70108566..deb6b330 100644 --- a/re2o/wsgi.py +++ b/re2o/wsgi.py @@ -32,9 +32,10 @@ https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ from __future__ import unicode_literals import os -from django.core.wsgi import get_wsgi_application -from os.path import dirname import sys +from os.path import dirname +from django.core.wsgi import get_wsgi_application + sys.path.append(dirname(dirname(__file__))) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") diff --git a/search/admin.py b/search/admin.py index bcdc4f1d..decd096a 100644 --- a/search/admin.py +++ b/search/admin.py @@ -21,8 +21,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""The field used in the admin view for the search app""" + from __future__ import unicode_literals -from django.contrib import admin - # Register your models here. diff --git a/search/forms.py b/search/forms.py index fa43be55..90f7407f 100644 --- a/search/forms.py +++ b/search/forms.py @@ -20,21 +20,72 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""The forms used by the search app""" + from __future__ import unicode_literals -from django.db.models import Q -from simple_search import BaseSearchForm +from django import forms +from django.forms import Form -from users.models import User, School +CHOICES_USER = ( + ('0', 'Actifs'), + ('1', 'Désactivés'), + ('2', 'Archivés'), +) -class UserSearchForm(BaseSearchForm): - class Meta: - base_qs = User.objects - search_fields = ('^name', 'description', 'specifications', '=id') +CHOICES_AFF = ( + ('0', 'Utilisateurs'), + ('1', 'Machines'), + ('2', 'Factures'), + ('3', 'Bannissements'), + ('4', 'Accès à titre gracieux'), + ('5', 'Chambres'), + ('6', 'Ports'), + ('7', 'Switchs'), +) - # assumes a fulltext index has been defined on the fields - # 'name,description,specifications,id' - fulltext_indexes = ( - ('name', 2), # name matches are weighted higher - ('name,description,specifications,id', 1), - ) + +def initial_choices(c): + """Return the choices that should be activated by default for a + given set of choices""" + return [i[0] for i in c] + + +class SearchForm(Form): + """The form for a simple search""" + q = forms.CharField(label='Search', max_length=100) + + +class SearchFormPlus(Form): + """The form for an advanced search (with filters)""" + q = forms.CharField( + label='Search', + max_length=100, + required=False + ) + u = forms.MultipleChoiceField( + label="Filtre utilisateurs", + required=False, + widget=forms.CheckboxSelectMultiple, + choices=CHOICES_USER, + initial=initial_choices(CHOICES_USER) + ) + a = forms.MultipleChoiceField( + label="Filtre affichage", + required=False, + widget=forms.CheckboxSelectMultiple, + choices=CHOICES_AFF, + initial=initial_choices(CHOICES_AFF) + ) + s = forms.DateField( + required=False, + label="Date de début", + help_text='DD/MM/YYYY', + input_formats=['%d/%m/%Y'] + ) + e = forms.DateField( + required=False, + help_text='DD/MM/YYYY', + input_formats=['%d/%m/%Y'], + label="Date de fin" + ) diff --git a/search/models.py b/search/models.py deleted file mode 100644 index 8d1fa0e4..00000000 --- a/search/models.py +++ /dev/null @@ -1,62 +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 Gabriel Détraz -# Copyright © 2017 Goulven Kermarec -# Copyright © 2017 Augustin Lemesle -# -# 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 __future__ import unicode_literals - -from django.db import models -from django import forms -from django.forms import Form -from django.forms import ModelForm - -CHOICES = ( - ('0', 'Actifs'), - ('1', 'Désactivés'), - ('2', 'Archivés'), -) - -CHOICES2 = ( - (1, 'Active'), - ("", 'Désactivée'), -) - -CHOICES3 = ( - ('0', 'Utilisateurs'), - ('1', 'Machines'), - ('2', 'Factures'), - ('3', 'Bannissements'), - ('4', 'Accès à titre gracieux'), - ('6', 'Switchs'), - ('5', 'Ports'), -) - - -class SearchForm(Form): - search_field = forms.CharField(label = 'Search', max_length = 100) - -class SearchFormPlus(Form): - search_field = forms.CharField(label = 'Search', max_length = 100, required=False) - filtre = forms.MultipleChoiceField(label="Filtre utilisateurs", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES) - connexion = forms.MultipleChoiceField(label="Filtre connexion", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES2) - affichage = forms.MultipleChoiceField(label="Filtre affichage", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES3) - date_deb = forms.DateField(required=False, label="Date de début", help_text='DD/MM/YYYY', input_formats=['%d/%m/%Y']) - date_fin = forms.DateField(required=False, help_text='DD/MM/YYYY', input_formats=['%d/%m/%Y'], label="Date de fin") diff --git a/search/templates/search/index.html b/search/templates/search/index.html index 859193fb..c043a22b 100644 --- a/search/templates/search/index.html +++ b/search/templates/search/index.html @@ -36,30 +36,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Résultats dans les machines :

{% include "machines/aff_machines.html" with machines_list=machines_list %} {% endif %} - {% if facture_list %} + {% if factures_list %}

Résultats dans les factures :

- {% include "cotisations/aff_cotisations.html" with facture_list=facture_list %} + {% include "cotisations/aff_cotisations.html" with facture_list=factures_list %} {% endif %} - {% if white_list %} + {% if whitelists_list %}

Résultats dans les accès à titre gracieux :

- {% include "users/aff_whitelists.html" with white_list=white_list %} + {% include "users/aff_whitelists.html" with white_list=whitelists_list %} {% endif %} - {% if ban_list %} + {% if bans_list %}

Résultats dans les banissements :

- {% include "users/aff_bans.html" with ban_list=ban_list %} + {% include "users/aff_bans.html" with ban_list=bans_list %} {% endif %} - {% if switch_list %} -

Résultats dans les switchs :

- {% include "topologie/aff_switch.html" with switch_list=switch_list %} + {% if rooms_list %} +

Résultats dans les chambres :

+ {% include "topologie/aff_chambres.html" with room_list=rooms_list %} {% endif %} - {% if port_list %} + {% if switch_ports_list %}

Résultats dans les ports :

- {% include "topologie/aff_port.html" with port_list=port_list %} + {% include "topologie/aff_port.html" with port_list=switch_ports_list %} {% endif %} - {% if not ban_list and not interfaces_list and not users_list and not facture_list and not white_list and not port_list and not switch_list%} + {% if switches_list %} +

Résultats dans les switchs :

+ {% include "topologie/aff_switch.html" with switch_list=switches_list %} + {% endif %} + {% if not users_list and not machines_list and not factures_list and not whitelists_list and not bans_list and not rooms_list and not switch_ports_list and not switches_list %}

Aucun résultat

+ {% else %} +
(Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)
{% endif %} -
(Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)



diff --git a/search/templates/search/search.html b/search/templates/search/search.html index adb5dd92..2c7fd0b6 100644 --- a/search/templates/search/search.html +++ b/search/templates/search/search.html @@ -28,11 +28,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}Recherche{% endblock %} {% block content %} -{% bootstrap_form_errors searchform %} +{% bootstrap_form_errors search_form %} -
- {% csrf_token %} - {% bootstrap_form searchform %} + + {% bootstrap_field search_form.q %} + {% include "buttons/multiple_checkbox_alt.html" with field=search_form.u %} + {% include "buttons/multiple_checkbox_alt.html" with field=search_form.a %} + {% bootstrap_field search_form.s %} + {% bootstrap_field search_form.e %} {% bootstrap_button "Search" button_type="submit" icon="search" %}
diff --git a/search/urls.py b/search/urls.py index 3b16fcd1..dc1490e5 100644 --- a/search/urls.py +++ b/search/urls.py @@ -20,6 +20,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""The urls used by the search app""" + from __future__ import unicode_literals from django.conf.urls import url @@ -28,5 +30,5 @@ from . import views urlpatterns = [ url(r'^$', views.search, name='search'), - url(r'^avance/$', views.searchp, name='searchp'), + url(r'^advanced/$', views.searchp, name='searchp'), ] diff --git a/search/views.py b/search/views.py index fa9a43e8..4c28de63 100644 --- a/search/views.py +++ b/search/views.py @@ -20,115 +20,326 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# App de recherche pour re2o -# Augustin lemesle, Gabriel Détraz, Goulven Kermarec -# Gplv2 +"""The views for the search app, responsible for finding the matches +Augustin lemesle, Gabriel Détraz, Goulven Kermarec, Maël Kervella +Gplv2""" + from __future__ import unicode_literals from django.shortcuts import render -from django.shortcuts import get_object_or_404 -from django.template.context_processors import csrf -from django.template import Context, RequestContext, loader from django.contrib.auth.decorators import login_required from django.db.models import Q from users.models import User, Ban, Whitelist -from machines.models import Machine, Interface -from topologie.models import Port, Switch +from machines.models import Machine +from topologie.models import Port, Switch, Room from cotisations.models import Facture -from search.models import SearchForm, SearchFormPlus from preferences.models import GeneralOption +from search.forms import ( + SearchForm, + SearchFormPlus, + CHOICES_USER, + CHOICES_AFF, + initial_choices +) +from re2o.utils import SortTable -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) -def search_result(search, type, request): - date_deb = None - date_fin = None - states=[] - co=[] - aff=[] - if(type): - aff = search.cleaned_data['affichage'] - co = search.cleaned_data['connexion'] - states = search.cleaned_data['filtre'] - date_deb = search.cleaned_data['date_deb'] - date_fin = search.cleaned_data['date_fin'] - date_query = Q() - if aff==[]: - aff = ['0','1','2','3','4','5','6'] - if date_deb != None: - date_query = date_query & Q(date__gte=date_deb) - if date_fin != None: - date_query = date_query & Q(date__lte=date_fin) - search = search.cleaned_data['search_field'] - query1 = Q() - for s in states: - query1 = query1 | Q(state = s) - - connexion = [] - - recherche = {'users_list': None, 'machines_list' : [], 'facture_list' : None, 'ban_list' : None, 'white_list': None, 'port_list': None, 'switch_list': None} +def is_int(variable): + """ Check if the variable can be casted to an integer """ - if request.user.has_perms(('cableur',)): - query = Q(user__pseudo__icontains = search) | Q(user__name__icontains = search) | Q(user__surname__icontains = search) + try: + int(variable) + except ValueError: + return False else: - query = (Q(user__pseudo__icontains = search) | Q(user__name__icontains = search) | Q(user__surname__icontains = search)) & Q(user = request.user) + return True - for i in aff: - if i == '0': - query_user_list = Q(room__name__icontains = search) | Q(pseudo__icontains = search) | Q(name__icontains = search) | Q(surname__icontains = search) & query1 - if request.user.has_perms(('cableur',)): - recherche['users_list'] = User.objects.filter(query_user_list).order_by('state', 'surname').distinct() - else : - recherche['users_list'] = User.objects.filter(query_user_list & Q(id=request.user.id)).order_by('state', 'surname').distinct() - if i == '1': - query_machine_list = Q(machine__user__pseudo__icontains = search) | Q(machine__user__name__icontains = search) | Q(machine__user__surname__icontains = search) | Q(mac_address__icontains = search) | Q(ipv4__ipv4__icontains = search) | Q(domain__name__icontains = search) | Q(domain__related_domain__name__icontains = search) - if request.user.has_perms(('cableur',)): - data = Interface.objects.filter(query_machine_list).distinct() - else: - data = Interface.objects.filter(query_machine_list & Q(machine__user__id = request.user.id)).distinct() - for d in data: - recherche['machines_list'].append(d.machine) - if i == '2': - recherche['facture_list'] = Facture.objects.filter(query & date_query).distinct() - if i == '3': - recherche['ban_list'] = Ban.objects.filter(query).distinct() - if i == '4': - recherche['white_list'] = Whitelist.objects.filter(query).distinct() - if i == '5': - recherche['port_list'] = Port.objects.filter(details__icontains = search).distinct() - if not request.user.has_perms(('cableur',)): - recherche['port_list'] = None - if i == '6': - recherche['switch_list'] = Switch.objects.filter(details__icontains = search).distinct() - if not request.user.has_perms(('cableur',)): - recherche['switch_list'] = None - options, created = GeneralOption.objects.get_or_create() - search_display_page = options.search_display_page +def get_results(query, request, filters={}): + """ Construct the correct filters to match differents fields of some models + with the given query according to the given filters. + The match field are either CharField or IntegerField that will be displayed + on the results page (else, one might not see why a result has matched the + query). IntegerField are matched against the query only if it can be casted + to an int.""" - for r in recherche: - if recherche[r] != None: - recherche[r] = recherche[r][:search_display_page] + start = filters.get('s', None) + end = filters.get('e', None) + user_state = filters.get('u', initial_choices(CHOICES_USER)) + aff = filters.get('a', initial_choices(CHOICES_AFF)) - recherche.update({'max_result': search_display_page}) + options, _ = GeneralOption.objects.get_or_create() + max_result = options.search_display_page + + results = { + 'users_list': User.objects.none(), + 'machines_list': Machine.objects.none(), + 'factures_list': Facture.objects.none(), + 'bans_list': Ban.objects.none(), + 'whitelists_list': Whitelist.objects.none(), + 'rooms_list': Room.objects.none(), + 'switch_ports_list': Port.objects.none(), + 'switches_list': Switch.objects.none() + } + + # Users + if '0' in aff: + filter_user_list = ( + Q( + surname__icontains=query + ) | Q( + adherent__name__icontains=query + ) | Q( + pseudo__icontains=query + ) | Q( + club__room__name__icontains=query + ) | Q( + adherent__room__name__icontains=query + ) + ) & Q(state__in=user_state) + if not request.user.has_perms(('cableur',)): + filter_user_list &= Q(id=request.user.id) + results['users_list'] = User.objects.filter(filter_user_list) + results['users_list'] = SortTable.sort( + results['users_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX + ) + + # Machines + if '1' in aff: + filter_machine_list = Q( + name__icontains=query + ) | ( + Q( + user__pseudo__icontains=query + ) & Q( + user__state__in=user_state + ) + ) | Q( + interface__domain__name__icontains=query + ) | Q( + interface__domain__related_domain__name__icontains=query + ) | Q( + interface__mac_address__icontains=query + ) | Q( + interface__ipv4__ipv4__icontains=query + ) + if not request.user.has_perms(('cableur',)): + filter_machine_list &= Q(user__id=request.user.id) + results['machines_list'] = Machine.objects.filter(filter_machine_list) + results['machines_list'] = SortTable.sort( + results['machines_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.MACHINES_INDEX + ) + + # Factures + if '2' in aff: + filter_facture_list = Q( + user__pseudo__icontains=query + ) & Q( + user__state__in=user_state + ) + if start is not None: + filter_facture_list &= Q(date__gte=start) + if end is not None: + filter_facture_list &= Q(date__lte=end) + results['factures_list'] = Facture.objects.filter(filter_facture_list) + results['factures_list'] = SortTable.sort( + results['factures_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_INDEX + ) + + # Bans + if '3' in aff: + date_filter = ( + Q( + user__pseudo__icontains=query + ) & Q( + user__state__in=user_state + ) + ) | Q( + raison__icontains=query + ) + if start is not None: + date_filter &= ( + Q(date_start__gte=start) & Q(date_end__gte=start) + ) | ( + Q(date_start__lte=start) & Q(date_end__gte=start) + ) | ( + Q(date_start__gte=start) & Q(date_end__lte=start) + ) + if end is not None: + date_filter &= ( + Q(date_start__lte=end) & Q(date_end__lte=end) + ) | ( + Q(date_start__lte=end) & Q(date_end__gte=end) + ) | ( + Q(date_start__gte=end) & Q(date_end__lte=end) + ) + results['bans_list'] = Ban.objects.filter(date_filter) + results['bans_list'] = SortTable.sort( + results['bans_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX_BAN + ) + + # Whitelists + if '4' in aff: + date_filter = ( + Q( + user__pseudo__icontains=query + ) & Q( + user__state__in=user_state + ) + ) | Q( + raison__icontains=query + ) + if start is not None: + date_filter &= ( + Q(date_start__gte=start) & Q(date_end__gte=start) + ) | ( + Q(date_start__lte=start) & Q(date_end__gte=start) + ) | ( + Q(date_start__gte=start) & Q(date_end__lte=start) + ) + if end is not None: + date_filter &= ( + Q(date_start__lte=end) & Q(date_end__lte=end) + ) | ( + Q(date_start__lte=end) & Q(date_end__gte=end) + ) | ( + Q(date_start__gte=end) & Q(date_end__lte=end) + ) + results['whitelists_list'] = Whitelist.objects.filter(date_filter) + results['whitelists_list'] = SortTable.sort( + results['whitelists_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX_WHITE + ) + + # Rooms + if '5' in aff and request.user.has_perms(('cableur',)): + filter_rooms_list = Q( + details__icontains=query + ) | Q( + name__icontains=query + ) | Q( + port__details=query + ) + results['rooms_list'] = Room.objects.filter(filter_rooms_list) + results['rooms_list'] = SortTable.sort( + results['rooms_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_ROOM + ) + + # Switch ports + if '6' in aff and request.user.has_perms(('cableur',)): + filter_ports_list = Q( + room__name__icontains=query + ) | Q( + machine_interface__domain__name__icontains=query + ) | Q( + related__switch__switch_interface__domain__name__icontains=query + ) | Q( + radius__icontains=query + ) | Q( + vlan_force__name__icontains=query + ) | Q( + details__icontains=query + ) + if is_int(query): + filter_ports_list |= Q( + port=query + ) + results['switch_ports_list'] = Port.objects.filter(filter_ports_list) + results['switch_ports_list'] = SortTable.sort( + results['switch_ports_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_PORT + ) + + # Switches + if '7' in aff and request.user.has_perms(('cableur',)): + filter_switches_list = Q( + switch_interface__domain__name__icontains=query + ) | Q( + switch_interface__ipv4__ipv4__icontains=query + ) | Q( + location__icontains=query + ) | Q( + stack__name__icontains=query + ) | Q( + model__reference__icontains=query + ) | Q( + model__constructor__name__icontains=query + ) | Q( + details__icontains=query + ) + if is_int(query): + filter_switches_list |= Q( + number=query + ) | Q( + stack_member_id=query + ) + results['switches_list'] = Switch.objects.filter(filter_switches_list) + results['switches_list'] = SortTable.sort( + results['switches_list'], + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX + ) + + for name, val in results.items(): + results[name] = val.distinct()[:max_result] + + results.update({'max_result': max_result}) + results.update({'search_term': query}) + + return results - return recherche @login_required def search(request): - search = SearchForm(request.POST or None) - if search.is_valid(): - return form(search_result(search, False, request), 'search/index.html',request) - return form({'searchform' : search}, 'search/search.html', request) + """ La page de recherche standard """ + search_form = SearchForm(request.GET or None) + if search_form.is_valid(): + return render( + request, + 'search/index.html', + get_results( + search_form.cleaned_data.get('q', ''), + request, + search_form.cleaned_data + ) + ) + return render(request, 'search/search.html', {'search_form': search_form}) + @login_required def searchp(request): - search = SearchFormPlus(request.POST or None) - if search.is_valid(): - return form(search_result(search, True, request), 'search/index.html',request) - return form({'searchform' : search}, 'search/search.html', request) + """ La page de recherche avancée """ + search_form = SearchFormPlus(request.GET or None) + if search_form.is_valid(): + return render( + request, + 'search/index.html', + get_results( + search_form.cleaned_data.get('q', ''), + request, + search_form.cleaned_data + ) + ) + return render(request, 'search/search.html', {'search_form': search_form}) diff --git a/static/css/bootstrap-tokenfield.css b/static/css/bootstrap-tokenfield.css new file mode 100644 index 00000000..ae12c1b7 --- /dev/null +++ b/static/css/bootstrap-tokenfield.css @@ -0,0 +1,210 @@ +/*! + * bootstrap-tokenfield + * https://github.com/sliptree/bootstrap-tokenfield + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT + */ +@-webkit-keyframes blink { + 0% { + border-color: #ededed; + } + 100% { + border-color: #b94a48; + } +} +@-moz-keyframes blink { + 0% { + border-color: #ededed; + } + 100% { + border-color: #b94a48; + } +} +@keyframes blink { + 0% { + border-color: #ededed; + } + 100% { + border-color: #b94a48; + } +} +.tokenfield { + height: auto; + min-height: 34px; + padding-bottom: 0px; +} +.tokenfield.focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); +} +.tokenfield .token { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + display: inline-block; + border: 1px solid #d9d9d9; + background-color: #ededed; + white-space: nowrap; + margin: -1px 5px 5px 0; + height: 22px; + vertical-align: top; + cursor: default; +} +.tokenfield .token:hover { + border-color: #b9b9b9; +} +.tokenfield .token.active { + border-color: #52a8ec; + border-color: rgba(82, 168, 236, 0.8); +} +.tokenfield .token.duplicate { + border-color: #ebccd1; + -webkit-animation-name: blink; + animation-name: blink; + -webkit-animation-duration: 0.1s; + animation-duration: 0.1s; + -webkit-animation-direction: normal; + animation-direction: normal; + -webkit-animation-timing-function: ease; + animation-timing-function: ease; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} +.tokenfield .token.invalid { + background: none; + border: 1px solid transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + border-bottom: 1px dotted #d9534f; +} +.tokenfield .token.invalid.active { + background: #ededed; + border: 1px solid #ededed; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.tokenfield .token .token-label { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 4px; + vertical-align: top; +} +.tokenfield .token .close { + font-family: Arial; + display: inline-block; + line-height: 100%; + font-size: 1.1em; + line-height: 1.49em; + margin-left: 5px; + float: none; + height: 100%; + vertical-align: top; + padding-right: 4px; +} +.tokenfield .token-input { + background: none; + width: 60px; + min-width: 60px; + border: 0; + height: 20px; + padding: 0; + margin-bottom: 6px; + -webkit-box-shadow: none; + box-shadow: none; +} +.tokenfield .token-input:focus { + border-color: transparent; + outline: 0; + /* IE6-9 */ + -webkit-box-shadow: none; + box-shadow: none; +} +.tokenfield.disabled { + cursor: not-allowed; + background-color: #eeeeee; +} +.tokenfield.disabled .token-input { + cursor: not-allowed; +} +.tokenfield.disabled .token:hover { + cursor: not-allowed; + border-color: #d9d9d9; +} +.tokenfield.disabled .token:hover .close { + cursor: not-allowed; + opacity: 0.2; + filter: alpha(opacity=20); +} +.has-warning .tokenfield.focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; +} +.has-error .tokenfield.focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} +.has-success .tokenfield.focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; +} +.tokenfield.input-sm, +.input-group-sm .tokenfield { + min-height: 30px; + padding-bottom: 0px; +} +.input-group-sm .token, +.tokenfield.input-sm .token { + height: 20px; + margin-bottom: 4px; +} +.input-group-sm .token-input, +.tokenfield.input-sm .token-input { + height: 18px; + margin-bottom: 5px; +} +.tokenfield.input-lg, +.input-group-lg .tokenfield { + height: auto; + min-height: 45px; + padding-bottom: 4px; +} +.input-group-lg .token, +.tokenfield.input-lg .token { + height: 25px; +} +.input-group-lg .token-label, +.tokenfield.input-lg .token-label { + line-height: 23px; +} +.input-group-lg .token .close, +.tokenfield.input-lg .token .close { + line-height: 1.3em; +} +.input-group-lg .token-input, +.tokenfield.input-lg .token-input { + height: 23px; + line-height: 23px; + margin-bottom: 6px; + vertical-align: top; +} +.tokenfield.rtl { + direction: rtl; + text-align: right; +} +.tokenfield.rtl .token { + margin: -1px 0 5px 5px; +} +.tokenfield.rtl .token .token-label { + padding-left: 0px; + padding-right: 4px; +} diff --git a/static/js/bootstrap-tokenfield/LICENSE.md b/static/js/bootstrap-tokenfield/LICENSE.md new file mode 100644 index 00000000..2449b356 --- /dev/null +++ b/static/js/bootstrap-tokenfield/LICENSE.md @@ -0,0 +1,23 @@ +#### Sliptree +- by Illimar Tambek for [Sliptree](http://sliptree.com) +- Copyright (c) 2013 by Sliptree + +Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License) + +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. \ No newline at end of file diff --git a/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js b/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js new file mode 100644 index 00000000..5b2759d4 --- /dev/null +++ b/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js @@ -0,0 +1,1042 @@ +/*! + * bootstrap-tokenfield + * https://github.com/sliptree/bootstrap-tokenfield + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // For CommonJS and CommonJS-like environments where a window with jQuery + // is present, execute the factory with the jQuery instance from the window object + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a Tokenfield-making factory as module.exports + // This accentuates the need for the creation of a real window or passing in a jQuery instance + // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($); + module.exports = global.window && global.window.$ ? + factory( global.window.$ ) : + function( input ) { + if ( !input.$ && !input.fn ) { + throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" ); + } + return factory( input.$ || input ); + }; + } else { + // Browser globals + factory(jQuery, window); + } +}(function ($, window) { + + "use strict"; // jshint ;_; + + /* TOKENFIELD PUBLIC CLASS DEFINITION + * ============================== */ + + var Tokenfield = function (element, options) { + var _self = this + + this.$element = $(element) + this.textDirection = this.$element.css('direction'); + + // Extend options + this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options) + + // Setup delimiters and trigger keys + this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter + this._triggerKeys = $.map(this._delimiters, function (delimiter) { + return delimiter.charCodeAt(0); + }); + this._firstDelimiter = this._delimiters[0]; + + // Check for whitespace, dash and special characters + var whitespace = $.inArray(' ', this._delimiters) + , dash = $.inArray('-', this._delimiters) + + if (whitespace >= 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..24d9c624 100644 --- a/templates/base.html +++ b/templates/base.html @@ -33,16 +33,23 @@ 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 %} + {% include "cookie_banner.html" %}
{% if user.has_access == True %} - + {% else %} {% endif %} diff --git a/users/templates/users/sidebar.html b/users/templates/users/sidebar.html index ef94c0a9..9a4312ff 100644 --- a/users/templates/users/sidebar.html +++ b/users/templates/users/sidebar.html @@ -30,11 +30,19 @@ with this program; if not, write to the Free Software Foundation, Inc., Créer un adhérent - - - Adhérents + + + Créer un club/association - + + + Adherents + + + + Clubs + + Bannissements diff --git a/users/templates/users/user.html b/users/templates/users/user.html index bbdc7fc5..ad6ddb64 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,school' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/users/urls.py b/users/urls.py index 43054fe5..e351dcca 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 @@ -28,43 +31,94 @@ from . import views urlpatterns = [ url(r'^new_user/$', views.new_user, name='new-user'), + url(r'^new_club/$', views.new_club, name='new-club'), url(r'^edit_info/(?P[0-9]+)$', views.edit_info, name='edit-info'), 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'^index_clubs/$', views.index_clubs, name='index-clubs'), url(r'^rest/mailing/$', views.mailing, name='mailing'), - ] - - diff --git a/users/views.py b/users/views.py index 66a5f7ad..ad6ebbba 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, Adherent, Club +from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm +from users.forms import DelListRightForm, NewListRightForm, FullAdherentForm +from users.forms import StateForm, FullClubForm +from users.forms import RightForm, SchoolForm, EditServiceUserForm +from users.forms import ServiceUserForm, ListRightForm, AdherentForm, ClubForm 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, SortTable def password_change_action(u_form, user, request, req=False): """ Fonction qui effectue le changeemnt de mdp bdd""" @@ -75,11 +79,13 @@ 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""" - user = InfoForm(request.POST or None) + """ Vue de création d'un nouvel utilisateur, + envoie un mail pour le mot de passe""" + user = AdherentForm(request.POST or None) if user.is_valid(): user = user.save(commit=False) with transaction.atomic(), reversion.create_revision(): @@ -87,39 +93,81 @@ 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 +@permission_required('cableur') +def new_club(request): + """ Vue de création d'un nouveau club, + envoie un mail pour le mot de passe""" + club = ClubForm(request.POST or None) + if club.is_valid(): + club = club.save(commit=False) + with transaction.atomic(), reversion.create_revision(): + club.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + club.reset_passwd_mail(request) + messages.success(request, "L'utilisateur %s a été crée, un mail\ + pour l'initialisation du mot de passe a été envoyé" % club.pseudo) + return redirect("/users/profil/" + str(club.id)) + return form({'userform': club}, 'users/user.html', request) + + +def select_user_edit_form(request, user): + """Fonction de choix du bon formulaire, en fonction de: + - droit + - type d'object + """ + if not request.user.has_perms(('cableur',)): + if user.is_class_adherent: + user = AdherentForm(request.POST or None, instance=user.adherent) + elif user.is_class_club: + user = ClubForm(request.POST or None, instance=user.club) + else: + if user.is_class_adherent: + user = FullAdherentForm(request.POST or None, instance=user.adherent) + elif user.is_class_club: + user = FullClubForm(request.POST or None, instance=user.club) + return user + + @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) - else: - user = InfoForm(request.POST or None, instance=user) + user = select_user_edit_form(request, user) if user.is_valid(): 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 +183,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 +203,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 +229,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 +256,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 +279,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 +310,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 +347,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 +359,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 +376,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 +415,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 +459,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 +507,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 +523,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 +572,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 +581,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,17 +592,30 @@ 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() + """ Affiche l'ensemble des adherents, need droit cableur """ + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - users_list = User.objects.select_related('room').order_by('state', 'name') + users_list = Adherent.objects.select_related('room') + users_list = SortTable.sort( + users_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX + ) paginator = Paginator(users_list, pagination_number) page = request.GET.get('page') try: @@ -500,13 +628,46 @@ 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_clubs(request): + """ Affiche l'ensemble des clubs, need droit cableur """ + options, _created = GeneralOption.objects.get_or_create() + pagination_number = options.pagination_number + clubs_list = Club.objects.select_related('room') + clubs_list = SortTable.sort( + clubs_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX + ) + paginator = Paginator(clubs_list, pagination_number) + page = request.GET.get('page') + try: + clubs_list = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + clubs_list = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + clubs_list = paginator.page(paginator.num_pages) + return render(request, 'users/index_clubs.html', {'clubs_list': clubs_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.select_related('user') + ban_list = SortTable.sort( + ban_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX_BAN + ) paginator = Paginator(ban_list, pagination_number) page = request.GET.get('page') try: @@ -515,17 +676,24 @@ 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') + white_list = SortTable.sort( + white_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX_BAN + ) paginator = Paginator(white_list, pagination_number) page = request.GET.get('page') try: @@ -534,92 +702,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 +822,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 +834,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 +844,43 @@ 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') + machines = SortTable.sort( + machines, + request.GET.get('col'), + request.GET.get('order'), + SortTable.MACHINES_INDEX + ) + factures = Facture.objects.filter(user=users) + factures = SortTable.sort( + factures, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_INDEX + ) + bans = Ban.objects.filter(user=users) + bans = SortTable.sort( + bans, + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX_BAN + ) + whitelists = Whitelist.objects.filter(user=users) + whitelists = SortTable.sort( + whitelists, + request.GET.get('col'), + request.GET.get('order'), + SortTable.USERS_INDEX_WHITE + ) 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 +896,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 +955,3 @@ def mailing(request): mails = all_has_access().values('email').distinct() seria = MailSerializer(mails, many=True) return JSONResponse(seria.data) -
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 %}
Connexion {% if request_user.has_access %} - Active + jusqu'au {{ request.user.end_access|date:"d b Y" }} {% else %} Désactivée {% endif %} @@ -149,8 +155,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Adhésion - {% if request_user.end_adhesion != None %} - {{ request_user.end_adhesion }} + {% if request_user.is_adherent %} + jusqu'au {{ request_user.end_adhesion|date:"d b Y" }} {% else %} Non adhérent {% endif %} diff --git a/templates/buttons/multiple_checkbox_alt.html b/templates/buttons/multiple_checkbox_alt.html new file mode 100644 index 00000000..6346632c --- /dev/null +++ b/templates/buttons/multiple_checkbox_alt.html @@ -0,0 +1,40 @@ +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +
+ +
+ {% for val in field.field.choices %} + + {% endfor %} +
+ {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
{{ field.help_text }}
+
diff --git a/templates/buttons/sort.html b/templates/buttons/sort.html new file mode 100644 index 00000000..e90fbd15 --- /dev/null +++ b/templates/buttons/sort.html @@ -0,0 +1,50 @@ +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +{% load url_insert_param %} + +{% spaceless %} +
+ {{ text }}  +
+ {% if prefix %} + {% with prefix|add:'_'|add:col as colname %} + + + + + + + {% endwith %} + {% else %} + + + + + + + {% endif %} +
+
+{% endspaceless %} diff --git a/templates/cookie_banner.html b/templates/cookie_banner.html new file mode 100644 index 00000000..cd524010 --- /dev/null +++ b/templates/cookie_banner.html @@ -0,0 +1,19 @@ +{% if not 'accept_cookies' in request.COOKIES%} + + +{% endif %} diff --git a/templates/pagination.html b/templates/pagination.html index ba90fe13..7ebd26c1 100644 --- a/templates/pagination.html +++ b/templates/pagination.html @@ -22,20 +22,22 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} +{% load url_insert_param %} + diff --git a/topologie/admin.py b/topologie/admin.py index 8dcce849..a4591222 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -20,27 +20,51 @@ # 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 from django.contrib import admin from reversion.admin import VersionAdmin -from .models import Port, Room, Switch, Stack +from .models import Port, Room, Switch, Stack, ModelSwitch, ConstructorSwitch + 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 + +class ModelSwitchAdmin(VersionAdmin): + """Administration d'un modèle de switch""" + pass + + +class ConstructorSwitchAdmin(VersionAdmin): + """Administration d'un constructeur d'un switch""" + pass + + admin.site.register(Port, PortAdmin) admin.site.register(Room, RoomAdmin) admin.site.register(Switch, SwitchAdmin) admin.site.register(Stack, StackAdmin) +admin.site.register(ModelSwitch, ModelSwitchAdmin) +admin.site.register(ConstructorSwitch, ConstructorSwitchAdmin) diff --git a/topologie/forms.py b/topologie/forms.py index 87a3917d..562b87e2 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -19,52 +19,150 @@ # 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 import forms +from django.forms import ModelForm +from .models import Port, Switch, Room, Stack, ModelSwitch, ConstructorSwitch + 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) + 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 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['switch_interface'].queryset = Interface.objects.all()\ + .select_related('domain__extension') 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) + self.fields['location'].label = 'Localisation' + self.fields['number'].label = 'Nombre de ports' + 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) + + +class CreatePortsForm(forms.Form): + """Permet de créer une liste de ports pour un switch.""" + begin = forms.IntegerField(label="Début :", min_value=0) + end = forms.IntegerField(label="Fin :", min_value=0) + + +class EditModelSwitchForm(ModelForm): + """Permet d'éediter un modèle de switch : nom et constructeur""" + class Meta: + model = ModelSwitch + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditModelSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class EditConstructorSwitchForm(ModelForm): + """Permet d'éediter le nom d'un constructeur""" + class Meta: + model = ConstructorSwitch + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditConstructorSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) diff --git a/topologie/migrations/0031_auto_20171015_2033.py b/topologie/migrations/0031_auto_20171015_2033.py new file mode 100644 index 00000000..674af6c6 --- /dev/null +++ b/topologie/migrations/0031_auto_20171015_2033.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0030_auto_20171004_0235'), + ] + + operations = [ + migrations.AlterField( + model_name='port', + name='port', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='stack', + name='member_id_max', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='stack', + name='member_id_min', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='switch', + name='number', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='switch', + name='stack_member_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/topologie/migrations/0032_auto_20171026_0338.py b/topologie/migrations/0032_auto_20171026_0338.py new file mode 100644 index 00000000..37548306 --- /dev/null +++ b/topologie/migrations/0032_auto_20171026_0338.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-26 01:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0031_auto_20171015_2033'), + ] + + operations = [ + migrations.CreateModel( + name='ConstructorSwitch', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='ModelSwitch', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reference', models.CharField(max_length=255)), + ('constructor', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='topologie.ConstructorSwitch')), + ], + ), + migrations.AddField( + model_name='switch', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='topologie.ModelSwitch'), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index c02c0c51..0f5e0449 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -20,32 +20,40 @@ # 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" name = models.CharField(max_length=32, blank=True, null=True) stack_id = models.CharField(max_length=32, unique=True) details = models.CharField(max_length=255, blank=True, null=True) - member_id_min = models.IntegerField() - member_id_max = models.IntegerField() + member_id_min = models.PositiveIntegerField() + member_id_max = models.PositiveIntegerField() def __str__(self): return " ".join([self.name, self.stack_id]) @@ -59,28 +67,47 @@ 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() + number = models.PositiveIntegerField() details = models.CharField(max_length=255, blank=True) - stack = models.ForeignKey(Stack, blank=True, null=True, on_delete=models.SET_NULL) - stack_member_id = models.IntegerField(blank=True, null=True) + stack = models.ForeignKey( + 'topologie.Stack', + blank=True, + null=True, + on_delete=models.SET_NULL + ) + stack_member_id = models.PositiveIntegerField(blank=True, null=True) + model = models.ForeignKey( + 'topologie.ModelSwitch', + blank=True, + null=True, + on_delete=models.SET_NULL + ) 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 +116,94 @@ 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 ModelSwitch(models.Model): + """Un modèle (au sens constructeur) de switch""" + PRETTY_NAME = "Modèle de switch" + reference = models.CharField(max_length=255) + constructor = models.ForeignKey( + 'topologie.ConstructorSwitch', + on_delete=models.PROTECT + ) + + def __str__(self): + return str(self.constructor) + ' ' + str(self.reference) + + +class ConstructorSwitch(models.Model): + """Un constructeur de switch""" + PRETTY_NAME = "Constructeur de switch" + name = models.CharField(max_length=255) + + def __str__(self): + return str(self.name) + 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'), - ) - - 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') + ('NO', 'NO'), + ('STRICT', 'STRICT'), + ('BLOQ', 'BLOQ'), + ('COMMON', 'COMMON'), + ) + + switch = models.ForeignKey( + 'Switch', + related_name="ports", + on_delete=models.CASCADE + ) + port = models.PositiveIntegerField() + 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 +214,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 +222,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 +252,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 +262,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_chambres.html b/topologie/templates/topologie/aff_chambres.html index d3393fc2..252ff808 100644 --- a/topologie/templates/topologie/aff_chambres.html +++ b/topologie/templates/topologie/aff_chambres.html @@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., - + diff --git a/topologie/templates/topologie/aff_constructor_switch.html b/topologie/templates/topologie/aff_constructor_switch.html new file mode 100644 index 00000000..02002f6c --- /dev/null +++ b/topologie/templates/topologie/aff_constructor_switch.html @@ -0,0 +1,54 @@ +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +{% if constructor_switch_list.paginator %} +{% include "pagination.html" with list=constructor_switch_list %} +{% endif %} + +
Chambre{% include "buttons/sort.html" with prefix='room' col='name' text='Chambre' %} Commentaire
+ + + + + + + {% for constructor_switch in constructor_switch_list %} + + + + + {% endfor %} +
{% include "buttons/sort.html" with prefix='constructor-switch' col='name' text='Constructeur' %}
{{constructor_switch}} + + + + {% if is_infra %} + + + + + + + {% endif %} +
diff --git a/topologie/templates/topologie/aff_model_switch.html b/topologie/templates/topologie/aff_model_switch.html new file mode 100644 index 00000000..2e84fb69 --- /dev/null +++ b/topologie/templates/topologie/aff_model_switch.html @@ -0,0 +1,56 @@ +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +{% if model_switch_list.paginator %} +{% include "pagination.html" with list=model_switch_list %} +{% endif %} + + + + + + + + + + {% for model_switch in model_switch_list %} + + + + + + {% endfor %} +
{% include "buttons/sort.html" with prefix='model-switch' col='reference' text='Référence' %}{% include "buttons/sort.html" with prefix='model-switch' col='constructor' text='Constructeur' %}
{{model_switch.reference}}{{model_switch.constructor}} + + + + {% if is_infra %} + + + + + + + {% endif %} +
diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html index 609d4349..6d1ca08e 100644 --- a/topologie/templates/topologie/aff_port.html +++ b/topologie/templates/topologie/aff_port.html @@ -25,12 +25,12 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - + + + + + + diff --git a/topologie/templates/topologie/aff_stacks.html b/topologie/templates/topologie/aff_stacks.html index 34e7b959..1a9d316e 100644 --- a/topologie/templates/topologie/aff_stacks.html +++ b/topologie/templates/topologie/aff_stacks.html @@ -25,37 +25,58 @@ with this program; if not, write to the Free Software Foundation, Inc.,
PortRoomInterface machineRelatedRadiusVlan forcé{% include "buttons/sort.html" with prefix='port' col='port' text='Port' %}{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %}{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %}{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %}{% include "buttons/sort.html" with prefix='port' col='radius' text='Radius' %}{% include "buttons/sort.html" with prefix='port' col='vlan' text='Vlan forcé' %} Détails
- - - + + + - - {% 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 %}
StackIDDetails{% include "buttons/sort.html" with prefix='stack' col='name' text='Stack' %}{% include "buttons/sort.html" with prefix='stack' col='id' text='ID' %}Détails 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/aff_switch.html b/topologie/templates/topologie/aff_switch.html index 7096909b..25f466e8 100644 --- a/topologie/templates/topologie/aff_switch.html +++ b/topologie/templates/topologie/aff_switch.html @@ -22,15 +22,20 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} +{% if switch_list.paginator %} +{% include "pagination.html" with list=switch_list %} +{% endif %} + - - - - - - + + + + + + + @@ -47,11 +52,15 @@ with this program; if not, write to the Free Software Foundation, Inc., + {% endfor %} diff --git a/topologie/templates/topologie/index.html b/topologie/templates/topologie/index.html index 72b522d0..6b17b6de 100644 --- a/topologie/templates/topologie/index.html +++ b/topologie/templates/topologie/index.html @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Switchs

{% if is_infra %} Ajouter un switch +
{% endif %} {% include "topologie/aff_switch.html" with switch_list=switch_list %}
diff --git a/topologie/templates/topologie/index_model_switch.html b/topologie/templates/topologie/index_model_switch.html new file mode 100644 index 00000000..784b5ea6 --- /dev/null +++ b/topologie/templates/topologie/index_model_switch.html @@ -0,0 +1,46 @@ +{% extends "topologie/sidebar.html" %} +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +{% load bootstrap3 %} + +{% block title %}Modèles de switches{% endblock %} + +{% block content %} +

Modèles de switches

+{% if is_infra %} + Ajouter un modèle +
+{% endif %} +{% include "topologie/aff_model_switch.html" with model_switch_list=model_switch_list %} +

Constructeurs de switches

+{% if is_infra %} + Ajouter un constructeur +
+{% endif %} +{% include "topologie/aff_constructor_switch.html" with constructor_switch_list=constructor_switch_list %} +
+
+
+{% endblock %} diff --git a/topologie/templates/topologie/index_p.html b/topologie/templates/topologie/index_p.html index 84159659..3f9356f2 100644 --- a/topologie/templates/topologie/index_p.html +++ b/topologie/templates/topologie/index_p.html @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if is_infra %} Editer Ajouter un port + Ajouter des ports {% endif %} {% include "topologie/aff_port.html" with port_list=port_list %}
diff --git a/topologie/templates/topologie/sidebar.html b/topologie/templates/topologie/sidebar.html index 833af2e3..a2d42896 100644 --- a/topologie/templates/topologie/sidebar.html +++ b/topologie/templates/topologie/sidebar.html @@ -37,4 +37,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Stacks + + + Modèles switches et constructeurs + {% endblock %} diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index 51cec7f6..1753161e 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,15 +47,23 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} {% if topoform %} - {% bootstrap_form topoform %} +

Réglage spécifiques du switch

+ {% massive_bootstrap_form topoform 'switch_interface' %} {% endif %} {% if machineform %} - {% bootstrap_form machineform %} +

Réglages généraux de la machine associée au switch

+ {% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %} - {% bootstrap_form interfaceform %} +

Réglages généraux de l'interface associée au switch

+ {% if i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} + {% else %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} + {% endif %} {% endif %} {% if domainform %} +

Nom de la machine

{% bootstrap_form domainform %} {% endif %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %} 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..752ebcc5 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 @@ -29,22 +35,74 @@ from . import views urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^new_switch/$', views.new_switch, name='new-switch'), + url(r'^create_ports/(?P[0-9]+)$', + views.create_ports, + name='create-ports'), url(r'^index_room/$', views.index_room, name='index-room'), 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'^history/(?Pmodel_switch)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pconstructor_switch)/(?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'), + url(r'^index_model_switch/$', + views.index_model_switch, + name='index-model-switch' + ), + url(r'^index_model_switch/$', + views.index_model_switch, + name='index-model-switch' + ), + url(r'^new_model_switch/$', + views.new_model_switch, + name='new-model-switch' + ), + url(r'^edit_model_switch/(?P[0-9]+)$', + views.edit_model_switch, + name='edit-model-switch'), + url(r'^del_model_switch/(?P[0-9]+)$', + views.del_model_switch, + name='del-model-switch'), + url(r'^new_constructor_switch/$', + views.new_constructor_switch, + name='new-constructor-switch' + ), + url(r'^edit_constructor_switch/(?P[0-9]+)$', + views.edit_constructor_switch, + name='edit-constructor-switch'), + url(r'^del_constructor_switch/(?P[0-9]+)$', + views.del_constructor_switch, + name='del-constructor-switch'), ] - diff --git a/topologie/views.py b/topologie/views.py index eba00e0c..7bed18aa 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -19,9 +19,24 @@ # 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 +import itertools + from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required @@ -32,12 +47,33 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 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.models import ( + Switch, + Port, + Room, + Stack, + ModelSwitch, + ConstructorSwitch +) +from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm +from topologie.forms import ( + AddPortForm, + EditRoomForm, + StackForm, + EditModelSwitchForm, + EditConstructorSwitchForm, + CreatePortsForm +) from users.views import form -from users.models import User - -from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from re2o.utils import SortTable +from machines.forms import ( + DomainForm, + NewMachineForm, + EditMachineForm, + EditInterfaceForm, + AddInterfaceForm +) +from machines.views import generate_ipv4_mbf_param from preferences.models import AssoOption, GeneralOption @@ -45,41 +81,78 @@ 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') - return render(request, 'topologie/index.html', {'switch_list': switch_list}) + switch_list = Switch.objects\ + .select_related('switch_interface__domain__extension')\ + .select_related('switch_interface__ipv4')\ + .select_related('switch_interface__domain')\ + .select_related('stack') + switch_list = SortTable.sort( + switch_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX + ) + options, _created = GeneralOption.objects.get_or_create() + pagination_number = options.pagination_number + paginator = Paginator(switch_list, pagination_number) + page = request.GET.get('page') + try: + switch_list = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + switch_list = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + switch_list = paginator.page(paginator.num_pages) + 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/") + elif object_name == 'model_switch': + try: + object_instance = ModelSwitch.objects.get(pk=object_id) + except ModelSwitch.DoesNotExist: + messages.error(request, "SwitchModel inexistant") + return redirect("/topologie/") + elif object_name == 'constructor_switch': + try: + object_instance = ConstructorSwitch.objects.get(pk=object_id) + except ConstructorSwitch.DoesNotExist: + messages.error(request, "SwitchConstructor inexistant") + 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 +165,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 +180,39 @@ 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('machine_interface__machine__user')\ + .select_related( + 'related__switch__switch_interface__domain__extension' + )\ + .select_related('switch') + port_list = SortTable.sort( + port_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_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() + room_list = Room.objects + room_list = SortTable.sort( + room_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_ROOM + ) + 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 +224,50 @@ 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') - 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\ + .prefetch_related('switch_set__switch_interface__domain__extension') + stack_list = SortTable.sort( + stack_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_STACK + ) + return render(request, 'topologie/index_stack.html', { + 'stack_list': stack_list + }) + + +@login_required +@permission_required('cableur') +def index_model_switch(request): + """ Affichage de l'ensemble des modèles de switches""" + model_switch_list = ModelSwitch.objects + constructor_switch_list = ConstructorSwitch.objects + model_switch_list = SortTable.sort( + model_switch_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_MODEL_SWITCH + ) + constructor_switch_list = SortTable.sort( + constructor_switch_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH + ) + return render(request, 'topologie/index_model_switch.html', { + 'model_switch_list': model_switch_list, + 'constructor_switch_list': constructor_switch_list, + }) @login_required @@ -152,16 +290,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 +316,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) @@ -190,32 +339,32 @@ def del_port(request,port_id): port.delete() reversion.set_user(request.user) reversion.set_comment("Destruction") - messages.success(request, "Le port a eté détruit") + messages.success(request, "Le port a été détruit") except ProtectedError: - messages.error(request, "Le port %s est affecté à un autre objet, impossible de le supprimer" % port) + 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 +375,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 +401,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 +422,35 @@ 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 = DomainForm( + request.POST or None, + ) 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 +460,144 @@ 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 create_ports(request, switch_id): + """ Création d'une liste de ports pour un switch.""" + try: + switch = Switch.objects.get(pk=switch_id) + except Switch.DoesNotExist: + messages.error(request, u"Switch inexistant") + return redirect("/topologie/") + + s_begin = s_end = 0 + nb_ports = switch.ports.count() + if nb_ports > 0: + ports = switch.ports.order_by('port').values('port') + s_begin = ports.first().get('port') + s_end = ports.last().get('port') + + port_form = CreatePortsForm( + request.POST or None, + initial={'begin': s_begin, 'end': s_end} + ) + if port_form.is_valid(): + begin = port_form.cleaned_data['begin'] + end = port_form.cleaned_data['end'] + if end < begin: + messages.error(request, "Port de fin inférieur au port de début !") + return redirect("/topologie/switch/" + str(switch.id)) + if end - begin > switch.number: + messages.error(request, "Ce switch ne peut avoir autant de ports.") + return redirect("/topologie/switch/" + str(switch.id)) + + begin_range = range(begin, s_begin) + end_range = range(s_end+1, end+1) + for i in itertools.chain(begin_range, end_range): + port = Port() + port.switch = switch + port.port = i + try: + with transaction.atomic(), reversion.create_revision(): + port.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Création du port %d" % i) + except IntegrityError: + messages.error(request, "Création d'un port existant.") + return redirect("/topologie/switch/" + str(switch.id)) + + return form({'topoform': port_form}, '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 = DomainForm( + request.POST or None, + 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 +611,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 +628,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 +643,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 +653,136 @@ 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) + + +@login_required +@permission_required('infra') +def new_model_switch(request): + """Nouveau modèle de switch""" + model_switch = EditModelSwitchForm(request.POST or None) + if model_switch.is_valid(): + with transaction.atomic(), reversion.create_revision(): + model_switch.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Le modèle a été créé") + return redirect("/topologie/index_model_switch/") + return form({'topoform': model_switch}, 'topologie/topo.html', request) + + +@login_required +@permission_required('infra') +def edit_model_switch(request, model_switch_id): + """ Edition d'un modèle de switch""" + try: + model_switch = ModelSwitch.objects.get(pk=model_switch_id) + except ModelSwitch.DoesNotExist: + messages.error(request, u"Modèle inconnu") + return redirect("/topologie/index_model_switch/") + model_switch = EditModelSwitchForm(request.POST or None, instance=model_switch) + if model_switch.is_valid(): + with transaction.atomic(), reversion.create_revision(): + model_switch.save() + reversion.set_user(request.user) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in model_switch.changed_data) + ) + messages.success(request, "Le modèle a bien été modifié") + return redirect("/topologie/index_model_switch/") + return form({'topoform': model_switch}, 'topologie/topo.html', request) + + +@login_required +@permission_required('infra') +def del_model_switch(request, model_switch_id): + """ Suppression d'un modèle de switch""" + try: + model_switch = ModelSwitch.objects.get(pk=model_switch_id) + except ModelSwitch.DoesNotExist: + messages.error(request, u"Modèle inexistant") + return redirect("/topologie/index_model_switch/") + if request.method == "POST": + try: + with transaction.atomic(), reversion.create_revision(): + model_switch.delete() + reversion.set_user(request.user) + reversion.set_comment("Destruction") + messages.success(request, "Le modèle a été détruit") + except ProtectedError: + messages.error(request, "Le modèle %s est affectée à un autre objet,\ + impossible de la supprimer (switch ou user)" % model_switch) + return redirect("/topologie/index_model_switch/") + return form({ + 'objet': model_switch, + 'objet_name': 'Modèle de switch' + }, 'topologie/delete.html', request) + + +@login_required +@permission_required('infra') +def new_constructor_switch(request): + """Nouveau constructeur de switch""" + constructor_switch = EditConstructorSwitchForm(request.POST or None) + if constructor_switch.is_valid(): + with transaction.atomic(), reversion.create_revision(): + constructor_switch.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Le constructeur a été créé") + return redirect("/topologie/index_model_switch/") + return form({'topoform': constructor_switch}, 'topologie/topo.html', request) + + +@login_required +@permission_required('infra') +def edit_constructor_switch(request, constructor_switch_id): + """ Edition d'un constructeur de switch""" + try: + constructor_switch = ConstructorSwitch.objects.get(pk=constructor_switch_id) + except ConstructorSwitch.DoesNotExist: + messages.error(request, u"Constructeur inconnu") + return redirect("/topologie/index_model_switch/") + constructor_switch = EditConstructorSwitchForm(request.POST or None, instance=constructor_switch) + if constructor_switch.is_valid(): + with transaction.atomic(), reversion.create_revision(): + constructor_switch.save() + reversion.set_user(request.user) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in constructor_switch.changed_data) + ) + messages.success(request, "Le modèle a bien été modifié") + return redirect("/topologie/index_model_switch/") + return form({'topoform': constructor_switch}, 'topologie/topo.html', request) + + +@login_required +@permission_required('infra') +def del_constructor_switch(request, constructor_switch_id): + """ Suppression d'un constructeur de switch""" + try: + constructor_switch = ConstructorSwitch.objects.get(pk=constructor_switch_id) + except ConstructorSwitch.DoesNotExist: + messages.error(request, u"Constructeur inexistant") + return redirect("/topologie/index_model_switch/") + if request.method == "POST": + try: + with transaction.atomic(), reversion.create_revision(): + constructor_switch.delete() + reversion.set_user(request.user) + reversion.set_comment("Destruction") + messages.success(request, "Le constructeur a été détruit") + except ProtectedError: + messages.error(request, "Le constructeur %s est affecté à un autre objet,\ + impossible de la supprimer (switch ou user)" % constructor_switch) + return redirect("/topologie/index_model_switch/") + return form({ + 'objet': constructor_switch, + 'objet_name': 'Constructeur de switch' + }, 'topologie/delete.html', request) diff --git a/users/admin.py b/users/admin.py index 85485311..bc650a85 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,68 +32,92 @@ 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, BanType, 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, BanType, 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', 'pseudo', - 'room', 'email', 'school', 'shell', 'state' ) - search_fields = ('name','surname','pseudo','room') + search_fields = ('surname', 'pseudo') 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 BanTypeAdmin(VersionAdmin): list_display = ('name', 'description') 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 @@ -97,27 +125,54 @@ 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', + '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': + ('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', + '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 @@ -133,15 +188,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..8aa2ffe7 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,35 @@ 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, Adherent, Club +from re2o.utils import 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 +73,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') + model = Adherent + fields = ('pseudo', '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 +120,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,24 +168,29 @@ 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) class Meta: - model = User - fields = ('pseudo', 'password', 'name', 'surname', 'email') + model = Adherent + fields = ('pseudo', 'password', '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 +204,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): + +class AdherentForm(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(AdherentForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Prénom' self.fields['surname'].label = 'Nom' self.fields['school'].label = 'Établissement' @@ -173,7 +269,7 @@ class BaseInfoForm(ModelForm): self.fields['school'].empty_label = "Séléctionner un établissement" class Meta: - model = User + model = Adherent fields = [ 'name', 'surname', @@ -186,14 +282,73 @@ 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): - class Meta(BaseInfoForm.Meta): + 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 ClubForm(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): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ClubForm, self).__init__(*args, prefix=prefix, **kwargs) + self.fields['surname'].label = 'Nom' + self.fields['school'].label = 'Établissement' + self.fields['comment'].label = 'Commentaire' + self.fields['room'].label = 'Local' + self.fields['room'].empty_label = "Pas de chambre" + self.fields['school'].empty_label = "Séléctionner un établissement" + + class Meta: + model = Club + fields = [ + 'surname', + 'pseudo', + 'email', + 'school', + 'comment', + 'room', + 'telephone', + ] + + 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() + if not telephone and preferences.is_tel_mandatory: + raise forms.ValidationError( + "Un numéro de téléphone valide est requis" + ) + return telephone + + +class FullAdherentForm(AdherentForm): + """Edition complète d'un user. Utilisé par admin, + permet d'editer normalement la chambre, ou le shell + Herite de la base""" + class Meta(AdherentForm.Meta): fields = [ 'name', 'surname', @@ -206,37 +361,61 @@ 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) - def clean_force(self): - if self.cleaned_data.get('force', False): - remove_user_room(self.cleaned_data.get('room')) - return +class FullClubForm(ClubForm): + """Edition complète d'un user. Utilisé par admin, + permet d'editer normalement la chambre, ou le shell + Herite de la base""" + class Meta(ClubForm.Meta): + fields = [ + 'surname', + 'pseudo', + 'email', + 'school', + 'comment', + 'room', + 'shell', + 'telephone', + ] -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 +423,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 +496,23 @@ 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.select_related('user'), + widget=forms.CheckboxSelectMultiple + ) def __init__(self, right, *args, **kwargs): super(DelRightForm, self).__init__(*args, **kwargs) - self.fields['rights'].queryset = Right.objects.filter(right=right) + self.fields['rights'].queryset = Right.objects.select_related('user')\ + .select_related('right').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 +520,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 +540,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/migrations/0056_auto_20171015_2033.py b/users/migrations/0056_auto_20171015_2033.py new file mode 100644 index 00000000..a47aca6a --- /dev/null +++ b/users/migrations/0056_auto_20171015_2033.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import users.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0055_auto_20171003_0556'), + ] + + operations = [ + migrations.AlterField( + model_name='listright', + name='gid', + field=models.PositiveIntegerField(null=True, unique=True), + ), + migrations.AlterField( + model_name='listright', + name='listright', + field=models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[a-z]+$', message='Les groupes unix ne peuvent contenir que des lettres minuscules')]), + ), + migrations.AlterField( + model_name='user', + name='rezo_rez_uid', + field=models.PositiveIntegerField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='user', + name='uid_number', + field=models.PositiveIntegerField(default=users.models.User.auto_uid, unique=True), + ), + ] diff --git a/users/migrations/0057_auto_20171023_0301.py b/users/migrations/0057_auto_20171023_0301.py new file mode 100644 index 00000000..d8a02c54 --- /dev/null +++ b/users/migrations/0057_auto_20171023_0301.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-23 01:01 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0056_auto_20171015_2033'), + ] + + operations = [ + migrations.CreateModel( + name='Adherent', + fields=[ + ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('usname', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + bases=('users.user',), + ), + migrations.CreateModel( + name='Club', + fields=[ + ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=('users.user',), + ), + migrations.RunSQL("insert into users_adherent (user_ptr_id, usname) select id, name from users_user", reverse_sql="insert into users_user (name) select usname from users_adherent"), + migrations.RemoveField( + model_name='user', + name='name', + ), + migrations.RenameField( + model_name='adherent', + old_name='usname', + new_name='name', + ), +] diff --git a/users/migrations/0058_auto_20171025_0154.py b/users/migrations/0058_auto_20171025_0154.py new file mode 100644 index 00000000..01e64fbc --- /dev/null +++ b/users/migrations/0058_auto_20171025_0154.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-24 23:54 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + +def create_move_room(apps, schema_editor): + User = apps.get_model('users', 'User') + Adherent = apps.get_model('users', 'Adherent') + Club = apps.get_model('users', 'Club') + db_alias = schema_editor.connection.alias + users = Adherent.objects.using(db_alias).all() + clubs = Club.objects.using(db_alias).all() + for user in users: + user.room_adherent_id = user.room_id + user.save(using=db_alias) + for user in clubs: + user.room_club_id = user.room_id + user.save(using=db_alias) + + +def delete_move_room(apps, schema_editor): + User = apps.get_model('users', 'User') + Adherent = apps.get_model('users', 'Adherent') + Club = apps.get_model('users', 'Club') + db_alias = schema_editor.connection.alias + users = Adherent.objects.using(db_alias).all() + clubs = Club.objects.using(db_alias).all() + for user in users: + user.room_id = user.room_adherent_id + user.save(using=db_alias) + for user in clubs: + user.room_id = user.room_club_id + user.save(using=db_alias) + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0031_auto_20171015_2033'), + ('users', '0057_auto_20171023_0301'), + ] + + operations = [ + migrations.AddField( + model_name='adherent', + name='room_adherent', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='topologie.Room'), + ), + migrations.AddField( + model_name='club', + name='room_club', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='topologie.Room'), + ), + migrations.RunPython(create_move_room, delete_move_room), + migrations.RemoveField( + model_name='user', + name='room', + ), + ] diff --git a/users/migrations/0059_auto_20171025_1854.py b/users/migrations/0059_auto_20171025_1854.py new file mode 100644 index 00000000..0ab2f9c4 --- /dev/null +++ b/users/migrations/0059_auto_20171025_1854.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-25 16:54 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0058_auto_20171025_0154'), + ] + + operations = [ + migrations.RenameField( + model_name='adherent', + old_name='room_adherent', + new_name='room', + ), + migrations.RenameField( + model_name='club', + old_name='room_club', + new_name='room', + ), + ] diff --git a/users/models.py b/users/models.py index d4872bac..2a00c5a9 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,53 +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 - -def remove_user_room(room): - """ Déménage de force l'ancien locataire de la chambre """ - try: - user = User.objects.get(room=room) - except User.DoesNotExist: - return - user.room = None - user.save() +# Utilitaires généraux def linux_user_check(login): @@ -76,33 +89,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 +134,17 @@ 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, + surname, + email, + password=None, + su=False + ): if not pseudo: raise ValueError('Users must have an username') @@ -139,7 +153,6 @@ class UserManager(BaseUserManager): user = self.model( pseudo=pseudo, - name=name, surname=surname, email=self.normalize_email(email), ) @@ -150,58 +163,112 @@ class UserManager(BaseUserManager): user.make_admin() return user - def create_user(self, pseudo, name, surname, email, password=None): + def create_user(self, pseudo, surname, email, password=None): """ Creates and saves a User with the given pseudo, name, surname, email, and password. """ - return self._create_user(pseudo, name, surname, email, password, False) + return self._create_user(pseudo, surname, email, password, False) - def create_superuser(self, pseudo, name, surname, email, password): + def create_superuser(self, pseudo, surname, email, password): """ Creates and saves a superuser with the given pseudo, name, surname, email, and password. """ - return self._create_user(pseudo, name, surname, email, password, True) + return self._create_user(pseudo, surname, email, password, True) class User(AbstractBaseUser): """ Definition de l'utilisateur de base. Champs principaux : name, surnname, pseudo, email, room, password Herite du django BaseUser et du système d'auth django""" - PRETTY_NAME = "Utilisateurs" + PRETTY_NAME = "Utilisateurs (clubs et adhérents)" STATE_ACTIVE = 0 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 + ) 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) + uid_number = models.PositiveIntegerField(default=auto_uid, unique=True) + rezo_rez_uid = models.PositiveIntegerField(unique=True, blank=True, null=True) USERNAME_FIELD = 'pseudo' - REQUIRED_FIELDS = ['name', 'surname', 'email'] + REQUIRED_FIELDS = ['surname', 'email'] objects = UserManager() + @cached_property + def name(self): + """Si il s'agit d'un adhérent, on renvoie le prénom""" + if self.is_class_adherent: + return self.adherent.name + else: + return '' + + @cached_property + def room(self): + """Alias vers room """ + if self.is_class_adherent: + return self.adherent.room + elif self.is_class_club: + return self.club.room + else: + raise NotImplementedError("Type inconnu") + + @cached_property + def class_name(self): + """Renvoie si il s'agit d'un adhérent ou d'un club""" + if hasattr(self, 'adherent'): + return "Adherent" + elif hasattr(self, 'club'): + return "Club" + else: + raise NotImplementedError("Type inconnu") + + @cached_property + def is_class_club(self): + return hasattr(self, 'club') + + @cached_property + def is_class_adherent(self): + return hasattr(self, 'adherent') + @property def is_active(self): """ Renvoie si l'user est à l'état actif""" @@ -223,7 +290,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: @@ -231,11 +299,15 @@ class User(AbstractBaseUser): def get_full_name(self): """ Renvoie le nom complet de l'user formaté nom/prénom""" - return '%s %s' % (self.name, self.surname) + name = self.name + if name: + return '%s %s' % (name, self.surname) + else: + return self.surname def get_short_name(self): """ Renvoie seulement le nom""" - return self.name + return self.surname def has_perms(self, perms, obj=None): """ Renvoie true si l'user dispose de la permission. @@ -247,7 +319,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 +327,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 +354,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,29 +372,67 @@ 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) + ) + ).filter( + Q(type_cotisation='All') | Q(type_cotisation='Adhesion') + ).aggregate(models.Max('date_end'))['date_end__max'] + return date_max + + def end_connexion(self): + """ Renvoie la date de fin de connexion 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) + ) + ).filter( + Q(type_cotisation='All') | Q(type_cotisation='Connexion') + ).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 + def is_connected(self): + """ Renvoie True si l'user est adhérent : si + self.end_adhesion()>now et end_connexion>now""" + end = self.end_connexion() + if not end: + return False + elif end < DT_NOW: + return False + else: + return self.is_adherent() + @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 +441,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,47 +452,70 @@ 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): #TODO consider service type """ Renvoie si un utilisateur a accès à internet """ - return self.state == User.STATE_ACTIVE \ - and not self.is_ban and (self.is_adherent() or self.is_whitelisted) + return self.state == User.STATE_ACTIVE\ + and not self.is_ban and (self.is_connected() or self.is_whitelisted) def end_access(self): """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)""" - if not self.end_adhesion(): + if not self.end_connexion(): if not self.end_whitelist: return None else: return self.end_whitelist else: if not self.end_whitelist: - return self.end_adhesion() - else: - return max(self.end_adhesion(), self.end_whitelist) + return self.end_connexion() + else: + return max(self.end_connexion(), 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 +537,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,29 +558,38 @@ 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 - mac_refresh : synchronise les machines de l'user""" + """ 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 + Si l'instance n'existe pas, on crée le ldapuser correspondant""" self.refresh_from_db() try: user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) except LdapUser.DoesNotExist: user_ldap = LdapUser(uidNumber=self.uid_number) + base = True + access_refresh = True + mac_refresh = True if base: user_ldap.name = self.pseudo user_ldap.sn = self.pseudo 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 +602,9 @@ 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 = [str(mac) for mac in Interface.objects.filter( + machine__user=self + ).values_list('mac_address', flat=True).distinct()] user_ldap.save() def ldap_del(self): @@ -467,53 +617,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({ - 'nom': str(self.name) + ' ' + str(self.surname), + 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': self.get_full_name(), '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 = { - 'name': str(req.user.name) + ' ' + str(req.user.surname), + 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': req.user.get_full_name(), '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 +699,36 @@ class User(AbstractBaseUser): domain.interface_parent = interface_cible domain.clean() domain.save() - except Exception as e: - return False, e + self.notif_auto_newmachine(interface_cible) + except Exception as error: + return False, error return True, "Ok" + def notif_auto_newmachine(self, interface): + """Notification mail lorsque une machine est automatiquement + ajoutée par le radius""" + template = loader.get_template('users/email_auto_newmachine') + assooptions, _created = AssoOption.objects.get_or_create() + general_options, _created = GeneralOption.objects.get_or_create() + context = Context({ + 'nom': self.get_full_name(), + 'mac_address' : interface.mac_address, + 'asso_name': assooptions.name, + 'interface_name' : interface.domain, + 'asso_email': assooptions.contact, + 'pseudo': self.pseudo, + }) + send_mail( + "Ajout automatique d'une machine / New machine autoregistered", + '', + general_options.email_from, + [self.email], + html_message=template.render(context) + ) + return + 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 +737,53 @@ 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 + +class Adherent(User): + PRETTY_NAME = "Adhérents" + name = models.CharField(max_length=255) + room = models.OneToOneField( + 'topologie.Room', + on_delete=models.PROTECT, + blank=True, + null=True + ) + pass + + +class Club(User): + PRETTY_NAME = "Clubs" + room = models.ForeignKey( + 'topologie.Room', + on_delete=models.PROTECT, + blank=True, + null=True + ) + pass + + +@receiver(post_save, sender=Adherent) +@receiver(post_save, sender=Club) @receiver(post_save, sender=User) def user_post_save(sender, **kwargs): """ Synchronisation post_save : envoie le mail de bienvenue si creation @@ -575,29 +795,46 @@ def user_post_save(sender, **kwargs): user.ldap_sync(base=True, access_refresh=True, mac_refresh=False) regen('mailing') + +@receiver(post_delete, sender=Adherent) +@receiver(post_delete, sender=Club) @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 +848,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 +857,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 +901,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 +927,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")]) - gid = models.IntegerField(unique=True, null=True) - details = models.CharField(help_text="Description", max_length=255, blank=True) + 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.PositiveIntegerField(unique=True, null=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) @@ -739,7 +1011,6 @@ class BanType(models.Model): def __str__(self): return "%s (%s)" % (self.name, self.description) - class Ban(models.Model): """ Bannissement d'un utilisateur donné pour un motif/effet fourni.""" PRETTY_NAME = "Liste des bannissements" @@ -754,25 +1025,32 @@ class Ban(models.Model): """ Prend en argument un objet ban, envoie un mail de notification. À terme, il serait envisageable de déporter cette méthode sur le modèle BanType afin de fournir un message différent suivant le type de Ban.""" - 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({ - 'name': str(self.user.name) + ' ' + str(self.user.surname), + 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': self.user.get_full_name(), '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""" @@ -789,6 +1067,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""" @@ -798,7 +1077,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) @@ -807,13 +1090,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) @@ -826,17 +1112,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' @@ -852,38 +1142,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 @@ -897,9 +1235,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'] @@ -908,38 +1249,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/aff_bans.html b/users/templates/users/aff_bans.html index 693a7539..78834123 100644 --- a/users/templates/users/aff_bans.html +++ b/users/templates/users/aff_bans.html @@ -29,10 +29,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
DnsIpv4LocalisationPortsStackid interne Stack{% include "buttons/sort.html" with prefix='switch' col='dns' text='Dns' %}{% include "buttons/sort.html" with prefix='switch' col='ip' text='Ipv4' %}{% include "buttons/sort.html" with prefix='switch' col='loc' text='Localisation' %}{% include "buttons/sort.html" with prefix='switch' col='ports' text='Ports' %}{% include "buttons/sort.html" with prefix='switch' col='stack' text='Stack' %}Id stackModèle Détails
{{switch.number}} {{switch.stack.name}} {{switch.stack_member_id}}{{switch.model}} {{switch.details}} - - - + {% include 'buttons/history.html' with href='topologie:history' name='switch' id=switch.pk%} + {% if is_infra %} + {% include 'buttons/edit.html' with href='topologie:edit-switch' id=switch.pk %} + {% include 'buttons/suppr.html' with href='machines:del-interface' id=switch.switch_interface.id %} + {% include 'buttons/add.html' with href='topologie:create-ports' id=switch.pk desc='Création de ports'%} + {% endif %}
- + - - + + diff --git a/users/templates/users/aff_clubs.html b/users/templates/users/aff_clubs.html new file mode 100644 index 00000000..71ac1606 --- /dev/null +++ b/users/templates/users/aff_clubs.html @@ -0,0 +1,57 @@ +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +{% if clubs_list.paginator %} +{% include "pagination.html" with list=clubs_list %} +{% endif %} + +
Utilisateur{% include "buttons/sort.html" with prefix='ban' col="user" text="Utilisateur" %} RaisonDate de débutDate de fin{% include "buttons/sort.html" with prefix='ban' col="start" text="Date de début" %}{% include "buttons/sort.html" with prefix='ban' col="end" text="Date de fin" %}
+ + + + + + + + + + + {% for club in clubs_list %} + + + + + + + + + {% endfor %} +
{% include "buttons/sort.html" with prefix='club' col="surname" text="Nom" %}{% include "buttons/sort.html" with prefix='club' col="pseudo" text="Pseudo" %}{% include "buttons/sort.html" with prefix='club' col="room" text="Chambre" %}Fin de cotisation leConnexionProfil
{{ club.surname }}{{ club.pseudo }}{{ club.room }}{% if club.is_adherent %}{{ club.end_adhesion }}{% else %}Non adhérent{% endif %}{% if club.has_access == True %} + Active + {% else %} + Désactivée + {% endif %} + +
+ diff --git a/users/templates/users/aff_users.html b/users/templates/users/aff_users.html index b4ded1f2..c03cfa74 100644 --- a/users/templates/users/aff_users.html +++ b/users/templates/users/aff_users.html @@ -29,10 +29,10 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - + + + + diff --git a/users/templates/users/aff_whitelists.html b/users/templates/users/aff_whitelists.html index 6665ad27..07d434c0 100644 --- a/users/templates/users/aff_whitelists.html +++ b/users/templates/users/aff_whitelists.html @@ -29,10 +29,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
PrénomNomPseudoChambre{% include "buttons/sort.html" with prefix='user' col="name" text="Prénom" %}{% include "buttons/sort.html" with prefix='user' col="surname" text="Nom" %}{% include "buttons/sort.html" with prefix='user' col="pseudo" text="Pseudo" %}{% include "buttons/sort.html" with prefix='user' col="room" text="Chambre" %} Fin de cotisation le Connexion Profil
- + - - + + diff --git a/users/templates/users/del_right.html b/users/templates/users/del_right.html index 740778a2..57e706a7 100644 --- a/users/templates/users/del_right.html +++ b/users/templates/users/del_right.html @@ -34,24 +34,42 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} -
Utilisateur{% include "buttons/sort.html" with prefix='white' col="user" text="Utilisateur" %} RaisonDate de débutDate de fin{% include "buttons/sort.html" with prefix='white' col="start" text="Date de début" %}{% include "buttons/sort.html" with prefix='white' col="end" text="Date de fin" %}
- - - {% for key, values in userform.items %} - - {% endfor %} - - - +
{{ key }}
+ {% for key, values in userform.items %} - {% bootstrap_form_errors values %} - - {% endfor %} + + + + + + {% endfor %} +
{{ values.rights }}
+ + {{ key }} ( {{values.rights|length }} users ) + +
+
+
    + {% for user in values.rights %} +
  • {{ user }}
  • + {% endfor %} +
+
+
- {% bootstrap_button "Modifier" button_type="submit" icon="star" %} + {% bootstrap_button "Supprimer" button_type="submit" icon="star" %} + +


diff --git a/users/templates/users/email_auto_newmachine b/users/templates/users/email_auto_newmachine new file mode 100644 index 00000000..d98cc160 --- /dev/null +++ b/users/templates/users/email_auto_newmachine @@ -0,0 +1,27 @@ +

Bonjour {{nom}}

+ +

Une nouvelle machine a automatiquement été inscrite sur votre compte.

+ +

Si vous êtes à l'origine de la connexion de cet appareil en filaire ou wifi, ne tenez pas compte de cette notification

+ +

La nouvelle machine possède l'adresse mac {{ mac_address }}, et s'est vu assigner le nom suivant : {{ interface_name }}

+ +

Vous pouvez à tout moment modifier ces informations sur votre compte en ligne

+ +

Si vous n'êtes pas à l'origine de cette connexion, merci de le signaler rapidement à {{asso_email}}

+ +

À bientôt,
+L'équipe de {{asso_name}}.

+ +

---

+ +

A new device has been automatically added on your account.

+ +

If you connected a new device recently, please don't take this mail into account

+ +

The new device has this mac address : {{ mac_address }}, and this name : {{ interface_name }}

+ +

Please contact us if you didn't connect a new device with this mail address {{asso_email}}.

+ +

Regards,
+The {{asso_name}} team.

diff --git a/users/templates/users/index_clubs.html b/users/templates/users/index_clubs.html new file mode 100644 index 00000000..4c3dd609 --- /dev/null +++ b/users/templates/users/index_clubs.html @@ -0,0 +1,37 @@ +{% extends "users/sidebar.html" %} +{% comment %} +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 +Copyright © 2017 Augustin Lemesle + +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. +{% endcomment %} + +{% load bootstrap3 %} + +{% block title %}Utilisateurs{% endblock %} + +{% block content %} +

Clubs

+ {% include "users/aff_clubs.html" with clubs_list=clubs_list %} +
+
+
+{% endblock %} + diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 267af0be..559ea57d 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}Profil{% endblock %} {% block content %} -

Adhérent

+

{{ user.class_name }}

Accès internetActifActif (jusqu'au {{ user.end_access }})Désactivé