8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-09 03:16:25 +00:00

Merge branch 'master' into ouverture_des_ports

This commit is contained in:
root 2017-10-15 18:35:35 +02:00
commit 932f64701a
64 changed files with 5478 additions and 1536 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ settings_local.py
re2o.png re2o.png
__pycache__/* __pycache__/*
static_files/* static_files/*
static/logo/*

View file

@ -28,23 +28,37 @@ from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
class FactureAdmin(VersionAdmin): class FactureAdmin(VersionAdmin):
list_display = ('user','paiement','date','valid','control') """Class admin d'une facture, tous les champs"""
pass
class VenteAdmin(VersionAdmin): 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): class ArticleAdmin(VersionAdmin):
list_display = ('name','prix','iscotisation','duration') """Class admin d'un article en vente"""
pass
class BanqueAdmin(VersionAdmin): class BanqueAdmin(VersionAdmin):
list_display = ('name',) """Class admin de la liste des banques (facture related)"""
pass
class PaiementAdmin(VersionAdmin): class PaiementAdmin(VersionAdmin):
list_display = ('moyen','type_paiement') """Class admin d'un moyen de paiement (facture related"""
pass
class CotisationAdmin(VersionAdmin): 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(Facture, FactureAdmin)
admin.site.register(Article, ArticleAdmin) admin.site.register(Article, ArticleAdmin)

View file

@ -19,74 +19,125 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from django import forms from django import forms
from django.forms import ModelForm, Form from django.forms import ModelForm, Form
from django import forms
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from .models import Article, Paiement, Facture, Banque, Vente from .models import Article, Paiement, Facture, Banque
class NewFactureForm(ModelForm): class NewFactureForm(ModelForm):
"""Creation d'une facture, moyen de paiement, banque et numero
de cheque"""
def __init__(self, *args, **kwargs): 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['cheque'].required = False
self.fields['banque'].required = False self.fields['banque'].required = False
self.fields['cheque'].label = 'Numero de chèque' self.fields['cheque'].label = 'Numero de chèque'
self.fields['banque'].empty_label = "Non renseigné" self.fields['banque'].empty_label = "Non renseigné"
self.fields['paiement'].empty_label = "Séléctionner un moyen de paiement" self.fields['paiement'].empty_label = "Séléctionner\
self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects.filter(type_paiement=1).first().id un moyen de paiement"
self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects\
.filter(type_paiement=1).first().id
class Meta: class Meta:
model = Facture model = Facture
fields = ['paiement','banque','cheque'] fields = ['paiement', 'banque', 'cheque']
def clean(self): def clean(self):
cleaned_data=super(NewFactureForm, self).clean() cleaned_data = super(NewFactureForm, self).clean()
paiement = cleaned_data.get("paiement") paiement = cleaned_data.get("paiement")
cheque = cleaned_data.get("cheque") cheque = cleaned_data.get("cheque")
banque = cleaned_data.get("banque") banque = cleaned_data.get("banque")
if not paiement: 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): 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 return cleaned_data
class CreditSoldeForm(NewFactureForm): class CreditSoldeForm(NewFactureForm):
"""Permet de faire des opérations sur le solde si il est activé"""
class Meta(NewFactureForm.Meta): class Meta(NewFactureForm.Meta):
model = Facture model = Facture
fields = ['paiement','banque','cheque'] fields = ['paiement', 'banque', 'cheque']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CreditSoldeForm, self).__init__(*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) montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True)
class SelectArticleForm(Form): class SelectArticleForm(Form):
article = forms.ModelChoiceField(queryset=Article.objects.all(), label="Article", required=True) """Selection d'un article lors de la creation d'une facture"""
quantity = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)], required=True) article = forms.ModelChoiceField(
queryset=Article.objects.all(),
label="Article",
required=True
)
quantity = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)],
required=True
)
class NewFactureFormPdf(Form): class NewFactureFormPdf(Form):
article = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Article") """Creation d'un pdf facture par le trésorier"""
number = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)]) article = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(),
label="Article"
)
number = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)]
)
paid = forms.BooleanField(label="Payé", required=False) paid = forms.BooleanField(label="Payé", required=False)
dest = forms.CharField(required=True, max_length=255, label="Destinataire") dest = forms.CharField(required=True, max_length=255, label="Destinataire")
chambre = forms.CharField(required=False, max_length=10, label="Adresse") 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): class EditFactureForm(NewFactureForm):
"""Edition d'une facture : moyen de paiement, banque, user parent"""
class Meta(NewFactureForm.Meta): class Meta(NewFactureForm.Meta):
fields = ['paiement','banque','cheque','user'] fields = ['paiement', 'banque', 'cheque', 'user']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditFactureForm, self).__init__(*args, **kwargs) super(EditFactureForm, self).__init__(*args, **kwargs)
self.fields['user'].label = 'Adherent' 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): class TrezEditFactureForm(EditFactureForm):
"""Vue pour édition controle trésorier"""
class Meta(EditFactureForm.Meta): class Meta(EditFactureForm.Meta):
fields = '__all__' fields = '__all__'
@ -97,38 +148,67 @@ class TrezEditFactureForm(EditFactureForm):
class ArticleForm(ModelForm): class ArticleForm(ModelForm):
"""Creation d'un article. Champs : nom, cotisation, durée"""
class Meta: class Meta:
model = Article model = Article
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): 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" self.fields['name'].label = "Désignation de l'article"
class DelArticleForm(Form): 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): 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: class Meta:
model = Paiement model = Paiement
fields = ['moyen', 'type_paiement'] fields = ['moyen', 'type_paiement']
def __init__(self, *args, **kwargs): 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['moyen'].label = 'Moyen de paiement à ajouter'
self.fields['type_paiement'].label = 'Type de paiement à ajouter' self.fields['type_paiement'].label = 'Type de paiement à ajouter'
class DelPaiementForm(Form): 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): class BanqueForm(ModelForm):
"""Creation d'une banque, field name"""
class Meta: class Meta:
model = Banque model = Banque
fields = ['name'] fields = ['name']
def __init__(self, *args, **kwargs): 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' self.fields['name'].label = 'Banque à ajouter'
class DelBanqueForm(Form): 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
)

View file

@ -20,59 +20,111 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from dateutil.relativedelta import relativedelta
from django.db import models from django.db import models
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from dateutil.relativedelta import relativedelta
from django.forms import ValidationError from django.forms import ValidationError
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db.models import Max from django.db.models import Max
from django.utils import timezone from django.utils import timezone
from machines.models import regen from machines.models import regen
class Facture(models.Model): 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" PRETTY_NAME = "Factures émises"
user = models.ForeignKey('users.User', on_delete=models.PROTECT) user = models.ForeignKey('users.User', on_delete=models.PROTECT)
paiement = models.ForeignKey('Paiement', 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) cheque = models.CharField(max_length=255, blank=True)
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
valid = models.BooleanField(default=True) valid = models.BooleanField(default=True)
control = models.BooleanField(default=False) control = models.BooleanField(default=False)
def prix(self): 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 return prix
def prix_total(self): 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): 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 return name
def __str__(self): def __str__(self):
return str(self.user) + ' ' + str(self.date) return str(self.user) + ' ' + str(self.date)
@receiver(post_save, sender=Facture) @receiver(post_save, sender=Facture)
def facture_post_save(sender, **kwargs): def facture_post_save(sender, **kwargs):
"""Post save d'une facture, synchronise l'user ldap"""
facture = kwargs['instance'] facture = kwargs['instance']
user = facture.user user = facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Facture) @receiver(post_delete, sender=Facture)
def facture_post_delete(sender, **kwargs): def facture_post_delete(sender, **kwargs):
"""Après la suppression d'une facture, on synchronise l'user ldap"""
user = kwargs['instance'].user user = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Vente(models.Model): 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" PRETTY_NAME = "Ventes effectuées"
facture = models.ForeignKey('Facture', on_delete=models.CASCADE) facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
@ -80,44 +132,67 @@ class Vente(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
prix = models.DecimalField(max_digits=5, decimal_places=2) prix = models.DecimalField(max_digits=5, decimal_places=2)
iscotisation = models.BooleanField() iscotisation = models.BooleanField()
duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True) duration = models.IntegerField(
help_text="Durée exprimée en mois entiers",
blank=True,
null=True)
def prix_total(self): def prix_total(self):
"""Renvoie le prix_total de self (nombre*prix)"""
return self.prix*self.number return self.prix*self.number
def update_cotisation(self): 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'): if hasattr(self, 'cotisation'):
cotisation = 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 return
def create_cotis(self, date_start=False): def create_cotis(self, date_start=False):
""" Update et crée l'objet cotisation associé à une facture, prend en argument l'user, la facture pour la quantitéi, et l'article pour la durée""" """Update et crée l'objet cotisation associé à une facture, prend
en argument l'user, la facture pour la quantitéi, et l'article pour
la durée"""
if not hasattr(self, 'cotisation'): if not hasattr(self, 'cotisation'):
cotisation=Cotisation(vente=self) cotisation = Cotisation(vente=self)
if date_start: if date_start:
end_adhesion = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self.facture.user).exclude(valid=False))).filter(date_start__lt=date_start).aggregate(Max('date_end'))['date_end__max'] end_adhesion = Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self.facture.user
).exclude(valid=False))
).filter(
date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max']
else: else:
end_adhesion = self.facture.user.end_adhesion() end_adhesion = self.facture.user.end_adhesion()
date_start = date_start or timezone.now() date_start = date_start or timezone.now()
end_adhesion = end_adhesion or date_start end_adhesion = end_adhesion or date_start
date_max = max(end_adhesion, date_start) date_max = max(end_adhesion, date_start)
cotisation.date_start = date_max 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 return
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# On verifie que si iscotisation, duration est présent # On verifie que si iscotisation, duration est présent
if self.iscotisation and not self.duration: if self.iscotisation and not self.duration:
raise ValidationError("Cotisation et durée doivent être présents ensembles") raise ValidationError("Cotisation et durée doivent être présents\
ensembles")
self.update_cotisation() self.update_cotisation()
super(Vente, self).save(*args, **kwargs) super(Vente, self).save(*args, **kwargs)
def __str__(self): def __str__(self):
return str(self.name) + ' ' + str(self.facture) return str(self.name) + ' ' + str(self.facture)
@receiver(post_save, sender=Vente) @receiver(post_save, sender=Vente)
def vente_post_save(sender, **kwargs): 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'] vente = kwargs['instance']
if hasattr(vente, 'cotisation'): if hasattr(vente, 'cotisation'):
vente.cotisation.vente = vente vente.cotisation.vente = vente
@ -128,14 +203,20 @@ def vente_post_save(sender, **kwargs):
user = vente.facture.user user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Vente) @receiver(post_delete, sender=Vente)
def vente_post_delete(sender, **kwargs): 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'] vente = kwargs['instance']
if vente.iscotisation: if vente.iscotisation:
user = vente.facture.user user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Article(models.Model): 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" PRETTY_NAME = "Articles en vente"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@ -154,7 +235,9 @@ class Article(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Banque(models.Model): class Banque(models.Model):
"""Liste des banques"""
PRETTY_NAME = "Banques enregistrées" PRETTY_NAME = "Banques enregistrées"
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
@ -162,7 +245,9 @@ class Banque(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Paiement(models.Model): class Paiement(models.Model):
"""Moyens de paiement"""
PRETTY_NAME = "Moyens de paiement" PRETTY_NAME = "Moyens de paiement"
PAYMENT_TYPES = ( PAYMENT_TYPES = (
(0, 'Autre'), (0, 'Autre'),
@ -179,11 +264,15 @@ class Paiement(models.Model):
self.moyen = self.moyen.title() self.moyen = self.moyen.title()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Un seul type de paiement peut-etre cheque..."""
if Paiement.objects.filter(type_paiement=1).count() > 1: 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) super(Paiement, self).save(*args, **kwargs)
class Cotisation(models.Model): class Cotisation(models.Model):
"""Objet cotisation, debut et fin, relié en onetoone à une vente"""
PRETTY_NAME = "Cotisations" PRETTY_NAME = "Cotisations"
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True) vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
@ -193,15 +282,19 @@ class Cotisation(models.Model):
def __str__(self): def __str__(self):
return str(self.vente) return str(self.vente)
@receiver(post_save, sender=Cotisation) @receiver(post_save, sender=Cotisation)
def cotisation_post_save(sender, **kwargs): def cotisation_post_save(sender, **kwargs):
"""Après modification d'une cotisation, regeneration des services"""
regen('dns') regen('dns')
regen('dhcp') regen('dhcp')
regen('mac_ip_list') regen('mac_ip_list')
regen('mailing') regen('mailing')
@receiver(post_delete, sender=Cotisation) @receiver(post_delete, sender=Cotisation)
def vente_post_delete(sender, **kwargs): def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, régénération des services"""
cotisation = kwargs['instance'] cotisation = kwargs['instance']
regen('mac_ip_list') regen('mac_ip_list')
regen('mailing') regen('mailing')

View file

@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load massive_bootstrap_form %}
{% block title %}Création et modification de factures{% endblock %} {% 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.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h3>Editer la facture</h3> <h3>Editer la facture</h3>
{% bootstrap_form factureform %} {% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }} {{ venteform.management_form }}
<h3>Articles de la facture</h3> <h3>Articles de la facture</h3>
<table class="table table-striped"> <table class="table table-striped">

View file

@ -38,18 +38,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ venteform.management_form }} {{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select --> <!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
<h3>Articles de la facture</h3> <h3>Articles de la facture</h3>
<div id="form_set"> <div id="form_set" class="form-group">
{% for form in venteform.forms %} {% for form in venteform.forms %}
<div class='product_to_sell'> <div class='product_to_sell form-inline'>
<p> Article : &nbsp;
{{ form.as_table }} {% bootstrap_form form label_class='sr-only' %}
</p> &nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-0-article-remove" type="button">
<span class="glyphicon glyphicon-remove"></span>
</button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<p>
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un article" id="add_one"> <input class="btn btn-primary btn-sm" role="button" value="Ajouter un article" id="add_one">
</p>
<p> <p>
Prix total : <span id="total_price">0,00</span> Prix total : <span id="total_price">0,00</span>
</p> </p>
@ -63,19 +65,23 @@ with this program; if not, write to the Free Software Foundation, Inc.,
prices[{{ article.id|escapejs }}] = {{ article.prix }}; prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %} {% endfor %}
var template = `<p>{{ venteform.empty_form.as_table }}</p>`; var template = `Article : &nbsp;
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="glyphicon glyphicon-remove"></span>
</button>`
function add_article(){ function add_article(){
// Index start at 0 => new_index = number of items // Index start at 0 => new_index = number of items
var new_index = var new_index =
document.getElementsByClassName('product_to_sell').length; document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value = document.getElementById('id_form-TOTAL_FORMS').value ++;
parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1;
var new_article = document.createElement('div'); 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); new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set') document.getElementById('form_set').appendChild(new_article);
.appendChild(new_article);
add_listenner_for_id(new_index); 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); .addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity') document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true); .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(){ function set_cheque_info_visibility() {
var visible = document.getElementById("id_paiement").value == document.getElementById("id_paiement").getAttribute('data-cheque'); var paiement = document.getElementById("id_Facture-paiement");
p = document.getElementById("id_paiement") var visible = paiement.value == paiement.getAttribute('data-cheque');
console.log(p); p = document.getElementById("id_Facture-paiement");
var display = 'none'; var display = 'none';
if (visible) { if (visible) {
display = 'block'; display = 'block';
} }
document.getElementById("id_cheque").parentNode.style.display = display; document.getElementById("id_Facture-cheque")
document.getElementById("id_banque").parentNode.style.display = display; .parentNode.style.display = display;
document.getElementById("id_Facture-banque")
.parentNode.style.display = display;
} }
// Add events manager when DOM is fully loaded // 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){ for (i = 0; i < product_count; ++i){
add_listenner_for_id(i); add_listenner_for_id(i);
} }
document.getElementById("id_paiement") document.getElementById("id_Facture-paiement")
.addEventListener("change", set_cheque_info_visibility, true); .addEventListener("change", set_cheque_info_visibility, true);
set_cheque_info_visibility(); set_cheque_info_visibility();
update_price(); update_price();

View file

@ -27,30 +27,96 @@ from django.conf.urls import url
from . import views from . import views
urlpatterns = [ urlpatterns = [
url(r'^new_facture/(?P<userid>[0-9]+)$', views.new_facture, name='new-facture'), url(r'^new_facture/(?P<userid>[0-9]+)$',
url(r'^edit_facture/(?P<factureid>[0-9]+)$', views.edit_facture, name='edit-facture'), views.new_facture,
url(r'^del_facture/(?P<factureid>[0-9]+)$', views.del_facture, name='del-facture'), name='new-facture'
url(r'^facture_pdf/(?P<factureid>[0-9]+)$', views.facture_pdf, name='facture-pdf'), ),
url(r'^new_facture_pdf/$', views.new_facture_pdf, name='new-facture-pdf'), url(r'^edit_facture/(?P<factureid>[0-9]+)$',
url(r'^credit_solde/(?P<userid>[0-9]+)$', views.credit_solde, name='credit-solde'), views.edit_facture,
url(r'^add_article/$', views.add_article, name='add-article'), name='edit-facture'
url(r'^edit_article/(?P<articleid>[0-9]+)$', views.edit_article, name='edit-article'), ),
url(r'^del_article/$', views.del_article, name='del-article'), url(r'^del_facture/(?P<factureid>[0-9]+)$',
url(r'^add_paiement/$', views.add_paiement, name='add-paiement'), views.del_facture,
url(r'^edit_paiement/(?P<paiementid>[0-9]+)$', views.edit_paiement, name='edit-paiement'), name='del-facture'
url(r'^del_paiement/$', views.del_paiement, name='del-paiement'), ),
url(r'^add_banque/$', views.add_banque, name='add-banque'), url(r'^facture_pdf/(?P<factureid>[0-9]+)$',
url(r'^edit_banque/(?P<banqueid>[0-9]+)$', views.edit_banque, name='edit-banque'), views.facture_pdf,
url(r'^del_banque/$', views.del_banque, name='del-banque'), name='facture-pdf'
url(r'^index_article/$', views.index_article, name='index-article'), ),
url(r'^index_banque/$', views.index_banque, name='index-banque'), url(r'^new_facture_pdf/$',
url(r'^index_paiement/$', views.index_paiement, name='index-paiement'), views.new_facture_pdf,
url(r'^history/(?P<object>facture)/(?P<id>[0-9]+)$', views.history, name='history'), name='new-facture-pdf'
url(r'^history/(?P<object>article)/(?P<id>[0-9]+)$', views.history, name='history'), ),
url(r'^history/(?P<object>paiement)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^credit_solde/(?P<userid>[0-9]+)$',
url(r'^history/(?P<object>banque)/(?P<id>[0-9]+)$', views.history, name='history'), views.credit_solde,
url(r'^control/$', views.control, name='control'), name='credit-solde'
),
url(r'^add_article/$',
views.add_article,
name='add-article'
),
url(r'^edit_article/(?P<articleid>[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<paiementid>[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<banqueid>[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/(?P<object>facture)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(r'^history/(?P<object>article)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(r'^history/(?P<object>paiement)/(?P<id>[0-9]+)$',
views.history,
name='history'),
url(r'^history/(?P<object>banque)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(r'^control/$',
views.control,
name='control'
),
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
] ]

View file

@ -24,44 +24,45 @@
# Goulven Kermarec, Gabriel Détraz # Goulven Kermarec, Gabriel Détraz
# Gplv2 # Gplv2
from __future__ import unicode_literals from __future__ import unicode_literals
import os
from django.shortcuts import render, redirect 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.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.auth.decorators import login_required, permission_required
from django.contrib import messages 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 import transaction
from django.forms import modelformset_factory, formset_factory from django.forms import modelformset_factory, formset_factory
import os from django.utils import timezone
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
# Import des models, forms et fonctions re2o
from .models import Facture, Article, Vente, Cotisation, Paiement, Banque
from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, CreditSoldeForm, SelectArticleForm
from users.models import User from users.models import User
from .tex import render_tex
from re2o.settings import LOGO_PATH from re2o.settings import LOGO_PATH
from re2o import settings from re2o import settings
from re2o.views import form
from preferences.models import OptionalUser, AssoOption, GeneralOption from preferences.models import OptionalUser, AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque
from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm
from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm
from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf
from .forms import SelectArticleForm, CreditSoldeForm
from .tex import render_tex
from dateutil.relativedelta import relativedelta
from django.utils import timezone
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def new_facture(request, userid): 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: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
except User.DoesNotExist: except User.DoesNotExist:
messages.error(request, u"Utilisateur inexistant" ) messages.error(request, u"Utilisateur inexistant")
return redirect("/cotisations/") return redirect("/cotisations/")
facture = Facture(user=user) facture = Facture(user=user)
# Le template a besoin de connaitre les articles pour le js # Le template a besoin de connaitre les articles pour le js
@ -70,50 +71,80 @@ def new_facture(request, userid):
facture_form = NewFactureForm(request.POST or None, instance=facture) facture_form = NewFactureForm(request.POST or None, instance=facture)
article_formset = formset_factory(SelectArticleForm)(request.POST or None) article_formset = formset_factory(SelectArticleForm)(request.POST or None)
if facture_form.is_valid() and article_formset.is_valid(): 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 articles = article_formset
# Si au moins un article est rempli # Si au moins un article est rempli
if any(art.cleaned_data for art in articles): 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 user_solde = options.user_solde
solde_negatif = options.solde_negatif 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 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 prix_total = 0
for art_item in articles: for art_item in articles:
if art_item.cleaned_data: 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: 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) return redirect("/users/profil/" + userid)
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_facture.save() new_facture_instance.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
for art_item in articles: for art_item in articles:
if art_item.cleaned_data: if art_item.cleaned_data:
article = art_item.cleaned_data['article'] article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity'] quantity = art_item.cleaned_data['quantity']
new_vente = Vente.objects.create(facture=new_facture, name=article.name, prix=article.prix, iscotisation=article.iscotisation, duration=article.duration, number=quantity) new_vente = Vente.objects.create(
facture=new_facture_instance,
name=article.name,
prix=article.prix,
iscotisation=article.iscotisation,
duration=article.duration,
number=quantity
)
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_vente.save() new_vente.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
if any(art_item.cleaned_data['article'].iscotisation for art_item in articles if art_item.cleaned_data): if any(art_item.cleaned_data['article'].iscotisation
messages.success(request, "La cotisation a été prolongée pour l'adhérent %s jusqu'au %s" % (user.pseudo, user.end_adhesion()) ) 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: else:
messages.success(request, "La facture a été crée") messages.success(request, "La facture a été crée")
return redirect("/users/profil/" + userid) return redirect("/users/profil/" + userid)
messages.error(request, u"Il faut au moins un article valide pour créer une facture" ) messages.error(
return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request) 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 @login_required
@permission_required('tresorier') @permission_required('tresorier')
def new_facture_pdf(request): 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) facture_form = NewFactureFormPdf(request.POST or None)
if facture_form.is_valid(): if facture_form.is_valid():
options, created = AssoOption.objects.get_or_create() options, _created = AssoOption.objects.get_or_create()
tbl = [] tbl = []
article = facture_form.cleaned_data['article'] article = facture_form.cleaned_data['article']
quantite = facture_form.cleaned_data['number'] quantite = facture_form.cleaned_data['number']
@ -121,71 +152,131 @@ def new_facture_pdf(request):
destinataire = facture_form.cleaned_data['dest'] destinataire = facture_form.cleaned_data['dest']
chambre = facture_form.cleaned_data['chambre'] chambre = facture_form.cleaned_data['chambre']
fid = facture_form.cleaned_data['fid'] fid = facture_form.cleaned_data['fid']
for a in article: for art in article:
tbl.append([a, quantite, a.prix * quantite]) tbl.append([art, quantite, art.prix * quantite])
prix_total = sum(a[2] for a in tbl) prix_total = sum(a[2] for a in tbl)
user = {'name':destinataire, 'room':chambre} 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 render_tex(request, 'cotisations/factures.tex', {
return form({'factureform': facture_form}, 'cotisations/facture.html', request) '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 @login_required
def facture_pdf(request, factureid): 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: try:
facture = Facture.objects.get(pk=factureid) facture = Facture.objects.get(pk=factureid)
except Facture.DoesNotExist: except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante" ) messages.error(request, u"Facture inexistante")
return redirect("/cotisations/") return redirect("/cotisations/")
if not request.user.has_perms(('cableur',)) and facture.user != request.user: if not request.user.has_perms(('cableur',))\
messages.error(request, "Vous ne pouvez pas afficher une facture ne vous appartenant pas sans droit 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)) return redirect("/users/profil/" + str(request.user.id))
if not facture.valid: 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)) return redirect("/users/profil/" + str(request.user.id))
vente = Vente.objects.all().filter(facture=facture) ventes_objects = Vente.objects.all().filter(facture=facture)
ventes = [] ventes = []
options, created = AssoOption.objects.get_or_create() options, _created = AssoOption.objects.get_or_create()
for v in vente: for vente in ventes_objects:
ventes.append([v, v.number, v.prix_total]) 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)}) 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def edit_facture(request, factureid): 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: try:
facture = Facture.objects.get(pk=factureid) facture = Facture.objects.get(pk=factureid)
except Facture.DoesNotExist: except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante" ) messages.error(request, u"Facture inexistante")
return redirect("/cotisations/") return redirect("/cotisations/")
if request.user.has_perms(['tresorier']): 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: 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/") return redirect("/cotisations/")
else: else:
facture_form = EditFactureForm(request.POST or None, instance=facture) facture_form = EditFactureForm(request.POST or None, instance=facture)
ventes_objects = Vente.objects.filter(facture=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) vente_form = vente_form_set(request.POST or None, queryset=ventes_objects)
if facture_form.is_valid() and vente_form.is_valid(): if facture_form.is_valid() and vente_form.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
facture_form.save() facture_form.save()
vente_form.save() vente_form.save()
reversion.set_user(request.user) 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") messages.success(request, "La facture a bien été modifiée")
return redirect("/cotisations/") 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def del_facture(request, factureid): def del_facture(request, factureid):
"""Suppression d'une facture. Supprime en cascade les ventes
et cotisations filles"""
try: try:
facture = Facture.objects.get(pk=factureid) facture = Facture.objects.get(pk=factureid)
except Facture.DoesNotExist: except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante" ) messages.error(request, u"Facture inexistante")
return redirect("/cotisations/") return redirect("/cotisations/")
if (facture.control or not facture.valid): if facture.control or not facture.valid:
messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier") messages.error(request, "Vous ne pouvez pas editer une facture\
controlée ou invalidée par le trésorier")
return redirect("/cotisations/") return redirect("/cotisations/")
if request.method == "POST": if request.method == "POST":
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -193,7 +284,11 @@ def del_facture(request, factureid):
reversion.set_user(request.user) reversion.set_user(request.user)
messages.success(request, "La facture a été détruite") messages.success(request, "La facture a été détruite")
return redirect("/cotisations/") 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 @login_required
@permission_required('cableur') @permission_required('cableur')
@ -202,7 +297,7 @@ def credit_solde(request, userid):
try: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
except User.DoesNotExist: except User.DoesNotExist:
messages.error(request, u"Utilisateur inexistant" ) messages.error(request, u"Utilisateur inexistant")
return redirect("/cotisations/") return redirect("/cotisations/")
facture = CreditSoldeForm(request.POST or None) facture = CreditSoldeForm(request.POST or None)
if facture.is_valid(): if facture.is_valid():
@ -212,7 +307,14 @@ def credit_solde(request, userid):
facture_instance.save() facture_instance.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") 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) new_vente = Vente.objects.create(
facture=facture_instance,
name="solde",
prix=facture.cleaned_data['montant'],
iscotisation=False,
duration=0,
number=1
)
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_vente.save() new_vente.save()
reversion.set_user(request.user) reversion.set_user(request.user)
@ -225,6 +327,13 @@ def credit_solde(request, userid):
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def add_article(request): 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) article = ArticleForm(request.POST or None)
if article.is_valid(): if article.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -235,27 +344,36 @@ def add_article(request):
return redirect("/cotisations/index_article/") return redirect("/cotisations/index_article/")
return form({'factureform': article}, 'cotisations/facture.html', request) return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def edit_article(request, articleid): def edit_article(request, articleid):
"""Edition d'un article (designation, prix, etc)
Réservé au trésorier"""
try: try:
article_instance = Article.objects.get(pk=articleid) article_instance = Article.objects.get(pk=articleid)
except Article.DoesNotExist: except Article.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante")
return redirect("/cotisations/index_article/") return redirect("/cotisations/index_article/")
article = ArticleForm(request.POST or None, instance=article_instance) article = ArticleForm(request.POST or None, instance=article_instance)
if article.is_valid(): if article.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
article.save() article.save()
reversion.set_user(request.user) 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é") messages.success(request, "Type d'article modifié")
return redirect("/cotisations/index_article/") return redirect("/cotisations/index_article/")
return form({'factureform': article}, 'cotisations/facture.html', request) return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def del_article(request): def del_article(request):
"""Suppression d'un article en vente"""
article = DelArticleForm(request.POST or None) article = DelArticleForm(request.POST or None)
if article.is_valid(): if article.is_valid():
article_del = article.cleaned_data['articles'] article_del = article.cleaned_data['articles']
@ -266,9 +384,12 @@ def del_article(request):
return redirect("/cotisations/index_article") return redirect("/cotisations/index_article")
return form({'factureform': article}, 'cotisations/facture.html', request) return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def add_paiement(request): def add_paiement(request):
"""Ajoute un moyen de paiement. Relié aux factures
via foreign key"""
paiement = PaiementForm(request.POST or None) paiement = PaiementForm(request.POST or None)
if paiement.is_valid(): if paiement.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -279,27 +400,35 @@ def add_paiement(request):
return redirect("/cotisations/index_paiement/") return redirect("/cotisations/index_paiement/")
return form({'factureform': paiement}, 'cotisations/facture.html', request) return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def edit_paiement(request, paiementid): def edit_paiement(request, paiementid):
"""Edition d'un moyen de paiement"""
try: try:
paiement_instance = Paiement.objects.get(pk=paiementid) paiement_instance = Paiement.objects.get(pk=paiementid)
except Paiement.DoesNotExist: except Paiement.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante")
return redirect("/cotisations/index_paiement/") return redirect("/cotisations/index_paiement/")
paiement = PaiementForm(request.POST or None, instance=paiement_instance) paiement = PaiementForm(request.POST or None, instance=paiement_instance)
if paiement.is_valid(): if paiement.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
paiement.save() paiement.save()
reversion.set_user(request.user) 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é") messages.success(request, "Type de paiement modifié")
return redirect("/cotisations/index_paiement/") return redirect("/cotisations/index_paiement/")
return form({'factureform': paiement}, 'cotisations/facture.html', request) return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def del_paiement(request): def del_paiement(request):
"""Suppression d'un moyen de paiement"""
paiement = DelPaiementForm(request.POST or None) paiement = DelPaiementForm(request.POST or None)
if paiement.is_valid(): if paiement.is_valid():
paiement_dels = paiement.cleaned_data['paiements'] paiement_dels = paiement.cleaned_data['paiements']
@ -309,15 +438,24 @@ def del_paiement(request):
paiement_del.delete() paiement_del.delete()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Destruction") 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: 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 redirect("/cotisations/index_paiement/")
return form({'factureform': paiement}, 'cotisations/facture.html', request) return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def add_banque(request): def add_banque(request):
"""Ajoute une banque à la liste des banques"""
banque = BanqueForm(request.POST or None) banque = BanqueForm(request.POST or None)
if banque.is_valid(): if banque.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -328,27 +466,35 @@ def add_banque(request):
return redirect("/cotisations/index_banque/") return redirect("/cotisations/index_banque/")
return form({'factureform': banque}, 'cotisations/facture.html', request) return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def edit_banque(request, banqueid): def edit_banque(request, banqueid):
"""Edite le nom d'une banque"""
try: try:
banque_instance = Banque.objects.get(pk=banqueid) banque_instance = Banque.objects.get(pk=banqueid)
except Banque.DoesNotExist: except Banque.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante")
return redirect("/cotisations/index_banque/") return redirect("/cotisations/index_banque/")
banque = BanqueForm(request.POST or None, instance=banque_instance) banque = BanqueForm(request.POST or None, instance=banque_instance)
if banque.is_valid(): if banque.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
banque.save() banque.save()
reversion.set_user(request.user) 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") messages.success(request, "Banque modifiée")
return redirect("/cotisations/index_banque/") return redirect("/cotisations/index_banque/")
return form({'factureform': banque}, 'cotisations/facture.html', request) return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def del_banque(request): def del_banque(request):
"""Supprime une banque"""
banque = DelBanqueForm(request.POST or None) banque = DelBanqueForm(request.POST or None)
if banque.is_valid(): if banque.is_valid():
banque_dels = banque.cleaned_data['banques'] banque_dels = banque.cleaned_data['banques']
@ -360,17 +506,25 @@ def del_banque(request):
reversion.set_comment("Destruction") reversion.set_comment("Destruction")
messages.success(request, "La banque a été supprimée") messages.success(request, "La banque a été supprimée")
except ProtectedError: 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 redirect("/cotisations/index_banque/")
return form({'factureform': banque}, 'cotisations/facture.html', request) return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('tresorier') @permission_required('tresorier')
def control(request): 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 pagination_number = options.pagination_number
facture_list = Facture.objects.order_by('date').reverse() facture_list = Facture.objects.order_by('date').reverse()
controlform_set = modelformset_factory(Facture, fields=('control','valid'), extra=0) controlform_set = modelformset_factory(
Facture,
fields=('control', 'valid'),
extra=0
)
paginator = Paginator(facture_list, pagination_number) paginator = Paginator(facture_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -379,7 +533,9 @@ def control(request):
facture_list = paginator.page(1) facture_list = paginator.page(1)
except EmptyPage: except EmptyPage:
facture_list = paginator.page(paginator.num.pages) facture_list = paginator.page(paginator.num.pages)
page_query = Facture.objects.order_by('date').reverse().filter(id__in=[facture.id for facture in facture_list]) page_query = Facture.objects.order_by('date').reverse().filter(
id__in=[facture.id for facture in facture_list]
)
controlform = controlform_set(request.POST or None, queryset=page_query) controlform = controlform_set(request.POST or None, queryset=page_query)
if controlform.is_valid(): if controlform.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -387,32 +543,50 @@ def control(request):
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Controle trésorier") reversion.set_comment("Controle trésorier")
return redirect("/cotisations/control/") 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def index_article(request): def index_article(request):
"""Affiche l'ensemble des articles en vente"""
article_list = Article.objects.order_by('name') 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def index_paiement(request): def index_paiement(request):
"""Affiche l'ensemble des moyens de paiement en vente"""
paiement_list = Paiement.objects.order_by('moyen') 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def index_banque(request): def index_banque(request):
"""Affiche l'ensemble des banques"""
banque_list = Banque.objects.order_by('name') 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def index(request): 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 pagination_number = options.pagination_number
facture_list = Facture.objects.order_by('date').select_related('user').select_related('paiement').prefetch_related('vente_set').reverse() facture_list = Facture.objects.order_by('date').select_related('user')\
.select_related('paiement').prefetch_related('vente_set').reverse()
paginator = Paginator(facture_list, pagination_number) paginator = Paginator(facture_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -423,41 +597,47 @@ def index(request):
except EmptyPage: 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.
facture_list = paginator.page(paginator.num_pages) 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 @login_required
def history(request, object, id): def history(request, object, object_id):
"""Affiche l'historique de chaque objet"""
if object == 'facture': if object == 'facture':
try: try:
object_instance = Facture.objects.get(pk=id) object_instance = Facture.objects.get(pk=object_id)
except Facture.DoesNotExist: except Facture.DoesNotExist:
messages.error(request, "Facture inexistante") messages.error(request, "Facture inexistante")
return redirect("/cotisations/") return redirect("/cotisations/")
if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: if not request.user.has_perms(('cableur',))\
messages.error(request, "Vous ne pouvez pas afficher l'historique d'une facture d'un autre user que vous sans droit 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)) return redirect("/users/profil/" + str(request.user.id))
elif object == 'paiement' and request.user.has_perms(('cableur',)): elif object == 'paiement' and request.user.has_perms(('cableur',)):
try: try:
object_instance = Paiement.objects.get(pk=id) object_instance = Paiement.objects.get(pk=object_id)
except Paiement.DoesNotExist: except Paiement.DoesNotExist:
messages.error(request, "Paiement inexistant") messages.error(request, "Paiement inexistant")
return redirect("/cotisations/") return redirect("/cotisations/")
elif object == 'article' and request.user.has_perms(('cableur',)): elif object == 'article' and request.user.has_perms(('cableur',)):
try: try:
object_instance = Article.objects.get(pk=id) object_instance = Article.objects.get(pk=object_id)
except Article.DoesNotExist: except Article.DoesNotExist:
messages.error(request, "Article inexistante") messages.error(request, "Article inexistante")
return redirect("/cotisations/") return redirect("/cotisations/")
elif object == 'banque' and request.user.has_perms(('cableur',)): elif object == 'banque' and request.user.has_perms(('cableur',)):
try: try:
object_instance = Banque.objects.get(pk=id) object_instance = Banque.objects.get(pk=object_id)
except Banque.DoesNotExist: except Banque.DoesNotExist:
messages.error(request, "Banque inexistante") messages.error(request, "Banque inexistante")
return redirect("/cotisations/") return redirect("/cotisations/")
else: else:
messages.error(request, "Objet inconnu") messages.error(request, "Objet inconnu")
return redirect("/cotisations/") return redirect("/cotisations/")
options, created = GeneralOption.objects.get_or_create() options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance) reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number) paginator = Paginator(reversions, pagination_number)
@ -470,4 +650,7 @@ def history(request, object, id):
except EmptyPage: 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.
reversions = paginator.page(paginator.num_pages) 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
})

View file

@ -3,6 +3,7 @@
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyirght © 2017 Daniel Stan
# Copyright © 2017 Gabriel Détraz # Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # 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 : Inspirés d'autres exemples trouvés ici :
https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/
Inspiré du travail de Daniel Stan au Crans
""" """
import logging import logging
import netaddr import netaddr
import radiusd # Module magique freeradius (radiusd.py is dummy) import radiusd # Module magique freeradius (radiusd.py is dummy)
import os
import binascii import binascii
import hashlib import hashlib
import os, sys import os, sys
import os, sys
proj_path = "/var/www/re2o/" proj_path = "/var/www/re2o/"
# This is so Django knows where to find stuff. # This is so Django knows where to find stuff.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings")

View file

@ -19,7 +19,10 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
@ -29,7 +32,9 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url(r'^stats_logs$', views.stats_logs, name='stats-logs'), url(r'^stats_logs$', views.stats_logs, name='stats-logs'),
url(r'^revert_action/(?P<revision_id>[0-9]+)$', views.revert_action, name='revert-action'), url(r'^revert_action/(?P<revision_id>[0-9]+)$',
views.revert_action,
name='revert-action'),
url(r'^stats_general/$', views.stats_general, name='stats-general'), url(r'^stats_general/$', views.stats_general, name='stats-general'),
url(r'^stats_models/$', views.stats_models, name='stats-models'), url(r'^stats_models/$', views.stats_models, name='stats-models'),
url(r'^stats_users/$', views.stats_users, name='stats-users'), url(r'^stats_users/$', views.stats_users, name='stats-users'),

View file

@ -23,62 +23,67 @@
# App de gestion des statistiques pour re2o # App de gestion des statistiques pour re2o
# Gabriel Détraz # Gabriel Détraz
# Gplv2 # 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 __future__ import unicode_literals
from django.http import HttpResponse
from django.shortcuts import render, redirect 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.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required 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 django.db.models import Count
from reversion.models import Revision from reversion.models import Revision
from reversion.models import Version, ContentType from reversion.models import Version, ContentType
from users.models import User, ServiceUser, Right, School, ListRight, ListShell, Ban, Whitelist from users.models import User, ServiceUser, Right, School, ListRight, ListShell
from users.models import all_has_access, all_whitelisted, all_baned, all_adherent from users.models import Ban, Whitelist
from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation from cotisations.models import Facture, Vente, Article, Banque, Paiement
from machines.models import Machine, MachineType, IpType, Extension, Interface, Domain, IpList from cotisations.models import Cotisation
from machines.views import all_active_assigned_interfaces_count, all_active_interfaces_count from machines.models import Machine, MachineType, IpType, Extension, Interface
from machines.models import Domain, IpList
from topologie.models import Switch, Port, Room from topologie.models import Switch, Port, Room
from preferences.models import GeneralOption from preferences.models import GeneralOption
from re2o.views import form
from django.utils import timezone from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent
from dateutil.relativedelta import relativedelta from re2o.utils import all_active_assigned_interfaces_count
from re2o.utils import all_active_interfaces_count
STATS_DICT = { STATS_DICT = {
0 : ["Tout", 36], 0: ["Tout", 36],
1 : ["1 mois", 1], 1: ["1 mois", 1],
2 : ["2 mois", 2], 2: ["2 mois", 2],
3 : ["6 mois", 6], 3: ["6 mois", 6],
4 : ["1 an", 12], 4: ["1 an", 12],
5 : ["2 an", 24], 5: ["2 an", 24],
} }
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index(request): 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 pagination_number = options.pagination_number
# The types of content kept for display # 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 # Select only wanted versions
versions = Version.objects.filter(content_type__in=ContentType.objects.filter(model__in=content_type_filter)).order_by('revision__date_created').reverse().select_related('revision') versions = Version.objects.filter(
content_type__in=ContentType.objects.filter(
model__in=content_type_filter
)
).order_by('revision__date_created').reverse().select_related('revision')
paginator = Paginator(versions, pagination_number) paginator = Paginator(versions, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -95,30 +100,38 @@ def index(request):
# Items to remove later because invalid # Items to remove later because invalid
to_remove = [] to_remove = []
# Parse every item (max = pagination_number) # Parse every item (max = pagination_number)
for i in range( len( versions.object_list ) ): for i in range(len(versions.object_list)):
if versions.object_list[i].object : if versions.object_list[i].object:
v = versions.object_list[i] version = versions.object_list[i]
versions.object_list[i] = { versions.object_list[i] = {
'rev_id' : v.revision.id, 'rev_id': version.revision.id,
'comment': v.revision.comment, 'comment': version.revision.comment,
'datetime': v.revision.date_created.strftime('%d/%m/%y %H:%M:%S'), 'datetime': version.revision.date_created.strftime(
'username': v.revision.user.get_username() if v.revision.user else '?', '%d/%m/%y %H:%M:%S'
'user_id': v.revision.user_id, ),
'version': v } 'username':
else : version.revision.user.get_username()
to_remove.insert(0,i) if version.revision.user else '?',
'user_id': version.revision.user_id,
'version': version}
else:
to_remove.insert(0, i)
# Remove all tagged invalid items # Remove all tagged invalid items
for i in to_remove : for i in to_remove:
versions.object_list.pop(i) versions.object_list.pop(i)
return render(request, 'logs/index.html', {'versions_list': versions}) return render(request, 'logs/index.html', {'versions_list': versions})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def stats_logs(request): 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 pagination_number = options.pagination_number
revisions = Revision.objects.all().order_by('date_created').reverse().select_related('user').prefetch_related('version_set__object') revisions = Revision.objects.all().order_by('date_created')\
.reverse().select_related('user')\
.prefetch_related('version_set__object')
paginator = Paginator(revisions, pagination_number) paginator = Paginator(revisions, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -129,7 +142,10 @@ def stats_logs(request):
except EmptyPage: 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) 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 @login_required
@permission_required('bureau') @permission_required('bureau')
@ -138,36 +154,64 @@ def revert_action(request, revision_id):
try: try:
revision = Revision.objects.get(id=revision_id) revision = Revision.objects.get(id=revision_id)
except Revision.DoesNotExist: except Revision.DoesNotExist:
messages.error(request, u"Revision inexistante" ) messages.error(request, u"Revision inexistante")
if request.method == "POST": if request.method == "POST":
revision.revert() revision.revert()
messages.success(request, "L'action a été supprimée") messages.success(request, "L'action a été supprimée")
return redirect("/logs/") 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def stats_general(request): def stats_general(request):
all_active_users = User.objects.filter(state=User.STATE_ACTIVE) """Statistiques générales affinées sur les ip, activées, utilisées par
ip = dict() range, et les statistiques générales sur les users : users actifs,
cotisants, activés, archivés, etc"""
ip_dict = dict()
for ip_range in IpType.objects.all(): for ip_range in IpType.objects.all():
all_ip = IpList.objects.filter(ip_type=ip_range) all_ip = IpList.objects.filter(ip_type=ip_range)
used_ip = Interface.objects.filter(ipv4__in=all_ip).count() 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() active_ip = all_active_assigned_interfaces_count().filter(
ip[ip_range] = [ip_range, all_ip.count(), used_ip, active_ip, all_ip.count()-used_ip] ipv4__in=IpList.objects.filter(ip_type=ip_range)
).count()
ip_dict[ip_range] = [ip_range, all_ip.count(),
used_ip, active_ip, all_ip.count()-used_ip]
stats = [ stats = [
[["Categorie", "Nombre d'utilisateurs"], { [["Categorie", "Nombre d'utilisateurs"], {
'active_users' : ["Users actifs", User.objects.filter(state=User.STATE_ACTIVE).count()], 'active_users': [
'inactive_users' : ["Users désactivés", User.objects.filter(state=User.STATE_DISABLED).count()], "Users actifs",
'archive_users' : ["Users archivés", User.objects.filter(state=User.STATE_ARCHIVE).count()], User.objects.filter(state=User.STATE_ACTIVE).count()],
'adherent_users' : ["Adhérents à l'association", all_adherent().count()], 'inactive_users': [
'connexion_users' : ["Utilisateurs bénéficiant d'une connexion", all_has_access().count()], "Users désactivés",
'ban_users' : ["Utilisateurs bannis", all_baned().count()], User.objects.filter(state=User.STATE_DISABLED).count()],
'whitelisted_user' : ["Utilisateurs bénéficiant d'une connexion gracieuse", all_whitelisted().count()], 'archive_users': [
'actives_interfaces' : ["Interfaces actives (ayant accès au reseau)", all_active_interfaces_count().count()], "Users archivés",
'actives_assigned_interfaces' : ["Interfaces actives et assignées ipv4", all_active_assigned_interfaces_count().count()] 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] [["Range d'ip", "Nombre d'ip totales", "Ip assignées",
"Ip assignées à une machine active", "Ip non assignées"], ip_dict]
] ]
return render(request, 'logs/stats_general.html', {'stats_list': stats}) return render(request, 'logs/stats_general.html', {'stats_list': stats})
@ -175,84 +219,117 @@ def stats_general(request):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def stats_models(request): 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 = { stats = {
'Users' : { 'Users': {
'users' : [User.PRETTY_NAME, User.objects.count()], 'users': [User.PRETTY_NAME, User.objects.count()],
'serviceuser' : [ServiceUser.PRETTY_NAME, ServiceUser.objects.count()], 'serviceuser': [ServiceUser.PRETTY_NAME,
'right' : [Right.PRETTY_NAME, Right.objects.count()], ServiceUser.objects.count()],
'school' : [School.PRETTY_NAME, School.objects.count()], 'right': [Right.PRETTY_NAME, Right.objects.count()],
'listright' : [ListRight.PRETTY_NAME, ListRight.objects.count()], 'school': [School.PRETTY_NAME, School.objects.count()],
'listshell' : [ListShell.PRETTY_NAME, ListShell.objects.count()], 'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()],
'ban' : [Ban.PRETTY_NAME, Ban.objects.count()], 'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()],
'whitelist' : [Whitelist.PRETTY_NAME, Whitelist.objects.count()] 'ban': [Ban.PRETTY_NAME, Ban.objects.count()],
'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()]
}, },
'Cotisations' : { 'Cotisations': {
'factures' : [Facture.PRETTY_NAME, Facture.objects.count()], 'factures': [Facture.PRETTY_NAME, Facture.objects.count()],
'vente' : [Vente.PRETTY_NAME, Vente.objects.count()], 'vente': [Vente.PRETTY_NAME, Vente.objects.count()],
'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()], 'cotisation': [Cotisation.PRETTY_NAME, Cotisation.objects.count()],
'article' : [Article.PRETTY_NAME, Article.objects.count()], 'article': [Article.PRETTY_NAME, Article.objects.count()],
'banque' : [Banque.PRETTY_NAME, Banque.objects.count()], 'banque': [Banque.PRETTY_NAME, Banque.objects.count()],
'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()],
}, },
'Machines' : { 'Machines': {
'machine' : [Machine.PRETTY_NAME, Machine.objects.count()], 'machine': [Machine.PRETTY_NAME, Machine.objects.count()],
'typemachine' : [MachineType.PRETTY_NAME, MachineType.objects.count()], 'typemachine': [MachineType.PRETTY_NAME,
'typeip' : [IpType.PRETTY_NAME, IpType.objects.count()], MachineType.objects.count()],
'extension' : [Extension.PRETTY_NAME, Extension.objects.count()], 'typeip': [IpType.PRETTY_NAME, IpType.objects.count()],
'interface' : [Interface.PRETTY_NAME, Interface.objects.count()], 'extension': [Extension.PRETTY_NAME, Extension.objects.count()],
'alias' : [Domain.PRETTY_NAME, Domain.objects.exclude(cname=None).count()], 'interface': [Interface.PRETTY_NAME, Interface.objects.count()],
'iplist' : [IpList.PRETTY_NAME, IpList.objects.count()], 'alias': [Domain.PRETTY_NAME,
Domain.objects.exclude(cname=None).count()],
'iplist': [IpList.PRETTY_NAME, IpList.objects.count()],
}, },
'Topologie' : { 'Topologie': {
'switch' : [Switch.PRETTY_NAME, Switch.objects.count()], 'switch': [Switch.PRETTY_NAME, Switch.objects.count()],
'port' : [Port.PRETTY_NAME, Port.objects.count()], 'port': [Port.PRETTY_NAME, Port.objects.count()],
'chambre' : [Room.PRETTY_NAME, Room.objects.count()], 'chambre': [Room.PRETTY_NAME, Room.objects.count()],
}, },
'Actions effectuées sur la base' : 'Actions effectuées sur la base':
{ {
'revision' : ["Nombre d'actions", Revision.objects.count()], '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 @login_required
@permission_required('cableur') @permission_required('cableur')
def stats_users(request): 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') onglet = request.GET.get('onglet')
try: try:
search_field = STATS_DICT[onglet] _search_field = STATS_DICT[onglet]
except: except KeyError:
search_field = STATS_DICT[0] _search_field = STATS_DICT[0]
onglet = 0 onglet = 0
start_date = timezone.now() + relativedelta(months=-search_field[1])
stats = { stats = {
'Utilisateur' : { 'Utilisateur': {
'Machines' : User.objects.annotate(num=Count('machine')).order_by('-num')[:10], 'Machines': User.objects.annotate(
'Facture' : User.objects.annotate(num=Count('facture')).order_by('-num')[:10], num=Count('machine')
'Bannissement' : User.objects.annotate(num=Count('ban')).order_by('-num')[:10], ).order_by('-num')[:10],
'Accès gracieux' : User.objects.annotate(num=Count('whitelist')).order_by('-num')[:10], 'Facture': User.objects.annotate(
'Droits' : User.objects.annotate(num=Count('right')).order_by('-num')[:10], 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' : { 'Etablissement': {
'Utilisateur' : School.objects.annotate(num=Count('user')).order_by('-num')[:10], 'Utilisateur': School.objects.annotate(
num=Count('user')
).order_by('-num')[:10],
}, },
'Moyen de paiement' : { 'Moyen de paiement': {
'Utilisateur' : Paiement.objects.annotate(num=Count('facture')).order_by('-num')[:10], 'Utilisateur': Paiement.objects.annotate(
num=Count('facture')
).order_by('-num')[:10],
}, },
'Banque' : { 'Banque': {
'Utilisateur' : Banque.objects.annotate(num=Count('facture')).order_by('-num')[:10], '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 @login_required
@permission_required('cableur') @permission_required('cableur')
def stats_actions(request): 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 = { stats = {
'Utilisateur' : { 'Utilisateur': {
'Action' : User.objects.annotate(num=Count('revision')).order_by('-num')[:40], 'Action': User.objects.annotate(
num=Count('revision')
).order_by('-num')[:40],
}, },
} }
return render(request, 'logs/stats_users.html', {'stats_list': stats}) return render(request, 'logs/stats_users.html', {'stats_list': stats})

View file

@ -40,7 +40,8 @@ class EditMachineForm(ModelForm):
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): 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' self.fields['name'].label = 'Nom de la machine'
class NewMachineForm(EditMachineForm): class NewMachineForm(EditMachineForm):
@ -57,7 +58,8 @@ class EditInterfaceForm(ModelForm):
fields = ['machine', 'type', 'ipv4', 'mac_address', 'details'] fields = ['machine', 'type', 'ipv4', 'mac_address', 'details']
def __init__(self, *args, **kwargs): 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['mac_address'].label = 'Adresse mac'
self.fields['type'].label = 'Type de machine' self.fields['type'].label = 'Type de machine'
self.fields['type'].empty_label = "Séléctionner un type de machine" self.fields['type'].empty_label = "Séléctionner un type de machine"
@ -110,9 +112,10 @@ class AliasForm(ModelForm):
fields = ['name','extension'] fields = ['name','extension']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
if 'infra' in kwargs: if 'infra' in kwargs:
infra = kwargs.pop('infra') infra = kwargs.pop('infra')
super(AliasForm, self).__init__(*args, **kwargs) super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs)
class DomainForm(AliasForm): class DomainForm(AliasForm):
class Meta(AliasForm.Meta): class Meta(AliasForm.Meta):
@ -125,7 +128,8 @@ class DomainForm(AliasForm):
initial = kwargs.get('initial', {}) initial = kwargs.get('initial', {})
initial['name'] = user.get_next_domain_name() initial['name'] = user.get_next_domain_name()
kwargs['initial'] = initial kwargs['initial'] = initial
super(DomainForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(DomainForm, self).__init__(*args, prefix=prefix, **kwargs)
class DelAliasForm(Form): class DelAliasForm(Form):
alias = forms.ModelMultipleChoiceField(queryset=Domain.objects.all(), label="Alias actuels", widget=forms.CheckboxSelectMultiple) alias = forms.ModelMultipleChoiceField(queryset=Domain.objects.all(), label="Alias actuels", widget=forms.CheckboxSelectMultiple)
@ -141,7 +145,8 @@ class MachineTypeForm(ModelForm):
fields = ['type','ip_type'] fields = ['type','ip_type']
def __init__(self, *args, **kwargs): 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['type'].label = 'Type de machine à ajouter'
self.fields['ip_type'].label = "Type d'ip relié" self.fields['ip_type'].label = "Type d'ip relié"
@ -153,9 +158,9 @@ class IpTypeForm(ModelForm):
model = IpType model = IpType
fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports'] fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports']
def __init__(self, *args, **kwargs): 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' self.fields['type'].label = 'Type ip à ajouter'
class EditIpTypeForm(IpTypeForm): class EditIpTypeForm(IpTypeForm):
@ -171,7 +176,8 @@ class ExtensionForm(ModelForm):
fields = ['name', 'need_infra', 'origin'] fields = ['name', 'need_infra', 'origin']
def __init__(self, *args, **kwargs): 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['name'].label = 'Extension à ajouter'
self.fields['origin'].label = 'Enregistrement A origin' self.fields['origin'].label = 'Enregistrement A origin'
@ -184,7 +190,8 @@ class MxForm(ModelForm):
fields = ['zone', 'priority', 'name'] fields = ['zone', 'priority', 'name']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MxForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(MxForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None) self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None)
class DelMxForm(Form): class DelMxForm(Form):
@ -196,25 +203,34 @@ class NsForm(ModelForm):
fields = ['zone', 'ns'] fields = ['zone', 'ns']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NsForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NsForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None) self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None)
class DelNsForm(Form): class DelNsForm(Form):
ns = forms.ModelMultipleChoiceField(queryset=Ns.objects.all(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple) ns = forms.ModelMultipleChoiceField(queryset=Ns.objects.all(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple)
class TextForm(ModelForm): class TxtForm(ModelForm):
class Meta: class Meta:
model = Text model = Text
fields = '__all__' fields = '__all__'
class DelTextForm(Form): def __init__(self, *args, **kwargs):
text = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Text actuels", widget=forms.CheckboxSelectMultiple) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(TxtForm, self).__init__(*args, prefix=prefix, **kwargs)
class DelTxtForm(Form):
txt = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Txt actuels", widget=forms.CheckboxSelectMultiple)
class NasForm(ModelForm): class NasForm(ModelForm):
class Meta: class Meta:
model = Nas model = Nas
fields = '__all__' 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): class DelNasForm(Form):
nas = forms.ModelMultipleChoiceField(queryset=Nas.objects.all(), label="Enregistrements Nas actuels", widget=forms.CheckboxSelectMultiple) nas = forms.ModelMultipleChoiceField(queryset=Nas.objects.all(), label="Enregistrements Nas actuels", widget=forms.CheckboxSelectMultiple)
@ -223,6 +239,10 @@ class ServiceForm(ModelForm):
model = Service model = Service
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs)
def save(self, commit=True): def save(self, commit=True):
instance = super(ServiceForm, self).save(commit=False) instance = super(ServiceForm, self).save(commit=False)
if commit: if commit:
@ -238,6 +258,10 @@ class VlanForm(ModelForm):
model = Vlan model = Vlan
fields = '__all__' 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): class DelVlanForm(Form):
vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple) vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple)
@ -246,8 +270,16 @@ class EditOuverturePortConfigForm(ModelForm):
model = Interface model = Interface
fields = ['port_lists'] fields = ['port_lists']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditOuverturePortConfigForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditOuverturePortListForm(ModelForm): class EditOuverturePortListForm(ModelForm):
class Meta: class Meta:
model = OuverturePortList model = OuverturePortList
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditOuverturePortListForm, self).__init__(*args, prefix=prefix, **kwargs)

View file

@ -31,15 +31,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
{% for text in text_list %} {% for txt in txt_list %}
<tr> <tr>
<td>{{ text.zone }}</td> <td>{{ txt.zone }}</td>
<td>{{ text.dns_entry }}</td> <td>{{ txt.dns_entry }}</td>
<td class="text-right"> <td class="text-right">
{% if is_infra %} {% 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 %} {% 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 %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -47,12 +47,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-ns' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement NS</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-ns' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement NS</a>
{% endif %} {% endif %}
{% include "machines/aff_ns.html" with ns_list=ns_list %} {% include "machines/aff_ns.html" with ns_list=ns_list %}
<h2>Liste des enregistrements Text</h2> <h2>Liste des enregistrements TXT</h2>
{% if is_infra %} {% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-text' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement TXT</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-txt' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement TXT</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-text' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement TXT</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-txt' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement TXT</a>
{% endif %} {% endif %}
{% include "machines/aff_text.html" with text_list=text_list %} {% include "machines/aff_txt.html" with txt_list=txt_list %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load bootstrap_form_typeahead %} {% load massive_bootstrap_form %}
{% block title %}Création et modification de machines{% endblock %} {% 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 %} {% if domainform %}
{% bootstrap_form_errors domainform %} {% bootstrap_form_errors domainform %}
{% endif %} {% 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 %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
@ -48,24 +78,56 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
{% if interfaceform %} {% if interfaceform %}
<h3>Interface</h3> <h3>Interface</h3>
{% if i_bft_param %} {% if i_mbf_param %}
{% if 'machine' in interfaceform.fields %} {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %}
{% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %}
{% else %} {% else %}
{% bootstrap_form_typeahead interfaceform 'ipv4' bft_param=i_bft_param %} {% massive_bootstrap_form interfaceform 'ipv4,machine' %}
{% endif %}
{% else %}
{% if 'machine' in interfaceform.fields %}
{% bootstrap_form_typeahead interfaceform 'ipv4,machine' %}
{% else %}
{% bootstrap_form_typeahead interfaceform 'ipv4' %}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if domainform %} {% if domainform %}
<h3>Domaine</h3> <h3>Domaine</h3>
{% bootstrap_form domainform %} {% bootstrap_form domainform %}
{% endif %} {% endif %}
{% if iptypeform %}
<h3>Type d'IP</h3>
{% bootstrap_form iptypeform %}
{% endif %}
{% if machinetypeform %}
<h3>Type de machine</h3>
{% bootstrap_form machinetypeform %}
{% endif %}
{% if extensionform %}
<h3>Extension</h3>
{% massive_bootstrap_form extensionform 'origin' %}
{% endif %}
{% if mxform %}
<h3>Enregistrement MX</h3>
{% massive_bootstrap_form mxform 'name' %}
{% endif %}
{% if nsform %}
<h3>Enregistrement NS</h3>
{% massive_bootstrap_form nsform 'ns' %}
{% endif %}
{% if txtform %}
<h3>Enregistrement TXT</h3>
{% bootstrap_form txtform %}
{% endif %}
{% if aliasform %}
<h3>Alias</h3>
{% bootstrap_form aliasform %}
{% endif %}
{% if serviceform %}
<h3>Service</h3>
{% massive_bootstrap_form serviceform 'servers' %}
{% endif %}
{% if vlanform %}
<h3>Vlan</h3>
{% bootstrap_form vlanform %}
{% endif %}
{% if nasform %}
<h3>NAS</h3>
{% bootstrap_form nasform %}
{% endif %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form> </form>
<br /> <br />

View file

@ -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_<fieldname>' and 'engine_<fieldname>' 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
[ '<field1>[,<field2>[,...]]' ]
[ {
[ 'choices': {
[ '<field1>': '<choices1>'
[, '<field2>': '<choices2>'
[, ... ] ] ]
} ]
[, 'engine': {
[ '<field1>': '<engine1>'
[, '<field2>': '<engine2>'
[, ... ] ] ]
} ]
[, 'match_func': {
[ '<field1>': '<match_func1>'
[, '<field2>': '<match_func2>'
[, ... ] ] ]
} ]
[, 'update_on': {
[ '<field1>': '<update_on1>'
[, '<field2>': '<update_on2>'
[, ... ] ] ]
} ]
} ]
[ <standard boostrap_form parameters> ]
%}
**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_<fieldname> """
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_<field_name> """
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 )
)

View file

@ -47,9 +47,9 @@ urlpatterns = [
url(r'^add_mx/$', views.add_mx, name='add-mx'), url(r'^add_mx/$', views.add_mx, name='add-mx'),
url(r'^edit_mx/(?P<mxid>[0-9]+)$', views.edit_mx, name='edit-mx'), url(r'^edit_mx/(?P<mxid>[0-9]+)$', views.edit_mx, name='edit-mx'),
url(r'^del_mx/$', views.del_mx, name='del-mx'), url(r'^del_mx/$', views.del_mx, name='del-mx'),
url(r'^add_text/$', views.add_text, name='add-text'), url(r'^add_txt/$', views.add_txt, name='add-txt'),
url(r'^edit_text/(?P<textid>[0-9]+)$', views.edit_text, name='edit-text'), url(r'^edit_txt/(?P<textid>[0-9]+)$', views.edit_txt, name='edit-txt'),
url(r'^del_text/$', views.del_text, name='del-text'), url(r'^del_txt/$', views.del_txt, name='del-txt'),
url(r'^add_ns/$', views.add_ns, name='add-ns'), url(r'^add_ns/$', views.add_ns, name='add-ns'),
url(r'^edit_ns/(?P<nsid>[0-9]+)$', views.edit_ns, name='edit-ns'), url(r'^edit_ns/(?P<nsid>[0-9]+)$', views.edit_ns, name='edit-ns'),
url(r'^del_ns/$', views.del_ns, name='del-ns'), url(r'^del_ns/$', views.del_ns, name='del-ns'),
@ -76,7 +76,7 @@ urlpatterns = [
url(r'^history/(?P<object>extension)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>extension)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>mx)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>mx)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>ns)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>ns)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>text)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>txt)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>iptype)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>iptype)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>alias)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>alias)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>vlan)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>vlan)/(?P<id>[0-9]+)$', views.history, name='history'),

View file

@ -79,8 +79,8 @@ from .forms import (
DelAliasForm, DelAliasForm,
NsForm, NsForm,
DelNsForm, DelNsForm,
TextForm, TxtForm,
DelTextForm, DelTxtForm,
MxForm, MxForm,
DelMxForm, DelMxForm,
VlanForm, VlanForm,
@ -110,58 +110,24 @@ from .models import (
OuverturePort OuverturePort
) )
from users.models import User from users.models import User
from users.models import all_has_access
from preferences.models import GeneralOption, OptionalMachine from preferences.models import GeneralOption, OptionalMachine
from .templatetags.bootstrap_form_typeahead import hidden_id, input_id
def filter_active_interfaces(q): from re2o.templatetags.massive_bootstrap_form import hidden_id, input_id
"""Filtre les machines autorisées à sortir sur internet dans une requête""" from re2o.utils import (
return q.filter( all_active_assigned_interfaces,
machine__in=Machine.objects.filter( all_has_access,
user__in=all_has_access() filter_active_interfaces
).filter(active=True)) \ )
.select_related('domain') \ from re2o.views import form
.select_related('machine') \
.select_related('type') \
.select_related('ipv4') \
.select_related('domain__extension') \
.select_related('ipv4__ip_type').distinct()
def all_active_interfaces():
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
return filter_active_interfaces(Interface.objects)
def all_active_assigned_interfaces():
"""
Renvoie l'ensemble des machines qui ont une ipv4 assignées et disposant de
l'accès internet
"""
return all_active_interfaces().filter(ipv4__isnull=False)
def all_active_interfaces_count():
""" Version light seulement pour compter"""
return Interface.objects.filter(
machine__in=Machine.objects.filter(user__in=all_has_access())\
.filter(active=True)
)
def all_active_assigned_interfaces_count():
""" Version light seulement pour compter"""
return all_active_interfaces_count().filter(ipv4__isnull=False)
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
def f_type_id( is_type_tt ): def f_type_id( is_type_tt ):
""" The id that will be used in HTML to store the value of the field """ 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 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 ) : 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'] f_ipv4 = form.fields['ipv4']
used_mtype_id = [] used_mtype_id = []
@ -189,7 +155,7 @@ def generate_ipv4_choices( form ) :
return choices return choices
def generate_ipv4_engine( is_type_tt ) : 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 ( return (
'new Bloodhound( {{' 'new Bloodhound( {{'
@ -203,7 +169,7 @@ def generate_ipv4_engine( is_type_tt ) :
) )
def generate_ipv4_match_func( 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 ( return (
'function(q, sync) {{' 'function(q, sync) {{'
@ -219,20 +185,20 @@ def generate_ipv4_match_func( is_type_tt ) :
type_id = f_type_id( is_type_tt ) type_id = f_type_id( is_type_tt )
) )
def generate_ipv4_bft_param( form, is_type_tt ): def generate_ipv4_mbf_param( form, is_type_tt ):
""" Generate all the parameters to use with the bootstrap_form_typeahead """ Generate all the parameters to use with the massive_bootstrap_form
tag """ tag """
i_choices = { 'ipv4': generate_ipv4_choices( form ) } i_choices = { 'ipv4': generate_ipv4_choices( form ) }
i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) }
i_match_func = { 'ipv4': generate_ipv4_match_func( 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_update_on = { 'ipv4': [f_type_id( is_type_tt )] }
i_bft_param = { i_mbf_param = {
'choices': i_choices, 'choices': i_choices,
'engine': i_engine, 'engine': i_engine,
'match_func': i_match_func, 'match_func': i_match_func,
'update_on': i_update_on 'update_on': i_update_on
} }
return i_bft_param return i_mbf_param
@login_required @login_required
def new_machine(request, userid): def new_machine(request, userid):
@ -282,8 +248,8 @@ def new_machine(request, userid):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "La machine a été créée") messages.success(request, "La machine a été créée")
return redirect("/users/profil/" + str(user.id)) return redirect("/users/profil/" + str(user.id))
i_bft_param = generate_ipv4_bft_param( interface, False ) i_mbf_param = generate_ipv4_mbf_param( interface, False )
return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request)
@login_required @login_required
def edit_interface(request, interfaceid): def edit_interface(request, interfaceid):
@ -322,8 +288,8 @@ def edit_interface(request, interfaceid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) 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") messages.success(request, "La machine a été modifiée")
return redirect("/users/profil/" + str(interface.machine.user.id)) return redirect("/users/profil/" + str(interface.machine.user.id))
i_bft_param = generate_ipv4_bft_param( interface_form, False ) i_mbf_param = generate_ipv4_mbf_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) return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request)
@login_required @login_required
def del_machine(request, machineid): def del_machine(request, machineid):
@ -381,8 +347,8 @@ def new_interface(request, machineid):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "L'interface a été ajoutée") messages.success(request, "L'interface a été ajoutée")
return redirect("/users/profil/" + str(machine.user.id)) return redirect("/users/profil/" + str(machine.user.id))
i_bft_param = generate_ipv4_bft_param( interface_form, False ) i_mbf_param = generate_ipv4_mbf_param( interface_form, False )
return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request)
@login_required @login_required
def del_interface(request, interfaceid): def del_interface(request, interfaceid):
@ -419,7 +385,7 @@ def add_iptype(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Ce type d'ip a été ajouté") messages.success(request, "Ce type d'ip a été ajouté")
return redirect("/machines/index_iptype") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -438,7 +404,7 @@ def edit_iptype(request, iptypeid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data))
messages.success(request, "Type d'ip modifié") messages.success(request, "Type d'ip modifié")
return redirect("/machines/index_iptype/") return redirect("/machines/index_iptype/")
return form({'machineform': iptype}, 'machines/machine.html', request) return form({'iptypeform': iptype}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -456,7 +422,7 @@ def del_iptype(request):
except ProtectedError: 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) 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 redirect("/machines/index_iptype")
return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request) return form({'iptypeform': iptype}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -469,7 +435,7 @@ def add_machinetype(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Ce type de machine a été ajouté") messages.success(request, "Ce type de machine a été ajouté")
return redirect("/machines/index_machinetype") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -487,7 +453,7 @@ def edit_machinetype(request, machinetypeid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data))
messages.success(request, "Type de machine modifié") messages.success(request, "Type de machine modifié")
return redirect("/machines/index_machinetype/") return redirect("/machines/index_machinetype/")
return form({'machineform': machinetype}, 'machines/machine.html', request) return form({'machinetypeform': machinetype}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -504,7 +470,7 @@ def del_machinetype(request):
except ProtectedError: 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) 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 redirect("/machines/index_machinetype")
return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request) return form({'machinetypeform': machinetype}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -517,7 +483,7 @@ def add_extension(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cette extension a été ajoutée") messages.success(request, "Cette extension a été ajoutée")
return redirect("/machines/index_extension") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -535,7 +501,7 @@ def edit_extension(request, extensionid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data))
messages.success(request, "Extension modifiée") messages.success(request, "Extension modifiée")
return redirect("/machines/index_extension/") return redirect("/machines/index_extension/")
return form({'machineform': extension}, 'machines/machine.html', request) return form({'extensionform': extension}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -552,7 +518,7 @@ def del_extension(request):
except ProtectedError: 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) 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 redirect("/machines/index_extension")
return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request) return form({'extensionform': extension}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -565,7 +531,7 @@ def add_mx(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement mx a été ajouté") messages.success(request, "Cet enregistrement mx a été ajouté")
return redirect("/machines/index_extension") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -583,7 +549,7 @@ def edit_mx(request, mxid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data))
messages.success(request, "Mx modifié") messages.success(request, "Mx modifié")
return redirect("/machines/index_extension/") return redirect("/machines/index_extension/")
return form({'machineform': mx}, 'machines/machine.html', request) return form({'mxform': mx}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -600,7 +566,7 @@ def del_mx(request):
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del) messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del)
return redirect("/machines/index_extension") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -613,7 +579,7 @@ def add_ns(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement ns a été ajouté") messages.success(request, "Cet enregistrement ns a été ajouté")
return redirect("/machines/index_extension") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -631,7 +597,7 @@ def edit_ns(request, nsid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data))
messages.success(request, "Ns modifié") messages.success(request, "Ns modifié")
return redirect("/machines/index_extension/") return redirect("/machines/index_extension/")
return form({'machineform': ns}, 'machines/machine.html', request) return form({'nsform': ns}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -648,55 +614,55 @@ def del_ns(request):
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del) messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del)
return redirect("/machines/index_extension") 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 @login_required
@permission_required('infra') @permission_required('infra')
def add_text(request): def add_txt(request):
text = TextForm(request.POST or None) txt = TxtForm(request.POST or None)
if text.is_valid(): if txt.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
text.save() txt.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement text a été ajouté") messages.success(request, "Cet enregistrement text a été ajouté")
return redirect("/machines/index_extension") 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 @login_required
@permission_required('infra') @permission_required('infra')
def edit_text(request, textid): def edit_txt(request, txtid):
try: try:
text_instance = Text.objects.get(pk=textid) txt_instance = Text.objects.get(pk=txtid)
except Text.DoesNotExist: except Text.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante" )
return redirect("/machines/index_extension/") return redirect("/machines/index_extension/")
text = TextForm(request.POST or None, instance=text_instance) txt = TxtForm(request.POST or None, instance=txt_instance)
if text.is_valid(): if txt.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
text.save() txt.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in text.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data))
messages.success(request, "Text modifié") messages.success(request, "Txt modifié")
return redirect("/machines/index_extension/") return redirect("/machines/index_extension/")
return form({'machineform': text}, 'machines/machine.html', request) return form({'txtform': txt}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_text(request): def del_txt(request):
text = DelTextForm(request.POST or None) txt = DelTxtForm(request.POST or None)
if text.is_valid(): if txt.is_valid():
text_dels = text.cleaned_data['text'] txt_dels = txt.cleaned_data['txt']
for text_del in text_dels: for txt_del in txt_dels:
try: try:
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
text_del.delete() txt_del.delete()
reversion.set_user(request.user) reversion.set_user(request.user)
messages.success(request, "Le text a été supprimé") messages.success(request, "Le txt a été supprimé")
except ProtectedError: 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 redirect("/machines/index_extension")
return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request) return form({'txtform': txt}, 'machines/machine.html', request)
@login_required @login_required
def add_alias(request, interfaceid): def add_alias(request, interfaceid):
@ -724,7 +690,7 @@ def add_alias(request, interfaceid):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet alias a été ajouté") messages.success(request, "Cet alias a été ajouté")
return redirect("/machines/index_alias/" + str(interfaceid)) 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 @login_required
def edit_alias(request, aliasid): def edit_alias(request, aliasid):
@ -744,7 +710,7 @@ def edit_alias(request, aliasid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data))
messages.success(request, "Alias modifié") messages.success(request, "Alias modifié")
return redirect("/machines/index_alias/" + str(alias_instance.cname.interface_parent.id)) 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 @login_required
def del_alias(request, interfaceid): def del_alias(request, interfaceid):
@ -768,7 +734,7 @@ def del_alias(request, interfaceid):
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del) messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del)
return redirect("/machines/index_alias/" + str(interfaceid)) 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 @login_required
@ -782,7 +748,7 @@ def add_service(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement service a été ajouté") messages.success(request, "Cet enregistrement service a été ajouté")
return redirect("/machines/index_service") return redirect("/machines/index_service")
return form({'machineform': service}, 'machines/machine.html', request) return form({'serviceform': service}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -800,7 +766,7 @@ def edit_service(request, serviceid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data))
messages.success(request, "Service modifié") messages.success(request, "Service modifié")
return redirect("/machines/index_service/") return redirect("/machines/index_service/")
return form({'machineform': service}, 'machines/machine.html', request) return form({'serviceform': service}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -817,7 +783,7 @@ def del_service(request):
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del) messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del)
return redirect("/machines/index_service") return redirect("/machines/index_service")
return form({'machineform': service}, 'machines/machine.html', request) return form({'serviceform': service}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -830,7 +796,7 @@ def add_vlan(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement vlan a été ajouté") messages.success(request, "Cet enregistrement vlan a été ajouté")
return redirect("/machines/index_vlan") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -848,7 +814,7 @@ def edit_vlan(request, vlanid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data))
messages.success(request, "Vlan modifié") messages.success(request, "Vlan modifié")
return redirect("/machines/index_vlan/") return redirect("/machines/index_vlan/")
return form({'machineform': vlan}, 'machines/machine.html', request) return form({'vlanform': vlan}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -865,7 +831,7 @@ def del_vlan(request):
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del) messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del)
return redirect("/machines/index_vlan") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -878,7 +844,7 @@ def add_nas(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement nas a été ajouté") messages.success(request, "Cet enregistrement nas a été ajouté")
return redirect("/machines/index_nas") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -896,7 +862,7 @@ def edit_nas(request, nasid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data))
messages.success(request, "Nas modifié") messages.success(request, "Nas modifié")
return redirect("/machines/index_nas/") return redirect("/machines/index_nas/")
return form({'machineform': nas}, 'machines/machine.html', request) return form({'nasform': nas}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -913,7 +879,7 @@ def del_nas(request):
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del) messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del)
return redirect("/machines/index_nas") 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 @login_required
@permission_required('cableur') @permission_required('cableur')
@ -1039,11 +1005,11 @@ def history(request, object, id):
except Mx.DoesNotExist: except Mx.DoesNotExist:
messages.error(request, "Mx inexistant") messages.error(request, "Mx inexistant")
return redirect("/machines/") return redirect("/machines/")
elif object == 'text' and request.user.has_perms(('cableur',)): elif object == 'txt' and request.user.has_perms(('cableur',)):
try: try:
object_instance = Text.objects.get(pk=id) object_instance = Text.objects.get(pk=id)
except Text.DoesNotExist: except Text.DoesNotExist:
messages.error(request, "Text inexistant") messages.error(request, "Txt inexistant")
return redirect("/machines/") return redirect("/machines/")
elif object == 'ns' and request.user.has_perms(('cableur',)): elif object == 'ns' and request.user.has_perms(('cableur',)):
try: try:

View file

@ -20,35 +20,53 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Classes admin pour les models de preferences
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from reversion.admin import VersionAdmin 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 OptionalUserAdmin(VersionAdmin):
"""Class admin options user"""
pass pass
class OptionalTopologieAdmin(VersionAdmin): class OptionalTopologieAdmin(VersionAdmin):
"""Class admin options topologie"""
pass pass
class OptionalMachineAdmin(VersionAdmin): class OptionalMachineAdmin(VersionAdmin):
"""Class admin options machines"""
pass pass
class GeneralOptionAdmin(VersionAdmin): class GeneralOptionAdmin(VersionAdmin):
"""Class admin options générales"""
pass pass
class ServiceAdmin(VersionAdmin): class ServiceAdmin(VersionAdmin):
"""Class admin gestion des services de la page d'accueil"""
pass pass
class AssoOptionAdmin(VersionAdmin): class AssoOptionAdmin(VersionAdmin):
"""Class admin options de l'asso"""
pass pass
class MailMessageOptionAdmin(VersionAdmin): class MailMessageOptionAdmin(VersionAdmin):
"""Class admin options mail"""
pass pass
admin.site.register(OptionalUser, OptionalUserAdmin) admin.site.register(OptionalUser, OptionalUserAdmin)
admin.site.register(OptionalMachine, OptionalMachineAdmin) admin.site.register(OptionalMachine, OptionalMachineAdmin)
admin.site.register(OptionalTopologie, OptionalTopologieAdmin) admin.site.register(OptionalTopologie, OptionalTopologieAdmin)

View file

@ -19,66 +19,116 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from django.forms import ModelForm, Form, ValidationError from django.forms import ModelForm, Form
from django import forms from django import forms
from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, AssoOption, MailMessageOption, Service from .models import OptionalUser, OptionalMachine, OptionalTopologie
from django.db.models import Q from .models import GeneralOption, AssoOption, MailMessageOption, Service
class EditOptionalUserForm(ModelForm): class EditOptionalUserForm(ModelForm):
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
class Meta: class Meta:
model = OptionalUser model = OptionalUser
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditOptionalUserForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['is_tel_mandatory'].label = 'Exiger un numéro de téléphone' super(EditOptionalUserForm, self).__init__(
self.fields['user_solde'].label = 'Activation du solde pour les utilisateurs' *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): class EditOptionalMachineForm(ModelForm):
"""Options machines (max de machines, etc)"""
class Meta: class Meta:
model = OptionalMachine model = OptionalMachine
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditOptionalMachineForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['password_machine'].label = "Possibilité d'attribuer un mot de passe par interface" super(EditOptionalMachineForm, self).__init__(
self.fields['max_lambdauser_interfaces'].label = "Maximum d'interfaces autorisées pour un user normal" *args,
self.fields['max_lambdauser_aliases'].label = "Maximum d'alias dns autorisés pour un user normal" 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): class EditOptionalTopologieForm(ModelForm):
"""Options de topologie, formulaire d'edition (vlan par default etc)"""
class Meta: class Meta:
model = OptionalTopologie model = OptionalTopologie
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditOptionalTopologieForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['vlan_decision_ok'].label = "Vlan où placer les machines après acceptation RADIUS" super(EditOptionalTopologieForm, self).__init__(
self.fields['vlan_decision_nok'].label = "Vlan où placer les machines après rejet RADIUS" *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): class EditGeneralOptionForm(ModelForm):
"""Options générales (affichages de résultats de recherche, etc)"""
class Meta: class Meta:
model = GeneralOption model = GeneralOption
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditGeneralOptionForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['search_display_page'].label = 'Resultats affichés dans une recherche' super(EditGeneralOptionForm, self).__init__(
self.fields['pagination_number'].label = 'Items par page, taille normale (ex users)' *args,
self.fields['pagination_large_number'].label = 'Items par page, taille élevée (machines)' prefix=prefix,
self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien de reinitialisation de mot de passe (en heures)' **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['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): class EditAssoOptionForm(ModelForm):
"""Options de l'asso (addresse, telephone, etc)"""
class Meta: class Meta:
model = AssoOption model = AssoOption
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): 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['name'].label = 'Nom de l\'asso'
self.fields['siret'].label = 'SIRET' self.fields['siret'].label = 'SIRET'
self.fields['adresse1'].label = 'Adresse (ligne 1)' self.fields['adresse1'].label = 'Adresse (ligne 1)'
@ -86,22 +136,44 @@ class EditAssoOptionForm(ModelForm):
self.fields['contact'].label = 'Email de contact' self.fields['contact'].label = 'Email de contact'
self.fields['telephone'].label = 'Numéro de téléphone' self.fields['telephone'].label = 'Numéro de téléphone'
self.fields['pseudo'].label = 'Pseudo d\'usage' 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): class EditMailMessageOptionForm(ModelForm):
"""Formulaire d'edition des messages de bienvenue personnalisés"""
class Meta: class Meta:
model = MailMessageOption model = MailMessageOption
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditMailMessageOptionForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['welcome_mail_fr'].label = 'Message dans le mail de bienvenue en français' super(EditMailMessageOptionForm, self).__init__(
self.fields['welcome_mail_en'].label = 'Message dans le mail de bienvenue en anglais' *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): class ServiceForm(ModelForm):
"""Edition, ajout de services sur la page d'accueil"""
class Meta: class Meta:
model = Service model = Service
fields = '__all__' 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): 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
)

View file

@ -20,26 +20,38 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from django.db import models from django.db import models
from cotisations.models import Paiement from cotisations.models import Paiement
from machines.models import Vlan
class OptionalUser(models.Model): 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" PRETTY_NAME = "Options utilisateur"
is_tel_mandatory = models.BooleanField(default=True) is_tel_mandatory = models.BooleanField(default=True)
user_solde = models.BooleanField(default=False) 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) gpg_fingerprint = models.BooleanField(default=True)
def clean(self): def clean(self):
"""Creation du mode de paiement par solde"""
if self.user_solde: if self.user_solde:
Paiement.objects.get_or_create(moyen="Solde") Paiement.objects.get_or_create(moyen="Solde")
class OptionalMachine(models.Model): 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" PRETTY_NAME = "Options machines"
password_machine = models.BooleanField(default=False) password_machine = models.BooleanField(default=False)
@ -47,21 +59,43 @@ class OptionalMachine(models.Model):
max_lambdauser_aliases = models.IntegerField(default=10) max_lambdauser_aliases = models.IntegerField(default=10)
ipv6 = models.BooleanField(default=False) ipv6 = models.BooleanField(default=False)
class OptionalTopologie(models.Model): 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" PRETTY_NAME = "Options topologie"
MACHINE = 'MACHINE' MACHINE = 'MACHINE'
DEFINED = 'DEFINED' DEFINED = 'DEFINED'
CHOICE_RADIUS = ( CHOICE_RADIUS = (
(MACHINE, 'Sur le vlan de la plage ip machine'), (MACHINE, 'Sur le vlan de la plage ip machine'),
(DEFINED, 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"'), (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): class GeneralOption(models.Model):
"""Options générales : nombre de resultats par page, nom du site,
temps les liens sont valides"""
PRETTY_NAME = "Options générales" PRETTY_NAME = "Options générales"
search_display_page = models.IntegerField(default=15) search_display_page = models.IntegerField(default=15)
@ -71,7 +105,10 @@ class GeneralOption(models.Model):
site_name = models.CharField(max_length=32, default="Re2o") site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@serveur.net") email_from = models.EmailField(default="www-data@serveur.net")
class Service(models.Model): 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) name = models.CharField(max_length=32)
url = models.URLField() url = models.URLField()
description = models.TextField() description = models.TextField()
@ -80,21 +117,32 @@ class Service(models.Model):
def __str__(self): def __str__(self):
return str(self.name) return str(self.name)
class AssoOption(models.Model): class AssoOption(models.Model):
"""Options générales de l'asso : siret, addresse, nom, etc"""
PRETTY_NAME = "Options de l'association" 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) siret = models.CharField(default="00000000000000", max_length=32)
adresse1 = models.CharField(default="1 Rue de exemple", max_length=128) adresse1 = models.CharField(default="1 Rue de exemple", max_length=128)
adresse2 = models.CharField(default="94230 Cachan", max_length=128) adresse2 = models.CharField(default="94230 Cachan", max_length=128)
contact = models.EmailField(default="contact@example.org") contact = models.EmailField(default="contact@example.org")
telephone = models.CharField(max_length=15, default="0000000000") telephone = models.CharField(max_length=15, default="0000000000")
pseudo = models.CharField(default="Asso", max_length=32) 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): class MailMessageOption(models.Model):
"""Reglages, mail de bienvenue et autre"""
PRETTY_NAME = "Options de corps de mail" PRETTY_NAME = "Options de corps de mail"
welcome_mail_fr = models.TextField(default="") welcome_mail_fr = models.TextField(default="")
welcome_mail_en = models.TextField(default="") welcome_mail_en = models.TextField(default="")

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% block title %}Création et modification des préférences{% endblock %} {% 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.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form options %} {% massive_bootstrap_form options 'utilisateur_asso' %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form> </form>
<br /> <br />

View file

@ -19,6 +19,9 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 from __future__ import unicode_literals
@ -28,15 +31,47 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^edit_options/(?P<section>OptionalUser)$', views.edit_options, name='edit-options'), url(
url(r'^edit_options/(?P<section>OptionalMachine)$', views.edit_options, name='edit-options'), r'^edit_options/(?P<section>OptionalUser)$',
url(r'^edit_options/(?P<section>OptionalTopologie)$', views.edit_options, name='edit-options'), views.edit_options,
url(r'^edit_options/(?P<section>GeneralOption)$', views.edit_options, name='edit-options'), name='edit-options'
url(r'^edit_options/(?P<section>AssoOption)$', views.edit_options, name='edit-options'), ),
url(r'^edit_options/(?P<section>MailMessageOption)$', views.edit_options, name='edit-options'), url(
r'^edit_options/(?P<section>OptionalMachine)$',
views.edit_options,
name='edit-options'
),
url(
r'^edit_options/(?P<section>OptionalTopologie)$',
views.edit_options,
name='edit-options'
),
url(
r'^edit_options/(?P<section>GeneralOption)$',
views.edit_options,
name='edit-options'
),
url(
r'^edit_options/(?P<section>AssoOption)$',
views.edit_options,
name='edit-options'
),
url(
r'^edit_options/(?P<section>MailMessageOption)$',
views.edit_options,
name='edit-options'
),
url(r'^add_services/$', views.add_services, name='add-services'), url(r'^add_services/$', views.add_services, name='add-services'),
url(r'^edit_services/(?P<servicesid>[0-9]+)$', views.edit_services, name='edit-services'), url(
r'^edit_services/(?P<servicesid>[0-9]+)$',
views.edit_services,
name='edit-services'
),
url(r'^del_services/$', views.del_services, name='del-services'), url(r'^del_services/$', views.del_services, name='del-services'),
url(r'^history/(?P<object>service)/(?P<id>[0-9]+)$', views.history, name='history'), url(
r'^history/(?P<object>service)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(r'^$', views.display_options, name='display-options'), url(r'^$', views.display_options, name='display-options'),
] ]

View file

@ -23,48 +23,53 @@
# App de gestion des machines pour re2o # App de gestion des machines pour re2o
# Gabriel Détraz, Augustin Lemesle # Gabriel Détraz, Augustin Lemesle
# Gplv2 # Gplv2
"""
Vue d'affichage, et de modification des réglages (réglages machine,
topologie, users, service...)
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import render from django.shortcuts import render, redirect
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.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required 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.db import transaction
from reversion.models import Version from reversion.models import Version
from reversion import revisions as reversion from reversion import revisions as reversion
from re2o.views import form
from .forms import ServiceForm, DelServiceForm 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 models
from . import forms from . import forms
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def display_options(request): def display_options(request):
useroptions, created = OptionalUser.objects.get_or_create() """Vue pour affichage des options (en vrac) classé selon les models
machineoptions, created = OptionalMachine.objects.get_or_create() correspondants dans un tableau"""
topologieoptions, created = OptionalTopologie.objects.get_or_create() useroptions, _created = OptionalUser.objects.get_or_create()
generaloptions, created = GeneralOption.objects.get_or_create() machineoptions, _created = OptionalMachine.objects.get_or_create()
assooptions, created = AssoOption.objects.get_or_create() topologieoptions, _created = OptionalTopologie.objects.get_or_create()
mailmessageoptions, created = MailMessageOption.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() 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 @login_required
@permission_required('admin') @permission_required('admin')
@ -73,23 +78,36 @@ def edit_options(request, section):
model = getattr(models, section, None) model = getattr(models, section, None)
form_instance = getattr(forms, 'Edit' + section + 'Form', None) form_instance = getattr(forms, 'Edit' + section + 'Form', None)
if model and form: if model and form:
options_instance, created = model.objects.get_or_create() options_instance, _created = model.objects.get_or_create()
options = form_instance(request.POST or None, instance=options_instance) options = form_instance(
request.POST or None,
instance=options_instance
)
if options.is_valid(): if options.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
options.save() options.save()
reversion.set_user(request.user) 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") messages.success(request, "Préférences modifiées")
return redirect("/preferences/") return redirect("/preferences/")
return form({'options': options}, 'preferences/edit_preferences.html', request) return form(
{'options': options},
'preferences/edit_preferences.html',
request
)
else: else:
messages.error(request, "Objet inconnu") messages.error(request, "Objet inconnu")
return redirect("/preferences/") return redirect("/preferences/")
@login_required @login_required
@permission_required('admin') @permission_required('admin')
def add_services(request): def add_services(request):
"""Ajout d'un service de la page d'accueil"""
services = ServiceForm(request.POST or None) services = ServiceForm(request.POST or None)
if services.is_valid(): if services.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -98,29 +116,45 @@ def add_services(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Cet enregistrement ns a été ajouté") messages.success(request, "Cet enregistrement ns a été ajouté")
return redirect("/preferences/") return redirect("/preferences/")
return form({'preferenceform': services}, 'preferences/preferences.html', request) return form(
{'preferenceform': services},
'preferences/preferences.html',
request
)
@login_required @login_required
@permission_required('admin') @permission_required('admin')
def edit_services(request, servicesid): def edit_services(request, servicesid):
"""Edition des services affichés sur la page d'accueil"""
try: try:
services_instance = Service.objects.get(pk=servicesid) services_instance = Service.objects.get(pk=servicesid)
except Service.DoesNotExist: except Service.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante")
return redirect("/preferences/") return redirect("/preferences/")
services = ServiceForm(request.POST or None, instance=services_instance) services = ServiceForm(request.POST or None, instance=services_instance)
if services.is_valid(): if services.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
services.save() services.save()
reversion.set_user(request.user) 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é") messages.success(request, "Service modifié")
return redirect("/preferences/") return redirect("/preferences/")
return form({'preferenceform': services}, 'preferences/preferences.html', request) return form(
{'preferenceform': services},
'preferences/preferences.html',
request
)
@login_required @login_required
@permission_required('admin') @permission_required('admin')
def del_services(request): def del_services(request):
"""Suppression d'un service de la page d'accueil"""
services = DelServiceForm(request.POST or None) services = DelServiceForm(request.POST or None)
if services.is_valid(): if services.is_valid():
services_dels = services.cleaned_data['services'] services_dels = services.cleaned_data['services']
@ -131,20 +165,28 @@ def del_services(request):
reversion.set_user(request.user) reversion.set_user(request.user)
messages.success(request, "Le services a été supprimée") messages.success(request, "Le services a été supprimée")
except ProtectedError: 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 redirect("/preferences/")
return form({'preferenceform': services}, 'preferences/preferences.html', request) return form(
{'preferenceform': services},
'preferences/preferences.html',
request
)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def history(request, object, id): def history(request, object_name, object_id):
if object == 'service': """Historique de creation et de modification d'un service affiché sur
la page d'accueil"""
if object_name == 'service':
try: try:
object_instance = Service.objects.get(pk=id) object_instance = Service.objects.get(pk=object_id)
except Service.DoesNotExist: except Service.DoesNotExist:
messages.error(request, "Service inexistant") messages.error(request, "Service inexistant")
return redirect("/preferences/") return redirect("/preferences/")
options, created = GeneralOption.objects.get_or_create() options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance) reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number) paginator = Paginator(reversions, pagination_number)
@ -157,4 +199,7 @@ def history(request, object, id):
except EmptyPage: 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.
reversions = paginator.page(paginator.num_pages) 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
})

View file

@ -19,15 +19,19 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from machines.models import Interface, Machine
from preferences.models import GeneralOption, OptionalMachine from preferences.models import GeneralOption, OptionalMachine
def context_user(request): def context_user(request):
general_options, created = GeneralOption.objects.get_or_create() """Fonction de context lorsqu'un user est logué (ou non),
machine_options, created = OptionalMachine.objects.get_or_create() 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 user = request.user
if user.is_authenticated(): if user.is_authenticated():
interfaces = user.user_interfaces() interfaces = user.user_interfaces()
@ -52,8 +56,8 @@ def context_user(request):
'is_bofh': is_bofh, 'is_bofh': is_bofh,
'is_trez': is_trez, 'is_trez': is_trez,
'is_infra': is_infra, 'is_infra': is_infra,
'is_admin' : is_admin, 'is_admin': is_admin,
'interfaces': interfaces, 'interfaces': interfaces,
'site_name': general_options.site_name, 'site_name': general_options.site_name,
'ipv6_enabled' : machine_options.ipv6, 'ipv6_enabled': machine_options.ipv6,
} }

View file

@ -0,0 +1,572 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django import template
from django.utils.safestring import mark_safe
from django.forms import TextInput
from django.forms.widgets import Select
from bootstrap3.templatetags.bootstrap3 import bootstrap_form
from bootstrap3.utils import render_tag
from bootstrap3.forms import render_field
register = template.Library()
@register.simple_tag
def massive_bootstrap_form(form, mbf_fields, *args, **kwargs):
"""
Render a form where some specific fields are rendered using Twitter
Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the
speed and UX when dealing with very large datasets (select with 50k+ elts
for instance).
When the fields specified should normally be rendered as a select with
single selectable option, Twitter Typeahead is used for a better display
and the matching query engine. When dealing with multiple selectable
options, sliptree's Bootstrap Tokenfield in addition with Typeahead.
For convenience, it accepts the same parameters as a standard bootstrap
can accept.
**Tag name**::
massive_bootstrap_form
**Parameters**:
form (required)
The form that is to be rendered
mbf_fields (optional)
A list of field names (comma separated) that should be rendered
with Typeahead/Tokenfield instead of the default bootstrap
renderer.
If not specified, all fields will be rendered as a normal bootstrap
field.
mbf_param (optional)
A dict of parameters for the massive_bootstrap_form tag. The
possible parameters are the following.
choices (optional)
A dict of strings representing the choices in JS. The keys of
the dict are the names of the concerned fields. The choices
must be an array of objects. Each of those objects must at
least have the fields 'key' (value to send) and 'value' (value
to display). Other fields can be added as desired.
For a more complex structure you should also consider
reimplementing the engine and the match_func.
If not specified, the key is the id of the object and the value
is its string representation as in a normal bootstrap form.
Example :
'choices' : {
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]',
'field_B':...,
...
}
engine (optional)
A dict of strings representating the engine used for matching
queries and possible values with typeahead. The keys of the
dict are the names of the concerned fields. The string is valid
JS code.
If not specified, BloodHound with relevant basic properties is
used.
Example :
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...}
match_func (optional)
A dict of strings representing a valid JS function used in the
dataset to overload the matching engine. The keys of the dict
are the names of the concerned fields. This function is used
the source of the dataset. This function receives 2 parameters,
the query and the synchronize function as specified in
typeahead.js documentation. If needed, the local variables
'choices_<fieldname>' and 'engine_<fieldname>' contains
respectively the array of all possible values and the engine
to match queries with possible values.
If not specified, the function used display up to the 10 first
elements if the query is empty and else the matching results.
Example :
'match_func' : {
'field_A': 'function(q, sync) { engine.search(q, sync); }',
'field_B': ...,
...
}
update_on (optional)
A dict of list of ids that the values depends on. The engine
and the typeahead properties are recalculated and reapplied.
Example :
'addition' : {
'field_A' : [ 'id0', 'id1', ... ] ,
'field_B' : ... ,
...
}
See boostrap_form_ for other arguments
**Usage**::
{% massive_bootstrap_form
form
[ '<field1>[,<field2>[,...]]' ]
[ mbf_param = {
[ 'choices': {
[ '<field1>': '<choices1>'
[, '<field2>': '<choices2>'
[, ... ] ] ]
} ]
[, 'engine': {
[ '<field1>': '<engine1>'
[, '<field2>': '<engine2>'
[, ... ] ] ]
} ]
[, 'match_func': {
[ '<field1>': '<match_func1>'
[, '<field2>': '<match_func2>'
[, ... ] ] ]
} ]
[, 'update_on': {
[ '<field1>': '<update_on1>'
[, '<field2>': '<update_on2>'
[, ... ] ] ]
} ]
} ]
[ <standard boostrap_form parameters> ]
%}
**Example**:
{% massive_bootstrap_form form 'ipv4' choices='[...]' %}
"""
fields = mbf_fields.split(',')
param = kwargs.pop('mbf_param', {})
exclude = param.get('exclude', '').split(',')
choices = param.get('choices', {})
engine = param.get('engine', {})
match_func = param.get('match_func', {})
update_on = param.get('update_on', {})
hidden_fields = [h.name for h in form.hidden_fields()]
html = ''
for f_name, f_value in form.fields.items() :
if not f_name in exclude :
if f_name in fields and not f_name in hidden_fields :
if not isinstance(f_value.widget, Select) :
raise ValueError(
('Field named {f_name} from {form} is not a Select and'
'can\'t be rendered with massive_bootstrap_form.'
).format(
f_name=f_name,
form=form
)
)
multiple = f_value.widget.allow_multiple_selected
f_bound = f_value.get_bound_field( form, f_name )
f_value.widget = TextInput(
attrs = {
'name': 'mbf_'+f_name,
'placeholder': f_value.empty_label
}
)
html += render_field(
f_value.get_bound_field( form, f_name ),
*args,
**kwargs
)
if multiple :
content = mbf_js(
f_name,
f_value,
f_bound,
multiple,
choices,
engine,
match_func,
update_on
)
else :
content = hidden_tag( f_bound, f_name ) + mbf_js(
f_name,
f_value,
f_bound,
multiple,
choices,
engine,
match_func,
update_on
)
html += render_tag(
'div',
content = content,
attrs = { 'id': custom_div_id( f_bound ) }
)
else:
html += render_field(
f_value.get_bound_field( form, f_name ),
*args,
**kwargs
)
return mark_safe( html )
def input_id( f_bound ) :
""" The id of the HTML input element """
return f_bound.auto_id
def hidden_id( f_bound ):
""" The id of the HTML hidden input element """
return input_id( f_bound ) + '_hidden'
def custom_div_id( f_bound ):
""" The id of the HTML div element containing values and script """
return input_id( f_bound ) + '_div'
def hidden_tag( f_bound, f_name ):
""" The HTML hidden input element """
return render_tag(
'input',
attrs={
'id': hidden_id( f_bound ),
'name': f_bound.html_name,
'type': 'hidden',
'value': f_bound.value() or ""
}
)
def mbf_js( f_name, f_value, f_bound, multiple,
choices_, engine_, match_func_, update_on_ ) :
""" The whole script to use """
choices = ( mark_safe( choices_[f_name] ) if f_name in choices_.keys()
else default_choices( f_value ) )
engine = ( mark_safe( engine_[f_name] ) if f_name in engine_.keys()
else default_engine ( f_name ) )
match_func = ( mark_safe( match_func_[f_name] )
if f_name in match_func_.keys() else default_match_func( f_name ) )
update_on = update_on_[f_name] if f_name in update_on_.keys() else []
if multiple :
js_content = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
'engine_{f_name} = {engine};'
'$( "#{input_id}" ).tokenfield( "destroy" );'
'$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
'}};'
'$( "#{input_id}" ).bind( "tokenfield:createtoken", {create} );'
'$( "#{input_id}" ).bind( "tokenfield:edittoken", {edit} );'
'$( "#{input_id}" ).bind( "tokenfield:removetoken", {remove} );'
'{updates}'
'$( "#{input_id}" ).ready( function() {{'
'setup_{f_name}();'
'{init_input}'
'}} );'
).format(
f_name = f_name,
choices = choices,
engine = engine,
input_id = input_id( f_bound ),
datasets = default_datasets( f_name, match_func ),
create = tokenfield_create( f_name, f_bound ),
edit = tokenfield_edit( f_name, f_bound ),
remove = tokenfield_remove( f_name, f_bound ),
updates = ''.join( [ (
'$( "#{u_id}" ).change( function() {{'
'setup_{f_name}();'
'{reset_input}'
'}} );'
).format(
u_id = u_id,
reset_input = tokenfield_reset_input( f_bound ),
f_name = f_name
) for u_id in update_on ]
),
init_input = tokenfield_init_input( f_name, f_bound ),
)
else :
js_content = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
'engine_{f_name} = {engine};'
'$( "#{input_id}" ).typeahead( "destroy" );'
'$( "#{input_id}" ).typeahead( {datasets} );'
'}};'
'$( "#{input_id}" ).bind( "typeahead:select", {select} );'
'$( "#{input_id}" ).bind( "typeahead:change", {change} );'
'{updates}'
'$( "#{input_id}" ).ready( function() {{'
'setup_{f_name}();'
'{init_input}'
'}} );'
).format(
f_name = f_name,
choices = choices,
engine = engine,
input_id = input_id( f_bound ),
datasets = default_datasets( f_name, match_func ),
select = typeahead_select( f_bound ),
change = typeahead_change( f_bound ),
updates = ''.join( [ (
'$( "#{u_id}" ).change( function() {{'
'setup_{f_name}();'
'{reset_input}'
'}} );'
).format(
u_id = u_id,
reset_input = typeahead_reset_input( f_bound ),
f_name = f_name
) for u_id in update_on ]
),
init_input = typeahead_init_input( f_name, f_bound ),
)
return render_tag( 'script', content=mark_safe( js_content ) )
def typeahead_init_input( f_name, f_bound ) :
""" The JS script to init the fields values """
init_key = f_bound.value() or '""'
return (
'$( "#{input_id}" ).typeahead("val", {init_val});'
'$( "#{hidden_id}" ).val( {init_key} );'
).format(
input_id = input_id( f_bound ),
init_val = '""' if init_key == '""' else
'engine_{f_name}.get( {init_key} )[0].value'.format(
f_name = f_name,
init_key = init_key
),
init_key = init_key,
hidden_id = hidden_id( f_bound )
)
def typeahead_reset_input( f_bound ) :
""" The JS script to reset the fields values """
return (
'$( "#{input_id}" ).typeahead("val", "");'
'$( "#{hidden_id}" ).val( "" );'
).format(
input_id = input_id( f_bound ),
hidden_id = hidden_id( f_bound )
)
def tokenfield_init_input( f_name, f_bound ) :
""" The JS script to init the fields values """
init_key = f_bound.value() or '""'
return (
'$( "#{input_id}" ).tokenfield("setTokens", {init_val});'
).format(
input_id = input_id( f_bound ),
init_val = '""' if init_key == '""' else (
'engine_{f_name}.get( {init_key} ).map('
'function(o) {{ return o.value; }}'
')').format(
f_name = f_name,
init_key = init_key
),
init_key = init_key,
)
def tokenfield_reset_input( f_bound ) :
""" The JS script to reset the fields values """
return (
'$( "#{input_id}" ).tokenfield("setTokens", "");'
).format(
input_id = input_id( f_bound ),
)
def default_choices( f_value ) :
""" The JS script creating the variable choices_<fieldname> """
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_<field_name> """
return (
'new Bloodhound({{'
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
'queryTokenizer: Bloodhound.tokenizers.whitespace,'
'local: choices_{f_name},'
'identify: function(obj) {{ return obj.key; }}'
'}})'
).format(
f_name = f_name
)
def default_datasets( f_name, match_func ) :
""" The JS script creating the datasets to use with typeahead """
return (
'{{'
'hint: true,'
'highlight: true,'
'minLength: 0'
'}},'
'{{'
'display: "value",'
'name: "{f_name}",'
'source: {match_func}'
'}}'
).format(
f_name = f_name,
match_func = match_func
)
def default_match_func ( f_name ) :
""" The JS script creating the matching function to use with typeahed """
return (
'function ( q, sync ) {{'
'if ( q === "" ) {{'
'var first = choices_{f_name}.slice( 0, 5 ).map('
'function ( obj ) {{ return obj.key; }}'
');'
'sync( engine_{f_name}.get( first ) );'
'}} else {{'
'engine_{f_name}.search( q, sync );'
'}}'
'}}'
).format(
f_name = f_name
)
def typeahead_select( f_bound ):
""" The JS script creating the function triggered when an item is
selected through typeahead """
return (
'function(evt, item) {{'
'$( "#{hidden_id}" ).val( item.key );'
'$( "#{hidden_id}" ).change();'
'return item;'
'}}'
).format(
hidden_id = hidden_id( f_bound )
)
def typeahead_change( f_bound ):
""" The JS script creating the function triggered when an item is changed
(i.e. looses focus and value has changed since the moment it gained focus
"""
return (
'function(evt) {{'
'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{'
'$( "#{hidden_id}" ).val( "" );'
'$( "#{hidden_id}" ).change();'
'}}'
'}}'
).format(
input_id = input_id( f_bound ),
hidden_id = hidden_id( f_bound )
)
def tokenfield_create( f_name, f_bound ):
""" The JS script triggered when a new token is created in tokenfield. """
return (
'function(evt) {{'
'var k = evt.attrs.key;'
'if (!k) {{'
'var data = evt.attrs.value;'
'var i = 0;'
'while ( i<choices_{f_name}.length &&'
'choices_{f_name}[i].value !== data ) {{'
'i++;'
'}}'
'if ( i === choices_{f_name}.length ) {{ return false; }}'
'k = choices_{f_name}[i].key;'
'}}'
'var new_input = document.createElement("input");'
'new_input.type = "hidden";'
'new_input.id = "{hidden_id}_"+k.toString();'
'new_input.value = k.toString();'
'new_input.name = "{name}";'
'$( "#{div_id}" ).append(new_input);'
'}}'
).format(
f_name = f_name,
hidden_id = hidden_id( f_bound ),
name = f_bound.html_name,
div_id = custom_div_id( f_bound )
)
def tokenfield_edit( f_name, f_bound ):
""" The JS script triggered when a token is edited in tokenfield. """
return (
'function(evt) {{'
'var k = evt.attrs.key;'
'if (!k) {{'
'var data = evt.attrs.value;'
'var i = 0;'
'while ( i<choices_{f_name}.length &&'
'choices_{f_name}[i].value !== data ) {{'
'i++;'
'}}'
'if ( i === choices_{f_name}.length ) {{ return true; }}'
'k = choices_{f_name}[i].key;'
'}}'
'var old_input = document.getElementById('
'"{hidden_id}_"+k.toString()'
');'
'old_input.parentNode.removeChild(old_input);'
'}}'
).format(
f_name = f_name,
hidden_id = hidden_id( f_bound )
)
def tokenfield_remove( f_name, f_bound ):
""" The JS script trigggered when a token is removed from tokenfield. """
return (
'function(evt) {{'
'var k = evt.attrs.key;'
'if (!k) {{'
'var data = evt.attrs.value;'
'var i = 0;'
'while ( i<choices_{f_name}.length &&'
'choices_{f_name}[i].value !== data ) {{'
'i++;'
'}}'
'if ( i === choices_{f_name}.length ) {{ return true; }}'
'k = choices_{f_name}[i].key;'
'}}'
'var old_input = document.getElementById('
'"{hidden_id}_"+k.toString()'
');'
'old_input.parentNode.removeChild(old_input);'
'}}'
).format(
f_name = f_name,
hidden_id = hidden_id( f_bound )
)

View file

@ -49,10 +49,16 @@ urlpatterns = [
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^users/', include('users.urls', namespace='users')), url(r'^users/', include('users.urls', namespace='users')),
url(r'^search/', include('search.urls', namespace='search')), 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'^machines/', include('machines.urls', namespace='machines')),
url(r'^topologie/', include('topologie.urls', namespace='topologie')), url(r'^topologie/', include('topologie.urls', namespace='topologie')),
url(r'^logs/', include('logs.urls', namespace='logs')), url(r'^logs/', include('logs.urls', namespace='logs')),
url(r'^preferences/', include('preferences.urls', namespace='preferences')), url(
r'^preferences/',
include('preferences.urls', namespace='preferences')
),
] ]

141
re2o/utils.py Normal file
View file

@ -0,0 +1,141 @@
# -*- 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 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(
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(
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)

View file

@ -19,25 +19,28 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from django.shortcuts import render from django.shortcuts import render
from django.shortcuts import get_object_or_404
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.template import Context, RequestContext, loader
from preferences.models import Service from preferences.models import Service
def form(ctx, template, request): def form(ctx, template, request):
c = ctx """Form générique, raccourci importé par les fonctions views du site"""
c.update(csrf(request)) context = ctx
return render(request, template, c) context.update(csrf(request))
return render(request, template, context)
def index(request): def index(request):
i = 0 """Affiche la liste des services sur la page d'accueil de re2o"""
services = [[], [], []] services = [[], [], []]
for indice, serv in enumerate(Service.objects.all()): for indice, serv in enumerate(Service.objects.all()):
services[indice % 3].append(serv) services[indice % 3].append(serv)
return form({'services_urls': services}, 're2o/index.html', request) return form({'services_urls': services}, 're2o/index.html', request)

View file

@ -32,9 +32,10 @@ https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
from django.core.wsgi import get_wsgi_application
from os.path import dirname
import sys import sys
from os.path import dirname
from django.core.wsgi import get_wsgi_application
sys.path.append(dirname(dirname(__file__))) sys.path.append(dirname(dirname(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings")

210
static/css/bootstrap-tokenfield.css vendored Normal file
View file

@ -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;
}

View file

@ -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.

File diff suppressed because it is too large Load diff

View file

@ -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.

View file

@ -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.

139
static/js/konami/konami.js Normal file
View file

@ -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;
}
}

316
static/js/sapphire.js Normal file
View file

@ -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<elt.obj.length ; i++) {
for (var p in elt.prop) {
if ( p === "color" ) { elt.prop[p][i] = elt.obj[i].style.color; }
else if ( p === "bgColor" ) { elt.prop[p][i] = elt.obj[i].style.backgroundColor; }
else if ( p === "display" ) { elt.prop[p][i] = elt.obj[i].style.display; }
}
}
},
alterProp: function(elt) {
for (var i=0 ; i<elt.obj.length ; i++) {
for (var p in elt.prop) {
if ( p === "color" ) { elt.obj[i].style.color = "white"; }
else if ( p === "bgColor" ) { elt.obj[i].style.backgroundColor = "transparent"; }
else if ( p === "display" ) { elt.obj[i].style.display = "none"; }
}
}
},
revertProp: function(elt) {
for (var i=0 ; i<elt.obj.length ; i++) {
for (var p in elt.prop) {
if ( p === "color" ) { elt.obj[i].style.color = elt.prop[p][i]; }
else if ( p === "bgColor" ) { elt.obj[i].style.backgroundColor = elt.prop[p][i]; }
else if ( p === "display" ) { elt.obj[i].style.display = elt.prop[p][i]; }
}
}
},
elts: {
alerts: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "alert"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
btns: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getClass(this, main, "btn"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
body: {
obj: undefined,
prop: {color: []},
get: function(main) {
this.obj = document.body;
for (var p in this.prop) { if ( p === "color" ) { this.prop[p] = this.obj.style.color; } }
},
alter: function() { for (var p in this.prop) { if ( p === "color" ) { this.obj.style.color = "white"; } } },
revert: function() { for (var p in this.prop) { if ( p === "color" ) { this.obj.style.color = this.prop[p]; } } }
},
captions: {
obj: undefined,
prop: {color: []},
get: function(main) { sapphire.getClass(this, main, "caption"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
helps: {
obj: undefined,
prop: {color: []},
get: function(main) { sapphire.getClass(this, main, "help-block"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
hrs: {
obj: undefined,
prop: {display: []},
get: function(main) { sapphire.getTag(this, main, "hr"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
inputs: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getTag(this, main, "input"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
listGroups: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getClass(this, main, "list-group-item"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
paginations: {
obj: [],
prop: {bgColor: []},
get: function(main) {
var a = main.getElementsByClassName("pagination");
for (var i=0 ; i<a.length ; i++) {
this.obj[i] = []; this.prop.bgColor[i] = [];
for (var j=0 ; j<a[i].children.length ; j++) {
this.obj[i][j] = a[i].children[j].children[0];
this.prop.bgColor[i][j] = this.obj[i][j].style.backgroundColor;
}
}
},
alter: function () {
for (var i=0 ; i<this.obj.length ; i++)
for (var j=0 ; j<this.obj[i].length ; j++)
for (var p in this.prop)
if ( p === "bgColor" ) { this.obj[i][j].style.backgroundColor = "transparent"; }
},
revert: function() {
for (var i=0 ; i<this.obj.length ; i++)
for (var j=0 ; j<this.obj[i].length ; j++)
for (var p in this.prop)
if ( p === "bgColor" ) { this.obj[i][j].style.backgroundColor = this.prop[p][i][j]; }
}
},
panelHeadings: {
obj: undefined,
prop: {bgColor: [], color: []},
get: function(main) { sapphire.getClass(this, main, "panel-heading"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
panels: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "panel"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
selects: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getTag(this, main, "select"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
sidenavs: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "sidenav"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
tds: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getTag(this, main, "td"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
thumbnails: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "thumbnail"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
trs: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getTag(this, main, "tr"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
}
},
columns: undefined,
alpha: undefined,
drops: undefined,
canvas: undefined,
init: function() {
var main = document.getElementById("main");
for (var e in sapphire.elts) { sapphire.elts[e].get(main); }
},
resize: function() {
var ctx = sapphire.canvas.getContext("2d");
var img = ctx.getImageData( 0, 0, sapphire.canvas.width, sapphire.canvas.height );
sapphire.canvas.width = window.innerWidth;
sapphire.canvas.height = window.innerHeight;
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height);
ctx.putImageData( img, 0, 0 );
sapphire.columns = sapphire.canvas.width/FONT_SIZE;
sapphire.alpha = Math.max( 0, Math.min( 1, TRAIL_TIME / ( sapphire.canvas.height/FONT_SIZE ) ) );
var newDrops = [];
for(var x = 0; x < sapphire.columns; x++) {
if ( sapphire.drops && sapphire.drops[x] ) { newDrops[x] = sapphire.drops[x] }
else {
newDrops[x] = [];
var nb = Math.floor(Math.random()*MAX_CHAR);
for (var y = 0; y < nb; y++)
newDrops[x][y] = 0;
}
}
sapphire.drops = newDrops;
},
run: function() {
sapphire.canvas = document.createElement("canvas");
document.body.appendChild(sapphire.canvas);
sapphire.canvas.style.position = "fixed";
sapphire.canvas.style.zIndex = -1;
sapphire.canvas.style.left = 0;
sapphire.canvas.style.top = 0;
var ctx = sapphire.canvas.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height);
function attenuateBackground() {
ctx.fillStyle = "rgba(0, 0, 0, "+sapphire.alpha+")";
ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height);
}
function drawMatrixRainDrop() {
ctx.fillStyle = RAIN_COLOR;
ctx.font = FONT_SIZE + "px arial";
for(var i = 0; i < sapphire.drops.length; i++) {
for (var j = 0; j < sapphire.drops[i].length; j++) {
var text = CHARACTERS[Math.floor(Math.random()*CHARACTERS.length)];
ctx.fillText(text, i*FONT_SIZE, sapphire.drops[i][j]*FONT_SIZE);
if(sapphire.drops[i][j]*FONT_SIZE > 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);

View file

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

View file

@ -33,10 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{# Load CSS and JavaScript #} {# Load CSS and JavaScript #}
{% bootstrap_css %} {% bootstrap_css %}
<link href="/static/css/typeaheadjs.css" rel="stylesheet"> <link href="/static/css/typeaheadjs.css" rel="stylesheet">
<link href="/static/css/bootstrap-tokenfield.css" rel="stylesheet">
{% comment %}<link href="/static/css/jquery-ui.css" rel="stylesheet">{% endcomment %}
{% bootstrap_javascript %} {% bootstrap_javascript %}
<script src="/static/js/typeahead.js"></script> <script src="/static/js/typeahead/typeahead.js"></script>
<script src="/static/js/handlebars.js"></script> <script src="/static/js/handlebars/handlebars.js"></script>
<script src="/static/js/konami/konami.js"></script>
<script src="/static/js/sapphire.js"> var s=Sapphire(); Konami(s.activate); </script>
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
{% comment %}<script src="/static/js/jquery-ui.js"></script>{% endcomment %}
<link rel="stylesheet" href="{% static "/css/base.css" %}"> <link rel="stylesheet" href="{% static "/css/base.css" %}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title> <title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>

View file

@ -20,6 +20,9 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
@ -28,18 +31,27 @@ from reversion.admin import VersionAdmin
from .models import Port, Room, Switch, Stack from .models import Port, Room, Switch, Stack
class StackAdmin(VersionAdmin): class StackAdmin(VersionAdmin):
"""Administration d'une stack de switches (inclus des switches)"""
pass pass
class SwitchAdmin(VersionAdmin): class SwitchAdmin(VersionAdmin):
"""Administration d'un switch"""
pass pass
class PortAdmin(VersionAdmin): class PortAdmin(VersionAdmin):
"""Administration d'un port de switches"""
pass pass
class RoomAdmin(VersionAdmin): class RoomAdmin(VersionAdmin):
"""Administration d'un chambre"""
pass pass
admin.site.register(Port, PortAdmin) admin.site.register(Port, PortAdmin)
admin.site.register(Room, RoomAdmin) admin.site.register(Room, RoomAdmin)
admin.site.register(Switch, SwitchAdmin) admin.site.register(Switch, SwitchAdmin)

View file

@ -19,52 +19,113 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from .models import Port, Switch, Room, Stack
from django.forms import ModelForm, Form
from machines.models import Interface from machines.models import Interface
from django.forms import ModelForm
from .models import Port, Switch, Room, Stack
class PortForm(ModelForm): class PortForm(ModelForm):
"""Formulaire pour la création d'un port d'un switch
Relié directement au modèle port"""
class Meta: class Meta:
model = Port model = Port
fields = '__all__' 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): 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): 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): def __init__(self, *args, **kwargs):
super(EditPortForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['machine_interface'].queryset = Interface.objects.all().select_related('domain__extension') super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['related'].queryset = Port.objects.all().select_related('switch__switch_interface__domain__extension').order_by('switch', 'port') self.fields['machine_interface'].queryset = Interface.objects.all()\
.select_related('domain__extension')
self.fields['related'].queryset = Port.objects.all()\
.select_related('switch__switch_interface__domain__extension')\
.order_by('switch', 'port')
class AddPortForm(ModelForm): class AddPortForm(ModelForm):
"""Permet d'ajouter un port de switch. Voir EditPortForm pour plus
d'informations"""
class Meta(PortForm.Meta): class Meta(PortForm.Meta):
fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details'] fields = ['port', 'room', 'machine_interface', 'related',
'radius', 'vlan_force', 'details']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs)
class StackForm(ModelForm): class StackForm(ModelForm):
"""Permet d'edition d'une stack : stack_id, et switches membres
de la stack"""
class Meta: class Meta:
model = Stack model = Stack
fields = '__all__' 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): class EditSwitchForm(ModelForm):
"""Permet d'éditer un switch : nom et nombre de ports"""
class Meta: class Meta:
model = Switch model = Switch
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditSwitchForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditSwitchForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['location'].label = 'Localisation' self.fields['location'].label = 'Localisation'
self.fields['number'].label = 'Nombre de ports' self.fields['number'].label = 'Nombre de ports'
class NewSwitchForm(ModelForm): 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): class Meta(EditSwitchForm.Meta):
fields = ['location', 'number', 'details', 'stack', 'stack_member_id'] fields = ['location', 'number', 'details', 'stack', 'stack_member_id']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NewSwitchForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditRoomForm(ModelForm): class EditRoomForm(ModelForm):
"""Permet d'éediter le nom et commentaire d'une prise murale"""
class Meta: class Meta:
model = Room model = Room
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs)

View file

@ -20,24 +20,32 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.dispatch import receiver 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 from django.core.exceptions import ValidationError
import reversion
from machines.models import Vlan
class Stack(models.Model): class Stack(models.Model):
""" Un objet stack. Regrouppe des switchs en foreign key """Un objet stack. Regrouppe des switchs en foreign key
, contient une id de stack, un switch id min et max dans ,contient une id de stack, un switch id min et max dans
le stack""" le stack"""
PRETTY_NAME = "Stack de switchs" PRETTY_NAME = "Stack de switchs"
@ -59,7 +67,9 @@ class Stack(models.Model):
def clean(self): def clean(self):
""" Verification que l'id_max < id_min""" """ Verification que l'id_max < id_min"""
if self.member_id_max < self.member_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): class Switch(models.Model):
""" Definition d'un switch. Contient un nombre de ports (number), """ Definition d'un switch. Contient un nombre de ports (number),
@ -69,18 +79,29 @@ class Switch(models.Model):
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. 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 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" 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) location = models.CharField(max_length=255)
number = models.IntegerField() number = models.IntegerField()
details = models.CharField(max_length=255, blank=True) details = models.CharField(max_length=255, blank=True)
stack = models.ForeignKey(Stack, blank=True, null=True, on_delete=models.SET_NULL) stack = models.ForeignKey(
Stack,
blank=True,
null=True,
on_delete=models.SET_NULL
)
stack_member_id = models.IntegerField(blank=True, null=True) stack_member_id = models.IntegerField(blank=True, null=True)
class Meta: class Meta:
unique_together = ('stack','stack_member_id') unique_together = ('stack', 'stack_member_id')
def __str__(self): def __str__(self):
return str(self.location) + ' ' + str(self.switch_interface) return str(self.location) + ' ' + str(self.switch_interface)
@ -89,10 +110,16 @@ class Switch(models.Model):
""" Verifie que l'id stack est dans le bon range""" """ Verifie que l'id stack est dans le bon range"""
if self.stack is not None: if self.stack is not None:
if self.stack_member_id 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): if (self.stack_member_id > self.stack.member_id_max) or\
raise ValidationError({'stack_member_id': "L'id de ce switch est en dehors des bornes permises pas la stack"}) (self.stack_member_id < self.stack.member_id_min):
raise ValidationError(
{'stack_member_id': "L'id de ce switch est en\
dehors des bornes permises pas la stack"}
)
else: else:
raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"}) raise ValidationError({'stack_member_id': "L'id dans la stack\
ne peut être nul"})
class Port(models.Model): class Port(models.Model):
""" Definition d'un port. Relié à un switch(foreign_key), """ Definition d'un port. Relié à un switch(foreign_key),
@ -102,7 +129,8 @@ class Port(models.Model):
- un autre port (uplink) (related) - un autre port (uplink) (related)
Champs supplémentaires : Champs supplémentaires :
- RADIUS (mode STRICT : connexion sur port uniquement si machine - 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 COMMON : vérification uniquement du statut de la machine
mode NO : accepte toute demande venant du port et place sur le vlan normal mode NO : accepte toute demande venant du port et place sur le vlan normal
mode BLOQ : rejet de toute authentification mode BLOQ : rejet de toute authentification
@ -119,11 +147,31 @@ class Port(models.Model):
switch = models.ForeignKey('Switch', related_name="ports") switch = models.ForeignKey('Switch', related_name="ports")
port = models.IntegerField() port = models.IntegerField()
room = models.ForeignKey('Room', on_delete=models.PROTECT, blank=True, null=True) room = models.ForeignKey(
machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True) 'Room',
related = models.OneToOneField('self', null=True, blank=True, related_name='related_port') 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') 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) details = models.CharField(max_length=255, blank=True)
class Meta: class Meta:
@ -142,23 +190,28 @@ class Port(models.Model):
related_port.save() related_port.save()
def clean(self): def clean(self):
""" Verifie que un seul de chambre, interface_parent et related_port est rempli. """ Verifie que un seul de chambre, interface_parent et related_port
Verifie que le related n'est pas le port lui-même.... 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 Verifie que le related n'est pas déjà occupé par une machine ou une
ce n'est pas le cas, applique la relation related chambre. Si ce n'est pas le cas, applique la relation related
Si un port related point vers self, on nettoie la relation 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 A priori pas d'autre solution que de faire ça à la main. A priori
un bloc transaction, donc pas de problème de cohérence""" tout cela est dans un bloc transaction, donc pas de problème de
cohérence"""
if hasattr(self, 'switch'): if hasattr(self, 'switch'):
if self.port > self.switch.number: if self.port > self.switch.number:
raise ValidationError("Ce port ne peut exister, numero trop élevé") raise ValidationError("Ce port ne peut exister,\
if self.room and self.machine_interface or self.room and self.related or self.machine_interface and self.related: numero trop élevé")
raise ValidationError("Chambre, interface et related_port sont mutuellement exclusifs") if self.room and self.machine_interface or self.room and\
if self.related==self: 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") raise ValidationError("On ne peut relier un port à lui même")
if self.related and not self.related.related: if self.related and not self.related.related:
if self.related.machine_interface or self.related.room: 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: else:
self.make_port_related() self.make_port_related()
elif hasattr(self, 'related_port'): elif hasattr(self, 'related_port'):
@ -167,8 +220,9 @@ class Port(models.Model):
def __str__(self): def __str__(self):
return str(self.switch) + " - " + str(self.port) return str(self.switch) + " - " + str(self.port)
class Room(models.Model): class Room(models.Model):
""" Une chambre/local contenant une prise murale""" """Une chambre/local contenant une prise murale"""
PRETTY_NAME = "Chambre/ Prise murale" PRETTY_NAME = "Chambre/ Prise murale"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@ -180,6 +234,8 @@ class Room(models.Model):
def __str__(self): def __str__(self):
return str(self.name) return str(self.name)
@receiver(post_delete, sender=Stack) @receiver(post_delete, sender=Stack)
def stack_post_delete(sender, **kwargs): 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)

View file

@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</thead> </thead>
{% for stack in stack_list %} {% for stack in stack_list %}
{% for switch in stack.switch_set.all %} {% for switch in stack.switch_set.all %}
<tbody>
<tr class="active"> <tr class="active">
{% if forloop.first %} {% if forloop.first %}
<td rowspan="{{ stack.switch_set.all|length }}">{{stack.name}}</td> <td rowspan="{{ stack.switch_set.all|length }}">{{stack.name}}</td>
@ -56,6 +57,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td> </td>
{% endif %} {% endif %}
</tr> </tr>
{% empty %}
<tr class="active">
<td>{{stack.name}}</td>
<td>{{stack.stack_id}}</td>
<td>{{stack.details}}</td>
<td>Aucun</td>
<td>
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'stack' stack.pk %}">
<i class="glyphicon glyphicon-time"></i>
</a>
{% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-stack' stack.id %}">
<i class="glyphicon glyphicon-edit"></i>
</a>
<a class="btn btn-danger btn-sm" role="button" title="Supprimer" href="{% url 'topologie:del-stack' stack.pk %}">
<i class="glyphicon glyphicon-trash"></i>
</a>
{% endif %}
</td>
{% endfor %} {% endfor %}
</tbody>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% block title %}Création et modification d'un switch{% endblock %} {% block title %}Création et modification d'un switch{% endblock %}
@ -46,13 +47,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% if topoform %} {% if topoform %}
{% bootstrap_form topoform %} {% massive_bootstrap_form topoform 'switch_interface' %}
{% endif %} {% endif %}
{% if machineform %} {% if machineform %}
{% bootstrap_form machineform %} {% massive_bootstrap_form machineform 'user' %}
{% endif %} {% endif %}
{% if interfaceform %} {% if interfaceform %}
{% bootstrap_form interfaceform %} {% if i_bft_param %}
{% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %}
{% else %}
{% massive_bootstrap_form interfaceform 'ipv4,machine' %}
{% endif %}
{% endif %} {% endif %}
{% if domainform %} {% if domainform %}
{% bootstrap_form domainform %} {% bootstrap_form domainform %}

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% block title %}Création et modificationd 'utilisateur{% endblock %} {% block title %}Création et modificationd 'utilisateur{% endblock %}
@ -32,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form topoform %} {% massive_bootstrap_form topoform 'room,related,machine_interface' %}
{%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %} {%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %}
</form> </form>
<br /> <br />

View file

@ -19,6 +19,12 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 from __future__ import unicode_literals
@ -33,18 +39,33 @@ urlpatterns = [
url(r'^new_room/$', views.new_room, name='new-room'), url(r'^new_room/$', views.new_room, name='new-room'),
url(r'^edit_room/(?P<room_id>[0-9]+)$', views.edit_room, name='edit-room'), url(r'^edit_room/(?P<room_id>[0-9]+)$', views.edit_room, name='edit-room'),
url(r'^del_room/(?P<room_id>[0-9]+)$', views.del_room, name='del-room'), url(r'^del_room/(?P<room_id>[0-9]+)$', views.del_room, name='del-room'),
url(r'^switch/(?P<switch_id>[0-9]+)$', views.index_port, name='index-port'), url(r'^switch/(?P<switch_id>[0-9]+)$',
url(r'^history/(?P<object>switch)/(?P<id>[0-9]+)$', views.history, name='history'), views.index_port,
url(r'^history/(?P<object>port)/(?P<id>[0-9]+)$', views.history, name='history'), name='index-port'),
url(r'^history/(?P<object>room)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>switch)/(?P<id>[0-9]+)$',
url(r'^history/(?P<object>stack)/(?P<id>[0-9]+)$', views.history, name='history'), views.history,
name='history'),
url(r'^history/(?P<object>port)/(?P<id>[0-9]+)$',
views.history,
name='history'),
url(r'^history/(?P<object>room)/(?P<id>[0-9]+)$',
views.history,
name='history'),
url(r'^history/(?P<object>stack)/(?P<id>[0-9]+)$',
views.history,
name='history'),
url(r'^edit_port/(?P<port_id>[0-9]+)$', views.edit_port, name='edit-port'), url(r'^edit_port/(?P<port_id>[0-9]+)$', views.edit_port, name='edit-port'),
url(r'^new_port/(?P<switch_id>[0-9]+)$', views.new_port, name='new-port'), url(r'^new_port/(?P<switch_id>[0-9]+)$', views.new_port, name='new-port'),
url(r'^del_port/(?P<port_id>[0-9]+)$', views.del_port, name='del-port'), url(r'^del_port/(?P<port_id>[0-9]+)$', views.del_port, name='del-port'),
url(r'^edit_switch/(?P<switch_id>[0-9]+)$', views.edit_switch, name='edit-switch'), url(r'^edit_switch/(?P<switch_id>[0-9]+)$',
views.edit_switch,
name='edit-switch'),
url(r'^new_stack/$', views.new_stack, name='new-stack'), url(r'^new_stack/$', views.new_stack, name='new-stack'),
url(r'^index_stack/$', views.index_stack, name='index-stack'), url(r'^index_stack/$', views.index_stack, name='index-stack'),
url(r'^edit_stack/(?P<stack_id>[0-9]+)$', views.edit_stack, name='edit-stack'), url(r'^edit_stack/(?P<stack_id>[0-9]+)$',
url(r'^del_stack/(?P<stack_id>[0-9]+)$', views.del_stack, name='del-stack'), views.edit_stack,
name='edit-stack'),
url(r'^del_stack/(?P<stack_id>[0-9]+)$',
views.del_stack,
name='del-stack'),
] ]

View file

@ -19,7 +19,20 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Page des vues de l'application topologie
Permet de créer, modifier et supprimer :
- un port (add_port, edit_port, del_port)
- un switch : les vues d'ajout et d'édition font appel aux forms de creation
de switch, mais aussi aux forms de machines.forms (domain, interface et
machine). Le views les envoie et les save en même temps. TODO : rationaliser
et faire que la creation de machines (interfaces, domain etc) soit gérée
coté models et forms de topologie
- une chambre (new_room, edit_room, del_room)
- une stack
- l'historique de tous les objets cités
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@ -33,11 +46,12 @@ from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
from topologie.models import Switch, Port, Room, Stack from topologie.models import Switch, Port, Room, Stack
from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm, AddPortForm, EditRoomForm, StackForm from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm
from topologie.forms import AddPortForm, EditRoomForm, StackForm
from users.views import form from users.views import form
from users.models import User
from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm
from machines.views import generate_ipv4_mbf_param
from preferences.models import AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption
@ -45,41 +59,52 @@ from preferences.models import AssoOption, GeneralOption
@permission_required('cableur') @permission_required('cableur')
def index(request): def index(request):
""" Vue d'affichage de tous les swicthes""" """ Vue d'affichage de tous les swicthes"""
switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack') switch_list = Switch.objects.order_by(
return render(request, 'topologie/index.html', {'switch_list': switch_list}) 'stack',
'stack_member_id',
'location'
)\
.select_related('switch_interface__domain__extension')\
.select_related('switch_interface__ipv4')\
.select_related('switch_interface__domain')\
.select_related('stack')
return render(request, 'topologie/index.html', {
'switch_list': switch_list
})
@login_required @login_required
@permission_required('cableur') @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""" """ Vue générique pour afficher l'historique complet d'un objet"""
if object == 'switch': if object_name == 'switch':
try: try:
object_instance = Switch.objects.get(pk=id) object_instance = Switch.objects.get(pk=object_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
messages.error(request, "Switch inexistant") messages.error(request, "Switch inexistant")
return redirect("/topologie/") return redirect("/topologie/")
elif object == 'port': elif object_name == 'port':
try: try:
object_instance = Port.objects.get(pk=id) object_instance = Port.objects.get(pk=object_id)
except Port.DoesNotExist: except Port.DoesNotExist:
messages.error(request, "Port inexistant") messages.error(request, "Port inexistant")
return redirect("/topologie/") return redirect("/topologie/")
elif object == 'room': elif object_name == 'room':
try: try:
object_instance = Room.objects.get(pk=id) object_instance = Room.objects.get(pk=object_id)
except Room.DoesNotExist: except Room.DoesNotExist:
messages.error(request, "Chambre inexistante") messages.error(request, "Chambre inexistante")
return redirect("/topologie/") return redirect("/topologie/")
elif object == 'stack': elif object_name == 'stack':
try: try:
object_instance = Stack.objects.get(pk=id) object_instance = Stack.objects.get(pk=object_id)
except Room.DoesNotExist: except Room.DoesNotExist:
messages.error(request, "Stack inexistante") messages.error(request, "Stack inexistante")
return redirect("/topologie/") return redirect("/topologie/")
else: else:
messages.error(request, "Objet inconnu") messages.error(request, "Objet inconnu")
return redirect("/topologie/") return redirect("/topologie/")
options, created = GeneralOption.objects.get_or_create() options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance) reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number) paginator = Paginator(reversions, pagination_number)
@ -92,7 +117,11 @@ def history(request, object, id):
except EmptyPage: 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.
reversions = paginator.page(paginator.num_pages) 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 @login_required
@permission_required('cableur') @permission_required('cableur')
@ -103,15 +132,25 @@ def index_port(request, switch_id):
except Switch.DoesNotExist: except Switch.DoesNotExist:
messages.error(request, u"Switch inexistant") messages.error(request, u"Switch inexistant")
return redirect("/topologie/") 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') port_list = Port.objects.filter(switch=switch)\
return render(request, 'topologie/index_p.html', {'port_list':port_list, 'id_switch':switch_id, 'nom_switch':switch}) .select_related('room')\
.select_related('machine_interface__domain__extension')\
.select_related('related')\
.select_related('switch')\
.order_by('port')
return render(request, 'topologie/index_p.html', {
'port_list': port_list,
'id_switch': switch_id,
'nom_switch': switch
})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_room(request): def index_room(request):
""" Affichage de l'ensemble des chambres""" """ Affichage de l'ensemble des chambres"""
room_list = Room.objects.order_by('name') room_list = Room.objects.order_by('name')
options, created = GeneralOption.objects.get_or_create() options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
paginator = Paginator(room_list, pagination_number) paginator = Paginator(room_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
@ -123,13 +162,20 @@ def index_room(request):
except EmptyPage: 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.
room_list = paginator.page(paginator.num_pages) 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 @login_required
@permission_required('infra') @permission_required('infra')
def index_stack(request): def index_stack(request):
stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension') """Affichage de la liste des stacks (affiche l'ensemble des switches)"""
return render(request, 'topologie/index_stack.html', {'stack_list': stack_list}) stack_list = Stack.objects.order_by('name')\
.prefetch_related('switch_set__switch_interface__domain__extension')
return render(request, 'topologie/index_stack.html', {
'stack_list': stack_list
})
@login_required @login_required
@ -152,16 +198,24 @@ def new_port(request, switch_id):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Port ajouté") messages.success(request, "Port ajouté")
except IntegrityError: 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 redirect("/topologie/switch/" + switch_id)
return form({'topoform':port}, 'topologie/topo.html', request) return form({'topoform': port}, 'topologie/topo.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_port(request, port_id): 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: 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: except Port.DoesNotExist:
messages.error(request, u"Port inexistant") messages.error(request, u"Port inexistant")
return redirect("/topologie/") return redirect("/topologie/")
@ -170,14 +224,17 @@ def edit_port(request, port_id):
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
port.save() port.save()
reversion.set_user(request.user) 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é") messages.success(request, "Le port a bien été modifié")
return redirect("/topologie/switch/" + str(port_object.switch.id)) 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 @login_required
@permission_required('infra') @permission_required('infra')
def del_port(request,port_id): def del_port(request, port_id):
""" Supprime le port""" """ Supprime le port"""
try: try:
port = Port.objects.get(pk=port_id) port = Port.objects.get(pk=port_id)
@ -192,30 +249,30 @@ def del_port(request,port_id):
reversion.set_comment("Destruction") reversion.set_comment("Destruction")
messages.success(request, "Le port a eté détruit") messages.success(request, "Le port a eté détruit")
except ProtectedError: 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 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 @login_required
@permission_required('infra') @permission_required('infra')
def new_stack(request): def new_stack(request):
"""Ajoute un nouveau stack : stack_id_min, max, et nombre de switches"""
stack = StackForm(request.POST or None) stack = StackForm(request.POST or None)
#if stack.is_valid(): if stack.is_valid():
if request.POST:
try:
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
stack.save() stack.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Stack crée") messages.success(request, "Stack crée")
except: return form({'topoform': stack}, 'topologie/topo.html', request)
messages.error(request, "Cette stack existe déjà")
return form({'topoform':stack}, 'topologie/topo.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_stack(request,stack_id): def edit_stack(request, stack_id):
"""Edition d'un stack (nombre de switches, nom...)"""
try: try:
stack = Stack.objects.get(pk=stack_id) stack = Stack.objects.get(pk=stack_id)
except Stack.DoesNotExist: except Stack.DoesNotExist:
@ -226,13 +283,19 @@ def edit_stack(request,stack_id):
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
stack.save() stack.save()
reversion.set_user(request.user) 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 redirect('/topologie/index_stack')
return form({'topoform':stack}, 'topologie/topo.html', request) return form({'topoform': stack}, 'topologie/topo.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_stack(request,stack_id): def del_stack(request, stack_id):
"""Supprime un stack"""
try: try:
stack = Stack.objects.get(pk=stack_id) stack = Stack.objects.get(pk=stack_id)
except Stack.DoesNotExist: except Stack.DoesNotExist:
@ -246,13 +309,16 @@ def del_stack(request,stack_id):
reversion.set_comment("Destruction") reversion.set_comment("Destruction")
messages.success(request, "La stack a eté détruite") messages.success(request, "La stack a eté détruite")
except ProtectedError: 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 redirect('/topologie/index_stack')
return form({'objet':stack}, 'topologie/delete.html', request) return form({'objet': stack}, 'topologie/delete.html', request)
@login_required @login_required
@permission_required('infra') @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: try:
stack = Stack.objects.get(pk=stack_id) stack = Stack.objects.get(pk=stack_id)
except Stack.DoesNotExist: except Stack.DoesNotExist:
@ -264,30 +330,36 @@ def edit_switchs_stack(request,stack_id):
context = {'stack': stack} context = {'stack': stack}
context['switchs_stack'] = stack.switchs_set.all() context['switchs_stack'] = stack.switchs_set.all()
context['switchs_autres'] = Switch.object.filter(stack=None) context['switchs_autres'] = Switch.object.filter(stack=None)
pass
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_switch(request): def new_switch(request):
""" Creation d'un switch. Cree en meme temps l'interface et la machine associée. """ Creation d'un switch. Cree en meme temps l'interface et la machine
Vue complexe. Appelle successivement les 4 models forms adaptés : machine, associée. Vue complexe. Appelle successivement les 4 models forms
interface, domain et switch""" adaptés : machine, interface, domain et switch"""
switch = NewSwitchForm(request.POST or None) switch = NewSwitchForm(request.POST or None)
machine = NewMachineForm(request.POST or None) machine = NewMachineForm(request.POST or None)
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) interface = AddInterfaceForm(
domain = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',))) request.POST or None,
infra=request.user.has_perms(('infra',))
)
domain = AliasForm(
request.POST or None,
infra=request.user.has_perms(('infra',))
)
if switch.is_valid() and machine.is_valid() and interface.is_valid(): 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 user = options.utilisateur_asso
if not user: 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/") return redirect("/topologie/")
new_machine = machine.save(commit=False) new_machine = machine.save(commit=False)
new_machine.user = user new_machine.user = user
new_interface = interface.save(commit=False) new_interface = interface.save(commit=False)
new_switch = switch.save(commit=False) new_switch_instance = switch.save(commit=False)
new_domain = domain.save(commit=False) new_domain_instance = domain.save(commit=False)
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_machine.save() new_machine.save()
reversion.set_user(request.user) reversion.set_user(request.user)
@ -297,58 +369,95 @@ def new_switch(request):
new_interface.save() new_interface.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
new_domain.interface_parent = new_interface new_domain_instance.interface_parent = new_interface
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_domain.save() new_domain_instance.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
new_switch.switch_interface = new_interface new_switch_instance.switch_interface = new_interface
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_switch.save() new_switch_instance.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") 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 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 @login_required
@permission_required('infra') @permission_required('infra')
def edit_switch(request, switch_id): def edit_switch(request, switch_id):
""" Edition d'un switch. Permet de chambre nombre de ports, place dans le stack, """ Edition d'un switch. Permet de chambre nombre de ports,
interface et machine associée""" place dans le stack, interface et machine associée"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
messages.error(request, u"Switch inexistant") messages.error(request, u"Switch inexistant")
return redirect("/topologie/") return redirect("/topologie/")
switch_form = EditSwitchForm(request.POST or None, instance=switch) switch_form = EditSwitchForm(request.POST or None, instance=switch)
machine_form = EditMachineForm(request.POST or None, instance=switch.switch_interface.machine) machine_form = EditMachineForm(
interface_form = EditInterfaceForm(request.POST or None, instance=switch.switch_interface) request.POST or None,
domain_form = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',)), instance=switch.switch_interface.domain) instance=switch.switch_interface.machine
if switch_form.is_valid() and machine_form.is_valid() and interface_form.is_valid(): )
interface_form = EditInterfaceForm(
request.POST or None,
instance=switch.switch_interface
)
domain_form = AliasForm(
request.POST or None,
infra=request.user.has_perms(('infra',)),
instance=switch.switch_interface.domain
)
if switch_form.is_valid() and machine_form.is_valid()\
and interface_form.is_valid():
new_interface = interface_form.save(commit=False) new_interface = interface_form.save(commit=False)
new_machine = machine_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) new_domain = domain_form.save(commit=False)
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
new_machine.save() new_machine.save()
reversion.set_user(request.user) 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(): with transaction.atomic(), reversion.create_revision():
new_interface.save() new_interface.save()
reversion.set_user(request.user) 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(): with transaction.atomic(), reversion.create_revision():
new_domain.save() new_domain.save()
reversion.set_user(request.user) 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(): with transaction.atomic(), reversion.create_revision():
new_switch.save() new_switch_instance.save()
reversion.set_user(request.user) 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é") messages.success(request, "Le switch a bien été modifié")
return redirect("/topologie/") 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 @login_required
@permission_required('infra') @permission_required('infra')
@ -362,7 +471,8 @@ def new_room(request):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "La chambre a été créé") messages.success(request, "La chambre a été créé")
return redirect("/topologie/index_room/") return redirect("/topologie/index_room/")
return form({'topoform':room}, 'topologie/topo.html', request) return form({'topoform': room}, 'topologie/topo.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -378,10 +488,13 @@ def edit_room(request, room_id):
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
room.save() room.save()
reversion.set_user(request.user) 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") messages.success(request, "La chambre a bien été modifiée")
return redirect("/topologie/index_room/") return redirect("/topologie/index_room/")
return form({'topoform':room}, 'topologie/topo.html', request) return form({'topoform': room}, 'topologie/topo.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -390,7 +503,7 @@ def del_room(request, room_id):
try: try:
room = Room.objects.get(pk=room_id) room = Room.objects.get(pk=room_id)
except Room.DoesNotExist: except Room.DoesNotExist:
messages.error(request, u"Chambre inexistante" ) messages.error(request, u"Chambre inexistante")
return redirect("/topologie/index_room/") return redirect("/topologie/index_room/")
if request.method == "POST": if request.method == "POST":
try: try:
@ -400,6 +513,10 @@ def del_room(request, room_id):
reversion.set_comment("Destruction") reversion.set_comment("Destruction")
messages.success(request, "La chambre/prise a été détruite") messages.success(request, "La chambre/prise a été détruite")
except ProtectedError: 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 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)

View file

@ -20,6 +20,10 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Definition des vues pour les admin. Classique, sauf pour users,
on fait appel à UserChange et ServiceUserChange, forms custom
"""
from __future__ import unicode_literals from __future__ import unicode_literals
@ -28,11 +32,15 @@ from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import User, ServiceUser, School, Right, ListRight, ListShell, Ban, Whitelist, Request, LdapUser, LdapServiceUser, LdapServiceUserGroup, LdapUserGroup from .models import User, ServiceUser, School, Right, ListRight, ListShell
from .forms import UserChangeForm, UserCreationForm, ServiceUserChangeForm, ServiceUserCreationForm from .models import Ban, Whitelist, Request, LdapUser, LdapServiceUser
from .models import LdapServiceUserGroup, LdapUserGroup
from .forms import UserChangeForm, UserCreationForm
from .forms import ServiceUserChangeForm, ServiceUserCreationForm
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
"""Administration d'un user"""
list_display = ( list_display = (
'name', 'name',
'surname', 'surname',
@ -43,51 +51,73 @@ class UserAdmin(admin.ModelAdmin):
'shell', 'shell',
'state' 'state'
) )
search_fields = ('name','surname','pseudo','room') search_fields = ('name', 'surname', 'pseudo', 'room')
class LdapUserAdmin(admin.ModelAdmin): class LdapUserAdmin(admin.ModelAdmin):
list_display = ('name','uidNumber','login_shell') """Administration du ldapuser"""
exclude = ('user_password','sambat_nt_password') list_display = ('name', 'uidNumber', 'login_shell')
exclude = ('user_password', 'sambat_nt_password')
search_fields = ('name',) search_fields = ('name',)
class LdapServiceUserAdmin(admin.ModelAdmin): class LdapServiceUserAdmin(admin.ModelAdmin):
"""Administration du ldapserviceuser"""
list_display = ('name',) list_display = ('name',)
exclude = ('user_password',) exclude = ('user_password',)
search_fields = ('name',) search_fields = ('name',)
class LdapUserGroupAdmin(admin.ModelAdmin): class LdapUserGroupAdmin(admin.ModelAdmin):
list_display = ('name','members','gid') """Administration du ldapusergroupe"""
list_display = ('name', 'members', 'gid')
search_fields = ('name',) search_fields = ('name',)
class LdapServiceUserGroupAdmin(admin.ModelAdmin): class LdapServiceUserGroupAdmin(admin.ModelAdmin):
"""Administration du ldap serviceusergroup"""
list_display = ('name',) list_display = ('name',)
search_fields = ('name',) search_fields = ('name',)
class SchoolAdmin(VersionAdmin): class SchoolAdmin(VersionAdmin):
list_display = ('name',) """Administration, gestion des écoles"""
pass
class ListRightAdmin(VersionAdmin): class ListRightAdmin(VersionAdmin):
"""Gestion de la liste des droits existants
Ne permet pas l'edition du gid (primarykey pour ldap)"""
list_display = ('listright',) list_display = ('listright',)
class ListShellAdmin(VersionAdmin): class ListShellAdmin(VersionAdmin):
list_display = ('shell',) """Gestion de la liste des shells coté admin"""
pass
class RightAdmin(VersionAdmin): class RightAdmin(VersionAdmin):
list_display = ('user', 'right') """Gestion de la liste des droits affectés"""
pass
class RequestAdmin(admin.ModelAdmin): class RequestAdmin(admin.ModelAdmin):
"""Gestion des request objet, ticket pour lien de reinit mot de passe"""
list_display = ('user', 'type', 'created_at', 'expires_at') list_display = ('user', 'type', 'created_at', 'expires_at')
class BanAdmin(VersionAdmin): class BanAdmin(VersionAdmin):
list_display = ('user', 'raison', 'date_start', 'date_end') """Gestion des bannissements"""
pass
class WhitelistAdmin(VersionAdmin): class WhitelistAdmin(VersionAdmin):
list_display = ('user', 'raison', 'date_start', 'date_end') """Gestion des whitelist"""
pass
class UserAdmin(VersionAdmin, BaseUserAdmin): class UserAdmin(VersionAdmin, BaseUserAdmin):
"""Gestion d'un user : modification des champs perso, mot de passe, etc"""
# The forms to add and change user instances # The forms to add and change user instances
form = UserChangeForm form = UserChangeForm
add_form = UserCreationForm add_form = UserCreationForm
@ -95,27 +125,56 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
# The fields to be used in displaying the User model. # The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin # These override the definitions on the base UserAdmin
# that reference specific fields on auth.User. # that reference specific fields on auth.User.
list_display = ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'shell') list_display = (
'pseudo',
'name',
'surname',
'email',
'school',
'is_admin',
'shell'
)
list_display = ('pseudo',) list_display = ('pseudo',)
list_filter = () list_filter = ()
fieldsets = ( fieldsets = (
(None, {'fields': ('pseudo', 'password')}), (None, {'fields': ('pseudo', 'password')}),
('Personal info', {'fields': ('name', 'surname', 'email', 'school','shell', 'uid_number')}), (
'Personal info',
{
'fields':
('name', 'surname', 'email', 'school', 'shell', 'uid_number')
}
),
('Permissions', {'fields': ('is_admin', )}), ('Permissions', {'fields': ('is_admin', )}),
) )
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user. # overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = ( add_fieldsets = (
(None, { (
None,
{
'classes': ('wide',), 'classes': ('wide',),
'fields': ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'password1', 'password2')} 'fields': (
'pseudo',
'name',
'surname',
'email',
'school',
'is_admin',
'password1',
'password2'
)
}
), ),
) )
search_fields = ('pseudo',) search_fields = ('pseudo',)
ordering = ('pseudo',) ordering = ('pseudo',)
filter_horizontal = () filter_horizontal = ()
class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): class ServiceUserAdmin(VersionAdmin, BaseUserAdmin):
"""Gestion d'un service user admin : champs personnels,
mot de passe; etc"""
# The forms to add and change user instances # The forms to add and change user instances
form = ServiceUserChangeForm form = ServiceUserChangeForm
add_form = ServiceUserCreationForm add_form = ServiceUserCreationForm
@ -131,15 +190,19 @@ class ServiceUserAdmin(VersionAdmin, BaseUserAdmin):
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user. # overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = ( add_fieldsets = (
(None, { (
None,
{
'classes': ('wide',), 'classes': ('wide',),
'fields': ('pseudo', 'password1', 'password2')} 'fields': ('pseudo', 'password1', 'password2')
}
), ),
) )
search_fields = ('pseudo',) search_fields = ('pseudo',)
ordering = ('pseudo',) ordering = ('pseudo',)
filter_horizontal = () filter_horizontal = ()
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(ServiceUser, ServiceUserAdmin) admin.site.register(ServiceUser, ServiceUserAdmin)
admin.site.register(LdapUser, LdapUserAdmin) admin.site.register(LdapUser, LdapUserAdmin)

View file

@ -20,8 +20,16 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 from __future__ import unicode_literals
@ -29,17 +37,34 @@ from django import forms
from django.forms import ModelForm, Form from django.forms import ModelForm, Form
from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from preferences.models import OptionalUser
from django.utils import timezone from django.utils import timezone
from .models import User, ServiceUser, Right, School, ListRight, Whitelist, Ban, Request, remove_user_room
from .models import get_admin_right from preferences.models import OptionalUser
from .models import User, ServiceUser, Right, School, ListRight, Whitelist
from .models import Ban, remove_user_room
NOW = timezone.now()
class PassForm(forms.Form): class PassForm(forms.Form):
passwd1 = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput) """Formulaire de changement de mot de passe. Verifie que les 2
passwd2 = forms.CharField(label=u'Saisir à nouveau le mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput) 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): def clean_passwd2(self):
"""Verifie que passwd1 et 2 sont identiques"""
# Check that the two password entries match # Check that the two password entries match
password1 = self.cleaned_data.get("passwd1") password1 = self.cleaned_data.get("passwd1")
password2 = self.cleaned_data.get("passwd2") password2 = self.cleaned_data.get("passwd2")
@ -47,18 +72,38 @@ class PassForm(forms.Form):
raise forms.ValidationError("Passwords don't match") raise forms.ValidationError("Passwords don't match")
return password2 return password2
class UserCreationForm(forms.ModelForm): class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required """A form for creating new users. Includes all the required
fields, plus a repeated password.""" 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) 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') 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: class Meta:
model = User model = User
fields = ('pseudo', 'name', 'surname', 'email') fields = ('pseudo', 'name', 'surname', 'email')
def clean_password2(self): def clean_password2(self):
"""Verifie que password1 et 2 sont identiques"""
# Check that the two password entries match # Check that the two password entries match
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
@ -74,17 +119,40 @@ class UserCreationForm(forms.ModelForm):
user.is_admin = self.cleaned_data.get("is_admin") user.is_admin = self.cleaned_data.get("is_admin")
return user return user
class ServiceUserCreationForm(forms.ModelForm): class ServiceUserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required """A form for creating new users. Includes all the required
fields, plus a repeated password.""" 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) 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: class Meta:
model = ServiceUser model = ServiceUser
fields = ('pseudo',) fields = ('pseudo',)
def clean_password2(self): def clean_password2(self):
"""Verifie que password1 et 2 sont indentiques"""
# Check that the two password entries match # Check that the two password entries match
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
@ -99,10 +167,13 @@ class ServiceUserCreationForm(forms.ModelForm):
user.save() user.save()
return user return user
class UserChangeForm(forms.ModelForm): class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on """A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's the user, but replaces the password field with admin's
password hash display field. password hash display field.
Formulaire pour la modification d'un user coté admin
""" """
password = ReadOnlyPasswordHashField() password = ReadOnlyPasswordHashField()
is_admin = forms.BooleanField(label='is admin', required=False) is_admin = forms.BooleanField(label='is admin', required=False)
@ -112,11 +183,13 @@ class UserChangeForm(forms.ModelForm):
fields = ('pseudo', 'password', 'name', 'surname', 'email') fields = ('pseudo', 'password', 'name', 'surname', 'email')
def __init__(self, *args, **kwargs): 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) print("User is admin : %s" % kwargs['instance'].is_admin)
self.initial['is_admin'] = kwargs['instance'].is_admin self.initial['is_admin'] = kwargs['instance'].is_admin
def clean_password(self): def clean_password(self):
"""Dummy fun"""
# Regardless of what the user provides, return the initial value. # Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the # This is done here, rather than on the field, because the
# field does not have access to the initial value # field does not have access to the initial value
@ -130,40 +203,62 @@ class UserChangeForm(forms.ModelForm):
user.save() user.save()
return user return user
class ServiceUserChangeForm(forms.ModelForm): class ServiceUserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on """A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's the user, but replaces the password field with admin's
password hash display field. password hash display field.
Formulaire pour l'edition des service users coté admin
""" """
password = ReadOnlyPasswordHashField() 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: class Meta:
model = ServiceUser model = ServiceUser
fields = ('pseudo',) fields = ('pseudo',)
def clean_password(self): def clean_password(self):
# Regardless of what the user provides, return the initial value. """Dummy fun"""
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"] return self.initial["password"]
class ResetPasswordForm(forms.Form): class ResetPasswordForm(forms.Form):
"""Formulaire de demande de reinitialisation de mot de passe,
mdp oublié"""
pseudo = forms.CharField(label=u'Pseudo', max_length=255) pseudo = forms.CharField(label=u'Pseudo', max_length=255)
email = forms.EmailField(max_length=255) email = forms.EmailField(max_length=255)
class MassArchiveForm(forms.Form): 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') date = forms.DateTimeField(help_text='%d/%m/%y')
def clean(self): def clean(self):
cleaned_data=super(MassArchiveForm, self).clean() cleaned_data = super(MassArchiveForm, self).clean()
date = cleaned_data.get("date") date = cleaned_data.get("date")
if date: if date:
if date>timezone.now(): if date > NOW:
raise forms.ValidationError("Impossible d'archiver des utilisateurs dont la fin d'accès se situe dans le futur !") raise forms.ValidationError("Impossible d'archiver des\
utilisateurs dont la fin d'accès se situe dans le futur !")
class BaseInfoForm(ModelForm): class BaseInfoForm(ModelForm):
"""Formulaire de base d'edition d'un user. Formulaire de base, utilisé
pour l'edition de self par self ou un cableur. On formate les champs
avec des label plus jolis"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BaseInfoForm, self).__init__(*args, **kwargs) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(BaseInfoForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Prénom' self.fields['name'].label = 'Prénom'
self.fields['surname'].label = 'Nom' self.fields['surname'].label = 'Nom'
self.fields['school'].label = 'Établissement' self.fields['school'].label = 'Établissement'
@ -186,13 +281,21 @@ class BaseInfoForm(ModelForm):
] ]
def clean_telephone(self): def clean_telephone(self):
"""Verifie que le tel est présent si 'option est validée
dans preferences"""
telephone = self.cleaned_data['telephone'] 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: 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 return telephone
class EditInfoForm(BaseInfoForm): class EditInfoForm(BaseInfoForm):
"""Edition complète d'un user. Utilisé par admin,
permet d'editer normalement la chambre, ou le shell
Herite de la base"""
class Meta(BaseInfoForm.Meta): class Meta(BaseInfoForm.Meta):
fields = [ fields = [
'name', 'name',
@ -206,37 +309,67 @@ class EditInfoForm(BaseInfoForm):
'telephone', 'telephone',
] ]
class InfoForm(EditInfoForm): class InfoForm(EditInfoForm):
""" Utile pour forcer un déménagement quand il y a déjà un user en place""" """ 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) Formuaire utilisé pour la creation initiale"""
force = forms.BooleanField(
label="Forcer le déménagement ?",
initial=False,
required=False
)
def clean_force(self): 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): if self.cleaned_data.get('force', False):
remove_user_room(self.cleaned_data.get('room')) remove_user_room(self.cleaned_data.get('room'))
return return
class UserForm(InfoForm): class UserForm(InfoForm):
""" Model form general""" """ Model form general"""
class Meta(InfoForm.Meta): class Meta(InfoForm.Meta):
fields = '__all__' fields = '__all__'
class PasswordForm(ModelForm): 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: class Meta:
model = User model = User
fields = ['password', 'pwd_ntlm'] 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): class ServiceUserForm(ModelForm):
""" Modification d'un service user""" """ 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: class Meta:
model = ServiceUser 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): 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): class Meta(ServiceUserForm.Meta):
fields = ['access_group','comment'] fields = ['access_group', 'comment']
class StateForm(ModelForm): class StateForm(ModelForm):
""" Changement de l'état d'un user""" """ Changement de l'état d'un user"""
@ -244,42 +377,70 @@ class StateForm(ModelForm):
model = User model = User
fields = ['state'] 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): class SchoolForm(ModelForm):
"""Edition, creation d'un école"""
class Meta: class Meta:
model = School model = School
fields = ['name'] fields = ['name']
def __init__(self, *args, **kwargs): 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' self.fields['name'].label = 'Établissement'
class ListRightForm(ModelForm): 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: class Meta:
model = ListRight model = ListRight
fields = ['listright', 'details'] fields = ['listright', 'details']
def __init__(self, *args, **kwargs): 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' self.fields['listright'].label = 'Nom du droit/groupe'
class NewListRightForm(ListRightForm): class NewListRightForm(ListRightForm):
"""Ajout d'un groupe/list de droit """
class Meta(ListRightForm.Meta): class Meta(ListRightForm.Meta):
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NewListRightForm, self).__init__(*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): 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): 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): class RightForm(ModelForm):
"""Assignation d'un droit à un user"""
def __init__(self, *args, **kwargs): 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'].label = 'Droit'
self.fields['right'].empty_label = "Choisir un nouveau droit" self.fields['right'].empty_label = "Choisir un nouveau droit"
@ -289,15 +450,22 @@ class RightForm(ModelForm):
class DelRightForm(Form): class DelRightForm(Form):
rights = forms.ModelMultipleChoiceField(queryset=Right.objects.all(), widget=forms.CheckboxSelectMultiple) """Suppression d'un droit d'un user"""
rights = forms.ModelMultipleChoiceField(
queryset=Right.objects.all(),
widget=forms.CheckboxSelectMultiple
)
def __init__(self, right, *args, **kwargs): def __init__(self, right, *args, **kwargs):
super(DelRightForm, self).__init__(*args, **kwargs) super(DelRightForm, self).__init__(*args, **kwargs)
self.fields['rights'].queryset = Right.objects.filter(right=right) self.fields['rights'].queryset = Right.objects.filter(right=right)
class BanForm(ModelForm): class BanForm(ModelForm):
"""Creation, edition d'un objet bannissement"""
def __init__(self, *args, **kwargs): 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' self.fields['date_end'].label = 'Date de fin'
class Meta: class Meta:
@ -305,15 +473,19 @@ class BanForm(ModelForm):
exclude = ['user'] exclude = ['user']
def clean_date_end(self): def clean_date_end(self):
"""Verification que date_end est après now"""
date_end = self.cleaned_data['date_end'] date_end = self.cleaned_data['date_end']
if date_end < timezone.now(): 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") 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 return date_end
class WhitelistForm(ModelForm): class WhitelistForm(ModelForm):
"""Creation, edition d'un objet whitelist"""
def __init__(self, *args, **kwargs): 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' self.fields['date_end'].label = 'Date de fin'
class Meta: class Meta:
@ -321,7 +493,9 @@ class WhitelistForm(ModelForm):
exclude = ['user'] exclude = ['user']
def clean_date_end(self): def clean_date_end(self):
"""Verification que la date_end est posterieur à now"""
date_end = self.cleaned_data['date_end'] date_end = self.cleaned_data['date_end']
if date_end < timezone.now(): 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") 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 return date_end

File diff suppressed because it is too large Load diff

View file

@ -35,19 +35,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <tbody>
<tr>
{% for key, values in userform.items %} {% for key, values in userform.items %}
<th>{{ key }}</th> <tr class="active">
{% endfor %} <td>
<a data-toggle="collapse" href="#collapseRight_{{key}}" aria-expanded="false" aria-controls="collapseRights_{{key}}">
<b>{{ key }}</b> ( {{values.rights|length }} users )
</a>
</td>
</tr> </tr>
</thead>
<tr> <tr>
{% for key, values in userform.items %} <td>
{% bootstrap_form_errors values %} <div class="collapse" id="collapseRight_{{key}}">
<th>{{ values.rights }}</th> <ul class="list-group" style="margin-bottom: 0px">
{% for user in values.rights %}
<li class="list-group-item col-xs-6 col-sm-4 col-md-3" style="border: none;">{{ user }}</li>
{% endfor %} {% endfor %}
</ul>
</div>
</td>
</tr> </tr>
{% endfor %}
</tbody>
</table> </table>
{% bootstrap_button "Modifier" button_type="submit" icon="star" %} {% bootstrap_button "Modifier" button_type="submit" icon="star" %}
</form> </form>

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% block title %}Création et modification d'utilisateur{% endblock %} {% 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.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form userform %} {% massive_bootstrap_form userform 'room' %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form> </form>
<br /> <br />

View file

@ -19,6 +19,9 @@
# You should have received a copy of the GNU General Public License along # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Definition des urls, pointant vers les views
"""
from __future__ import unicode_literals from __future__ import unicode_literals
@ -32,39 +35,88 @@ urlpatterns = [
url(r'^state/(?P<userid>[0-9]+)$', views.state, name='state'), url(r'^state/(?P<userid>[0-9]+)$', views.state, name='state'),
url(r'^password/(?P<userid>[0-9]+)$', views.password, name='password'), url(r'^password/(?P<userid>[0-9]+)$', views.password, name='password'),
url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'), url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'),
url(r'^edit_serviceuser/(?P<userid>[0-9]+)$', views.edit_serviceuser, name='edit-serviceuser'), url(
url(r'^del_serviceuser/(?P<userid>[0-9]+)$', views.del_serviceuser, name='del-serviceuser'), r'^edit_serviceuser/(?P<userid>[0-9]+)$',
views.edit_serviceuser,
name='edit-serviceuser'
),
url(
r'^del_serviceuser/(?P<userid>[0-9]+)$',
views.del_serviceuser,
name='del-serviceuser'
),
url(r'^add_ban/(?P<userid>[0-9]+)$', views.add_ban, name='add-ban'), url(r'^add_ban/(?P<userid>[0-9]+)$', views.add_ban, name='add-ban'),
url(r'^edit_ban/(?P<banid>[0-9]+)$', views.edit_ban, name='edit-ban'), url(r'^edit_ban/(?P<banid>[0-9]+)$', views.edit_ban, name='edit-ban'),
url(r'^add_whitelist/(?P<userid>[0-9]+)$', views.add_whitelist, name='add-whitelist'), url(
url(r'^edit_whitelist/(?P<whitelistid>[0-9]+)$', views.edit_whitelist, name='edit-whitelist'), r'^add_whitelist/(?P<userid>[0-9]+)$',
views.add_whitelist,
name='add-whitelist'
),
url(
r'^edit_whitelist/(?P<whitelistid>[0-9]+)$',
views.edit_whitelist,
name='edit-whitelist'
),
url(r'^add_right/(?P<userid>[0-9]+)$', views.add_right, name='add-right'), url(r'^add_right/(?P<userid>[0-9]+)$', views.add_right, name='add-right'),
url(r'^del_right/$', views.del_right, name='del-right'), url(r'^del_right/$', views.del_right, name='del-right'),
url(r'^add_school/$', views.add_school, name='add-school'), url(r'^add_school/$', views.add_school, name='add-school'),
url(r'^edit_school/(?P<schoolid>[0-9]+)$', views.edit_school, name='edit-school'), url(
r'^edit_school/(?P<schoolid>[0-9]+)$',
views.edit_school,
name='edit-school'
),
url(r'^del_school/$', views.del_school, name='del-school'), url(r'^del_school/$', views.del_school, name='del-school'),
url(r'^add_listright/$', views.add_listright, name='add-listright'), url(r'^add_listright/$', views.add_listright, name='add-listright'),
url(r'^edit_listright/(?P<listrightid>[0-9]+)$', views.edit_listright, name='edit-listright'), url(
r'^edit_listright/(?P<listrightid>[0-9]+)$',
views.edit_listright,
name='edit-listright'
),
url(r'^del_listright/$', views.del_listright, name='del-listright'), url(r'^del_listright/$', views.del_listright, name='del-listright'),
url(r'^profil/(?P<userid>[0-9]+)$', views.profil, name='profil'), url(r'^profil/(?P<userid>[0-9]+)$', views.profil, name='profil'),
url(r'^index_ban/$', views.index_ban, name='index-ban'), url(r'^index_ban/$', views.index_ban, name='index-ban'),
url(r'^index_white/$', views.index_white, name='index-white'), url(r'^index_white/$', views.index_white, name='index-white'),
url(r'^index_school/$', views.index_school, name='index-school'), url(r'^index_school/$', views.index_school, name='index-school'),
url(r'^index_listright/$', views.index_listright, name='index-listright'), 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'^mon_profil/$', views.mon_profil, name='mon-profil'),
url(r'^process/(?P<token>[a-z0-9]{32})/$', views.process, name='process'), url(r'^process/(?P<token>[a-z0-9]{32})/$', views.process, name='process'),
url(r'^reset_password/$', views.reset_password, name='reset-password'), url(r'^reset_password/$', views.reset_password, name='reset-password'),
url(r'^mass_archive/$', views.mass_archive, name='mass-archive'), url(r'^mass_archive/$', views.mass_archive, name='mass-archive'),
url(r'^history/(?P<object>user)/(?P<id>[0-9]+)$', views.history, name='history'), url(
url(r'^history/(?P<object>ban)/(?P<id>[0-9]+)$', views.history, name='history'), r'^history/(?P<object>user)/(?P<id>[0-9]+)$',
url(r'^history/(?P<object>whitelist)/(?P<id>[0-9]+)$', views.history, name='history'), views.history,
url(r'^history/(?P<object>school)/(?P<id>[0-9]+)$', views.history, name='history'), name='history'
url(r'^history/(?P<object>listright)/(?P<id>[0-9]+)$', views.history, name='history'), ),
url(r'^history/(?P<object>serviceuser)/(?P<id>[0-9]+)$', views.history, name='history'), url(
r'^history/(?P<object>ban)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(
r'^history/(?P<object>whitelist)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(
r'^history/(?P<object>school)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(
r'^history/(?P<object>listright)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(
r'^history/(?P<object>serviceuser)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url(r'^rest/mailing/$', views.mailing, name='mailing'), url(r'^rest/mailing/$', views.mailing, name='mailing'),
] ]

View file

@ -23,20 +23,25 @@
# App de gestion des users pour re2o # App de gestion des users pour re2o
# Goulven Kermarec, Gabriel Détraz, Lemesle Augustin # Goulven Kermarec, Gabriel Détraz, Lemesle Augustin
# Gplv2 # 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 __future__ import unicode_literals
from django.shortcuts import get_object_or_404, render, redirect 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.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required 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.db import IntegrityError
from django.core.mail import send_mail
from django.utils import timezone from django.utils import timezone
from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt 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.models import Version
from reversion import revisions as reversion from reversion import revisions as reversion
from users.serializers import MailSerializer from users.serializers import MailSerializer
from users.models import User, Right, Ban, Whitelist, School, ListRight, Request, ServiceUser, all_has_access from users.models import User, Right, Ban, Whitelist, School, ListRight
from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm, DelListRightForm, NewListRightForm from users.models import Request, ServiceUser
from users.forms import EditInfoForm, InfoForm, BaseInfoForm, StateForm, RightForm, SchoolForm, EditServiceUserForm, ServiceUserForm, ListRightForm from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm
from cotisations.models import Facture from users.forms import DelListRightForm, NewListRightForm
from machines.models import Machine, Interface from users.forms import InfoForm, BaseInfoForm, StateForm
from users.forms import RightForm, SchoolForm, EditServiceUserForm
from users.forms import ServiceUserForm, ListRightForm
from users.forms import MassArchiveForm, PassForm, ResetPasswordForm from 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 from re2o.views import form
from re2o.utils import all_has_access
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
def password_change_action(u_form, user, request, req=False): def password_change_action(u_form, user, request, req=False):
""" Fonction qui effectue le changeemnt de mdp bdd""" """ Fonction qui effectue le changeemnt de mdp bdd"""
@ -75,10 +79,12 @@ def password_change_action(u_form, user, request, req=False):
return redirect("/") return redirect("/")
return redirect("/users/profil/" + str(user.id)) return redirect("/users/profil/" + str(user.id))
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def new_user(request): def new_user(request):
""" Vue de création d'un nouvel utilisateur, envoie un mail pour le mot de passe""" """ Vue de création d'un nouvel utilisateur,
envoie un mail pour le mot de passe"""
user = InfoForm(request.POST or None) user = InfoForm(request.POST or None)
if user.is_valid(): if user.is_valid():
user = user.save(commit=False) user = user.save(commit=False)
@ -87,21 +93,25 @@ def new_user(request):
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
user.reset_passwd_mail(request) 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 redirect("/users/profil/" + str(user.id))
return form({'userform': user}, 'users/user.html', request) return form({'userform': user}, 'users/user.html', request)
@login_required @login_required
def edit_info(request, userid): def edit_info(request, userid):
""" Edite un utilisateur à partir de son id, """ Edite un utilisateur à partir de son id,
si l'id est différent de request.user, vérifie la possession du droit cableur """ si l'id est différent de request.user, vérifie la
possession du droit cableur """
try: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
except User.DoesNotExist: except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant") messages.error(request, "Utilisateur inexistant")
return redirect("/users/") return redirect("/users/")
if not request.user.has_perms(('cableur',)) and user != request.user: 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)) return redirect("/users/profil/" + str(request.user.id))
if not request.user.has_perms(('cableur',)): if not request.user.has_perms(('cableur',)):
user = BaseInfoForm(request.POST or None, instance=user) user = BaseInfoForm(request.POST or None, instance=user)
@ -111,15 +121,19 @@ def edit_info(request, userid):
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
user.save() user.save()
reversion.set_user(request.user) 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é") messages.success(request, "L'user a bien été modifié")
return redirect("/users/profil/" + userid) return redirect("/users/profil/" + userid)
return form({'userform': user}, 'users/user.html', request) return form({'userform': user}, 'users/user.html', request)
@login_required @login_required
@permission_required('bureau') @permission_required('bureau')
def state(request, userid): 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: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
except User.DoesNotExist: except User.DoesNotExist:
@ -135,12 +149,15 @@ def state(request, userid):
elif state.cleaned_data['state'] == User.STATE_DISABLED: elif state.cleaned_data['state'] == User.STATE_DISABLED:
user.state = User.STATE_DISABLED user.state = User.STATE_DISABLED
reversion.set_user(request.user) 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() user.save()
messages.success(request, "Etat changé avec succès") messages.success(request, "Etat changé avec succès")
return redirect("/users/profil/" + userid) return redirect("/users/profil/" + userid)
return form({'userform': state}, 'users/user.html', request) return form({'userform': state}, 'users/user.html', request)
@login_required @login_required
def password(request, userid): def password(request, userid):
""" Reinitialisation d'un mot de passe à partir de l'userid, """ Reinitialisation d'un mot de passe à partir de l'userid,
@ -152,16 +169,20 @@ def password(request, userid):
messages.error(request, "Utilisateur inexistant") messages.error(request, "Utilisateur inexistant")
return redirect("/users/") return redirect("/users/")
if not request.user.has_perms(('cableur',)) and user != request.user: 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)) 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): if not request.user.has_perms(('bureau',)) and user != request.user\
messages.error(request, "Il faut les droits bureau pour modifier le mot de passe d'un membre actif") 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)) return redirect("/users/profil/" + str(request.user.id))
u_form = PassForm(request.POST or None) u_form = PassForm(request.POST or None)
if u_form.is_valid(): if u_form.is_valid():
return password_change_action(u_form, user, request) return password_change_action(u_form, user, request)
return form({'userform': u_form}, 'users/user.html', request) return form({'userform': u_form}, 'users/user.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_serviceuser(request): def new_serviceuser(request):
@ -174,15 +195,20 @@ def new_serviceuser(request):
user_object.save() user_object.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") 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 redirect("/users/index_serviceusers/")
return form({'userform': user}, 'users/user.html', request) return form({'userform': user}, 'users/user.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_serviceuser(request, userid): def edit_serviceuser(request, userid):
""" Edite un utilisateur à partir de son id, """ Edite un utilisateur à partir de son id,
si l'id est différent de request.user, vérifie la possession du droit cableur """ si l'id est différent de request.user,
vérifie la possession du droit cableur """
try: try:
user = ServiceUser.objects.get(pk=userid) user = ServiceUser.objects.get(pk=userid)
except ServiceUser.DoesNotExist: except ServiceUser.DoesNotExist:
@ -196,18 +222,22 @@ def edit_serviceuser(request, userid):
user_object.set_password(user.cleaned_data['password']) user_object.set_password(user.cleaned_data['password'])
user_object.save() user_object.save()
reversion.set_user(request.user) 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é") messages.success(request, "L'user a bien été modifié")
return redirect("/users/index_serviceusers") return redirect("/users/index_serviceusers")
return form({'userform': user}, 'users/user.html', request) return form({'userform': user}, 'users/user.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_serviceuser(request, userid): def del_serviceuser(request, userid):
"""Suppression d'un ou plusieurs serviceusers"""
try: try:
user = ServiceUser.objects.get(pk=userid) user = ServiceUser.objects.get(pk=userid)
except ServiceUser.DoesNotExist: except ServiceUser.DoesNotExist:
messages.error(request, u"Utilisateur inexistant" ) messages.error(request, u"Utilisateur inexistant")
return redirect("/users/") return redirect("/users/")
if request.method == "POST": if request.method == "POST":
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -215,7 +245,12 @@ def del_serviceuser(request, userid):
reversion.set_user(request.user) reversion.set_user(request.user)
messages.success(request, "L'user a été détruite") messages.success(request, "L'user a été détruite")
return redirect("/users/index_serviceusers/") 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 @login_required
@permission_required('bureau') @permission_required('bureau')
@ -241,28 +276,33 @@ def add_right(request, userid):
return redirect("/users/profil/" + userid) return redirect("/users/profil/" + userid)
return form({'userform': right}, 'users/user.html', request) return form({'userform': right}, 'users/user.html', request)
@login_required @login_required
@permission_required('bureau') @permission_required('bureau')
def del_right(request): def del_right(request):
""" Supprimer un droit à un user, need droit bureau """ """ Supprimer un droit à un user, need droit bureau """
user_right_list = dict() user_right_list = dict()
for right in ListRight.objects.all(): for right in ListRight.objects.all():
user_right_list[right]= DelRightForm(right, request.POST or None) user_right_list[right] = DelRightForm(right, request.POST or None)
for keys, right_item in user_right_list.items(): for _keys, right_item in user_right_list.items():
if right_item.is_valid(): if right_item.is_valid():
right_del = right_item.cleaned_data['rights'] right_del = right_item.cleaned_data['rights']
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
reversion.set_user(request.user) 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() right_del.delete()
messages.success(request, "Droit retiré avec succès") messages.success(request, "Droit retiré avec succès")
return redirect("/users/") return redirect("/users/")
return form({'userform': user_right_list}, 'users/del_right.html', request) return form({'userform': user_right_list}, 'users/del_right.html', request)
@login_required @login_required
@permission_required('bofh') @permission_required('bofh')
def add_ban(request, userid): 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""" Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement"""
try: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
@ -273,7 +313,7 @@ def add_ban(request, userid):
ban = BanForm(request.POST or None, instance=ban_instance) ban = BanForm(request.POST or None, instance=ban_instance)
if ban.is_valid(): if ban.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
ban_object = ban.save() _ban_object = ban.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "Bannissement ajouté") messages.success(request, "Bannissement ajouté")
@ -285,10 +325,12 @@ def add_ban(request, userid):
) )
return form({'userform': ban}, 'users/user.html', request) return form({'userform': ban}, 'users/user.html', request)
@login_required @login_required
@permission_required('bofh') @permission_required('bofh')
def edit_ban(request, banid): 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""" Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement"""
try: try:
ban_instance = Ban.objects.get(pk=banid) ban_instance = Ban.objects.get(pk=banid)
@ -300,23 +342,31 @@ def edit_ban(request, banid):
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
ban.save() ban.save()
reversion.set_user(request.user) 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é") messages.success(request, "Bannissement modifié")
return redirect("/users/") return redirect("/users/")
return form({'userform': ban}, 'users/user.html', request) return form({'userform': ban}, 'users/user.html', request)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def add_whitelist(request, userid): def add_whitelist(request, userid):
""" Accorder un accès gracieux, temporaire ou permanent. Need droit cableur """ Accorder un accès gracieux, temporaire ou permanent.
Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, raison obligatoire""" Need droit cableur
Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement,
raison obligatoire"""
try: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
except User.DoesNotExist: except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant") messages.error(request, "Utilisateur inexistant")
return redirect("/users/") return redirect("/users/")
whitelist_instance = Whitelist(user=user) 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(): if whitelist.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
whitelist.save() whitelist.save()
@ -331,30 +381,40 @@ def add_whitelist(request, userid):
) )
return form({'userform': whitelist}, 'users/user.html', request) return form({'userform': whitelist}, 'users/user.html', request)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def edit_whitelist(request, whitelistid): def edit_whitelist(request, whitelistid):
""" Editer un accès gracieux, temporaire ou permanent. Need droit cableur """ Editer un accès gracieux, temporaire ou permanent.
Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, raison obligatoire""" Need droit cableur
Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement,
raison obligatoire"""
try: try:
whitelist_instance = Whitelist.objects.get(pk=whitelistid) whitelist_instance = Whitelist.objects.get(pk=whitelistid)
except Whitelist.DoesNotExist: except Whitelist.DoesNotExist:
messages.error(request, "Entrée inexistante") messages.error(request, "Entrée inexistante")
return redirect("/users/") 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(): if whitelist.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
whitelist.save() whitelist.save()
reversion.set_user(request.user) 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") messages.success(request, "Whitelist modifiée")
return redirect("/users/") return redirect("/users/")
return form({'userform': whitelist}, 'users/user.html', request) return form({'userform': whitelist}, 'users/user.html', request)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def add_school(request): 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) school = SchoolForm(request.POST or None)
if school.is_valid(): if school.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -365,30 +425,37 @@ def add_school(request):
return redirect("/users/index_school/") return redirect("/users/index_school/")
return form({'userform': school}, 'users/user.html', request) return form({'userform': school}, 'users/user.html', request)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def edit_school(request, schoolid): 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: try:
school_instance = School.objects.get(pk=schoolid) school_instance = School.objects.get(pk=schoolid)
except School.DoesNotExist: except School.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante")
return redirect("/users/") return redirect("/users/")
school = SchoolForm(request.POST or None, instance=school_instance) school = SchoolForm(request.POST or None, instance=school_instance)
if school.is_valid(): if school.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
school.save() school.save()
reversion.set_user(request.user) 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é") messages.success(request, "Établissement modifié")
return redirect("/users/index_school/") return redirect("/users/index_school/")
return form({'userform': school}, 'users/user.html', request) return form({'userform': school}, 'users/user.html', request)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def del_school(request): def del_school(request):
""" Supprimer un établissement d'enseignement à la base de donnée, need cableur """ Supprimer un établissement d'enseignement à la base de donnée,
Objet protégé, possible seulement si aucun user n'est affecté à l'établissement """ need cableur
Objet protégé, possible seulement si aucun user n'est affecté à
l'établissement """
school = DelSchoolForm(request.POST or None) school = DelSchoolForm(request.POST or None)
if school.is_valid(): if school.is_valid():
school_dels = school.cleaned_data['schools'] school_dels = school.cleaned_data['schools']
@ -406,6 +473,7 @@ def del_school(request):
return redirect("/users/index_school/") return redirect("/users/index_school/")
return form({'userform': school}, 'users/user.html', request) return form({'userform': school}, 'users/user.html', request)
@login_required @login_required
@permission_required('bureau') @permission_required('bureau')
def add_listright(request): def add_listright(request):
@ -421,29 +489,38 @@ def add_listright(request):
return redirect("/users/index_listright/") return redirect("/users/index_listright/")
return form({'userform': listright}, 'users/user.html', request) return form({'userform': listright}, 'users/user.html', request)
@login_required @login_required
@permission_required('bureau') @permission_required('bureau')
def edit_listright(request, listrightid): 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: try:
listright_instance = ListRight.objects.get(pk=listrightid) listright_instance = ListRight.objects.get(pk=listrightid)
except ListRight.DoesNotExist: except ListRight.DoesNotExist:
messages.error(request, u"Entrée inexistante" ) messages.error(request, u"Entrée inexistante")
return redirect("/users/") 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(): if listright.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
listright.save() listright.save()
reversion.set_user(request.user) 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é") messages.success(request, "Droit modifié")
return redirect("/users/index_listright/") return redirect("/users/index_listright/")
return form({'userform': listright}, 'users/user.html', request) return form({'userform': listright}, 'users/user.html', request)
@login_required @login_required
@permission_required('bureau') @permission_required('bureau')
def del_listright(request): 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) listright = DelListRightForm(request.POST or None)
if listright.is_valid(): if listright.is_valid():
listright_dels = listright.cleaned_data['listrights'] listright_dels = listright.cleaned_data['listrights']
@ -461,6 +538,7 @@ def del_listright(request):
return redirect("/users/index_listright/") return redirect("/users/index_listright/")
return form({'userform': listright}, 'users/user.html', request) return form({'userform': listright}, 'users/user.html', request)
@login_required @login_required
@permission_required('bureau') @permission_required('bureau')
def mass_archive(request): def mass_archive(request):
@ -469,7 +547,10 @@ def mass_archive(request):
to_archive_list = [] to_archive_list = []
if to_archive_date.is_valid(): if to_archive_date.is_valid():
date = to_archive_date.cleaned_data['date'] 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: if "valider" in request.POST:
for user in to_archive_list: for user in to_archive_list:
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -477,15 +558,22 @@ def mass_archive(request):
user.save() user.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Archivage") reversion.set_comment("Archivage")
messages.success(request, "%s users ont été archivés" % len(to_archive_list)) messages.success(request, "%s users ont été archivés" % len(
to_archive_list
))
return redirect("/users/") return redirect("/users/")
return form({'userform': to_archive_date, 'to_archive_list': to_archive_list}, 'users/mass_archive.html', request) return form(
{'userform': to_archive_date, 'to_archive_list': to_archive_list},
'users/mass_archive.html',
request
)
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index(request): def index(request):
""" Affiche l'ensemble des users, need droit cableur """ """ Affiche l'ensemble des users, need droit cableur """
options, created = GeneralOption.objects.get_or_create() options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
users_list = User.objects.select_related('room').order_by('state', 'name') users_list = User.objects.select_related('room').order_by('state', 'name')
paginator = Paginator(users_list, pagination_number) paginator = Paginator(users_list, pagination_number)
@ -500,13 +588,15 @@ def index(request):
users_list = paginator.page(paginator.num_pages) users_list = paginator.page(paginator.num_pages)
return render(request, 'users/index.html', {'users_list': users_list}) return render(request, 'users/index.html', {'users_list': users_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_ban(request): def index_ban(request):
""" Affiche l'ensemble des ban, need droit cableur """ """ 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 pagination_number = options.pagination_number
ban_list = Ban.objects.order_by('date_start').select_related('user').reverse() ban_list = Ban.objects.order_by('date_start')\
.select_related('user').reverse()
paginator = Paginator(ban_list, pagination_number) paginator = Paginator(ban_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -519,13 +609,15 @@ def index_ban(request):
ban_list = paginator.page(paginator.num_pages) ban_list = paginator.page(paginator.num_pages)
return render(request, 'users/index_ban.html', {'ban_list': ban_list}) return render(request, 'users/index_ban.html', {'ban_list': ban_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_white(request): def index_white(request):
""" Affiche l'ensemble des whitelist, need droit cableur """ """ 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 pagination_number = options.pagination_number
white_list = Whitelist.objects.select_related('user').order_by('date_start') white_list = Whitelist.objects.select_related('user')\
.order_by('date_start')
paginator = Paginator(white_list, pagination_number) paginator = Paginator(white_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -542,84 +634,106 @@ def index_white(request):
{'white_list': white_list} {'white_list': white_list}
) )
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_school(request): def index_school(request):
""" Affiche l'ensemble des établissement, need droit cableur """ """ Affiche l'ensemble des établissement, need droit cableur """
school_list = School.objects.order_by('name') 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def index_listright(request): def index_listright(request):
""" Affiche l'ensemble des droits , need droit cableur """ """ Affiche l'ensemble des droits , need droit cableur """
listright_list = ListRight.objects.order_by('listright') 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 @login_required
@permission_required('cableur') @permission_required('cableur')
def index_serviceusers(request): def index_serviceusers(request):
""" Affiche les users de services (pour les accès ldap)""" """ Affiche les users de services (pour les accès ldap)"""
serviceusers_list = ServiceUser.objects.order_by('pseudo') 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 @login_required
def history(request, object, id): def history(request, object_name, object_id):
""" Affichage de l'historique : (acl, argument) """ Affichage de l'historique : (acl, argument)
user : self or cableur, userid, user : self or cableur, userid,
ban : self or cableur, banid, ban : self or cableur, banid,
whitelist : self or cableur, whitelistid, whitelist : self or cableur, whitelistid,
school : cableur, schoolid, school : cableur, schoolid,
listright : cableur, listrightid """ listright : cableur, listrightid """
if object == 'user': if object_name == 'user':
try: try:
object_instance = User.objects.get(pk=id) object_instance = User.objects.get(pk=object_id)
except User.DoesNotExist: except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant") messages.error(request, "Utilisateur inexistant")
return redirect("/users/") return redirect("/users/")
if not request.user.has_perms(('cableur',)) and object_instance != request.user: if not request.user.has_perms(('cableur',)) and\
messages.error(request, "Vous ne pouvez pas afficher l'historique d'un autre user que vous sans droit cableur") 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)) return redirect("/users/profil/" + str(request.user.id))
elif object == 'serviceuser' and request.user.has_perms(('cableur',)): elif object_name == 'serviceuser' and request.user.has_perms(('cableur',)):
try: try:
object_instance = ServiceUser.objects.get(pk=id) object_instance = ServiceUser.objects.get(pk=object_id)
except ServiceUser.DoesNotExist: except ServiceUser.DoesNotExist:
messages.error(request, "User service inexistant") messages.error(request, "User service inexistant")
return redirect("/users/") return redirect("/users/")
elif object == 'ban': elif object_name == 'ban':
try: try:
object_instance = Ban.objects.get(pk=id) object_instance = Ban.objects.get(pk=object_id)
except Ban.DoesNotExist: except Ban.DoesNotExist:
messages.error(request, "Bannissement inexistant") messages.error(request, "Bannissement inexistant")
return redirect("/users/") return redirect("/users/")
if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: if not request.user.has_perms(('cableur',)) and\
messages.error(request, "Vous ne pouvez pas afficher les bans d'un autre user que vous sans droit cableur") 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)) return redirect("/users/profil/" + str(request.user.id))
elif object == 'whitelist': elif object_name == 'whitelist':
try: try:
object_instance = Whitelist.objects.get(pk=id) object_instance = Whitelist.objects.get(pk=object_id)
except Whiltelist.DoesNotExist: except Whitelist.DoesNotExist:
messages.error(request, "Whitelist inexistant") messages.error(request, "Whitelist inexistant")
return redirect("/users/") return redirect("/users/")
if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: if not request.user.has_perms(('cableur',)) and\
messages.error(request, "Vous ne pouvez pas afficher les whitelist d'un autre user que vous sans droit cableur") 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)) return redirect("/users/profil/" + str(request.user.id))
elif object == 'school' and request.user.has_perms(('cableur',)): elif object_name == 'school' and request.user.has_perms(('cableur',)):
try: try:
object_instance = School.objects.get(pk=id) object_instance = School.objects.get(pk=object_id)
except School.DoesNotExist: except School.DoesNotExist:
messages.error(request, "Ecole inexistante") messages.error(request, "Ecole inexistante")
return redirect("/users/") return redirect("/users/")
elif object == 'listright' and request.user.has_perms(('cableur',)): elif object_name == 'listright' and request.user.has_perms(('cableur',)):
try: try:
object_instance = ListRight.objects.get(pk=id) object_instance = ListRight.objects.get(pk=object_id)
except ListRight.DoesNotExist: except ListRight.DoesNotExist:
messages.error(request, "Droit inexistant") messages.error(request, "Droit inexistant")
return redirect("/users/") return redirect("/users/")
else: else:
messages.error(request, "Objet inconnu") messages.error(request, "Objet inconnu")
return redirect("/users/") return redirect("/users/")
options, created = GeneralOption.objects.get_or_create() options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance) reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number) paginator = Paginator(reversions, pagination_number)
@ -632,7 +746,11 @@ def history(request, object, id):
except EmptyPage: 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.
reversions = paginator.page(paginator.num_pages) 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 @login_required
@ -640,6 +758,7 @@ def mon_profil(request):
""" Lien vers profil, renvoie request.id à la fonction """ """ Lien vers profil, renvoie request.id à la fonction """
return redirect("/users/profil/" + str(request.user.id)) return redirect("/users/profil/" + str(request.user.id))
@login_required @login_required
def profil(request, userid): def profil(request, userid):
""" Affiche un profil, self or cableur, prend un userid en argument """ """ Affiche un profil, self or cableur, prend un userid en argument """
@ -649,14 +768,19 @@ def profil(request, userid):
messages.error(request, "Utilisateur inexistant") messages.error(request, "Utilisateur inexistant")
return redirect("/users/") return redirect("/users/")
if not request.user.has_perms(('cableur',)) and users != request.user: 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)) 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') machines = Machine.objects.filter(user=users).select_related('user')\
factures = Facture.objects.filter(user__pseudo=users) .prefetch_related('interface_set__domain__extension')\
bans = Ban.objects.filter(user__pseudo=users) .prefetch_related('interface_set__ipv4__ip_type__extension')\
whitelists = Whitelist.objects.filter(user__pseudo=users) .prefetch_related('interface_set__type')\
.prefetch_related('interface_set__domain__related_domain__extension')
factures = Facture.objects.filter(user=users)
bans = Ban.objects.filter(user=users)
whitelists = Whitelist.objects.filter(user=users)
list_droits = Right.objects.filter(user=users) 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 user_solde = options.user_solde
return render( return render(
request, request,
@ -672,46 +796,56 @@ def profil(request, userid):
} }
) )
def reset_password(request): def reset_password(request):
""" Reintialisation du mot de passe si mdp oublié """ """ Reintialisation du mot de passe si mdp oublié """
userform = ResetPasswordForm(request.POST or None) userform = ResetPasswordForm(request.POST or None)
if userform.is_valid(): if userform.is_valid():
try: 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: except User.DoesNotExist:
messages.error(request, "Cet utilisateur n'existe pas") messages.error(request, "Cet utilisateur n'existe pas")
return form({'userform': userform}, 'users/user.html', request) return form({'userform': userform}, 'users/user.html', request)
user.reset_passwd_mail(request) user.reset_passwd_mail(request)
messages.success(request, "Un mail pour l'initialisation du mot de passe a été envoyé") messages.success(request, "Un mail pour l'initialisation du mot\
de passe a été envoyé")
redirect("/") redirect("/")
return form({'userform': userform}, 'users/user.html', request) return form({'userform': userform}, 'users/user.html', request)
def process(request, token): def process(request, token):
"""Process, lien pour la reinitialisation du mot de passe"""
valid_reqs = Request.objects.filter(expires_at__gt=timezone.now()) valid_reqs = Request.objects.filter(expires_at__gt=timezone.now())
req = get_object_or_404(valid_reqs, token=token) req = get_object_or_404(valid_reqs, token=token)
if req.type == Request.PASSWD: if req.type == Request.PASSWD:
return process_passwd(request, req) return process_passwd(request, req)
elif req.type == Request.EMAIL:
return process_email(request, req=req)
else: else:
messages.error(request, "Entrée incorrecte, contactez un admin") messages.error(request, "Entrée incorrecte, contactez un admin")
redirect("/") redirect("/")
def process_passwd(request, req): 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) u_form = PassForm(request.POST or None)
user = req.user user = req.user
if u_form.is_valid(): if u_form.is_valid():
return password_change_action(u_form, user, request, req=req) return password_change_action(u_form, user, request, req=req)
return form({'userform': u_form}, 'users/user.html', request) return form({'userform': u_form}, 'users/user.html', request)
""" Framework Rest """
class JSONResponse(HttpResponse): class JSONResponse(HttpResponse):
""" Framework Rest """
def __init__(self, data, **kwargs): def __init__(self, data, **kwargs):
content = JSONRenderer().render(data) content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json' kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs) super(JSONResponse, self).__init__(content, **kwargs)
@csrf_exempt @csrf_exempt
@login_required @login_required
@permission_required('serveur') @permission_required('serveur')
@ -721,4 +855,3 @@ def mailing(request):
mails = all_has_access().values('email').distinct() mails = all_has_access().values('email').distinct()
seria = MailSerializer(mails, many=True) seria = MailSerializer(mails, many=True)
return JSONResponse(seria.data) return JSONResponse(seria.data)