8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-26 06:32:26 +00:00

Merge branch master into granuban

This commit is contained in:
Daniel STAN 2017-11-05 21:15:31 +01:00
commit 23cd06d589
126 changed files with 9400 additions and 2061 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,24 +19,45 @@
# 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.db.models import Q
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
@ -48,45 +69,90 @@ class NewFactureForm(ModelForm):
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):
article = forms.ModelChoiceField(queryset=Article.objects.all(), label="Article", required=True) class SelectUserArticleForm(Form):
quantity = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)], required=True) """Selection d'un article lors de la creation d'une facture"""
article = forms.ModelChoiceField(
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')),
label="Article",
required=True
)
quantity = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)],
required=True
)
class SelectClubArticleForm(Form):
"""Selection d'un article lors de la creation d'une facture"""
article = forms.ModelChoiceField(
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')),
label="Article",
required=True
)
quantity = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)],
required=True
)
class NewFactureFormPdf(Form): 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 +163,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

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-15 18:33
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0023_auto_20170902_1303'),
]
operations = [
migrations.AlterField(
model_name='article',
name='duration',
field=models.PositiveIntegerField(blank=True, help_text='Durée exprimée en mois entiers', null=True, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='vente',
name='duration',
field=models.PositiveIntegerField(blank=True, help_text='Durée exprimée en mois entiers', null=True),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-27 03:02
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0024_auto_20171015_2033'),
]
operations = [
migrations.AddField(
model_name='article',
name='type_user',
field=models.CharField(choices=[('Adherent', 'Adherent'), ('Club', 'Club'), ('All', 'All')], default='All', max_length=255),
),
]

View file

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-27 23:26
from __future__ import unicode_literals
from django.db import migrations, models
def create_type(apps, schema_editor):
Cotisation = apps.get_model('cotisations', 'Cotisation')
Vente = apps.get_model('cotisations', 'Vente')
Article = apps.get_model('cotisations', 'Article')
db_alias = schema_editor.connection.alias
articles = Article.objects.using(db_alias).all()
ventes = Vente.objects.using(db_alias).all()
cotisations = Cotisation.objects.using(db_alias).all()
for article in articles:
if article.iscotisation:
article.type_cotisation='All'
article.save(using=db_alias)
for vente in ventes:
if vente.iscotisation:
vente.type_cotisation='All'
vente.save(using=db_alias)
for cotisation in cotisations:
cotisation.type_cotisation='All'
cotisation.save(using=db_alias)
def delete_type(apps, schema_editor):
Vente = apps.get_model('cotisations', 'Vente')
Article = apps.get_model('cotisations', 'Article')
db_alias = schema_editor.connection.alias
articles = Articles.objects.using(db_alias).all()
ventes = Vente.objects.using(db_alias).all()
for article in articles:
if article.type_cotisation:
article.iscotisation=True
else:
article.iscotisation=False
article.save(using=db_alias)
for vente in ventes:
if vente.iscotisation:
vente.iscotisation=True
else:
vente.iscotisation=False
vente.save(using=db_alias)
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0025_article_type_user'),
]
operations = [
migrations.AddField(
model_name='article',
name='type_cotisation',
field=models.CharField(blank=True, choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], default=None, max_length=255, null=True),
),
migrations.AddField(
model_name='cotisation',
name='type_cotisation',
field=models.CharField(choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], max_length=255),
),
migrations.AddField(
model_name='vente',
name='type_cotisation',
field=models.CharField(blank=True, choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], max_length=255, null=True),
),
migrations.RunPython(create_type, delete_type),
migrations.RemoveField(
model_name='article',
name='iscotisation',
),
migrations.RemoveField(
model_name='vente',
name='iscotisation',
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-29 10:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0026_auto_20171028_0126'),
]
operations = [
migrations.AlterField(
model_name='article',
name='name',
field=models.CharField(max_length=255),
),
]

View file

@ -20,141 +20,269 @@
# 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 import Q
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"
COTISATION_TYPE = (
('Connexion', 'Connexion'),
('Adhesion', 'Adhesion'),
('All', 'All'),
)
facture = models.ForeignKey('Facture', on_delete=models.CASCADE) facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
number = models.IntegerField(validators=[MinValueValidator(1)]) number = models.IntegerField(validators=[MinValueValidator(1)])
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() duration = models.PositiveIntegerField(
duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True) help_text="Durée exprimée en mois entiers",
blank=True,
null=True)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
blank=True,
null=True,
max_length=255
)
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
if not hasattr(self, 'cotisation'): en argument l'user, la facture pour la quantitéi, et l'article pour
la durée"""
if not hasattr(self, 'cotisation') and self.type_cotisation:
cotisation = Cotisation(vente=self) cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation
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_cotisation = Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self.facture.user
).exclude(valid=False))
).filter(Q(type_cotisation='All') | Q(type_cotisation=self.type_cotisation)
).filter(
date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max']
elif self.type_cotisation=="Adhesion":
end_cotisation = self.facture.user.end_adhesion()
else: else:
end_adhesion = self.facture.user.end_adhesion() end_cotisation = self.facture.user.end_connexion()
date_start = date_start or timezone.now() date_start = date_start or timezone.now()
end_adhesion = end_adhesion or date_start end_cotisation = end_cotisation or date_start
date_max = max(end_adhesion, date_start) date_max = max(end_cotisation, 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.type_cotisation 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
vente.cotisation.save() vente.cotisation.save()
if vente.iscotisation: if vente.type_cotisation:
vente.create_cotis() vente.create_cotis()
vente.cotisation.save() vente.cotisation.save()
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.type_cotisation:
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) USER_TYPES = (
('Adherent', 'Adherent'),
('Club', 'Club'),
('All', 'All'),
)
COTISATION_TYPE = (
('Connexion', 'Connexion'),
('Adhesion', 'Adhesion'),
('All', 'All'),
)
name = models.CharField(max_length=255)
prix = models.DecimalField(max_digits=5, decimal_places=2) prix = models.DecimalField(max_digits=5, decimal_places=2)
iscotisation = models.BooleanField() duration = models.PositiveIntegerField(
duration = models.IntegerField(
help_text="Durée exprimée en mois entiers", help_text="Durée exprimée en mois entiers",
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)]) validators=[MinValueValidator(0)])
type_user = models.CharField(
choices=USER_TYPES,
default='All',
max_length=255
)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
default=None,
blank=True,
null=True,
max_length=255
)
unique_together = ('name', 'type_user')
def clean(self): def clean(self):
if self.name.lower() == "solde": if self.name.lower() == "solde":
raise ValidationError("Solde est un nom d'article invalide") raise ValidationError("Solde est un nom d'article invalide")
if self.type_cotisation and not self.duration:
raise ValidationError(
"La durée est obligatoire si il s'agit d'une cotisation"
)
def __str__(self): 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 +290,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,29 +309,47 @@ 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"
COTISATION_TYPE = (
('Connexion', 'Connexion'),
('Adhesion', 'Adhesion'),
('All', 'All'),
)
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True) vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
max_length=255,
)
date_start = models.DateTimeField() date_start = models.DateTimeField()
date_end = models.DateTimeField() date_end = models.DateTimeField()
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

@ -27,8 +27,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Article</th> <th>Article</th>
<th>Prix</th> <th>Prix</th>
<th>Cotisation</th> <th>Type Cotisation</th>
<th>Durée (mois)</th> <th>Durée (mois)</th>
<th>Article pour</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -36,8 +37,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ article.name }}</td> <td>{{ article.name }}</td>
<td>{{ article.prix }}</td> <td>{{ article.prix }}</td>
<td>{{ article.iscotisation }}</td> <td>{{ article.type_cotisation }}</td>
<td>{{ article.duration }}</td> <td>{{ article.duration }}</td>
<td>{{ article.type_user }}</td>
<td class="text-right"> <td class="text-right">
{% if is_trez %} {% if is_trez %}
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'cotisations:edit-article' article.id %}"> <a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'cotisations:edit-article' article.id %}">

View file

@ -29,12 +29,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Utilisateur</th> <th>{% include "buttons/sort.html" with prefix='cotis' col='user' text='Utilisateur' %}</th>
<th>Designation</th> <th>Designation</th>
<th>Prix total</th> <th>Prix total</th>
<th>Moyen de paiement</th> <th>{% include "buttons/sort.html" with prefix='cotis' col='paiement' text='Moyen de paiement' %}</th>
<th>Date</th> <th>{% include "buttons/sort.html" with prefix='cotis' col='date' text='Date' %}</th>
<th></th> <th>{% include "buttons/sort.html" with prefix='cotis' col='id' text='Id facture' %}</th>
<th></th> <th></th>
<th></th> <th></th>
</tr> </tr>
@ -46,17 +46,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ facture.prix_total }}</td> <td>{{ facture.prix_total }}</td>
<td>{{ facture.paiement }}</td> <td>{{ facture.paiement }}</td>
<td>{{ facture.date }}</td> <td>{{ facture.date }}</td>
<td>{{ facture.id }}</td>
{% if is_cableur %} {% if is_cableur %}
<td> <td>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="editionfacture" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-default dropdown-toggle" type="button" id="editionfacture" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Modifier Edition
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="editionfacture"> <ul class="dropdown-menu" aria-labelledby="editionfacture">
{% if facture.valid and not facture.control or is_trez %} {% if facture.valid and not facture.control or is_trez %}
<li><a href="{% url 'cotisations:edit-facture' facture.id %}"><i class="glyphicon glyphicon-bitcoin"></i> Editer</a></li> <li><a href="{% url 'cotisations:edit-facture' facture.id %}"><i class="glyphicon glyphicon-bitcoin"></i> Modifier</a></li>
<li><a href="{% url 'cotisations:del-facture' facture.id %}"><i class="glyphicon glyphicon-trash"></i> Supprimer</a></li> <li><a href="{% url 'cotisations:del-facture' facture.id %}"><i class="glyphicon glyphicon-trash"></i> Supprimer</a></li>
<li><a href="{% url 'cotisations:history' 'facture' facture.id %}"><i class="glyphicon glyphicon-time"></i> Historique</a></li>
{% else %} {% else %}
<li>Facture controlée</li> <li>Facture controlée</li>
{% endif %} {% endif %}
@ -74,11 +76,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<font color="red">Facture invalide</font> <font color="red">Facture invalide</font>
{% endif %} {% endif %}
</td> </td>
<td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'cotisations:history' 'facture' facture.id %}">
<i class="glyphicon glyphicon-time"></i>
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -40,14 +40,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<thead> <thead>
<tr> <tr>
<th>Profil</th> <th>Profil</th>
<th>Nom</th> <th>{% include "buttons/sort.html" with prefix='control' col='name' text='Nom' %}</th>
<th>Prénom</th> <th>{% include "buttons/sort.html" with prefix='control' col='surname' text='Prénom' %}</th>
<th>{% include "buttons/sort.html" with prefix='control' col='id' text='Id facture' %}</th>
<th>{% include "buttons/sort.html" with prefix='control' col='user-id' text='Id user' %}</th>
<th>Designation</th> <th>Designation</th>
<th>Prix total</th> <th>Prix total</th>
<th>Moyen de paiement</th> <th>{% include "buttons/sort.html" with prefix='control' col='paiement' text='Moyen de paiement' %}</th>
<th>Date</th> <th>{% include "buttons/sort.html" with prefix='control' col='date' text='Date' %}</th>
<th>Validité</th> <th>{% include "buttons/sort.html" with prefix='control' col='valid' text='Valide' %}</th>
<th>Controle trésorier</th> <th>{% include "buttons/sort.html" with prefix='control' col='control' text='Contrôlée' %}</th>
</tr> </tr>
</thead> </thead>
{% for form in controlform.forms %} {% for form in controlform.forms %}
@ -58,6 +60,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td> </td>
<td>{{ form.instance.user.name }}</td> <td>{{ form.instance.user.name }}</td>
<td>{{ form.instance.user.surname }}</td> <td>{{ form.instance.user.surname }}</td>
<td>{{ form.instance.id }}</td>
<td>{{ form.instance.user.id }}</td>
<td>{{ form.instance.name }}</td> <td>{{ form.instance.name }}</td>
<td>{{ form.instance.prix_total }}</td> <td>{{ form.instance.prix_total }}</td>
<td>{{ form.instance.paiement }}</td> <td>{{ form.instance.paiement }}</td>

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_name>facture)/(?P<object_id>[0-9]+)$',
views.history,
name='history'
),
url(r'^history/(?P<object_name>article)/(?P<object_id>[0-9]+)$',
views.history,
name='history'
),
url(r'^history/(?P<object_name>paiement)/(?P<object_id>[0-9]+)$',
views.history,
name='history'),
url(r'^history/(?P<object_name>banque)/(?P<object_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,40 +24,54 @@
# 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.db.models import Q
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 re2o.utils import SortTable
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,
ArticleForm,
DelArticleForm,
PaiementForm,
DelPaiementForm,
BanqueForm,
DelBanqueForm,
NewFactureFormPdf,
SelectUserArticleForm,
SelectClubArticleForm,
CreditSoldeForm
)
from .tex import render_tex
from dateutil.relativedelta import relativedelta
from django.utils import timezone
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required @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:
@ -65,55 +79,90 @@ def new_facture(request, userid):
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
article_list = Article.objects.all() article_list = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
# On envoie la form fature et un formset d'articles # On envoie la form fature et un formset d'articles
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) if request.user.is_class_club:
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
else:
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
if facture_form.is_valid() and article_formset.is_valid(): 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,
type_cotisation=article.type_cotisation,
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'].type_cotisation
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 +170,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 +302,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')
@ -212,7 +325,12 @@ 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'],
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 +343,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,9 +360,12 @@ 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:
@ -248,14 +376,20 @@ def edit_article(request, articleid):
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 +400,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,9 +416,11 @@ 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:
@ -292,14 +431,20 @@ def edit_paiement(request, paiementid):
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 +454,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,9 +482,11 @@ 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:
@ -341,14 +497,20 @@ def edit_banque(request, banqueid):
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 +522,31 @@ 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.select_related('user').select_related('paiement')
controlform_set = modelformset_factory(Facture, fields=('control','valid'), extra=0) facture_list = SortTable.sort(
facture_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CONTROL
)
controlform_set = modelformset_factory(
Facture,
fields=('control', 'valid'),
extra=0
)
paginator = Paginator(facture_list, pagination_number) paginator = Paginator(facture_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -379,40 +555,63 @@ 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]) controlform = controlform_set(request.POST or None, queryset=facture_list.object_list)
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():
controlform.save() controlform.save()
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.select_related('user')\
.select_related('paiement').prefetch_related('vente_set')
facture_list = SortTable.sort(
facture_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_INDEX
)
paginator = Paginator(facture_list, pagination_number) paginator = Paginator(facture_list, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -423,41 +622,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_name, object_id):
if object == 'facture': """Affiche l'historique de chaque objet"""
if object_name == '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_name == '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_name == '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_name == '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 +675,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
})

BIN
docs_utils/re2o-archi.dia Normal file

Binary file not shown.

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")
@ -248,6 +247,9 @@ def check_user_machine_and_register(nas_type, username, mac_address):
return (False, u"Machine enregistrée sur le compte d'un autre user...", '') return (False, u"Machine enregistrée sur le compte d'un autre user...", '')
elif not interface.is_active: elif not interface.is_active:
return (False, u"Machine desactivée", '') return (False, u"Machine desactivée", '')
elif not interface.ipv4:
interface.assign_ipv4()
return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm)
else: else:
return (True, u"Access ok", user.pwd_ntlm) return (True, u"Access ok", user.pwd_ntlm)
elif nas_type: elif nas_type:
@ -293,10 +295,11 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
if not port.room: if not port.room:
return (sw_name, u'Chambre inconnue', VLAN_NOK) return (sw_name, u'Chambre inconnue', VLAN_NOK)
room_user = User.objects.filter(room=Room.objects.filter(name=port.room)) room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room))
if not room_user: if not room_user:
return (sw_name, u'Chambre non cotisante', VLAN_NOK) return (sw_name, u'Chambre non cotisante', VLAN_NOK)
elif not room_user.first().has_access(): for user in room_user:
if not user.has_access():
return (sw_name, u'Chambre resident desactive', VLAN_NOK) return (sw_name, u'Chambre resident desactive', VLAN_NOK)
# else: user OK, on passe à la verif MAC # else: user OK, on passe à la verif MAC
@ -310,9 +313,12 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
elif not port.room: elif not port.room:
return (sw_name, u'Chambre et machine inconnues', VLAN_NOK) return (sw_name, u'Chambre et machine inconnues', VLAN_NOK)
else: else:
room_user = User.objects.filter(room=Room.objects.filter(name=port.room)) if not room_user:
room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room))
if not room_user: if not room_user:
return (sw_name, u'Machine et propriétaire de la chambre inconnus', VLAN_NOK) return (sw_name, u'Machine et propriétaire de la chambre inconnus', VLAN_NOK)
elif room_user.count() > 1:
return (sw_name, u'Machine inconnue, il y a au moins 2 users dans la chambre/local -> ajout de mac automatique impossible', VLAN_NOK)
elif not room_user.first().has_access(): elif not room_user.first().has_access():
return (sw_name, u'Machine inconnue et adhérent non cotisant', VLAN_NOK) return (sw_name, u'Machine inconnue et adhérent non cotisant', VLAN_NOK)
else: else:
@ -321,8 +327,13 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN) return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN)
else: else:
return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK)
elif not interface.first().is_active: else:
interface = interface.first()
if not interface.is_active:
return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK)
elif not interface.ipv4:
interface.assign_ipv4()
return (sw_name, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN)
else: else:
return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN) return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN)

View file

@ -33,8 +33,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Objet modifié</th> <th>Objet modifié</th>
<th>Type de l'objet</th> <th>Type de l'objet</th>
<th>Modification par</th> <th>{% include "buttons/sort.html" with prefix='logs' col='author' text='Modification par' %}</th>
<th>Date de modification</th> <th>{% include "buttons/sort.html" with prefix='logs' col='date' text='Date de modification' %}</th>
<th>Commentaire</th> <th>Commentaire</th>
<th></th> <th></th>
</tr> </tr>

View file

@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>{% include "buttons/sort.html" with prefix='sum' col='date' text='Date' %}</th>
<th>Modification</th> <th>Modification</th>
<th></th> <th></th>
</tr> </tr>

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,35 +23,78 @@
# 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 (
from users.models import all_has_access, all_whitelisted, all_baned, all_adherent User,
from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation ServiceUser,
from machines.models import Machine, MachineType, IpType, Extension, Interface, Domain, IpList Right,
from machines.views import all_active_assigned_interfaces_count, all_active_interfaces_count School,
from topologie.models import Switch, Port, Room ListRight,
ListShell,
Ban,
Whitelist,
Adherent,
Club
)
from cotisations.models import (
Facture,
Vente,
Article,
Banque,
Paiement,
Cotisation
)
from machines.models import (
Machine,
MachineType,
IpType,
Extension,
Interface,
Domain,
IpList,
OuverturePortList,
Service,
Vlan,
Nas,
SOA,
Mx,
Ns
)
from topologie.models import (
Switch,
Port,
Room,
Stack,
ModelSwitch,
ConstructorSwitch
)
from preferences.models import GeneralOption from 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, SortTable
STATS_DICT = { STATS_DICT = {
0: ["Tout", 36], 0: ["Tout", 36],
@ -62,23 +105,28 @@ STATS_DICT = {
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
)
).select_related('revision')
versions = SortTable.sort(
versions,
request.GET.get('col'),
request.GET.get('order'),
SortTable.LOGS_INDEX
)
paginator = Paginator(versions, pagination_number) paginator = Paginator(versions, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -97,28 +145,41 @@ def index(request):
# 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':
version.revision.user.get_username()
if version.revision.user else '?',
'user_id': version.revision.user_id,
'version': version}
else: else:
to_remove.insert(0, i) 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().select_related('user')\
.prefetch_related('version_set__object')
revisions = SortTable.sort(
revisions,
request.GET.get('col'),
request.GET.get('order'),
SortTable.LOGS_STATS_LOGS
)
paginator = Paginator(revisions, pagination_number) paginator = Paginator(revisions, pagination_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -129,7 +190,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')
@ -143,31 +207,91 @@ def revert_action(request, revision_id):
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,
for ip_range in IpType.objects.all(): cotisants, activés, archivés, etc"""
ip_dict = dict()
for ip_range in IpType.objects.select_related('vlan').all():
all_ip = IpList.objects.filter(ip_type=ip_range) 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, ip_range.vlan, all_ip.count(),
used_ip, active_ip, all_ip.count()-used_ip]
_all_adherent = all_adherent()
_all_has_access = all_has_access()
_all_baned = all_baned()
_all_whitelisted = all_whitelisted()
_all_active_interfaces_count = all_active_interfaces_count()
_all_active_assigned_interfaces_count = all_active_assigned_interfaces_count()
stats = [ stats = [
[["Categorie", "Nombre d'utilisateurs"], { [["Categorie", "Nombre d'utilisateurs (total club et adhérents)", "Nombre d'adhérents", "Nombre de clubs"], {
'active_users' : ["Users actifs", User.objects.filter(state=User.STATE_ACTIVE).count()], '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()], Adherent.objects.filter(state=Adherent.STATE_ACTIVE).count(),
'connexion_users' : ["Utilisateurs bénéficiant d'une connexion", all_has_access().count()], Club.objects.filter(state=Club.STATE_ACTIVE).count()],
'ban_users' : ["Utilisateurs bannis", all_baned().count()], 'inactive_users': [
'whitelisted_user' : ["Utilisateurs bénéficiant d'une connexion gracieuse", all_whitelisted().count()], "Users désactivés",
'actives_interfaces' : ["Interfaces actives (ayant accès au reseau)", all_active_interfaces_count().count()], User.objects.filter(state=User.STATE_DISABLED).count(),
'actives_assigned_interfaces' : ["Interfaces actives et assignées ipv4", all_active_assigned_interfaces_count().count()] Adherent.objects.filter(state=Adherent.STATE_DISABLED).count(),
Club.objects.filter(state=Club.STATE_DISABLED).count()],
'archive_users': [
"Users archivés",
User.objects.filter(state=User.STATE_ARCHIVE).count(),
Adherent.objects.filter(state=Adherent.STATE_ARCHIVE).count(),
Club.objects.filter(state=Club.STATE_ARCHIVE).count()],
'adherent_users': [
"Cotisant à l'association",
_all_adherent.count(),
_all_adherent.exclude(adherent__isnull=True).count(),
_all_adherent.exclude(club__isnull=True).count()],
'connexion_users': [
"Utilisateurs bénéficiant d'une connexion",
_all_has_access.count(),
_all_has_access.exclude(adherent__isnull=True).count(),
_all_has_access.exclude(club__isnull=True).count()],
'ban_users': [
"Utilisateurs bannis",
_all_baned.count(),
_all_baned.exclude(adherent__isnull=True).count(),
_all_baned.exclude(club__isnull=True).count()],
'whitelisted_user': [
"Utilisateurs bénéficiant d'une connexion gracieuse",
_all_whitelisted.count(),
_all_whitelisted.exclude(adherent__isnull=True).count(),
_all_whitelisted.exclude(club__isnull=True).count()],
'actives_interfaces': [
"Interfaces actives (ayant accès au reseau)",
_all_active_interfaces_count.count(),
_all_active_interfaces_count.exclude(
machine__user__adherent__isnull=True
).count(),
_all_active_interfaces_count.exclude(
machine__user__club__isnull=True
).count()],
'actives_assigned_interfaces': [
"Interfaces actives et assignées ipv4",
_all_active_assigned_interfaces_count.count(),
_all_active_assigned_interfaces_count.exclude(
machine__user__adherent__isnull=True
).count(),
_all_active_assigned_interfaces_count.exclude(
machine__user__club__isnull=True
).count()]
}], }],
[["Range d'ip", "Nombre d'ip totales", "Ip assignées", "Ip assignées à une machine active", "Ip non assignées"] ,ip] [["Range d'ip", "Vlan", "Nombre d'ip totales", "Ip assignées",
"Ip assignées à une machine active", "Ip non assignées"], ip_dict]
] ]
return render(request, 'logs/stats_general.html', {'stats_list': stats}) return render(request, 'logs/stats_general.html', {'stats_list': stats})
@ -175,11 +299,16 @@ 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()], 'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()],
'clubs': [Club.PRETTY_NAME, Club.objects.count()],
'serviceuser': [ServiceUser.PRETTY_NAME,
ServiceUser.objects.count()],
'right': [Right.PRETTY_NAME, Right.objects.count()], 'right': [Right.PRETTY_NAME, Right.objects.count()],
'school': [School.PRETTY_NAME, School.objects.count()], 'school': [School.PRETTY_NAME, School.objects.count()],
'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()], 'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()],
@ -193,21 +322,41 @@ def stats_models(request):
'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,
MachineType.objects.count()],
'typeip': [IpType.PRETTY_NAME, IpType.objects.count()], 'typeip': [IpType.PRETTY_NAME, IpType.objects.count()],
'extension': [Extension.PRETTY_NAME, Extension.objects.count()], 'extension': [Extension.PRETTY_NAME, Extension.objects.count()],
'interface': [Interface.PRETTY_NAME, Interface.objects.count()], 'interface': [Interface.PRETTY_NAME, Interface.objects.count()],
'alias' : [Domain.PRETTY_NAME, Domain.objects.exclude(cname=None).count()], 'alias': [Domain.PRETTY_NAME,
Domain.objects.exclude(cname=None).count()],
'iplist': [IpList.PRETTY_NAME, IpList.objects.count()], 'iplist': [IpList.PRETTY_NAME, IpList.objects.count()],
'service': [Service.PRETTY_NAME, Service.objects.count()],
'ouvertureportlist': [
OuverturePortList.PRETTY_NAME,
OuverturePortList.objects.count()
],
'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()],
'SOA': [Mx.PRETTY_NAME, Mx.objects.count()],
'Mx': [Mx.PRETTY_NAME, Mx.objects.count()],
'Ns': [Ns.PRETTY_NAME, Ns.objects.count()],
'nas': [Nas.PRETTY_NAME, Nas.objects.count()],
}, },
'Topologie': { '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()],
'stack': [Stack.PRETTY_NAME, Stack.objects.count()],
'modelswitch': [
ModelSwitch.PRETTY_NAME,
ModelSwitch.objects.count()
],
'constructorswitch': [
ConstructorSwitch.PRETTY_NAME,
ConstructorSwitch.objects.count()
],
}, },
'Actions effectuées sur la base': 'Actions effectuées sur la base':
{ {
@ -216,43 +365,72 @@ def stats_models(request):
} }
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

@ -27,58 +27,79 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import IpType, Machine, MachineType, Domain, IpList, Interface from .models import IpType, Machine, MachineType, Domain, IpList, Interface
from .models import Extension, Mx, Ns, Vlan, Text, Nas, Service, OuverturePort from .models import Extension, SOA, Mx, Ns, Vlan, Text, Nas, Service
from .models import OuverturePortList from .models import OuverturePort, OuverturePortList
class MachineAdmin(VersionAdmin): class MachineAdmin(VersionAdmin):
pass pass
class IpTypeAdmin(VersionAdmin): class IpTypeAdmin(VersionAdmin):
pass pass
class MachineTypeAdmin(VersionAdmin): class MachineTypeAdmin(VersionAdmin):
pass pass
class VlanAdmin(VersionAdmin): class VlanAdmin(VersionAdmin):
pass pass
class ExtensionAdmin(VersionAdmin): class ExtensionAdmin(VersionAdmin):
pass pass
class SOAAdmin(VersionAdmin):
pass
class MxAdmin(VersionAdmin): class MxAdmin(VersionAdmin):
pass pass
class NsAdmin(VersionAdmin): class NsAdmin(VersionAdmin):
pass pass
class TextAdmin(VersionAdmin): class TextAdmin(VersionAdmin):
pass pass
class NasAdmin(VersionAdmin): class NasAdmin(VersionAdmin):
pass pass
class IpListAdmin(VersionAdmin): class IpListAdmin(VersionAdmin):
pass pass
class OuverturePortAdmin(VersionAdmin): class OuverturePortAdmin(VersionAdmin):
pass pass
class OuverturePortListAdmin(VersionAdmin): class OuverturePortListAdmin(VersionAdmin):
pass pass
class InterfaceAdmin(VersionAdmin): class InterfaceAdmin(VersionAdmin):
list_display = ('machine','type','mac_address','ipv4','details') list_display = ('machine','type','mac_address','ipv4','details')
class DomainAdmin(VersionAdmin): class DomainAdmin(VersionAdmin):
list_display = ('interface_parent', 'name', 'extension', 'cname') list_display = ('interface_parent', 'name', 'extension', 'cname')
class ServiceAdmin(VersionAdmin): class ServiceAdmin(VersionAdmin):
list_display = ('service_type', 'min_time_regen', 'regular_time_regen') list_display = ('service_type', 'min_time_regen', 'regular_time_regen')
admin.site.register(Machine, MachineAdmin) admin.site.register(Machine, MachineAdmin)
admin.site.register(MachineType, MachineTypeAdmin) admin.site.register(MachineType, MachineTypeAdmin)
admin.site.register(IpType, IpTypeAdmin) admin.site.register(IpType, IpTypeAdmin)
admin.site.register(Extension, ExtensionAdmin) admin.site.register(Extension, ExtensionAdmin)
admin.site.register(SOA, SOAAdmin)
admin.site.register(Mx, MxAdmin) admin.site.register(Mx, MxAdmin)
admin.site.register(Ns, NsAdmin) admin.site.register(Ns, NsAdmin)
admin.site.register(Text, TextAdmin) admin.site.register(Text, TextAdmin)

View file

@ -21,55 +21,97 @@
# 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.
"""
Formulaires d'ajout, edition et suppressions de :
- machines
- interfaces
- domain (noms de machine)
- alias (cname)
- service (dhcp, dns..)
- ns (serveur dns)
- mx (serveur mail)
- ports ouverts et profils d'ouverture par interface
"""
from __future__ import unicode_literals from __future__ import unicode_literals
import re from django.forms import ModelForm, Form
from django.forms import ModelForm, Form, ValidationError
from django import forms from django import forms
from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType, OuverturePortList, OuverturePort
from django.db.models import Q
from django.core.validators import validate_email
from users.models import User from .models import (
Domain,
Machine,
Interface,
IpList,
MachineType,
Extension,
SOA,
Mx,
Text,
Ns,
Service,
Vlan,
Nas,
IpType,
OuverturePortList,
)
class EditMachineForm(ModelForm): class EditMachineForm(ModelForm):
"""Formulaire d'édition d'une machine"""
class Meta: class Meta:
model = Machine model = Machine
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):
"""Creation d'une machine, ne renseigne que le nom"""
class Meta(EditMachineForm.Meta): class Meta(EditMachineForm.Meta):
fields = ['name'] fields = ['name']
class BaseEditMachineForm(EditMachineForm): class BaseEditMachineForm(EditMachineForm):
"""Edition basique, ne permet que de changer le nom et le statut.
Réservé aux users sans droits spécifiques"""
class Meta(EditMachineForm.Meta): class Meta(EditMachineForm.Meta):
fields = ['name', 'active'] fields = ['name', 'active']
class EditInterfaceForm(ModelForm): class EditInterfaceForm(ModelForm):
"""Edition d'une interface. Edition complète"""
class Meta: class Meta:
model = Interface model = Interface
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"
if "ipv4" in self.fields: if "ipv4" in self.fields:
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" self.fields['ipv4'].empty_label = "Assignation automatique\
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) de l'ipv4"
self.fields['ipv4'].queryset = IpList.objects.filter(
interface__isnull=True
)
# Add it's own address # Add it's own address
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance) self.fields['ipv4'].queryset |= IpList.objects.filter(
interface=self.instance
)
if "machine" in self.fields: if "machine" in self.fields:
self.fields['machine'].queryset = Machine.objects.all().select_related('user') self.fields['machine'].queryset = Machine.objects.all()\
.select_related('user')
class AddInterfaceForm(EditInterfaceForm): class AddInterfaceForm(EditInterfaceForm):
"""Ajout d'une interface à une machine. En fonction des droits,
affiche ou non l'ensemble des ip disponibles"""
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['type', 'ipv4', 'mac_address', 'details'] fields = ['type', 'ipv4', 'mac_address', 'details']
@ -78,16 +120,27 @@ class AddInterfaceForm(EditInterfaceForm):
super(AddInterfaceForm, self).__init__(*args, **kwargs) super(AddInterfaceForm, self).__init__(*args, **kwargs)
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
if not infra: if not infra:
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False)) self.fields['type'].queryset = MachineType.objects.filter(
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)) ip_type__in=IpType.objects.filter(need_infra=False)
)
self.fields['ipv4'].queryset = IpList.objects.filter(
interface__isnull=True
).filter(ip_type__in=IpType.objects.filter(need_infra=False))
else: else:
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) self.fields['ipv4'].queryset = IpList.objects.filter(
interface__isnull=True
)
class NewInterfaceForm(EditInterfaceForm): class NewInterfaceForm(EditInterfaceForm):
"""Formulaire light, sans choix de l'ipv4; d'ajout d'une interface"""
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['type', 'mac_address', 'details'] fields = ['type', 'mac_address', 'details']
class BaseEditInterfaceForm(EditInterfaceForm): class BaseEditInterfaceForm(EditInterfaceForm):
"""Edition basique d'une interface. En fonction des droits,
ajoute ou non l'ensemble des ipv4 disponibles (infra)"""
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['type', 'ipv4', 'mac_address', 'details'] fields = ['type', 'ipv4', 'mac_address', 'details']
@ -96,133 +149,271 @@ class BaseEditInterfaceForm(EditInterfaceForm):
super(BaseEditInterfaceForm, self).__init__(*args, **kwargs) super(BaseEditInterfaceForm, self).__init__(*args, **kwargs)
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
if not infra: if not infra:
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False)) self.fields['type'].queryset = MachineType.objects.filter(
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)) ip_type__in=IpType.objects.filter(need_infra=False)
)
self.fields['ipv4'].queryset = IpList.objects.filter(
interface__isnull=True
).filter(ip_type__in=IpType.objects.filter(need_infra=False))
# Add it's own address # Add it's own address
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance) self.fields['ipv4'].queryset |= IpList.objects.filter(
interface=self.instance
)
else: else:
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) self.fields['ipv4'].queryset = IpList.objects.filter(
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance) interface__isnull=True
)
self.fields['ipv4'].queryset |= IpList.objects.filter(
interface=self.instance
)
class AliasForm(ModelForm): class AliasForm(ModelForm):
"""Ajout d'un alias (et edition), CNAME, contenant nom et extension"""
class Meta: class Meta:
model = Domain model = Domain
fields = ['name', 'extension'] fields = ['name', 'extension']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'infra' in kwargs: prefix = kwargs.pop('prefix', self.Meta.model.__name__)
infra = kwargs.pop('infra') infra = kwargs.pop('infra')
super(AliasForm, self).__init__(*args, **kwargs) super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs)
if not infra:
self.fields['extension'].queryset = Extension.objects.filter(
need_infra=False
)
class DomainForm(AliasForm):
class Meta(AliasForm.Meta): class DomainForm(ModelForm):
"""Ajout et edition d'un enregistrement de nom, relié à interface"""
class Meta:
model = Domain
fields = ['name'] fields = ['name']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'user' in kwargs: if 'user' in kwargs:
user = kwargs.pop('user') user = kwargs.pop('user')
nb_machine = kwargs.pop('nb_machine')
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) """Suppression d'un ou plusieurs objets alias"""
alias = forms.ModelMultipleChoiceField(
queryset=Domain.objects.all(),
label="Alias actuels",
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
interface = kwargs.pop('interface') interface = kwargs.pop('interface')
super(DelAliasForm, self).__init__(*args, **kwargs) super(DelAliasForm, self).__init__(*args, **kwargs)
self.fields['alias'].queryset = Domain.objects.filter(cname__in=Domain.objects.filter(interface_parent=interface)) self.fields['alias'].queryset = Domain.objects.filter(
cname__in=Domain.objects.filter(interface_parent=interface)
)
class MachineTypeForm(ModelForm): class MachineTypeForm(ModelForm):
"""Ajout et edition d'un machinetype, relié à un iptype"""
class Meta: class Meta:
model = MachineType model = MachineType
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é"
class DelMachineTypeForm(Form): class DelMachineTypeForm(Form):
machinetypes = forms.ModelMultipleChoiceField(queryset=MachineType.objects.all(), label="Types de machines actuelles", widget=forms.CheckboxSelectMultiple) """Suppression d'un ou plusieurs machinetype"""
machinetypes = forms.ModelMultipleChoiceField(
queryset=MachineType.objects.all(),
label="Types de machines actuelles",
widget=forms.CheckboxSelectMultiple
)
class IpTypeForm(ModelForm): class IpTypeForm(ModelForm):
"""Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de
stop après creation"""
class Meta: class Meta:
model = IpType model = IpType
fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan'] fields = ['type', 'extension', 'need_infra', 'domaine_ip_start',
'domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports']
def __init__(self, *args, **kwargs): 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):
"""Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait
synchroniser les objets iplist"""
class Meta(IpTypeForm.Meta): class Meta(IpTypeForm.Meta):
fields = ['extension','type','need_infra', 'prefix_v6', 'vlan'] fields = ['extension', 'type', 'need_infra', 'prefix_v6', 'vlan',
'ouverture_ports']
class DelIpTypeForm(Form): class DelIpTypeForm(Form):
iptypes = forms.ModelMultipleChoiceField(queryset=IpType.objects.all(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple) """Suppression d'un ou plusieurs iptype"""
iptypes = forms.ModelMultipleChoiceField(
queryset=IpType.objects.all(),
label="Types d'ip actuelles",
widget=forms.CheckboxSelectMultiple
)
class ExtensionForm(ModelForm): class ExtensionForm(ModelForm):
"""Formulaire d'ajout et edition d'une extension"""
class Meta: class Meta:
model = Extension model = Extension
fields = ['name', 'need_infra', 'origin'] fields = '__all__'
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'
self.fields['origin_v6'].label = 'Enregistrement AAAA origin'
self.fields['soa'].label = 'En-tête SOA à utiliser'
class DelExtensionForm(Form): class DelExtensionForm(Form):
extensions = forms.ModelMultipleChoiceField(queryset=Extension.objects.all(), label="Extensions actuelles", widget=forms.CheckboxSelectMultiple) """Suppression d'une ou plusieurs extensions"""
extensions = forms.ModelMultipleChoiceField(
queryset=Extension.objects.all(),
label="Extensions actuelles",
widget=forms.CheckboxSelectMultiple
)
class SOAForm(ModelForm):
"""Ajout et edition d'un SOA"""
class Meta:
model = SOA
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(SOAForm, self).__init__(*args, prefix=prefix, **kwargs)
class DelSOAForm(Form):
"""Suppression d'un ou plusieurs SOA"""
soa = forms.ModelMultipleChoiceField(
queryset=SOA.objects.all(),
label="SOA actuels",
widget=forms.CheckboxSelectMultiple
)
class MxForm(ModelForm): class MxForm(ModelForm):
"""Ajout et edition d'un MX"""
class Meta: class Meta:
model = Mx model = Mx
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__)
self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None) super(MxForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].queryset = Domain.objects.exclude(
interface_parent=None
).select_related('extension')
class DelMxForm(Form): class DelMxForm(Form):
mx = forms.ModelMultipleChoiceField(queryset=Mx.objects.all(), label="MX actuels", widget=forms.CheckboxSelectMultiple) """Suppression d'un ou plusieurs MX"""
mx = forms.ModelMultipleChoiceField(
queryset=Mx.objects.all(),
label="MX actuels",
widget=forms.CheckboxSelectMultiple
)
class NsForm(ModelForm): class NsForm(ModelForm):
"""Ajout d'un NS pour une zone
On exclue les CNAME dans les objets domain (interdit par la rfc)
donc on prend uniquemet """
class Meta: class Meta:
model = Ns model = Ns
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__)
self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None) super(NsForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['ns'].queryset = Domain.objects.exclude(
interface_parent=None
).select_related('extension')
class DelNsForm(Form): class DelNsForm(Form):
ns = forms.ModelMultipleChoiceField(queryset=Ns.objects.all(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple) """Suppresion d'un ou plusieurs NS"""
ns = forms.ModelMultipleChoiceField(
queryset=Ns.objects.all(),
label="Enregistrements NS actuels",
widget=forms.CheckboxSelectMultiple
)
class TextForm(ModelForm):
class TxtForm(ModelForm):
"""Ajout d'un txt pour une zone"""
class Meta: 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):
"""Suppression d'un ou plusieurs TXT"""
txt = forms.ModelMultipleChoiceField(
queryset=Text.objects.all(),
label="Enregistrements Txt actuels",
widget=forms.CheckboxSelectMultiple
)
class NasForm(ModelForm): class NasForm(ModelForm):
"""Ajout d'un type de nas (machine d'authentification,
swicths, bornes...)"""
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) """Suppression d'un ou plusieurs nas"""
nas = forms.ModelMultipleChoiceField(
queryset=Nas.objects.all(),
label="Enregistrements Nas actuels",
widget=forms.CheckboxSelectMultiple
)
class ServiceForm(ModelForm): class ServiceForm(ModelForm):
"""Ajout et edition d'une classe de service : dns, dhcp, etc"""
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)
self.fields['servers'].queryset = Interface.objects.all()\
.select_related('domain__extension')
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:
@ -230,24 +421,63 @@ class ServiceForm(ModelForm):
instance.process_link(self.cleaned_data.get('servers')) instance.process_link(self.cleaned_data.get('servers'))
return instance return instance
class DelServiceForm(Form): class DelServiceForm(Form):
service = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), label="Services actuels", widget=forms.CheckboxSelectMultiple) """Suppression d'un ou plusieurs service"""
service = forms.ModelMultipleChoiceField(
queryset=Service.objects.all(),
label="Services actuels",
widget=forms.CheckboxSelectMultiple
)
class VlanForm(ModelForm): class VlanForm(ModelForm):
"""Ajout d'un vlan : id, nom"""
class Meta: class Meta:
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) """Suppression d'un ou plusieurs vlans"""
vlan = forms.ModelMultipleChoiceField(
queryset=Vlan.objects.all(),
label="Vlan actuels",
widget=forms.CheckboxSelectMultiple
)
class EditOuverturePortConfigForm(ModelForm): class EditOuverturePortConfigForm(ModelForm):
"""Edition de la liste des profils d'ouverture de ports
pour l'interface"""
class Meta: class Meta:
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):
"""Edition de la liste des ports et profils d'ouverture
des ports"""
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

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-03 16:08
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('machines', '0059_iptype_prefix_v6'),
]
operations = [
migrations.AddField(
model_name='iptype',
name='ouverture_ports',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='machines.OuverturePortList'),
),
]

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-15 18:33
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0060_iptype_ouverture_ports'),
]
operations = [
migrations.AlterField(
model_name='mx',
name='priority',
field=models.PositiveIntegerField(unique=True),
),
migrations.AlterField(
model_name='ouvertureport',
name='begin',
field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(65535)]),
),
migrations.AlterField(
model_name='ouvertureport',
name='end',
field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(65535)]),
),
migrations.AlterField(
model_name='vlan',
name='vlan_id',
field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(4095)]),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-18 14:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0061_auto_20171015_2033'),
]
operations = [
migrations.AddField(
model_name='extension',
name='origin_v6',
field=models.GenericIPAddressField(blank=True, null=True, protocol='IPv6'),
),
]

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-19 22:40
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import machines.models
class Migration(migrations.Migration):
dependencies = [
('machines', '0062_extension_origin_v6'),
]
operations = [
migrations.CreateModel(
name='SOA',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('mail', models.EmailField(help_text='Email du contact pour la zone', max_length=254)),
('refresh', models.PositiveIntegerField(default=86400, help_text='Secondes avant que les DNS secondaires doivent demander le serial du DNS primaire pour détecter une modification')),
('retry', models.PositiveIntegerField(default=7200, help_text='Secondes avant que les DNS secondaires fassent une nouvelle demande de serial en cas de timeout du DNS primaire')),
('expire', models.PositiveIntegerField(default=3600000, help_text='Secondes après lesquelles les DNS secondaires arrêtent de de répondre aux requêtes en cas de timeout du DNS primaire')),
('ttl', models.PositiveIntegerField(default=172800, help_text='Time To Live')),
],
),
migrations.AddField(
model_name='extension',
name='soa',
field=models.ForeignKey(default=machines.models.SOA.new_default_soa, on_delete=django.db.models.deletion.CASCADE, to='machines.SOA'),
),
]

View file

@ -23,45 +23,60 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import timedelta
import re
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
from django.db import models from django.db import models
from django.db.models.signals import post_save, pre_delete, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.forms import ValidationError from django.forms import ValidationError
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils import timezone from django.utils import timezone
from django.core.validators import MaxValueValidator
from macaddress.fields import MACAddressField from macaddress.fields import MACAddressField
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
from django.core.validators import MinValueValidator,MaxValueValidator
import re
from reversion import revisions as reversion
from datetime import timedelta
class Machine(models.Model): class Machine(models.Model):
""" Class définissant une machine, object parent user, objets fils interfaces""" """ Class définissant une machine, object parent user, objets fils
interfaces"""
PRETTY_NAME = "Machine" PRETTY_NAME = "Machine"
user = models.ForeignKey('users.User', on_delete=models.PROTECT) user = models.ForeignKey('users.User', on_delete=models.PROTECT)
name = models.CharField(max_length=255, help_text="Optionnel", blank=True, null=True) name = models.CharField(
max_length=255,
help_text="Optionnel",
blank=True,
null=True
)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
def __str__(self): def __str__(self):
return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name) return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name)
class MachineType(models.Model): class MachineType(models.Model):
""" Type de machine, relié à un type d'ip, affecté aux interfaces""" """ Type de machine, relié à un type d'ip, affecté aux interfaces"""
PRETTY_NAME = "Type de machine" PRETTY_NAME = "Type de machine"
type = models.CharField(max_length=255) type = models.CharField(max_length=255)
ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True) ip_type = models.ForeignKey(
'IpType',
on_delete=models.PROTECT,
blank=True,
null=True
)
def all_interfaces(self): def all_interfaces(self):
""" Renvoie toutes les interfaces (cartes réseaux) de type machinetype""" """ Renvoie toutes les interfaces (cartes réseaux) de type
machinetype"""
return Interface.objects.filter(type=self) return Interface.objects.filter(type=self)
def __str__(self): def __str__(self):
return self.type return self.type
class IpType(models.Model): class IpType(models.Model):
""" Type d'ip, définissant un range d'ip, affecté aux machine types""" """ Type d'ip, définissant un range d'ip, affecté aux machine types"""
PRETTY_NAME = "Type d'ip" PRETTY_NAME = "Type d'ip"
@ -71,8 +86,22 @@ class IpType(models.Model):
need_infra = models.BooleanField(default=False) need_infra = models.BooleanField(default=False)
domaine_ip_start = models.GenericIPAddressField(protocol='IPv4') domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4') domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
prefix_v6 = models.GenericIPAddressField(protocol='IPv6', null=True, blank=True) prefix_v6 = models.GenericIPAddressField(
vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True) protocol='IPv6',
null=True,
blank=True
)
vlan = models.ForeignKey(
'Vlan',
on_delete=models.PROTECT,
blank=True,
null=True
)
ouverture_ports = models.ForeignKey(
'OuverturePortList',
blank=True,
null=True
)
@cached_property @cached_property
def ip_range(self): def ip_range(self):
@ -95,18 +124,22 @@ class IpType(models.Model):
def free_ip(self): def free_ip(self):
""" Renvoie toutes les ip libres associées au type donné (self)""" """ Renvoie toutes les ip libres associées au type donné (self)"""
return IpList.objects.filter(interface__isnull=True).filter(ip_type=self) return IpList.objects.filter(
interface__isnull=True
).filter(ip_type=self)
def gen_ip_range(self): def gen_ip_range(self):
""" Cree les IpList associées au type self. Parcours pédestrement et crée """ Cree les IpList associées au type self. Parcours pédestrement et
les ip une par une. Si elles existent déjà, met à jour le type associé crée les ip une par une. Si elles existent déjà, met à jour le type
à l'ip""" associé à l'ip"""
# Creation du range d'ip dans les objets iplist # Creation du range d'ip dans les objets iplist
networks = [] networks = []
for net in self.ip_range.cidrs(): for net in self.ip_range.cidrs():
networks += net.iter_hosts() networks += net.iter_hosts()
ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks] ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks]
listes_ip = IpList.objects.filter(ipv4__in=[str(ip) for ip in networks]) listes_ip = IpList.objects.filter(
ipv4__in=[str(ip) for ip in networks]
)
# Si il n'y a pas d'ip, on les crée # Si il n'y a pas d'ip, on les crée
if not listes_ip: if not listes_ip:
IpList.objects.bulk_create(ip_obj) IpList.objects.bulk_create(ip_obj)
@ -116,9 +149,11 @@ class IpType(models.Model):
return return
def del_ip_range(self): def del_ip_range(self):
""" Methode dépréciée, IpList est en mode cascade et supprimé automatiquement""" """ Methode dépréciée, IpList est en mode cascade et supprimé
automatiquement"""
if Interface.objects.filter(ipv4__in=self.ip_objects()): if Interface.objects.filter(ipv4__in=self.ip_objects()):
raise ValidationError("Une ou plusieurs ip du range sont affectées, impossible de supprimer le range") raise ValidationError("Une ou plusieurs ip du range sont\
affectées, impossible de supprimer le range")
for ip in self.ip_objects(): for ip in self.ip_objects():
ip.delete() ip.delete()
@ -132,11 +167,13 @@ class IpType(models.Model):
raise ValidationError("Domaine end doit être après start...") raise ValidationError("Domaine end doit être après start...")
# On ne crée pas plus grand qu'un /16 # On ne crée pas plus grand qu'un /16
if self.ip_range.size > 65536: if self.ip_range.size > 65536:
raise ValidationError("Le range est trop gros, vous ne devez pas créer plus grand qu'un /16") raise ValidationError("Le range est trop gros, vous ne devez\
pas créer plus grand qu'un /16")
# On check que les / ne se recoupent pas # On check que les / ne se recoupent pas
for element in IpType.objects.all().exclude(pk=self.pk): for element in IpType.objects.all().exclude(pk=self.pk):
if not self.ip_set.isdisjoint(element.ip_set): if not self.ip_set.isdisjoint(element.ip_set):
raise ValidationError("Le range indiqué n'est pas disjoint des ranges existants") raise ValidationError("Le range indiqué n'est pas disjoint\
des ranges existants")
# On formate le prefix v6 # On formate le prefix v6
if self.prefix_v6: if self.prefix_v6:
self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network) self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
@ -149,17 +186,20 @@ class IpType(models.Model):
def __str__(self): def __str__(self):
return self.type return self.type
class Vlan(models.Model): class Vlan(models.Model):
""" Un vlan : vlan_id et nom""" """ Un vlan : vlan_id et nom
On limite le vlan id entre 0 et 4096, comme défini par la norme"""
PRETTY_NAME = "Vlans" PRETTY_NAME = "Vlans"
vlan_id = models.IntegerField() vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
comment = models.CharField(max_length=256, blank=True) comment = models.CharField(max_length=256, blank=True)
def __str__(self): def __str__(self):
return self.name return self.name
class Nas(models.Model): class Nas(models.Model):
""" Les nas. Associé à un machine_type. """ Les nas. Associé à un machine_type.
Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour
@ -173,48 +213,161 @@ class Nas(models.Model):
) )
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
nas_type = models.ForeignKey('MachineType', on_delete=models.PROTECT, related_name='nas_type') nas_type = models.ForeignKey(
machine_type = models.ForeignKey('MachineType', on_delete=models.PROTECT, related_name='machinetype_on_nas') 'MachineType',
port_access_mode = models.CharField(choices=AUTH, default=default_mode, max_length=32) on_delete=models.PROTECT,
related_name='nas_type'
)
machine_type = models.ForeignKey(
'MachineType',
on_delete=models.PROTECT,
related_name='machinetype_on_nas'
)
port_access_mode = models.CharField(
choices=AUTH,
default=default_mode,
max_length=32
)
autocapture_mac = models.BooleanField(default=False) autocapture_mac = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.name return self.name
class SOA(models.Model):
"""
Un enregistrement SOA associé à une extension
Les valeurs par défault viennent des recommandations RIPE :
https://www.ripe.net/publications/docs/ripe-203
"""
PRETTY_NAME = "Enregistrement SOA"
name = models.CharField(max_length=255)
mail = models.EmailField(
help_text='Email du contact pour la zone'
)
refresh = models.PositiveIntegerField(
default=86400, # 24 hours
help_text='Secondes avant que les DNS secondaires doivent demander le\
serial du DNS primaire pour détecter une modification'
)
retry = models.PositiveIntegerField(
default=7200, # 2 hours
help_text='Secondes avant que les DNS secondaires fassent une nouvelle\
demande de serial en cas de timeout du DNS primaire'
)
expire = models.PositiveIntegerField(
default=3600000, # 1000 hours
help_text='Secondes après lesquelles les DNS secondaires arrêtent de\
de répondre aux requêtes en cas de timeout du DNS primaire'
)
ttl = models.PositiveIntegerField(
default=172800, # 2 days
help_text='Time To Live'
)
def __str__(self):
return str(self.name)
@cached_property
def dns_soa_param(self):
"""
Renvoie la partie de l'enregistrement SOA correspondant aux champs :
<refresh> ; refresh
<retry> ; retry
<expire> ; expire
<ttl> ; TTL
"""
return (
' {refresh}; refresh\n'
' {retry}; retry\n'
' {expire}; expire\n'
' {ttl}; TTL'
).format(
refresh=str(self.refresh).ljust(12),
retry=str(self.retry).ljust(12),
expire=str(self.expire).ljust(12),
ttl=str(self.ttl).ljust(12)
)
@cached_property
def dns_soa_mail(self):
""" Renvoie le mail dans l'enregistrement SOA """
mail_fields = str(self.mail).split('@')
return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.'
@classmethod
def new_default_soa(cls):
""" Fonction pour créer un SOA par défaut, utile pour les nouvelles
extensions .
/!\ Ne jamais supprimer ou renommer cette fonction car elle est
utilisée dans les migrations de la BDD. """
return cls.objects.get_or_create(name="SOA to edit", mail="postmaser@example.com")[0].pk
class Extension(models.Model): class Extension(models.Model):
""" Extension dns type example.org. Précise si tout le monde peut l'utiliser, """ Extension dns type example.org. Précise si tout le monde peut
associé à un origin (ip d'origine)""" l'utiliser, associé à un origin (ip d'origine)"""
PRETTY_NAME = "Extensions dns" PRETTY_NAME = "Extensions dns"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
need_infra = models.BooleanField(default=False) need_infra = models.BooleanField(default=False)
origin = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) origin = models.OneToOneField(
'IpList',
on_delete=models.PROTECT,
blank=True,
null=True
)
origin_v6 = models.GenericIPAddressField(
protocol='IPv6',
null=True,
blank=True
)
soa = models.ForeignKey(
'SOA',
on_delete=models.CASCADE,
default=SOA.new_default_soa
)
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS A""" """ Une entrée DNS A et AAAA sur origin (zone self)"""
return "@ IN A " + str(self.origin) entry = ""
if self.origin:
entry += "@ IN A " + str(self.origin)
if self.origin_v6:
if entry:
entry += "\n"
entry += "@ IN AAAA " + str(self.origin_v6)
return entry
def __str__(self): def __str__(self):
return self.name return self.name
class Mx(models.Model): class Mx(models.Model):
""" Entrées des MX. Enregistre la zone (extension) associée et la priorité """ Entrées des MX. Enregistre la zone (extension) associée et la
priorité
Todo : pouvoir associer un MX à une interface """ Todo : pouvoir associer un MX à une interface """
PRETTY_NAME = "Enregistrements MX" PRETTY_NAME = "Enregistrements MX"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
priority = models.IntegerField(unique=True) priority = models.PositiveIntegerField(unique=True)
name = models.OneToOneField('Domain', on_delete=models.PROTECT) name = models.OneToOneField('Domain', on_delete=models.PROTECT)
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
return "@ IN MX " + str(self.priority) + " " + str(self.name) """Renvoie l'entrée DNS complète pour un MX à mettre dans les
fichiers de zones"""
return "@ IN MX " + str(self.priority).ljust(3) + " " + str(self.name)
def __str__(self): def __str__(self):
return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)
class Ns(models.Model): class Ns(models.Model):
"""Liste des enregistrements name servers par zone considéérée"""
PRETTY_NAME = "Enregistrements NS" PRETTY_NAME = "Enregistrements NS"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
@ -222,36 +375,47 @@ class Ns(models.Model):
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
"""Renvoie un enregistrement NS complet pour les filezones"""
return "@ IN NS " + str(self.ns) return "@ IN NS " + str(self.ns)
def __str__(self): def __str__(self):
return str(self.zone) + ' ' + str(self.ns) return str(self.zone) + ' ' + str(self.ns)
class Text(models.Model): class Text(models.Model):
""" Un enregistrement TXT associé à une extension""" """ Un enregistrement TXT associé à une extension"""
PRETTY_NAME = "Enregistrement text" PRETTY_NAME = "Enregistrement TXT"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
field1 = models.CharField(max_length=255) field1 = models.CharField(max_length=255)
field2 = models.CharField(max_length=255) field2 = models.CharField(max_length=255)
def __str__(self): def __str__(self):
return str(self.zone) + " : " + str(self.field1) + " " + str(self.field2) return str(self.zone) + " : " + str(self.field1) + " " +\
str(self.field2)
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
return str(self.field1) + " IN TXT " + str(self.field2) """Renvoie l'enregistrement TXT complet pour le fichier de zone"""
return str(self.field1).ljust(15) + " IN TXT " + str(self.field2)
class Interface(models.Model): class Interface(models.Model):
""" Une interface. Objet clef de l'application machine : """ Une interface. Objet clef de l'application machine :
- une address mac unique. Possibilité de la rendre unique avec le typemachine - une address mac unique. Possibilité de la rendre unique avec le
typemachine
- une onetoone vers IpList pour attribution ipv4 - une onetoone vers IpList pour attribution ipv4
- le type parent associé au range ip et à l'extension - le type parent associé au range ip et à l'extension
- un objet domain associé contenant son nom - un objet domain associé contenant son nom
- la liste des ports oiuvert""" - la liste des ports oiuvert"""
PRETTY_NAME = "Interface" PRETTY_NAME = "Interface"
ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) ipv4 = models.OneToOneField(
'IpList',
on_delete=models.PROTECT,
blank=True,
null=True
)
mac_address = MACAddressField(integer=False, unique=True) mac_address = MACAddressField(integer=False, unique=True)
machine = models.ForeignKey('Machine', on_delete=models.CASCADE) machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
type = models.ForeignKey('MachineType', on_delete=models.PROTECT) type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
@ -265,12 +429,14 @@ class Interface(models.Model):
user = self.machine.user user = self.machine.user
return machine.active and user.has_access() return machine.active and user.has_access()
@cached_property @cached_property
def ipv6_object(self): def ipv6_object(self):
""" Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent""" """ Renvoie un objet type ipv6 à partir du prefix associé à
l'iptype parent"""
if self.type.ip_type.prefix_v6: if self.type.ip_type.prefix_v6:
return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network) return EUI(self.mac_address).ipv6(
IPNetwork(self.type.ip_type.prefix_v6).network
)
else: else:
return None return None
@ -284,7 +450,8 @@ class Interface(models.Model):
return str(EUI(self.mac_address, dialect=mac_bare)).lower() return str(EUI(self.mac_address, dialect=mac_bare)).lower()
def filter_macaddress(self): def filter_macaddress(self):
""" Tente un formatage mac_bare, si échoue, lève une erreur de validation""" """ Tente un formatage mac_bare, si échoue, lève une erreur de
validation"""
try: try:
self.mac_address = str(EUI(self.mac_address)) self.mac_address = str(EUI(self.mac_address))
except: except:
@ -293,6 +460,15 @@ class Interface(models.Model):
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
""" Formate l'addresse mac en mac_bare (fonction filter_mac) """ Formate l'addresse mac en mac_bare (fonction filter_mac)
et assigne une ipv4 dans le bon range si inexistante ou incohérente""" et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
# If type was an invalid value, django won't create an attribute type
# but try clean() as we may be able to create it from another value
# so even if the error as yet been detected at this point, django
# continues because the error might not prevent us from creating the
# instance.
# But in our case, it's impossible to create a type value so we raise
# the error.
if not hasattr(self, 'type') :
raise ValidationError("Le type d'ip choisi n'est pas valide")
self.filter_macaddress() self.filter_macaddress()
self.mac_address = str(EUI(self.mac_address)) or None self.mac_address = str(EUI(self.mac_address)) or None
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
@ -305,7 +481,8 @@ class Interface(models.Model):
if free_ips: if free_ips:
self.ipv4 = free_ips[0] self.ipv4 = free_ips[0]
else: else:
raise ValidationError("Il n'y a plus d'ip disponibles dans le slash") raise ValidationError("Il n'y a plus d'ip disponibles\
dans le slash")
return return
def unassign_ipv4(self): def unassign_ipv4(self):
@ -320,8 +497,10 @@ class Interface(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.filter_macaddress() self.filter_macaddress()
# On verifie la cohérence en forçant l'extension par la méthode # On verifie la cohérence en forçant l'extension par la méthode
if self.ipv4:
if self.type.ip_type != self.ipv4.ip_type: if self.type.ip_type != self.ipv4.ip_type:
raise ValidationError("L'ipv4 et le type de la machine ne correspondent pas") raise ValidationError("L'ipv4 et le type de la machine ne\
correspondent pas")
super(Interface, self).save(*args, **kwargs) super(Interface, self).save(*args, **kwargs)
def __str__(self): def __str__(self):
@ -340,18 +519,34 @@ class Interface(models.Model):
def may_have_port_open(self): def may_have_port_open(self):
""" True si l'interface a une ip et une ip publique. """ True si l'interface a une ip et une ip publique.
Permet de ne pas exporter des ouvertures sur des ip privées (useless)""" Permet de ne pas exporter des ouvertures sur des ip privées
(useless)"""
return self.ipv4 and not self.has_private_ip() return self.ipv4 and not self.has_private_ip()
class Domain(models.Model): class Domain(models.Model):
""" Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les """ Objet domain. Enregistrement A et CNAME en même temps : permet de
alias et les nom de machines, suivant si interface_parent ou cname sont remplis""" stocker les alias et les nom de machines, suivant si interface_parent
ou cname sont remplis"""
PRETTY_NAME = "Domaine dns" PRETTY_NAME = "Domaine dns"
interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True) interface_parent = models.OneToOneField(
name = models.CharField(help_text="Obligatoire et unique, ne doit pas comporter de points", max_length=255) 'Interface',
on_delete=models.CASCADE,
blank=True,
null=True
)
name = models.CharField(
help_text="Obligatoire et unique, ne doit pas comporter de points",
max_length=255
)
extension = models.ForeignKey('Extension', on_delete=models.PROTECT) extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
cname = models.ForeignKey('self', null=True, blank=True, related_name='related_domain') cname = models.ForeignKey(
'self',
null=True,
blank=True,
related_name='related_domain'
)
class Meta: class Meta:
unique_together = (("name", "extension"),) unique_together = (("name", "extension"),)
@ -370,21 +565,26 @@ class Domain(models.Model):
""" Validation : """ Validation :
- l'objet est bien soit A soit CNAME - l'objet est bien soit A soit CNAME
- le cname est pas pointé sur lui-même - le cname est pas pointé sur lui-même
- le nom contient bien les caractères autorisés par la norme dns et moins de 63 caractères au total - le nom contient bien les caractères autorisés par la norme
dns et moins de 63 caractères au total
- le couple nom/extension est bien unique""" - le couple nom/extension est bien unique"""
if self.get_extension(): if self.get_extension():
self.extension = self.get_extension() self.extension = self.get_extension()
""" Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """
if self.interface_parent and self.cname: if self.interface_parent and self.cname:
raise ValidationError("On ne peut créer à la fois A et CNAME") raise ValidationError("On ne peut créer à la fois A et CNAME")
if self.cname == self: if self.cname == self:
raise ValidationError("On ne peut créer un cname sur lui même") raise ValidationError("On ne peut créer un cname sur lui même")
HOSTNAME_LABEL_PATTERN = re.compile("(?!-)[A-Z\d-]+(?<!-)$", re.IGNORECASE) HOSTNAME_LABEL_PATTERN = re.compile(
"(?!-)[A-Z\d-]+(?<!-)$",
re.IGNORECASE
)
dns = self.name.lower() dns = self.name.lower()
if len(dns) > 63: if len(dns) > 63:
raise ValidationError("Le nom de domaine %s est trop long (maximum de 63 caractères)." % dns) raise ValidationError("Le nom de domaine %s est trop long\
(maximum de 63 caractères)." % dns)
if not HOSTNAME_LABEL_PATTERN.match(dns): if not HOSTNAME_LABEL_PATTERN.match(dns):
raise ValidationError("Ce nom de domaine %s contient des carractères interdits." % dns) raise ValidationError("Ce nom de domaine %s contient des\
carractères interdits." % dns)
self.validate_unique() self.validate_unique()
super(Domain, self).clean() super(Domain, self).clean()
@ -392,10 +592,11 @@ class Domain(models.Model):
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS""" """ Une entrée DNS"""
if self.cname: if self.cname:
return str(self.name) + " IN CNAME " + str(self.cname) + "." return str(self.name).ljust(15) + " IN CNAME " + str(self.cname) + "."
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" Empèche le save sans extension valide. Force à avoir appellé clean avant""" """ Empèche le save sans extension valide.
Force à avoir appellé clean avant"""
if not self.get_extension(): if not self.get_extension():
raise ValidationError("Extension invalide") raise ValidationError("Extension invalide")
self.full_clean() self.full_clean()
@ -404,6 +605,7 @@ class Domain(models.Model):
def __str__(self): def __str__(self):
return str(self.name) + str(self.extension) return str(self.name) + str(self.extension)
class IpList(models.Model): class IpList(models.Model):
PRETTY_NAME = "Addresses ipv4" PRETTY_NAME = "Addresses ipv4"
@ -412,13 +614,15 @@ class IpList(models.Model):
@cached_property @cached_property
def need_infra(self): def need_infra(self):
""" Permet de savoir si un user basique peut assigner cette ip ou non""" """ Permet de savoir si un user basique peut assigner cette ip ou
non"""
return self.ip_type.need_infra return self.ip_type.need_infra
def clean(self): def clean(self):
""" Erreur si l'ip_type est incorrect""" """ Erreur si l'ip_type est incorrect"""
if not str(self.ipv4) in self.ip_type.ip_set_as_str: if not str(self.ipv4) in self.ip_type.ip_set_as_str:
raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!") raise ValidationError("L'ipv4 et le range de l'iptype ne\
correspondent pas!")
return return
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -428,24 +632,38 @@ class IpList(models.Model):
def __str__(self): def __str__(self):
return self.ipv4 return self.ipv4
class Service(models.Model): class Service(models.Model):
""" Definition d'un service (dhcp, dns, etc)""" """ Definition d'un service (dhcp, dns, etc)"""
PRETTY_NAME = "Services à générer (dhcp, dns, etc)"
service_type = models.CharField(max_length=255, blank=True, unique=True) service_type = models.CharField(max_length=255, blank=True, unique=True)
min_time_regen = models.DurationField(default=timedelta(minutes=1), help_text="Temps minimal avant nouvelle génération du service") min_time_regen = models.DurationField(
regular_time_regen = models.DurationField(default=timedelta(hours=1), help_text="Temps maximal avant nouvelle génération du service") default=timedelta(minutes=1),
help_text="Temps minimal avant nouvelle génération du service"
)
regular_time_regen = models.DurationField(
default=timedelta(hours=1),
help_text="Temps maximal avant nouvelle génération du service"
)
servers = models.ManyToManyField('Interface', through='Service_link') servers = models.ManyToManyField('Interface', through='Service_link')
def ask_regen(self): def ask_regen(self):
""" Marque à True la demande de régénération pour un service x """ """ Marque à True la demande de régénération pour un service x """
Service_link.objects.filter(service=self).exclude(asked_regen=True).update(asked_regen=True) Service_link.objects.filter(service=self).exclude(asked_regen=True)\
.update(asked_regen=True)
return return
def process_link(self, servers): def process_link(self, servers):
""" Django ne peut créer lui meme les relations manytomany avec table intermediaire explicite""" """ Django ne peut créer lui meme les relations manytomany avec table
for serv in servers.exclude(pk__in=Interface.objects.filter(service=self)): intermediaire explicite"""
for serv in servers.exclude(
pk__in=Interface.objects.filter(service=self)
):
link = Service_link(service=self, server=serv) link = Service_link(service=self, server=serv)
link.save() link.save()
Service_link.objects.filter(service=self).exclude(server__in=servers).delete() Service_link.objects.filter(service=self).exclude(server__in=servers)\
.delete()
return return
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -454,15 +672,20 @@ class Service(models.Model):
def __str__(self): def __str__(self):
return str(self.service_type) return str(self.service_type)
def regen(service): def regen(service):
""" Fonction externe pour régérération d'un service, prend un objet service en arg""" """ Fonction externe pour régérération d'un service, prend un objet service
en arg"""
obj = Service.objects.filter(service_type=service) obj = Service.objects.filter(service_type=service)
if obj: if obj:
obj[0].ask_regen() obj[0].ask_regen()
return return
class Service_link(models.Model): class Service_link(models.Model):
""" Definition du lien entre serveurs et services""" """ Definition du lien entre serveurs et services"""
PRETTY_NAME = "Relation entre service et serveur"
service = models.ForeignKey('Service', on_delete=models.CASCADE) service = models.ForeignKey('Service', on_delete=models.CASCADE)
server = models.ForeignKey('Interface', on_delete=models.CASCADE) server = models.ForeignKey('Interface', on_delete=models.CASCADE)
last_regen = models.DateTimeField(auto_now_add=True) last_regen = models.DateTimeField(auto_now_add=True)
@ -475,11 +698,16 @@ class Service_link(models.Model):
self.save() self.save()
def need_regen(self): def need_regen(self):
""" Décide si le temps minimal écoulé est suffisant pour provoquer une régénération de service""" """ Décide si le temps minimal écoulé est suffisant pour provoquer une
if (self.asked_regen and (self.last_regen + self.service.min_time_regen) < timezone.now()) or (self.last_regen + self.service.regular_time_regen) < timezone.now(): régénération de service"""
return True return bool(
else: (self.asked_regen and (
return False self.last_regen + self.service.min_time_regen
) < timezone.now()
) or (
self.last_regen + self.service.regular_time_regen
) < timezone.now()
)
def __str__(self): def __str__(self):
return str(self.server) + " " + str(self.service) return str(self.server) + " " + str(self.service)
@ -487,22 +715,43 @@ class Service_link(models.Model):
class OuverturePortList(models.Model): class OuverturePortList(models.Model):
"""Liste des ports ouverts sur une interface.""" """Liste des ports ouverts sur une interface."""
name = models.CharField(help_text="Nom de la configuration des ports.", max_length=255) PRETTY_NAME = "Profil d'ouverture de ports"
name = models.CharField(
help_text="Nom de la configuration des ports.",
max_length=255
)
def __str__(self): def __str__(self):
return self.name return self.name
def tcp_ports_in(self): def tcp_ports_in(self):
return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN) """Renvoie la liste des ports ouverts en TCP IN pour ce profil"""
return self.ouvertureport_set.filter(
protocole=OuverturePort.TCP,
io=OuverturePort.IN
)
def udp_ports_in(self): def udp_ports_in(self):
return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN) """Renvoie la liste des ports ouverts en UDP IN pour ce profil"""
return self.ouvertureport_set.filter(
protocole=OuverturePort.UDP,
io=OuverturePort.IN
)
def tcp_ports_out(self): def tcp_ports_out(self):
return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT) """Renvoie la liste des ports ouverts en TCP OUT pour ce profil"""
return self.ouvertureport_set.filter(
protocole=OuverturePort.TCP,
io=OuverturePort.OUT
)
def udp_ports_out(self): def udp_ports_out(self):
return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT) """Renvoie la liste des ports ouverts en UDP OUT pour ce profil"""
return self.ouvertureport_set.filter(
protocole=OuverturePort.UDP,
io=OuverturePort.OUT
)
class OuverturePort(models.Model): class OuverturePort(models.Model):
@ -511,14 +760,21 @@ class OuverturePort(models.Model):
Les ports de la plage sont compris entre begin et en inclus. Les ports de la plage sont compris entre begin et en inclus.
Si begin == end alors on ne représente qu'un seul port. Si begin == end alors on ne représente qu'un seul port.
On limite les ports entre 0 et 65535, tels que défini par la RFC
""" """
PRETTY_NAME = "Plage de port ouverte"
TCP = 'T' TCP = 'T'
UDP = 'U' UDP = 'U'
IN = 'I' IN = 'I'
OUT = 'O' OUT = 'O'
begin = models.IntegerField() begin = models.PositiveIntegerField(validators=[MaxValueValidator(65535)])
end = models.IntegerField() end = models.PositiveIntegerField(validators=[MaxValueValidator(65535)])
port_list = models.ForeignKey('OuverturePortList', on_delete=models.CASCADE) port_list = models.ForeignKey(
'OuverturePortList',
on_delete=models.CASCADE
)
protocole = models.CharField( protocole = models.CharField(
max_length=1, max_length=1,
choices=( choices=(
@ -542,88 +798,135 @@ class OuverturePort(models.Model):
return '-'.join([str(self.begin), str(self.end)]) return '-'.join([str(self.begin), str(self.end)])
def show_port(self): def show_port(self):
"""Formatage plus joli, alias pour str"""
return str(self) return str(self)
@receiver(post_save, sender=Machine) @receiver(post_save, sender=Machine)
def machine_post_save(sender, **kwargs): def machine_post_save(sender, **kwargs):
"""Synchronisation ldap et régen parefeu/dhcp lors de la modification
d'une machine"""
user = kwargs['instance'].user user = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
regen('dhcp') regen('dhcp')
regen('mac_ip_list') regen('mac_ip_list')
@receiver(post_delete, sender=Machine) @receiver(post_delete, sender=Machine)
def machine_post_delete(sender, **kwargs): def machine_post_delete(sender, **kwargs):
"""Synchronisation ldap et régen parefeu/dhcp lors de la suppression
d'une machine"""
machine = kwargs['instance'] machine = kwargs['instance']
user = machine.user user = machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
regen('dhcp') regen('dhcp')
regen('mac_ip_list') regen('mac_ip_list')
@receiver(post_save, sender=Interface) @receiver(post_save, sender=Interface)
def interface_post_save(sender, **kwargs): def interface_post_save(sender, **kwargs):
"""Synchronisation ldap et régen parefeu/dhcp lors de la modification
d'une interface"""
interface = kwargs['instance'] interface = kwargs['instance']
user = interface.machine.user user = interface.machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
if not interface.may_have_port_open() and interface.port_lists.all():
interface.port_lists.clear()
# Regen services # Regen services
regen('dhcp') regen('dhcp')
regen('mac_ip_list') regen('mac_ip_list')
@receiver(post_delete, sender=Interface) @receiver(post_delete, sender=Interface)
def interface_post_delete(sender, **kwargs): def interface_post_delete(sender, **kwargs):
"""Synchronisation ldap et régen parefeu/dhcp lors de la suppression
d'une interface"""
interface = kwargs['instance'] interface = kwargs['instance']
user = interface.machine.user user = interface.machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
@receiver(post_save, sender=IpType) @receiver(post_save, sender=IpType)
def iptype_post_save(sender, **kwargs): def iptype_post_save(sender, **kwargs):
"""Generation des objets ip après modification d'un range ip"""
iptype = kwargs['instance'] iptype = kwargs['instance']
iptype.gen_ip_range() iptype.gen_ip_range()
@receiver(post_save, sender=MachineType) @receiver(post_save, sender=MachineType)
def machine_post_save(sender, **kwargs): def machine_post_save(sender, **kwargs):
"""Mise à jour des interfaces lorsque changement d'attribution
d'une machinetype (changement iptype parent)"""
machinetype = kwargs['instance'] machinetype = kwargs['instance']
for interface in machinetype.all_interfaces(): for interface in machinetype.all_interfaces():
interface.update_type() interface.update_type()
@receiver(post_save, sender=Domain) @receiver(post_save, sender=Domain)
def domain_post_save(sender, **kwargs): def domain_post_save(sender, **kwargs):
"""Regeneration dns après modification d'un domain object"""
regen('dns') regen('dns')
@receiver(post_delete, sender=Domain) @receiver(post_delete, sender=Domain)
def domain_post_delete(sender, **kwargs): def domain_post_delete(sender, **kwargs):
"""Regeneration dns après suppression d'un domain object"""
regen('dns') regen('dns')
@receiver(post_save, sender=Extension) @receiver(post_save, sender=Extension)
def extension_post_save(sender, **kwargs): def extension_post_save(sender, **kwargs):
"""Regeneration dns après modification d'une extension"""
regen('dns') regen('dns')
@receiver(post_delete, sender=Extension) @receiver(post_delete, sender=Extension)
def extension_post_selete(sender, **kwargs): def extension_post_selete(sender, **kwargs):
"""Regeneration dns après suppression d'une extension"""
regen('dns') regen('dns')
@receiver(post_save, sender=SOA)
def soa_post_save(sender, **kwargs):
"""Regeneration dns après modification d'un SOA"""
regen('dns')
@receiver(post_delete, sender=SOA)
def soa_post_delete(sender, **kwargs):
"""Regeneration dns après suppresson d'un SOA"""
regen('dns')
@receiver(post_save, sender=Mx) @receiver(post_save, sender=Mx)
def mx_post_save(sender, **kwargs): def mx_post_save(sender, **kwargs):
"""Regeneration dns après modification d'un MX"""
regen('dns') regen('dns')
@receiver(post_delete, sender=Mx) @receiver(post_delete, sender=Mx)
def mx_post_delete(sender, **kwargs): def mx_post_delete(sender, **kwargs):
"""Regeneration dns après suppresson d'un MX"""
regen('dns') regen('dns')
@receiver(post_save, sender=Ns) @receiver(post_save, sender=Ns)
def ns_post_save(sender, **kwargs): def ns_post_save(sender, **kwargs):
"""Regeneration dns après modification d'un NS"""
regen('dns') regen('dns')
@receiver(post_delete, sender=Ns) @receiver(post_delete, sender=Ns)
def ns_post_delete(sender, **kwargs): def ns_post_delete(sender, **kwargs):
"""Regeneration dns après modification d'un NS"""
regen('dns') regen('dns')
@receiver(post_save, sender=Text) @receiver(post_save, sender=Text)
def text_post_save(sender, **kwargs): def text_post_save(sender, **kwargs):
"""Regeneration dns après modification d'un TXT"""
regen('dns') regen('dns')
@receiver(post_delete, sender=Text) @receiver(post_delete, sender=Text)
def text_post_delete(sender, **kwargs): def text_post_delete(sender, **kwargs):
"""Regeneration dns après modification d'un TX"""
regen('dns') regen('dns')

View file

@ -24,20 +24,42 @@
#Augustin Lemesle #Augustin Lemesle
from rest_framework import serializers from rest_framework import serializers
from machines.models import Interface, IpType, Extension, IpList, MachineType, Domain, Text, Mx, Service_link, Ns from machines.models import (
Interface,
IpType,
Extension,
IpList,
MachineType,
Domain,
Text,
Mx,
Service_link,
Ns,
OuverturePortList,
OuverturePort
)
class IpTypeField(serializers.RelatedField): class IpTypeField(serializers.RelatedField):
"""Serialisation d'une iptype, renvoie son evaluation str"""
def to_representation(self, value): def to_representation(self, value):
return value.type return value.type
class IpListSerializer(serializers.ModelSerializer): class IpListSerializer(serializers.ModelSerializer):
"""Serialisation d'une iplist, ip_type etant une foreign_key,
on evalue sa methode str"""
ip_type = IpTypeField(read_only=True) ip_type = IpTypeField(read_only=True)
class Meta: class Meta:
model = IpList model = IpList
fields = ('ipv4', 'ip_type') fields = ('ipv4', 'ip_type')
class InterfaceSerializer(serializers.ModelSerializer): class InterfaceSerializer(serializers.ModelSerializer):
"""Serialisation d'une interface, ipv4, domain et extension sont
des foreign_key, on les override et on les evalue avec des fonctions
get_..."""
ipv4 = IpListSerializer(read_only=True) ipv4 = IpListSerializer(read_only=True)
mac_address = serializers.SerializerMethodField('get_macaddress') mac_address = serializers.SerializerMethodField('get_macaddress')
domain = serializers.SerializerMethodField('get_dns') domain = serializers.SerializerMethodField('get_dns')
@ -56,7 +78,9 @@ class InterfaceSerializer(serializers.ModelSerializer):
def get_macaddress(self, obj): def get_macaddress(self, obj):
return str(obj.mac_address) return str(obj.mac_address)
class FullInterfaceSerializer(serializers.ModelSerializer): class FullInterfaceSerializer(serializers.ModelSerializer):
"""Serialisation complete d'une interface avec l'ipv6 en plus"""
ipv4 = IpListSerializer(read_only=True) ipv4 = IpListSerializer(read_only=True)
mac_address = serializers.SerializerMethodField('get_macaddress') mac_address = serializers.SerializerMethodField('get_macaddress')
domain = serializers.SerializerMethodField('get_dns') domain = serializers.SerializerMethodField('get_dns')
@ -75,24 +99,70 @@ class FullInterfaceSerializer(serializers.ModelSerializer):
def get_macaddress(self, obj): def get_macaddress(self, obj):
return str(obj.mac_address) return str(obj.mac_address)
class ExtensionNameField(serializers.RelatedField): class ExtensionNameField(serializers.RelatedField):
"""Evaluation str d'un objet extension (.example.org)"""
def to_representation(self, value): def to_representation(self, value):
return value.name return value.name
class TypeSerializer(serializers.ModelSerializer): class TypeSerializer(serializers.ModelSerializer):
"""Serialisation d'un iptype : extension et la liste des
ouvertures de port son evalués en get_... etant des
foreign_key ou des relations manytomany"""
extension = ExtensionNameField(read_only=True) extension = ExtensionNameField(read_only=True)
ouverture_ports_tcp_in = serializers\
.SerializerMethodField('get_port_policy_input_tcp')
ouverture_ports_tcp_out = serializers\
.SerializerMethodField('get_port_policy_output_tcp')
ouverture_ports_udp_in = serializers\
.SerializerMethodField('get_port_policy_input_udp')
ouverture_ports_udp_out = serializers\
.SerializerMethodField('get_port_policy_output_udp')
class Meta: class Meta:
model = IpType model = IpType
fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop') fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop',
'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out',
'ouverture_ports_udp_in', 'ouverture_ports_udp_out',)
def get_port_policy(self, obj, protocole, io):
if obj.ouverture_ports is None:
return []
return map(
str,
obj.ouverture_ports.ouvertureport_set.filter(
protocole=protocole
).filter(io=io)
)
def get_port_policy_input_tcp(self, obj):
"""Renvoie la liste des ports ouverts en entrée tcp"""
return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.IN)
def get_port_policy_output_tcp(self, obj):
"""Renvoie la liste des ports ouverts en sortie tcp"""
return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.OUT)
def get_port_policy_input_udp(self, obj):
"""Renvoie la liste des ports ouverts en entrée udp"""
return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.IN)
def get_port_policy_output_udp(self, obj):
"""Renvoie la liste des ports ouverts en sortie udp"""
return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.OUT)
class ExtensionSerializer(serializers.ModelSerializer): class ExtensionSerializer(serializers.ModelSerializer):
"""Serialisation d'une extension : origin_ip et la zone sont
des foreign_key donc evalués en get_..."""
origin = serializers.SerializerMethodField('get_origin_ip') origin = serializers.SerializerMethodField('get_origin_ip')
zone_entry = serializers.SerializerMethodField('get_zone_name') zone_entry = serializers.SerializerMethodField('get_zone_name')
soa = serializers.SerializerMethodField('get_soa_data')
class Meta: class Meta:
model = Extension model = Extension
fields = ('name', 'origin', 'zone_entry') fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa')
def get_origin_ip(self, obj): def get_origin_ip(self, obj):
return obj.origin.ipv4 return obj.origin.ipv4
@ -100,7 +170,13 @@ class ExtensionSerializer(serializers.ModelSerializer):
def get_zone_name(self, obj): def get_zone_name(self, obj):
return str(obj.dns_entry) return str(obj.dns_entry)
def get_soa_data(self, obj):
return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param }
class MxSerializer(serializers.ModelSerializer): class MxSerializer(serializers.ModelSerializer):
"""Serialisation d'un MX, evaluation du nom, de la zone
et du serveur cible, etant des foreign_key"""
name = serializers.SerializerMethodField('get_entry_name') name = serializers.SerializerMethodField('get_entry_name')
zone = serializers.SerializerMethodField('get_zone_name') zone = serializers.SerializerMethodField('get_zone_name')
mx_entry = serializers.SerializerMethodField('get_mx_name') mx_entry = serializers.SerializerMethodField('get_mx_name')
@ -118,7 +194,10 @@ class MxSerializer(serializers.ModelSerializer):
def get_mx_name(self, obj): def get_mx_name(self, obj):
return str(obj.dns_entry) return str(obj.dns_entry)
class TextSerializer(serializers.ModelSerializer): class TextSerializer(serializers.ModelSerializer):
"""Serialisation d'un txt : zone cible et l'entrée txt
sont evaluées à part"""
zone = serializers.SerializerMethodField('get_zone_name') zone = serializers.SerializerMethodField('get_zone_name')
text_entry = serializers.SerializerMethodField('get_text_name') text_entry = serializers.SerializerMethodField('get_text_name')
@ -132,7 +211,10 @@ class TextSerializer(serializers.ModelSerializer):
def get_text_name(self, obj): def get_text_name(self, obj):
return str(obj.dns_entry) return str(obj.dns_entry)
class NsSerializer(serializers.ModelSerializer): class NsSerializer(serializers.ModelSerializer):
"""Serialisation d'un NS : la zone, l'entrée ns complète et le serveur
ns sont évalués à part"""
zone = serializers.SerializerMethodField('get_zone_name') zone = serializers.SerializerMethodField('get_zone_name')
ns = serializers.SerializerMethodField('get_domain_name') ns = serializers.SerializerMethodField('get_domain_name')
ns_entry = serializers.SerializerMethodField('get_text_name') ns_entry = serializers.SerializerMethodField('get_text_name')
@ -150,7 +232,10 @@ class NsSerializer(serializers.ModelSerializer):
def get_text_name(self, obj): def get_text_name(self, obj):
return str(obj.dns_entry) return str(obj.dns_entry)
class DomainSerializer(serializers.ModelSerializer): class DomainSerializer(serializers.ModelSerializer):
"""Serialisation d'un domain, extension, cname sont des foreign_key,
et l'entrée complète, sont évalués à part"""
extension = serializers.SerializerMethodField('get_zone_name') extension = serializers.SerializerMethodField('get_zone_name')
cname = serializers.SerializerMethodField('get_alias_name') cname = serializers.SerializerMethodField('get_alias_name')
cname_entry = serializers.SerializerMethodField('get_cname_name') cname_entry = serializers.SerializerMethodField('get_cname_name')
@ -168,7 +253,9 @@ class DomainSerializer(serializers.ModelSerializer):
def get_cname_name(self, obj): def get_cname_name(self, obj):
return str(obj.dns_entry) return str(obj.dns_entry)
class ServiceServersSerializer(serializers.ModelSerializer): class ServiceServersSerializer(serializers.ModelSerializer):
"""Evaluation d'un Service, et serialisation"""
server = serializers.SerializerMethodField('get_server_name') server = serializers.SerializerMethodField('get_server_name')
service = serializers.SerializerMethodField('get_service_name') service = serializers.SerializerMethodField('get_service_name')
need_regen = serializers.SerializerMethodField('get_regen_status') need_regen = serializers.SerializerMethodField('get_regen_status')
@ -185,3 +272,31 @@ class ServiceServersSerializer(serializers.ModelSerializer):
def get_regen_status(self, obj): def get_regen_status(self, obj):
return obj.need_regen() return obj.need_regen()
class OuverturePortsSerializer(serializers.Serializer):
"""Serialisation de l'ouverture des ports"""
ipv4 = serializers.SerializerMethodField()
ipv6 = serializers.SerializerMethodField()
def get_ipv4():
return {i.ipv4.ipv4:
{
"tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()],
"tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()],
"udp_in":[j.udp_ports_in() for j in i.port_lists.all()],
"udp_out":[j.udp_ports_out() for j in i.port_lists.all()],
}
for i in Interface.objects.all() if i.ipv4
}
def get_ipv6():
return {i.ipv6:
{
"tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()],
"tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()],
"udp_in":[j.udp_ports_in() for j in i.port_lists.all()],
"udp_out":[j.udp_ports_out() for j in i.port_lists.all()],
}
for i in Interface.objects.all() if i.ipv6
}

View file

@ -26,8 +26,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<thead> <thead>
<tr> <tr>
<th>Extension</th> <th>Extension</th>
<th>Autorisation infra pour utiliser l'extension</th> <th>Droit infra pour utiliser ?</th>
<th>Enregistrement SOA</th>
<th>Enregistrement A origin</th> <th>Enregistrement A origin</th>
{% if ipv6_enabled %}
<th>Enregistrement AAAA origin</th>
{% endif %}
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -35,7 +39,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ extension.name }}</td> <td>{{ extension.name }}</td>
<td>{{ extension.need_infra }}</td> <td>{{ extension.need_infra }}</td>
<td>{{ extension.soa}}</td>
<td>{{ extension.origin }}</td> <td>{{ extension.origin }}</td>
{% if ipv6_enabled %}
<td>{{ extension.origin_v6 }}</td>
{% endif %}
<td class="text-right"> <td class="text-right">
{% if is_infra %} {% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}

View file

@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Fin</th> <th>Fin</th>
<th>Préfixe v6</th> <th>Préfixe v6</th>
<th>Sur vlan</th> <th>Sur vlan</th>
<th>Ouverture ports par défault</th>
<th></th> <th></th>
<th></th> <th></th>
</tr> </tr>
@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ type.domaine_ip_stop }}</td> <td>{{ type.domaine_ip_stop }}</td>
<td>{{ type.prefix_v6 }}</td> <td>{{ type.prefix_v6 }}</td>
<td>{{ type.vlan }}</td> <td>{{ type.vlan }}</td>
<td>{{ type.ouverture_ports }}</td>
<td class="text-right"> <td class="text-right">
{% if is_infra %} {% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}

View file

@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<col width="144px"> <col width="144px">
</colgroup> </colgroup>
<thead> <thead>
<th>Nom DNS</th> <th>{% include "buttons/sort.html" with prefix='machine' col='name' text='Nom DNS' %}</th>
<th>Type</th> <th>Type</th>
<th>MAC</th> <th>MAC</th>
<th>IP</th> <th>IP</th>
@ -44,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% for machine in machines_list %} {% for machine in machines_list %}
<tr class="info"> <tr class="info">
<td colspan="4"> <td colspan="4">
<b>{{ machine.name }}</b> <i class="glyphicon glyphicon-chevron-right"></i> <b>{{ machine.name|default:'<i>Pas de nom</i>' }}</b> <i class="glyphicon glyphicon-chevron-right"></i>
<a href="{% url 'users:profil' userid=machine.user.id %}" title="Voir le profil"> <a href="{% url 'users:profil' userid=machine.user.id %}" title="Voir le profil">
<i class="glyphicon glyphicon-user"></i> {{ machine.user }} <i class="glyphicon glyphicon-user"></i> {{ machine.user }}
</a> </a>

View file

@ -0,0 +1,56 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
<table class="table table-striped">
<thead>
<tr>
<th>Nom</th>
<th>Mail</th>
<th>Refresh</th>
<th>Retry</th>
<th>Expire</th>
<th>TTL</th>
<th></th>
<th></th>
</tr>
</thead>
{% for soa in soa_list %}
<tr>
<td>{{ soa.name }}</td>
<td>{{ soa.mail }}</td>
<td>{{ soa.refresh }}</td>
<td>{{ soa.retry }}</td>
<td>{{ soa.expire }}</td>
<td>{{ soa.ttl }}</td>
<td class="text-right">
{% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
{% endif %}
{% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %}
</td>
</tr>
{% endfor %}
</table>

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

@ -35,6 +35,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
{% include "machines/aff_extension.html" with extension_list=extension_list %} {% include "machines/aff_extension.html" with extension_list=extension_list %}
<h2>Liste des enregistrements SOA</h2>
{% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-soa' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement SOA</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-soa' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement SOA</a>
{% endif %}
{% include "machines/aff_soa.html" with soa_list=soa_list %}
<h2>Liste des enregistrements MX</h2> <h2>Liste des enregistrements MX</h2>
{% if is_infra %} {% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-mx' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement MX</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-mx' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement MX</a>
@ -47,12 +53,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

@ -31,8 +31,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h2>Liste des nas</h2> <h2>Liste des nas</h2>
<h5>La correpondance nas-machinetype relie le type de nas à un type de machine. <h5>La correpondance nas-machinetype relie le type de nas à un type de machine.
Elle est utile pour l'autoenregistrement des macs par radius, et permet de choisir le type de machine à affecter aux machines en fonction du type de nas</h5> Elle est utile pour l'autoenregistrement des macs par radius, et permet de choisir le type de machine à affecter aux machines en fonction du type de nas</h5>
{% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-nas' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un type de nas</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-nas' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un type de nas</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-nas' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un ou plusieurs types nas</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-nas' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un ou plusieurs types nas</a>
{% endif %}
{% include "machines/aff_nas.html" with nas_list=nas_list %} {% include "machines/aff_nas.html" with nas_list=nas_list %}
<br /> <br />
<br /> <br />

View file

@ -29,8 +29,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h2>Liste des vlans</h2> <h2>Liste des vlans</h2>
{% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-vlan' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un vlan</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-vlan' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un vlan</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-vlan' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un ou plusieurs vlan</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-vlan' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un ou plusieurs vlan</a>
{% endif %}
{% include "machines/aff_vlan.html" with vlan_list=vlan_list %} {% include "machines/aff_vlan.html" with vlan_list=vlan_list %}
<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,60 @@ 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 soaform %}
<h3>Enregistrement SOA</h3>
{% bootstrap_form soaform %}
{% 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

@ -58,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if is_cableur %} {% if is_cableur %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-portlist" %}"> <a class="list-group-item list-group-item-info" href="{% url "machines:index-portlist" %}">
<i class="glyphicon glyphicon-list"></i> <i class="glyphicon glyphicon-list"></i>
Configuration de ports Ouverture de ports
</a> </a>
{%endif%} {%endif%}
{% endblock %} {% endblock %}

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

@ -44,12 +44,15 @@ urlpatterns = [
url(r'^add_extension/$', views.add_extension, name='add-extension'), url(r'^add_extension/$', views.add_extension, name='add-extension'),
url(r'^edit_extension/(?P<extensionid>[0-9]+)$', views.edit_extension, name='edit-extension'), url(r'^edit_extension/(?P<extensionid>[0-9]+)$', views.edit_extension, name='edit-extension'),
url(r'^del_extension/$', views.del_extension, name='del-extension'), url(r'^del_extension/$', views.del_extension, name='del-extension'),
url(r'^add_soa/$', views.add_soa, name='add-soa'),
url(r'^edit_soa/(?P<soaid>[0-9]+)$', views.edit_soa, name='edit-soa'),
url(r'^del_soa/$', views.del_soa, name='del-soa'),
url(r'^add_mx/$', views.add_mx, name='add-mx'), url(r'^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'),
@ -74,9 +77,10 @@ urlpatterns = [
url(r'^history/(?P<object>interface)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>interface)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>machinetype)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>machinetype)/(?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>extension)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>soa)/(?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'),
@ -93,6 +97,7 @@ urlpatterns = [
url(r'^rest/text/$', views.text, name='text'), url(r'^rest/text/$', views.text, name='text'),
url(r'^rest/zones/$', views.zones, name='zones'), url(r'^rest/zones/$', views.zones, name='zones'),
url(r'^rest/service_servers/$', views.service_servers, name='service-servers'), url(r'^rest/service_servers/$', views.service_servers, name='service-servers'),
url(r'^rest/ouverture_ports/$', views.ouverture_ports, name='ouverture-ports'),
url(r'index_portlist/$', views.index_portlist, name='index-portlist'), url(r'index_portlist/$', views.index_portlist, name='index-portlist'),
url(r'^edit_portlist/(?P<pk>[0-9]+)$', views.edit_portlist, name='edit-portlist'), url(r'^edit_portlist/(?P<pk>[0-9]+)$', views.edit_portlist, name='edit-portlist'),
url(r'^del_portlist/(?P<pk>[0-9]+)$', views.del_portlist, name='del-portlist'), url(r'^del_portlist/(?P<pk>[0-9]+)$', views.del_portlist, name='del-portlist'),

View file

@ -43,56 +43,101 @@ from django.contrib.auth import authenticate, login
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer from machines.serializers import ( FullInterfaceSerializer,
InterfaceSerializer,
TypeSerializer,
DomainSerializer,
TextSerializer,
MxSerializer,
ExtensionSerializer,
ServiceServersSerializer,
NsSerializer,
OuverturePortsSerializer
)
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
import re import re
from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm from .forms import (
from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm NewMachineForm,
EditMachineForm,
EditInterfaceForm,
AddInterfaceForm,
MachineTypeForm,
DelMachineTypeForm,
ExtensionForm,
DelExtensionForm,
BaseEditInterfaceForm,
BaseEditMachineForm
)
from .forms import (
EditIpTypeForm,
IpTypeForm,
DelIpTypeForm,
DomainForm,
AliasForm,
DelAliasForm,
SOAForm,
DelSOAForm,
NsForm,
DelNsForm,
TxtForm,
DelTxtForm,
MxForm,
DelMxForm,
VlanForm,
DelVlanForm,
ServiceForm,
DelServiceForm,
NasForm,
DelNasForm
)
from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm
from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort from .models import (
IpType,
Machine,
Interface,
IpList,
MachineType,
Extension,
SOA,
Mx,
Ns,
Domain,
Service,
Service_link,
Vlan,
Nas,
Text,
OuverturePortList,
OuverturePort
)
from users.models import User from users.models import 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 from re2o.utils import (
all_active_assigned_interfaces,
def all_active_interfaces(): all_has_access,
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """ filter_active_interfaces,
return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)).select_related('domain').select_related('machine').select_related('type').select_related('ipv4').select_related('domain__extension').select_related('ipv4__ip_type').distinct() SortTable
)
def all_active_assigned_interfaces(): from re2o.views import form
""" 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 = []
choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},' choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},'
mtype_id = -1 mtype_id = -1
for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id')).order_by('mtype_id', 'id') : for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id'))\
.order_by('mtype_id', 'id') :
if mtype_id != ip.mtype_id : if mtype_id != ip.mtype_id :
mtype_id = ip.mtype_id mtype_id = ip.mtype_id
used_mtype_id.append(mtype_id) used_mtype_id.append(mtype_id)
@ -112,7 +157,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( {{'
@ -126,7 +171,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) {{'
@ -142,25 +187,27 @@ 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_gen_select = { 'ipv4': False }
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,
'gen_select': i_gen_select
} }
return i_bft_param return i_mbf_param
@login_required @login_required
def new_machine(request, userid): def new_machine(request, userid):
""" Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain """ Fonction de creation d'une machine. Cree l'objet machine,
à partir de model forms. le sous objet interface et l'objet domain à partir de model forms.
Trop complexe, devrait être simplifié""" Trop complexe, devrait être simplifié"""
try: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
@ -171,15 +218,16 @@ def new_machine(request, userid):
max_lambdauser_interfaces = options.max_lambdauser_interfaces max_lambdauser_interfaces = options.max_lambdauser_interfaces
if not request.user.has_perms(('cableur',)): if not request.user.has_perms(('cableur',)):
if user != request.user: if user != request.user:
messages.error(request, "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit") messages.error(
request,
"Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit")
return redirect("/users/profil/" + str(request.user.id)) return redirect("/users/profil/" + str(request.user.id))
if user.user_interfaces().count() >= max_lambdauser_interfaces: if user.user_interfaces().count() >= max_lambdauser_interfaces:
messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces) messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces)
return redirect("/users/profil/" + str(request.user.id)) return redirect("/users/profil/" + str(request.user.id))
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(request.POST or None, infra=request.user.has_perms(('infra',)))
nb_machine = Interface.objects.filter(machine__user=userid).count() domain = DomainForm(request.POST or None, user=user)
domain = DomainForm(request.POST or None, user=user, nb_machine=nb_machine)
if machine.is_valid() and interface.is_valid(): if machine.is_valid() and interface.is_valid():
new_machine = machine.save(commit=False) new_machine = machine.save(commit=False)
new_machine.user = user new_machine.user = user
@ -203,8 +251,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):
@ -243,8 +291,8 @@ def edit_interface(request, interfaceid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) 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):
@ -302,8 +350,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):
@ -340,7 +388,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')
@ -359,7 +407,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')
@ -377,7 +425,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')
@ -390,7 +438,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')
@ -408,7 +456,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')
@ -425,7 +473,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')
@ -438,7 +486,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')
@ -456,7 +504,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')
@ -473,7 +521,55 @@ 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
@permission_required('infra')
def add_soa(request):
soa = SOAForm(request.POST or None)
if soa.is_valid():
with transaction.atomic(), reversion.create_revision():
soa.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement SOA a été ajouté")
return redirect("/machines/index_extension")
return form({'soaform': soa}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
def edit_soa(request, soaid):
try:
soa_instance = SOA.objects.get(pk=soaid)
except SOA.DoesNotExist:
messages.error(request, u"Entrée inexistante" )
return redirect("/machines/index_extension/")
soa = SOAForm(request.POST or None, instance=soa_instance)
if soa.is_valid():
with transaction.atomic(), reversion.create_revision():
soa.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in soa.changed_data))
messages.success(request, "SOA modifié")
return redirect("/machines/index_extension/")
return form({'soaform': soa}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
def del_soa(request):
soa = DelSOAForm(request.POST or None)
if soa.is_valid():
soa_dels = soa.cleaned_data['soa']
for soa_del in soa_dels:
try:
with transaction.atomic(), reversion.create_revision():
soa_del.delete()
reversion.set_user(request.user)
messages.success(request, "Le SOA a été supprimée")
except ProtectedError:
messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del)
return redirect("/machines/index_extension")
return form({'soaform': soa}, 'machines/machine.html', request)
@login_required @login_required
@permission_required('infra') @permission_required('infra')
@ -486,7 +582,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')
@ -504,7 +600,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')
@ -521,7 +617,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')
@ -534,7 +630,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')
@ -552,7 +648,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')
@ -569,55 +665,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):
@ -645,7 +741,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):
@ -665,7 +761,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):
@ -689,7 +785,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
@ -703,7 +799,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')
@ -721,7 +817,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')
@ -738,7 +834,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')
@ -751,7 +847,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')
@ -769,7 +865,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')
@ -786,7 +882,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')
@ -799,7 +895,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')
@ -817,7 +913,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')
@ -834,14 +930,20 @@ 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')
def index(request): def index(request):
options, created = GeneralOption.objects.get_or_create() options, created = GeneralOption.objects.get_or_create()
pagination_large_number = options.pagination_large_number pagination_large_number = options.pagination_large_number
machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension').order_by('pk') machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension')
machines_list = SortTable.sort(
machines_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.MACHINES_INDEX
)
paginator = Paginator(machines_list, pagination_large_number) paginator = Paginator(machines_list, pagination_large_number)
page = request.GET.get('page') page = request.GET.get('page')
try: try:
@ -857,13 +959,13 @@ def index(request):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_iptype(request): def index_iptype(request):
iptype_list = IpType.objects.select_related('extension').order_by('type') iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type')
return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list}) return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_vlan(request): def index_vlan(request):
vlan_list = Vlan.objects.order_by('vlan_id') vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id')
return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list}) return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list})
@login_required @login_required
@ -875,17 +977,18 @@ def index_machinetype(request):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_nas(request): def index_nas(request):
nas_list = Nas.objects.select_related('machine_type').order_by('name') nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name')
return render(request, 'machines/index_nas.html', {'nas_list':nas_list}) return render(request, 'machines/index_nas.html', {'nas_list':nas_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_extension(request): def index_extension(request):
extension_list = Extension.objects.select_related('origin').order_by('name') extension_list = Extension.objects.select_related('origin').select_related('soa').order_by('name')
soa_list = SOA.objects.order_by('name')
mx_list = Mx.objects.order_by('zone').select_related('zone').select_related('name__extension') mx_list = Mx.objects.order_by('zone').select_related('zone').select_related('name__extension')
ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension') ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension')
text_list = Text.objects.all().select_related('zone') text_list = Text.objects.all().select_related('zone')
return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list}) return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'soa_list': soa_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list})
@login_required @login_required
def index_alias(request, interfaceid): def index_alias(request, interfaceid):
@ -903,8 +1006,8 @@ def index_alias(request, interfaceid):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_service(request): def index_service(request):
service_list = Service.objects.all() service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all()
servers_list = Service_link.objects.all() servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all()
return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list}) return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list})
@login_required @login_required
@ -954,17 +1057,23 @@ def history(request, object, id):
except Extension.DoesNotExist: except Extension.DoesNotExist:
messages.error(request, "Extension inexistante") messages.error(request, "Extension inexistante")
return redirect("/machines/") return redirect("/machines/")
elif object == 'soa' and request.user.has_perms(('cableur',)):
try:
object_instance = SOA.objects.get(pk=id)
except SOA.DoesNotExist:
messages.error(request, "SOA inexistant")
return redirect("/machines/")
elif object == 'mx' and request.user.has_perms(('cableur',)): elif object == 'mx' and request.user.has_perms(('cableur',)):
try: try:
object_instance = Mx.objects.get(pk=id) object_instance = Mx.objects.get(pk=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:
@ -1012,7 +1121,9 @@ def history(request, object, id):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_portlist(request): def index_portlist(request):
port_list = OuverturePortList.objects.all().order_by('name') port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set')\
.prefetch_related('interface_set__domain__extension')\
.prefetch_related('interface_set__machine__user').order_by('name')
return render(request, "machines/index_portlist.html", {'port_list':port_list}) return render(request, "machines/index_portlist.html", {'port_list':port_list})
@login_required @login_required
@ -1203,6 +1314,34 @@ def service_servers(request):
@csrf_exempt @csrf_exempt
@login_required @login_required
@permission_required('serveur') @permission_required('serveur')
def ouverture_ports(request):
r = {'ipv4':{}, 'ipv6':{}}
for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'):
pl = {
"tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))),
"tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))),
"udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))),
"udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))),
}
for i in filter_active_interfaces(o.interface_set):
if i.may_have_port_open():
d = r['ipv4'].get(i.ipv4.ipv4, {})
d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"])
d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"])
d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"])
d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"])
r['ipv4'][i.ipv4.ipv4] = d
if i.ipv6_object:
d = r['ipv6'].get(i.ipv6, {})
d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"])
d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"])
d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"])
d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"])
r['ipv6'][i.ipv6] = d
return JSONResponse(r)
@csrf_exempt
@login_required
@permission_required('serveur')
def regen_achieved(request): def regen_achieved(request):
obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server']))) obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server'])))
if obj: if obj:

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

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-15 15:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0020_optionalmachine_ipv6'),
]
operations = [
migrations.AlterField(
model_name='optionaltopologie',
name='radius_general_policy',
field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-15 15:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0021_auto_20171015_1741'),
]
operations = [
migrations.AlterField(
model_name='optionaltopologie',
name='radius_general_policy',
field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-15 18:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0022_auto_20171015_1758'),
]
operations = [
migrations.AlterField(
model_name='optionaltopologie',
name='radius_general_policy',
field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32),
),
]

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_name>service)/(?P<object_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,11 +116,17 @@ 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:
@ -113,14 +137,24 @@ def edit_services(request, servicesid):
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()

View file

@ -0,0 +1,809 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" Templatetag used to render massive django form selects into bootstrap
forms that can still be manipulating even if there is multiple tens of
thousands of elements in the select. It's made possible using JS libaries
Twitter Typeahead and Splitree's Tokenfield.
See docstring of massive_bootstrap_form for a detailed explaantion on how
to use this templatetag.
"""
from django import template
from django.utils.safestring import mark_safe
from django.forms import TextInput
from django.forms.widgets import Select
from bootstrap3.utils import render_tag
from bootstrap3.forms import render_field
register = template.Library()
@register.simple_tag
def massive_bootstrap_form(form, mbf_fields, *args, **kwargs):
"""
Render a form where some specific fields are rendered using Twitter
Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the
speed and UX when dealing with very large datasets (select with 50k+ elts
for instance).
When the fields specified should normally be rendered as a select with
single selectable option, Twitter Typeahead is used for a better display
and the matching query engine. When dealing with multiple selectable
options, sliptree's Bootstrap Tokenfield in addition with Typeahead.
For convenience, it accepts the same parameters as a standard bootstrap
can accept.
**Tag name**::
massive_bootstrap_form
**Parameters**:
form (required)
The form that is to be rendered
mbf_fields (optional)
A list of field names (comma separated) that should be rendered
with Typeahead/Tokenfield instead of the default bootstrap
renderer.
If not specified, all fields will be rendered as a normal bootstrap
field.
mbf_param (optional)
A dict of parameters for the massive_bootstrap_form tag. The
possible parameters are the following.
choices (optional)
A dict of strings representing the choices in JS. The keys of
the dict are the names of the concerned fields. The choices
must be an array of objects. Each of those objects must at
least have the fields 'key' (value to send) and 'value' (value
to display). Other fields can be added as desired.
For a more complex structure you should also consider
reimplementing the engine and the match_func.
If not specified, the key is the id of the object and the value
is its string representation as in a normal bootstrap form.
Example :
'choices' : {
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]',
'field_B':...,
...
}
engine (optional)
A dict of strings representating the engine used for matching
queries and possible values with typeahead. The keys of the
dict are the names of the concerned fields. The string is valid
JS code.
If not specified, BloodHound with relevant basic properties is
used.
Example :
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...}
match_func (optional)
A dict of strings representing a valid JS function used in the
dataset to overload the matching engine. The keys of the dict
are the names of the concerned fields. This function is used
the source of the dataset. This function receives 2 parameters,
the query and the synchronize function as specified in
typeahead.js documentation. If needed, the local variables
'choices_<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 :
'update_on' : {
'field_A' : [ 'id0', 'id1', ... ] ,
'field_B' : ... ,
...
}
gen_select (optional)
A dict of boolean telling if the form should either generate
the normal select (set to true) and then use it to generate
the possible choices and then remove it or either (set to
false) generate the choices variable in this tag and do not
send any select.
Sending the select before can be usefull to permit the use
without any JS enabled but it will execute more code locally
for the client so the loading might be slower.
If not specified, this variable is set to true for each field
Example :
'gen_select' : {
'field_A': True ,
'field_B': ... ,
...
}
See boostrap_form_ for other arguments
**Usage**::
{% massive_bootstrap_form
form
[ '<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>'
[, ... ] ] ]
} ],
[, 'gen_select': {
[ '<field1>': '<gen_select1>'
[, '<field2>': '<gen_select2>'
[, ... ] ] ]
} ]
} ]
[ <standard boostrap_form parameters> ]
%}
**Example**:
{% massive_bootstrap_form form 'ipv4' choices='[...]' %}
"""
mbf_form = MBFForm(form, mbf_fields.split(','), *args, **kwargs)
return mbf_form.render()
class MBFForm():
""" An object to hold all the information and useful methods needed to
create and render a massive django form into an actual HTML and JS
code able to handle it correctly.
Every field that is not listed is rendered as a normal bootstrap_field.
"""
def __init__(self, form, mbf_fields, *args, **kwargs):
# The django form object
self.form = form
# The fields on which to use JS
self.fields = mbf_fields
# Other bootstrap_form arguments to render the fields
self.args = args
self.kwargs = kwargs
# Fields to exclude form the form rendering
self.exclude = self.kwargs.get('exclude', '').split(',')
# All the mbf parameters specified byt the user
param = kwargs.pop('mbf_param', {})
self.choices = param.get('choices', {})
self.engine = param.get('engine', {})
self.match_func = param.get('match_func', {})
self.update_on = param.get('update_on', {})
self.gen_select = param.get('gen_select', {})
self.hidden_fields = [h.name for h in self.form.hidden_fields()]
# HTML code to insert inside a template
self.html = ""
def render(self):
""" HTML code for the fully rendered form with all the necessary form
"""
for name, field in self.form.fields.items():
if not name in self.exclude:
if name in self.fields and not name in self.hidden_fields:
mbf_field = MBFField(
name,
field,
field.get_bound_field(self.form, name),
self.choices.get(name, None),
self.engine.get(name, None),
self.match_func.get(name, None),
self.update_on.get(name, None),
self.gen_select.get(name, True),
*self.args,
**self.kwargs
)
self.html += mbf_field.render()
else:
self.html += render_field(
field.get_bound_field(self.form, name),
*self.args,
**self.kwargs
)
return mark_safe(self.html)
class MBFField():
""" An object to hold all the information and useful methods needed to
create and render a massive django form field into an actual HTML and JS
code able to handle it correctly.
Twitter Typeahead is used for the display and the matching of queries and
in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage
multiple values.
A div with only non visible elements is created after the div containing
the displayed input. It's used to store the actual data that will be sent
to the server """
def __init__(self, name_, field_, bound_, choices_, engine_, match_func_,
update_on_, gen_select_, *args_, **kwargs_):
# Verify this field is a Select (or MultipleSelect) (only supported)
if not isinstance(field_.widget, Select):
raise ValueError(
('Field named {f_name} is not a Select and'
'can\'t be rendered with massive_bootstrap_form.'
).format(
f_name=name_
)
)
# Name of the field
self.name = name_
# Django field object
self.field = field_
# Bound Django field associated with field
self.bound = bound_
# Id for the main visible input
self.input_id = self.bound.auto_id
# Id for a hidden input used to store the value
self.hidden_id = self.input_id + '_hidden'
# Id for another div containing hidden inputs and script
self.div2_id = self.input_id + '_div'
# Should the standard select should be generated
self.gen_select = gen_select_
# Is it select with multiple values possible (use of tokenfield)
self.multiple = self.field.widget.allow_multiple_selected
# JS for the choices variable (user specified or default)
self.choices = choices_ or self.default_choices()
# JS for the engine variable (typeahead) (user specified or default)
self.engine = engine_ or self.default_engine()
# JS for the matching function (typeahead) (user specified or default)
self.match_func = match_func_ or self.default_match_func()
# JS for the datasets variable (typeahead) (user specified or default)
self.datasets = self.default_datasets()
# Ids of other fields to bind a reset/reload with when changed
self.update_on = update_on_ or []
# Whole HTML code to insert in the template
self.html = ""
# JS code in the script tag
self.js_script = ""
# Input tag to display instead of select
self.replace_input = None
# Other bootstrap_form arguments to render the fields
self.args = args_
self.kwargs = kwargs_
def default_choices(self):
""" JS code of the variable choices_<fieldname> """
if self.gen_select:
return (
'function plop(o) {{'
'var c = [];'
'for( let i=0 ; i<o.length ; i++) {{'
' c.push( {{ key: o[i].value, value: o[i].text }} );'
'}}'
'return c;'
'}} ($("#{select_id}")[0].options)'
).format(
select_id=self.input_id
)
else:
return '[{objects}]'.format(
objects=','.join(
['{{key:{k},value:"{v}"}}'.format(
k=choice[0] if choice[0] != '' else '""',
v=choice[1]
) for choice in self.field.choices]
)
)
def default_engine(self):
""" Default JS code of the variable engine_<field_name> """
return (
'new Bloodhound({{'
' datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
' queryTokenizer: Bloodhound.tokenizers.whitespace,'
' local: choices_{name},'
' identify: function(obj) {{ return obj.key; }}'
'}})'
).format(
name=self.name
)
def default_datasets(self):
""" Default JS script of the datasets to use with typeahead """
return (
'{{'
' hint: true,'
' highlight: true,'
' minLength: 0'
'}},'
'{{'
' display: "value",'
' name: "{name}",'
' source: {match_func}'
'}}'
).format(
name=self.name,
match_func=self.match_func
)
def default_match_func(self):
""" Default JS code of the matching function to use with typeahed """
return (
'function ( q, sync ) {{'
' if ( q === "" ) {{'
' var first = choices_{name}.slice( 0, 5 ).map('
' function ( obj ) {{ return obj.key; }}'
' );'
' sync( engine_{name}.get( first ) );'
' }} else {{'
' engine_{name}.search( q, sync );'
' }}'
'}}'
).format(
name=self.name
)
def render(self):
""" HTML code for the fully rendered field """
self.gen_displayed_div()
self.gen_hidden_div()
return mark_safe(self.html)
def gen_displayed_div(self):
""" Generate HTML code for the div that contains displayed tags """
if self.gen_select:
self.html += render_field(
self.bound,
*self.args,
**self.kwargs
)
self.field.widget = TextInput(
attrs={
'name': 'mbf_'+self.name,
'placeholder': self.field.empty_label
}
)
self.replace_input = render_field(
self.bound,
*self.args,
**self.kwargs
)
if not self.gen_select:
self.html += self.replace_input
def gen_hidden_div(self):
""" Generate HTML code for the div that contains hidden tags """
self.gen_full_js()
content = self.js_script
if not self.multiple and not self.gen_select:
content += self.hidden_input()
self.html += render_tag(
'div',
content=content,
attrs={'id': self.div2_id}
)
def hidden_input(self):
""" HTML for the hidden input element """
return render_tag(
'input',
attrs={
'id': self.hidden_id,
'name': self.bound.html_name,
'type': 'hidden',
'value': self.bound.value() or ""
}
)
def gen_full_js(self):
""" Generate the full script tag containing the JS code """
self.create_js()
self.fill_js()
self.get_script()
def create_js(self):
""" Generate a template for the whole script to use depending on
gen_select and multiple """
if self.gen_select:
if self.multiple:
self.js_script = (
'$( "#{input_id}" ).ready( function() {{'
' var choices_{f_name} = {choices};'
' {del_select}'
' var engine_{f_name};'
' var setup_{f_name} = function() {{'
' engine_{f_name} = {engine};'
' $( "#{input_id}" ).tokenfield( "destroy" );'
' $( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
' }};'
' $( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );'
' $( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
' $( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
' {tok_updates}'
' setup_{f_name}();'
' {tok_init_input}'
'}} );'
)
else:
self.js_script = (
'$( "#{input_id}" ).ready( function() {{'
' var choices_{f_name} = {choices};'
' {del_select}'
' {gen_hidden}'
' var engine_{f_name};'
' var setup_{f_name} = function() {{'
' engine_{f_name} = {engine};'
' $( "#{input_id}" ).typeahead( "destroy" );'
' $( "#{input_id}" ).typeahead( {datasets} );'
' }};'
' $( "#{input_id}" ).bind( "typeahead:select", {typ_select} );'
' $( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
' {typ_updates}'
' setup_{f_name}();'
' {typ_init_input}'
'}} );'
)
else:
if self.multiple:
self.js_script = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
' engine_{f_name} = {engine};'
' $( "#{input_id}" ).tokenfield( "destroy" );'
' $( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
'}};'
'$( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );'
'$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
'$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
'{tok_updates}'
'$( "#{input_id}" ).ready( function() {{'
' setup_{f_name}();'
' {tok_init_input}'
'}} );'
)
else:
self.js_script = (
'var choices_{f_name} ={choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
' engine_{f_name} = {engine};'
' $( "#{input_id}" ).typeahead( "destroy" );'
' $( "#{input_id}" ).typeahead( {datasets} );'
'}};'
'$( "#{input_id}" ).bind( "typeahead:select", {typ_select} );'
'$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
'{typ_updates}'
'$( "#{input_id}" ).ready( function() {{'
' setup_{f_name}();'
' {typ_init_input}'
'}} );'
)
def fill_js(self):
""" Fill the template with the correct values """
self.js_script = self.js_script.format(
f_name=self.name,
choices=self.choices,
del_select=self.del_select(),
gen_hidden=self.gen_hidden(),
engine=self.engine,
input_id=self.input_id,
datasets=self.datasets,
typ_select=self.typeahead_select(),
typ_change=self.typeahead_change(),
tok_create=self.tokenfield_create(),
tok_edit=self.tokenfield_edit(),
tok_remove=self.tokenfield_remove(),
typ_updates=self.typeahead_updates(),
tok_updates=self.tokenfield_updates(),
tok_init_input=self.tokenfield_init_input(),
typ_init_input=self.typeahead_init_input()
)
def get_script(self):
""" Insert the JS code inside a script tag """
self.js_script = render_tag('script', content=mark_safe(self.js_script))
def del_select(self):
""" JS code to delete the select if it has been generated and replace
it with an input. """
return (
'var p = $("#{select_id}").parent()[0];'
'var new_input = `{replace_input}`;'
'p.innerHTML = new_input;'
).format(
select_id=self.input_id,
replace_input=self.replace_input
)
def gen_hidden(self):
""" JS code to add a hidden tag to store the value. """
return (
'var d = $("#{div2_id}")[0];'
'var i = document.createElement("input");'
'i.id = "{hidden_id}";'
'i.name = "{html_name}";'
'i.value = "";'
'i.type = "hidden";'
'd.appendChild(i);'
).format(
div2_id=self.div2_id,
hidden_id=self.hidden_id,
html_name=self.bound.html_name
)
def typeahead_init_input(self):
""" JS code to init the fields values """
init_key = self.bound.value() or '""'
return (
'$( "#{input_id}" ).typeahead("val", {init_val});'
'$( "#{hidden_id}" ).val( {init_key} );'
).format(
input_id=self.input_id,
init_val='""' if init_key == '""' else
'engine_{name}.get( {init_key} )[0].value'.format(
name=self.name,
init_key=init_key
),
init_key=init_key,
hidden_id=self.hidden_id
)
def typeahead_reset_input(self):
""" JS code to reset the fields values """
return (
'$( "#{input_id}" ).typeahead("val", "");'
'$( "#{hidden_id}" ).val( "" );'
).format(
input_id=self.input_id,
hidden_id=self.hidden_id
)
def typeahead_select(self):
""" JS code to create the function triggered when an item is selected
through typeahead """
return (
'function(evt, item) {{'
' $( "#{hidden_id}" ).val( item.key );'
' $( "#{hidden_id}" ).change();'
' return item;'
'}}'
).format(
hidden_id=self.hidden_id
)
def typeahead_change(self):
""" JS code of the function triggered when an item is changed (i.e.
looses focus and value has changed since the moment it gained focus )
"""
return (
'function(evt) {{'
' if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{'
' $( "#{hidden_id}" ).val( "" );'
' $( "#{hidden_id}" ).change();'
' }}'
'}}'
).format(
input_id=self.input_id,
hidden_id=self.hidden_id
)
def typeahead_updates(self):
""" JS code for binding external fields changes with a reset """
reset_input = self.typeahead_reset_input()
updates = [
(
'$( "#{u_id}" ).change( function() {{'
' setup_{name}();'
' {reset_input}'
'}} );'
).format(
u_id=u_id,
name=self.name,
reset_input=reset_input
) for u_id in self.update_on]
return ''.join(updates)
def tokenfield_init_input(self):
""" JS code to init the fields values """
init_key = self.bound.value() or '""'
return (
'$( "#{input_id}" ).tokenfield("setTokens", {init_val});'
).format(
input_id=self.input_id,
init_val='""' if init_key == '""' else (
'engine_{name}.get( {init_key} ).map('
' function(o) {{ return o.value; }}'
')').format(
name=self.name,
init_key=init_key
)
)
def tokenfield_reset_input(self):
""" JS code to reset the fields values """
return (
'$( "#{input_id}" ).tokenfield("setTokens", "");'
).format(
input_id=self.input_id
)
def tokenfield_create(self):
""" JS code triggered when a new token is created in tokenfield. """
return (
'function(evt) {{'
' var k = evt.attrs.key;'
' if (!k) {{'
' var data = evt.attrs.value;'
' var i = 0;'
' while ( i<choices_{name}.length &&'
' choices_{name}[i].value !== data ) {{'
' i++;'
' }}'
' if ( i === choices_{name}.length ) {{ return false; }}'
' k = choices_{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 = "{html_name}";'
' $( "#{div2_id}" ).append(new_input);'
'}}'
).format(
name=self.name,
hidden_id=self.hidden_id,
html_name=self.bound.html_name,
div2_id=self.div2_id
)
def tokenfield_edit(self):
""" JS code 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_{name}.length &&'
' choices_{name}[i].value !== data ) {{'
' i++;'
' }}'
' if ( i === choices_{name}.length ) {{ return true; }}'
' k = choices_{name}[i].key;'
' }}'
' var old_input = document.getElementById('
' "{hidden_id}_"+k.toString()'
' );'
' old_input.parentNode.removeChild(old_input);'
'}}'
).format(
name=self.name,
hidden_id=self.hidden_id
)
def tokenfield_remove(self):
""" JS code 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_{name}.length &&'
' choices_{name}[i].value !== data ) {{'
' i++;'
' }}'
' if ( i === choices_{name}.length ) {{ return true; }}'
' k = choices_{name}[i].key;'
' }}'
' var old_input = document.getElementById('
' "{hidden_id}_"+k.toString()'
' );'
' old_input.parentNode.removeChild(old_input);'
'}}'
).format(
name=self.name,
hidden_id=self.hidden_id
)
def tokenfield_updates(self):
""" JS code for binding external fields changes with a reset """
reset_input = self.tokenfield_reset_input()
updates = [
(
'$( "#{u_id}" ).change( function() {{'
' setup_{name}();'
' {reset_input}'
'}} );'
).format(
u_id=u_id,
name=self.name,
reset_input=reset_input
) for u_id in self.update_on]
return ''.join(updates)

View file

@ -0,0 +1,105 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Templatetag used to write a URL (specified or current one) and adding
or inserting specific parameters into the query part without deleting
the other parameters.
"""
from django import template
register = template.Library()
@register.simple_tag
def url_insert_param(url="", **kwargs):
"""
Return the URL with some specific parameters inserted into the query
part. If a URL has already some parameters, those requested will be
modified if already exisiting or will be added and the other parameters
will stay unmodified. If parameters with the same name are already in the
URL and a value is specified for this parameter, it will replace all
existing parameters.
**Tag name**::
url_insert_param
**Parameters**:
url (optional)
The URL to use as a base. The parameters will be added to this URL.
If not specified, it will only return the query part of the URL
("?a=foo&b=bar" for example).
Example : "https://example.com/bar?foo=0&thing=abc"
other arguments
Any other key-value argument will be used. The key is considered as
the name of the parameter to insert/modify and the value is the one
used.
Example : q="foo" search="bar" name="johnDoe"
will return as ?<existing_param>&q=foo&search=bar&name=johnDoe
**Usage**::
{% url_insert_param [URL] [param1=val1 [param2=val2 [...]]] %}
**Example**::
{% url_insert_param a=0 b="bar" %}
return "?a=0&b=bar"
{% url_insert_param "url.net/foo.html" a=0 b="bar" %}
return "url.net/foo.html?a=0&b=bar"
{% url_insert_param "url.net/foo.html?c=keep" a=0 b="bar" %}
return "url.net/foo.html?c=keep&a=0&b=bar"
{% url_insert_param "url.net/foo.html?a=del" a=0 b="bar" %}
return "url.net/foo.html?a=0&b=bar"
{% url_insert_param "url.net/foo.html?a=del&c=keep" a=0 b="bar" %}
return "url.net/foo.hmtl?a=0&c=keep&b=bar"
"""
# Get existing parameters in the url
params = {}
if '?' in url:
url, parameters = url.split('?', maxsplit=1)
for parameter in parameters.split('&'):
p_name, p_value = parameter.split('=', maxsplit=1)
if p_name not in params:
params[p_name] = []
params[p_name].append(p_value)
# Add the request parameters to the list of parameters
for key, value in kwargs.items():
params[key] = [value]
# Write the url
url += '?'
for param, value_list in params.items():
for value in value_list:
url += str(param) + '=' + str(value) + '&'
# Remove the last '&' (or '?' if no parameters)
return url[:-1]

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')
),
] ]

264
re2o/utils.py Normal file
View file

@ -0,0 +1,264 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# -*- coding: utf-8 -*-
# David Sinquin, Gabriel Détraz, Goulven Kermarec
"""
Regroupe les fonctions transversales utiles
Fonction :
- récupérer tous les utilisateurs actifs
- récupérer toutes les machines
- récupérer tous les bans
etc
"""
from __future__ import unicode_literals
from django.utils import timezone
from django.db.models import Q
from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine
from users.models import Adherent, User, Ban, Whitelist
from preferences.models import Service
DT_NOW = timezone.now()
def all_adherent(search_time=DT_NOW):
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est
qu'une seule requete sql
Inspecte les factures de l'user et ses cotisation, regarde si elles
sont posterieur à now (end_time)"""
return User.objects.filter(
facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter(
Q(type_cotisation='All') | Q(type_cotisation='Adhesion'),
cotisation__in=Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.all().exclude(valid=False)
)
).filter(date_end__gt=search_time)
)
)
).distinct()
def all_baned(search_time=DT_NOW):
""" Fonction renvoyant tous les users bannis """
return User.objects.filter(
ban__in=Ban.objects.filter(
date_end__gt=search_time
)
).distinct()
def all_whitelisted(search_time=DT_NOW):
""" Fonction renvoyant tous les users whitelistes """
return User.objects.filter(
whitelist__in=Whitelist.objects.filter(
date_end__gt=search_time
)
).distinct()
def all_has_access(search_time=DT_NOW):
""" Renvoie tous les users beneficiant d'une connexion
: user adherent ou whiteliste et non banni """
return User.objects.filter(
Q(state=User.STATE_ACTIVE) &
~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
(Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
Q(facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter(
Q(type_cotisation='All') | Q(type_cotisation='Connexion'),
vente__in=Vente.objects.filter(
facture__in=Facture.objects.all()
.exclude(valid=False)
)
).filter(date_end__gt=search_time)
)
)))
).distinct()
def filter_active_interfaces(interface_set):
"""Filtre les machines autorisées à sortir sur internet dans une requête"""
return interface_set.filter(
machine__in=Machine.objects.filter(
user__in=all_has_access()
).filter(active=True)
).select_related('domain').select_related('machine')\
.select_related('type').select_related('ipv4')\
.select_related('domain__extension').select_related('ipv4__ip_type')\
.distinct()
def all_active_interfaces():
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
return filter_active_interfaces(Interface.objects)
def all_active_assigned_interfaces():
""" Renvoie l'ensemble des machines qui ont une ipv4 assignées et
disposant de l'accès internet"""
return all_active_interfaces().filter(ipv4__isnull=False)
def all_active_interfaces_count():
""" Version light seulement pour compter"""
return Interface.objects.filter(
machine__in=Machine.objects.filter(
user__in=all_has_access()
).filter(active=True)
)
def all_active_assigned_interfaces_count():
""" Version light seulement pour compter"""
return all_active_interfaces_count().filter(ipv4__isnull=False)
class SortTable:
""" Class gathering uselful stuff to sort the colums of a table, according
to the column and order requested. It's used with a dict of possible
values and associated model_fields """
# All the possible possible values
# The naming convention is based on the URL or the views function
# The syntax to describe the sort to apply is a dict where the keys are
# the url value and the values are a list of model field name to use to
# order the request. They are applied in the order they are given.
# A 'default' might be provided to specify what to do if the requested col
# doesn't match any keys.
USERS_INDEX = {
'user_name': ['name'],
'user_surname': ['surname'],
'user_pseudo': ['pseudo'],
'user_room': ['room'],
'default': ['state', 'pseudo']
}
USERS_INDEX_BAN = {
'ban_user': ['user__pseudo'],
'ban_start': ['date_start'],
'ban_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_WHITE = {
'white_user': ['user__pseudo'],
'white_start': ['date_start'],
'white_end': ['date_end'],
'default': ['-date_end']
}
MACHINES_INDEX = {
'machine_name': ['name'],
'default': ['pk']
}
COTISATIONS_INDEX = {
'cotis_user': ['user__pseudo'],
'cotis_paiement': ['paiement__moyen'],
'cotis_date': ['date'],
'cotis_id': ['id'],
'default': ['-date']
}
COTISATIONS_CONTROL = {
'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'],
'control_paiement': ['paiement'],
'control_date': ['date'],
'control_valid': ['valid'],
'control_control': ['control'],
'control_id': ['id'],
'control_user-id': ['user__id'],
'default': ['-date']
}
TOPOLOGIE_INDEX = {
'switch_dns': ['switch_interface__domain__name'],
'switch_ip': ['switch_interface__ipv4__ipv4'],
'switch_loc': ['location'],
'switch_ports': ['number'],
'switch_stack': ['stack__name'],
'default': ['location', 'stack', 'stack_member_id']
}
TOPOLOGIE_INDEX_PORT = {
'port_port': ['port'],
'port_room': ['room__name'],
'port_interface': ['machine_interface__domain__name'],
'port_related': ['related__switch__name'],
'port_radius': ['radius'],
'port_vlan': ['vlan_force__name'],
'default': ['port']
}
TOPOLOGIE_INDEX_ROOM = {
'room_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_STACK = {
'stack_name': ['name'],
'stack_id': ['stack_id'],
'default': ['stack_id'],
}
TOPOLOGIE_INDEX_MODEL_SWITCH = {
'model_switch_name': ['reference'],
'model_switch__contructor' : ['constructor__name'],
'default': ['reference'],
}
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
'room_name': ['name'],
'default': ['name'],
}
LOGS_INDEX = {
'sum_date': ['revision__date_created'],
'default': ['-revision__date_created'],
}
LOGS_STATS_LOGS = {
'logs_author': ['user__name'],
'logs_date': ['date_created'],
'default': ['-date_created']
}
@staticmethod
def sort(request, col, order, values):
""" Check if the given values are possible and add .order_by() and
a .reverse() as specified according to those values """
fields = values.get(col, None)
if not fields:
fields = values.get('default', [])
request = request.order_by(*fields)
if values.get(col, None) and order == 'desc':
return request.reverse()
else:
return request
def remove_user_room(room):
""" Déménage de force l'ancien locataire de la chambre """
try:
user = Adherent.objects.get(room=room)
except Adherent.DoesNotExist:
return
user.room = None
user.save()

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")

View file

@ -21,8 +21,8 @@
# 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.
"""The field used in the admin view for the search app"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib import admin
# Register your models here. # Register your models here.

View file

@ -20,21 +20,72 @@
# 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.
"""The forms used by the search app"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db.models import Q from django import forms
from simple_search import BaseSearchForm from django.forms import Form
from users.models import User, School CHOICES_USER = (
('0', 'Actifs'),
class UserSearchForm(BaseSearchForm): ('1', 'Désactivés'),
class Meta: ('2', 'Archivés'),
base_qs = User.objects )
search_fields = ('^name', 'description', 'specifications', '=id')
CHOICES_AFF = (
# assumes a fulltext index has been defined on the fields ('0', 'Utilisateurs'),
# 'name,description,specifications,id' ('1', 'Machines'),
fulltext_indexes = ( ('2', 'Factures'),
('name', 2), # name matches are weighted higher ('3', 'Bannissements'),
('name,description,specifications,id', 1), ('4', 'Accès à titre gracieux'),
('5', 'Chambres'),
('6', 'Ports'),
('7', 'Switchs'),
)
def initial_choices(c):
"""Return the choices that should be activated by default for a
given set of choices"""
return [i[0] for i in c]
class SearchForm(Form):
"""The form for a simple search"""
q = forms.CharField(label='Search', max_length=100)
class SearchFormPlus(Form):
"""The form for an advanced search (with filters)"""
q = forms.CharField(
label='Search',
max_length=100,
required=False
)
u = forms.MultipleChoiceField(
label="Filtre utilisateurs",
required=False,
widget=forms.CheckboxSelectMultiple,
choices=CHOICES_USER,
initial=initial_choices(CHOICES_USER)
)
a = forms.MultipleChoiceField(
label="Filtre affichage",
required=False,
widget=forms.CheckboxSelectMultiple,
choices=CHOICES_AFF,
initial=initial_choices(CHOICES_AFF)
)
s = forms.DateField(
required=False,
label="Date de début",
help_text='DD/MM/YYYY',
input_formats=['%d/%m/%Y']
)
e = forms.DateField(
required=False,
help_text='DD/MM/YYYY',
input_formats=['%d/%m/%Y'],
label="Date de fin"
) )

View file

@ -1,62 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import unicode_literals
from django.db import models
from django import forms
from django.forms import Form
from django.forms import ModelForm
CHOICES = (
('0', 'Actifs'),
('1', 'Désactivés'),
('2', 'Archivés'),
)
CHOICES2 = (
(1, 'Active'),
("", 'Désactivée'),
)
CHOICES3 = (
('0', 'Utilisateurs'),
('1', 'Machines'),
('2', 'Factures'),
('3', 'Bannissements'),
('4', 'Accès à titre gracieux'),
('6', 'Switchs'),
('5', 'Ports'),
)
class SearchForm(Form):
search_field = forms.CharField(label = 'Search', max_length = 100)
class SearchFormPlus(Form):
search_field = forms.CharField(label = 'Search', max_length = 100, required=False)
filtre = forms.MultipleChoiceField(label="Filtre utilisateurs", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES)
connexion = forms.MultipleChoiceField(label="Filtre connexion", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES2)
affichage = forms.MultipleChoiceField(label="Filtre affichage", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES3)
date_deb = forms.DateField(required=False, label="Date de début", help_text='DD/MM/YYYY', input_formats=['%d/%m/%Y'])
date_fin = forms.DateField(required=False, help_text='DD/MM/YYYY', input_formats=['%d/%m/%Y'], label="Date de fin")

View file

@ -36,30 +36,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h2>Résultats dans les machines : </h2> <h2>Résultats dans les machines : </h2>
{% include "machines/aff_machines.html" with machines_list=machines_list %} {% include "machines/aff_machines.html" with machines_list=machines_list %}
{% endif %} {% endif %}
{% if facture_list %} {% if factures_list %}
<h2>Résultats dans les factures : </h2> <h2>Résultats dans les factures : </h2>
{% include "cotisations/aff_cotisations.html" with facture_list=facture_list %} {% include "cotisations/aff_cotisations.html" with facture_list=factures_list %}
{% endif %} {% endif %}
{% if white_list %} {% if whitelists_list %}
<h2>Résultats dans les accès à titre gracieux : </h2> <h2>Résultats dans les accès à titre gracieux : </h2>
{% include "users/aff_whitelists.html" with white_list=white_list %} {% include "users/aff_whitelists.html" with white_list=whitelists_list %}
{% endif %} {% endif %}
{% if ban_list %} {% if bans_list %}
<h2>Résultats dans les banissements : </h2> <h2>Résultats dans les banissements : </h2>
{% include "users/aff_bans.html" with ban_list=ban_list %} {% include "users/aff_bans.html" with ban_list=bans_list %}
{% endif %} {% endif %}
{% if switch_list %} {% if rooms_list %}
<h2>Résultats dans les switchs : </h2> <h2>Résultats dans les chambres : </h2>
{% include "topologie/aff_switch.html" with switch_list=switch_list %} {% include "topologie/aff_chambres.html" with room_list=rooms_list %}
{% endif %} {% endif %}
{% if port_list %} {% if switch_ports_list %}
<h2>Résultats dans les ports : </h2> <h2>Résultats dans les ports : </h2>
{% include "topologie/aff_port.html" with port_list=port_list %} {% include "topologie/aff_port.html" with port_list=switch_ports_list %}
{% endif %} {% endif %}
{% if not ban_list and not interfaces_list and not users_list and not facture_list and not white_list and not port_list and not switch_list%} {% if switches_list %}
<h2>Résultats dans les switchs : </h2>
{% include "topologie/aff_switch.html" with switch_list=switches_list %}
{% endif %}
{% if not users_list and not machines_list and not factures_list and not whitelists_list and not bans_list and not rooms_list and not switch_ports_list and not switches_list %}
<h3>Aucun résultat</h3> <h3>Aucun résultat</h3>
{% endif %} {% else %}
<h6>(Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)</h6> <h6>(Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)</h6>
{% endif %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -28,11 +28,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}Recherche{% endblock %} {% block title %}Recherche{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors searchform %} {% bootstrap_form_errors search_form %}
<form class="form" method="post"> <form class="form">
{% csrf_token %} {% bootstrap_field search_form.q %}
{% bootstrap_form searchform %} {% include "buttons/multiple_checkbox_alt.html" with field=search_form.u %}
{% include "buttons/multiple_checkbox_alt.html" with field=search_form.a %}
{% bootstrap_field search_form.s %}
{% bootstrap_field search_form.e %}
{% bootstrap_button "Search" button_type="submit" icon="search" %} {% bootstrap_button "Search" button_type="submit" icon="search" %}
</form> </form>
<br /> <br />

View file

@ -20,6 +20,8 @@
# 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.
"""The urls used by the search app"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
@ -28,5 +30,5 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.search, name='search'), url(r'^$', views.search, name='search'),
url(r'^avance/$', views.searchp, name='searchp'), url(r'^advanced/$', views.searchp, name='searchp'),
] ]

View file

@ -20,115 +20,326 @@
# 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.
# App de recherche pour re2o """The views for the search app, responsible for finding the matches
# Augustin lemesle, Gabriel Détraz, Goulven Kermarec Augustin lemesle, Gabriel Détraz, Goulven Kermarec, Maël Kervella
# Gplv2 Gplv2"""
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 import Context, RequestContext, loader
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Q from django.db.models import Q
from users.models import User, Ban, Whitelist from users.models import User, Ban, Whitelist
from machines.models import Machine, Interface from machines.models import Machine
from topologie.models import Port, Switch from topologie.models import Port, Switch, Room
from cotisations.models import Facture from cotisations.models import Facture
from search.models import SearchForm, SearchFormPlus
from preferences.models import GeneralOption from preferences.models import GeneralOption
from search.forms import (
SearchForm,
SearchFormPlus,
CHOICES_USER,
CHOICES_AFF,
initial_choices
)
from re2o.utils import SortTable
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
def search_result(search, type, request): def is_int(variable):
date_deb = None """ Check if the variable can be casted to an integer """
date_fin = None
states=[]
co=[]
aff=[]
if(type):
aff = search.cleaned_data['affichage']
co = search.cleaned_data['connexion']
states = search.cleaned_data['filtre']
date_deb = search.cleaned_data['date_deb']
date_fin = search.cleaned_data['date_fin']
date_query = Q()
if aff==[]:
aff = ['0','1','2','3','4','5','6']
if date_deb != None:
date_query = date_query & Q(date__gte=date_deb)
if date_fin != None:
date_query = date_query & Q(date__lte=date_fin)
search = search.cleaned_data['search_field']
query1 = Q()
for s in states:
query1 = query1 | Q(state = s)
connexion = [] try:
int(variable)
recherche = {'users_list': None, 'machines_list' : [], 'facture_list' : None, 'ban_list' : None, 'white_list': None, 'port_list': None, 'switch_list': None} except ValueError:
return False
if request.user.has_perms(('cableur',)):
query = Q(user__pseudo__icontains = search) | Q(user__name__icontains = search) | Q(user__surname__icontains = search)
else: else:
query = (Q(user__pseudo__icontains = search) | Q(user__name__icontains = search) | Q(user__surname__icontains = search)) & Q(user = request.user) return True
for i in aff: def get_results(query, request, filters={}):
if i == '0': """ Construct the correct filters to match differents fields of some models
query_user_list = Q(room__name__icontains = search) | Q(pseudo__icontains = search) | Q(name__icontains = search) | Q(surname__icontains = search) & query1 with the given query according to the given filters.
if request.user.has_perms(('cableur',)): The match field are either CharField or IntegerField that will be displayed
recherche['users_list'] = User.objects.filter(query_user_list).order_by('state', 'surname').distinct() on the results page (else, one might not see why a result has matched the
else : query). IntegerField are matched against the query only if it can be casted
recherche['users_list'] = User.objects.filter(query_user_list & Q(id=request.user.id)).order_by('state', 'surname').distinct() to an int."""
if i == '1':
query_machine_list = Q(machine__user__pseudo__icontains = search) | Q(machine__user__name__icontains = search) | Q(machine__user__surname__icontains = search) | Q(mac_address__icontains = search) | Q(ipv4__ipv4__icontains = search) | Q(domain__name__icontains = search) | Q(domain__related_domain__name__icontains = search) start = filters.get('s', None)
if request.user.has_perms(('cableur',)): end = filters.get('e', None)
data = Interface.objects.filter(query_machine_list).distinct() user_state = filters.get('u', initial_choices(CHOICES_USER))
else: aff = filters.get('a', initial_choices(CHOICES_AFF))
data = Interface.objects.filter(query_machine_list & Q(machine__user__id = request.user.id)).distinct()
for d in data: options, _ = GeneralOption.objects.get_or_create()
recherche['machines_list'].append(d.machine) max_result = options.search_display_page
if i == '2':
recherche['facture_list'] = Facture.objects.filter(query & date_query).distinct() results = {
if i == '3': 'users_list': User.objects.none(),
recherche['ban_list'] = Ban.objects.filter(query).distinct() 'machines_list': Machine.objects.none(),
if i == '4': 'factures_list': Facture.objects.none(),
recherche['white_list'] = Whitelist.objects.filter(query).distinct() 'bans_list': Ban.objects.none(),
if i == '5': 'whitelists_list': Whitelist.objects.none(),
recherche['port_list'] = Port.objects.filter(details__icontains = search).distinct() 'rooms_list': Room.objects.none(),
'switch_ports_list': Port.objects.none(),
'switches_list': Switch.objects.none()
}
# Users
if '0' in aff:
filter_user_list = (
Q(
surname__icontains=query
) | Q(
adherent__name__icontains=query
) | Q(
pseudo__icontains=query
) | Q(
club__room__name__icontains=query
) | Q(
adherent__room__name__icontains=query
)
) & Q(state__in=user_state)
if not request.user.has_perms(('cableur',)): if not request.user.has_perms(('cableur',)):
recherche['port_list'] = None filter_user_list &= Q(id=request.user.id)
if i == '6': results['users_list'] = User.objects.filter(filter_user_list)
recherche['switch_list'] = Switch.objects.filter(details__icontains = search).distinct() results['users_list'] = SortTable.sort(
results['users_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.USERS_INDEX
)
# Machines
if '1' in aff:
filter_machine_list = Q(
name__icontains=query
) | (
Q(
user__pseudo__icontains=query
) & Q(
user__state__in=user_state
)
) | Q(
interface__domain__name__icontains=query
) | Q(
interface__domain__related_domain__name__icontains=query
) | Q(
interface__mac_address__icontains=query
) | Q(
interface__ipv4__ipv4__icontains=query
)
if not request.user.has_perms(('cableur',)): if not request.user.has_perms(('cableur',)):
recherche['switch_list'] = None filter_machine_list &= Q(user__id=request.user.id)
options, created = GeneralOption.objects.get_or_create() results['machines_list'] = Machine.objects.filter(filter_machine_list)
search_display_page = options.search_display_page results['machines_list'] = SortTable.sort(
results['machines_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.MACHINES_INDEX
)
for r in recherche: # Factures
if recherche[r] != None: if '2' in aff:
recherche[r] = recherche[r][:search_display_page] filter_facture_list = Q(
user__pseudo__icontains=query
) & Q(
user__state__in=user_state
)
if start is not None:
filter_facture_list &= Q(date__gte=start)
if end is not None:
filter_facture_list &= Q(date__lte=end)
results['factures_list'] = Facture.objects.filter(filter_facture_list)
results['factures_list'] = SortTable.sort(
results['factures_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_INDEX
)
recherche.update({'max_result': search_display_page}) # Bans
if '3' in aff:
date_filter = (
Q(
user__pseudo__icontains=query
) & Q(
user__state__in=user_state
)
) | Q(
raison__icontains=query
)
if start is not None:
date_filter &= (
Q(date_start__gte=start) & Q(date_end__gte=start)
) | (
Q(date_start__lte=start) & Q(date_end__gte=start)
) | (
Q(date_start__gte=start) & Q(date_end__lte=start)
)
if end is not None:
date_filter &= (
Q(date_start__lte=end) & Q(date_end__lte=end)
) | (
Q(date_start__lte=end) & Q(date_end__gte=end)
) | (
Q(date_start__gte=end) & Q(date_end__lte=end)
)
results['bans_list'] = Ban.objects.filter(date_filter)
results['bans_list'] = SortTable.sort(
results['bans_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.USERS_INDEX_BAN
)
# Whitelists
if '4' in aff:
date_filter = (
Q(
user__pseudo__icontains=query
) & Q(
user__state__in=user_state
)
) | Q(
raison__icontains=query
)
if start is not None:
date_filter &= (
Q(date_start__gte=start) & Q(date_end__gte=start)
) | (
Q(date_start__lte=start) & Q(date_end__gte=start)
) | (
Q(date_start__gte=start) & Q(date_end__lte=start)
)
if end is not None:
date_filter &= (
Q(date_start__lte=end) & Q(date_end__lte=end)
) | (
Q(date_start__lte=end) & Q(date_end__gte=end)
) | (
Q(date_start__gte=end) & Q(date_end__lte=end)
)
results['whitelists_list'] = Whitelist.objects.filter(date_filter)
results['whitelists_list'] = SortTable.sort(
results['whitelists_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.USERS_INDEX_WHITE
)
# Rooms
if '5' in aff and request.user.has_perms(('cableur',)):
filter_rooms_list = Q(
details__icontains=query
) | Q(
name__icontains=query
) | Q(
port__details=query
)
results['rooms_list'] = Room.objects.filter(filter_rooms_list)
results['rooms_list'] = SortTable.sort(
results['rooms_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.TOPOLOGIE_INDEX_ROOM
)
# Switch ports
if '6' in aff and request.user.has_perms(('cableur',)):
filter_ports_list = Q(
room__name__icontains=query
) | Q(
machine_interface__domain__name__icontains=query
) | Q(
related__switch__switch_interface__domain__name__icontains=query
) | Q(
radius__icontains=query
) | Q(
vlan_force__name__icontains=query
) | Q(
details__icontains=query
)
if is_int(query):
filter_ports_list |= Q(
port=query
)
results['switch_ports_list'] = Port.objects.filter(filter_ports_list)
results['switch_ports_list'] = SortTable.sort(
results['switch_ports_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.TOPOLOGIE_INDEX_PORT
)
# Switches
if '7' in aff and request.user.has_perms(('cableur',)):
filter_switches_list = Q(
switch_interface__domain__name__icontains=query
) | Q(
switch_interface__ipv4__ipv4__icontains=query
) | Q(
location__icontains=query
) | Q(
stack__name__icontains=query
) | Q(
model__reference__icontains=query
) | Q(
model__constructor__name__icontains=query
) | Q(
details__icontains=query
)
if is_int(query):
filter_switches_list |= Q(
number=query
) | Q(
stack_member_id=query
)
results['switches_list'] = Switch.objects.filter(filter_switches_list)
results['switches_list'] = SortTable.sort(
results['switches_list'],
request.GET.get('col'),
request.GET.get('order'),
SortTable.TOPOLOGIE_INDEX
)
for name, val in results.items():
results[name] = val.distinct()[:max_result]
results.update({'max_result': max_result})
results.update({'search_term': query})
return results
return recherche
@login_required @login_required
def search(request): def search(request):
search = SearchForm(request.POST or None) """ La page de recherche standard """
if search.is_valid(): search_form = SearchForm(request.GET or None)
return form(search_result(search, False, request), 'search/index.html',request) if search_form.is_valid():
return form({'searchform' : search}, 'search/search.html', request) return render(
request,
'search/index.html',
get_results(
search_form.cleaned_data.get('q', ''),
request,
search_form.cleaned_data
)
)
return render(request, 'search/search.html', {'search_form': search_form})
@login_required @login_required
def searchp(request): def searchp(request):
search = SearchFormPlus(request.POST or None) """ La page de recherche avancée """
if search.is_valid(): search_form = SearchFormPlus(request.GET or None)
return form(search_result(search, True, request), 'search/index.html',request) if search_form.is_valid():
return form({'searchform' : search}, 'search/search.html', request) return render(
request,
'search/index.html',
get_results(
search_form.cleaned_data.get('q', ''),
request,
search_form.cleaned_data
)
)
return render(request, 'search/search.html', {'search_form': search_form})

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,16 +33,23 @@ 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>
</head> </head>
<body> <body>
{% include "cookie_banner.html" %}
<div id="wrap"> <div id="wrap">
<nav class="navbar navbar-inverse"> <nav class="navbar navbar-inverse">
<div class="container-fluid"> <div class="container-fluid">
@ -66,10 +73,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
</ul> </ul>
<div class="col-sm-3 col-md-3 navbar-right"> <div class="col-sm-3 col-md-3 navbar-right">
<form action="{% url "search:search"%}" method="POST" class="navbar-form" role="search"> <form action="{% url "search:search"%}" class="navbar-form" role="search">
{% csrf_token %}
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" placeholder="Search" name="search_field" id="search-term"> <input type="text" class="form-control" placeholder="Search" name="q" id="search-term" {% if search_term %}value="{{ search_term }}"{% endif %}>
<div class="input-group-btn"> <div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button> <button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button>
<a href="{% url "search:searchp" %}" class="btn btn-default" role="button"><i class="glyphicon glyphicon-plus"></i></a> <a href="{% url "search:searchp" %}" class="btn btn-default" role="button"><i class="glyphicon glyphicon-plus"></i></a>
@ -140,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th scope="row">Connexion</th> <th scope="row">Connexion</th>
<td class="text-right"> <td class="text-right">
{% if request_user.has_access %} {% if request_user.has_access %}
<font color="green">Active</font> <font color="green">jusqu'au {{ request.user.end_access|date:"d b Y" }}</font>
{% else %} {% else %}
<font color="red">Désactivée</font> <font color="red">Désactivée</font>
{% endif %} {% endif %}
@ -149,8 +155,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th scope="row">Adhésion</th> <th scope="row">Adhésion</th>
<td class="text-right"> <td class="text-right">
{% if request_user.end_adhesion != None %} {% if request_user.is_adherent %}
<font color="green">{{ request_user.end_adhesion }}</font> <font color="green">jusqu'au {{ request_user.end_adhesion|date:"d b Y" }}</font>
{% else %} {% else %}
<font color="red">Non adhérent</font> <font color="red">Non adhérent</font>
{% endif %} {% endif %}

View file

@ -0,0 +1,40 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
<div class="form-group {% if field.form.errors %}{% if field.errors %}has-error{% else %}has-success{% endif %}{% endif %}">
<label class="control-label" for="{{ field.id_for_label }}">
{{ field.label }}
</label>
<div id="{{ field.auto_id }}" data-toggle="buttons">
{% for val in field.field.choices %}
<label for="id_u_{{ val.0 }}" class="btn btn-default{% if val.0 in field.initial %} active{% endif %}">
<input {% if val.0 in field.initial %}checked="checked" {% endif %}class="" id="id_u_{{ val.0 }}" name="{{ field.name }}" title="" type="checkbox" value="{{ val.0 }}" /> {{ val.1 }}
</label>
{% endfor %}
</div>
{% for error in field.errors %}
<div class="help-block">{{ error }}</div>
{% endfor %}
<div class="help-block">{{ field.help_text }}</div>
</div>

View file

@ -0,0 +1,50 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load url_insert_param %}
{% spaceless %}
<div style="display: flex; padding: 0;">
{{ text }}&nbsp;
<div style="display: grid; font-size: 9px; line-height: 1; margin: auto 0;">
{% if prefix %}
{% with prefix|add:'_'|add:col as colname %}
<a role="button" href="{% url_insert_param request.get_full_path col=colname order='asc' %}" title="{{ desc|default:"Tri croissant" }}">
<span class="glyphicon glyphicon-triangle-top"></span>
</a>
<a role="button" href="{% url_insert_param request.get_full_path col=colname order='desc' %}" title="{{ desc|default:"Tri décroissant" }}">
<span class="glyphicon glyphicon-triangle-bottom"></span>
</a>
{% endwith %}
{% else %}
<a role="button" href="{% url_insert_param request.get_full_path col=col order='asc' %}" title="{{ desc|default:"Tri croissant" }}">
<span class="glyphicon glyphicon-triangle-top"></span>
</a>
<a role="button" href="{% url_insert_param request.get_full_path col=col order='desc' %}" title="{{ desc|default:"Tri décroissant" }}">
<span class="glyphicon glyphicon-triangle-bottom"></span>
</a>
{% endif %}
</div>
</div>
{% endspaceless %}

View file

@ -0,0 +1,19 @@
{% if not 'accept_cookies' in request.COOKIES%}
<script>
function accept_cookie() {
var d = new Date();
var expiration_time = 7 * 24 * 60 * 60 * 1000; // Accepte les cookies pendant 7 jours.
d.setTime(d.getTime() + expiration_time);
var expires = "expires="+ d.toUTCString();
document.cookie = "accept_cookies=1;" + expires + ";path=/";
var banner = document.getElementById("cookie_banner");
banner.parentNode.removeChild(banner);
}
</script>
<div class="navbar text-center" id="cookie_banner">
<p>Ce site utilise des cookies. En poursuivant sur ce site j'accepte l'utilisation des cookies sur ce site.</p>
<a class="btn btn-primary btn-sm" role="button" onclick="accept_cookie();" title="Accepter">
J'ai compris !
</a>
</div>
{% endif %}

View file

@ -22,20 +22,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load url_insert_param %}
<ul class="pagination nav navbar-nav"> <ul class="pagination nav navbar-nav">
{% if list.has_previous %} {% if list.has_previous %}
<li><a href="?page=1"> << </a></li> <li><a href="{% url_insert_param request.get_full_path page=1 %}"> << </a></li>
<li><a href="?page={{ list.previous_page_number }}"> < </a></li> <li><a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}"> < </a></li>
{% endif %} {% endif %}
{% for page in list.paginator.page_range %} {% for page in list.paginator.page_range %}
{% if list.number <= page|add:"3" and list.number >= page|add:"-3" %} {% if list.number <= page|add:"3" and list.number >= page|add:"-3" %}
<li class="{% if list.number == page %}active{% endif %}"><a href="?page={{page }}">{{ page }}</a></li> <li class="{% if list.number == page %}active{% endif %}"><a href="{% url_insert_param request.get_full_path page=page %}">{{ page }}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if list.has_next %} {% if list.has_next %}
<li><a href="?page={{ list.next_page_number }}"> > </a></li> <li><a href="{% url_insert_param request.get_full_path page=list.next_page_number %}"> > </a></li>
<li><a href="?page={{ list.paginator.page_range|length }}"> >> </a></li> <li><a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}"> >> </a></li>
{% endif %} {% endif %}
</ul> </ul>

View file

@ -20,27 +20,51 @@
# 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
from django.contrib import admin from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import Port, Room, Switch, Stack from .models import Port, Room, Switch, Stack, ModelSwitch, ConstructorSwitch
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
class ModelSwitchAdmin(VersionAdmin):
"""Administration d'un modèle de switch"""
pass
class ConstructorSwitchAdmin(VersionAdmin):
"""Administration d'un constructeur d'un switch"""
pass
admin.site.register(Port, PortAdmin) admin.site.register(Port, PortAdmin)
admin.site.register(Room, RoomAdmin) admin.site.register(Room, RoomAdmin)
admin.site.register(Switch, SwitchAdmin) admin.site.register(Switch, SwitchAdmin)
admin.site.register(Stack, StackAdmin) admin.site.register(Stack, StackAdmin)
admin.site.register(ModelSwitch, ModelSwitchAdmin)
admin.site.register(ConstructorSwitch, ConstructorSwitchAdmin)

View file

@ -19,52 +19,150 @@
# 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 import forms
from django.forms import ModelForm
from .models import Port, Switch, Room, Stack, ModelSwitch, ConstructorSwitch
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)
self.fields['machine_interface'].queryset = Interface.objects.all()\
.select_related('domain__extension')
self.fields['related'].queryset = Port.objects.all()\
.select_related('switch__switch_interface__domain__extension')\
.order_by('switch', 'port')
class StackForm(ModelForm): 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['switch_interface'].queryset = Interface.objects.all()\
.select_related('domain__extension')
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)
self.fields['location'].label = 'Localisation'
self.fields['number'].label = 'Nombre de ports'
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)
class CreatePortsForm(forms.Form):
"""Permet de créer une liste de ports pour un switch."""
begin = forms.IntegerField(label="Début :", min_value=0)
end = forms.IntegerField(label="Fin :", min_value=0)
class EditModelSwitchForm(ModelForm):
"""Permet d'éediter un modèle de switch : nom et constructeur"""
class Meta:
model = ModelSwitch
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditModelSwitchForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditConstructorSwitchForm(ModelForm):
"""Permet d'éediter le nom d'un constructeur"""
class Meta:
model = ConstructorSwitch
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditConstructorSwitchForm, self).__init__(*args, prefix=prefix, **kwargs)

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-15 18:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0030_auto_20171004_0235'),
]
operations = [
migrations.AlterField(
model_name='port',
name='port',
field=models.PositiveIntegerField(),
),
migrations.AlterField(
model_name='stack',
name='member_id_max',
field=models.PositiveIntegerField(),
),
migrations.AlterField(
model_name='stack',
name='member_id_min',
field=models.PositiveIntegerField(),
),
migrations.AlterField(
model_name='switch',
name='number',
field=models.PositiveIntegerField(),
),
migrations.AlterField(
model_name='switch',
name='stack_member_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-26 01:38
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('topologie', '0031_auto_20171015_2033'),
]
operations = [
migrations.CreateModel(
name='ConstructorSwitch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='ModelSwitch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference', models.CharField(max_length=255)),
('constructor', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='topologie.ConstructorSwitch')),
],
),
migrations.AddField(
model_name='switch',
name='model',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='topologie.ModelSwitch'),
),
]

View file

@ -20,19 +20,27 @@
# 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):
@ -44,8 +52,8 @@ class Stack(models.Model):
name = models.CharField(max_length=32, blank=True, null=True) name = models.CharField(max_length=32, blank=True, null=True)
stack_id = models.CharField(max_length=32, unique=True) stack_id = models.CharField(max_length=32, unique=True)
details = models.CharField(max_length=255, blank=True, null=True) details = models.CharField(max_length=255, blank=True, null=True)
member_id_min = models.IntegerField() member_id_min = models.PositiveIntegerField()
member_id_max = models.IntegerField() member_id_max = models.PositiveIntegerField()
def __str__(self): def __str__(self):
return " ".join([self.name, self.stack_id]) return " ".join([self.name, self.stack_id])
@ -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,15 +79,32 @@ 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.PositiveIntegerField()
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_member_id = models.IntegerField(blank=True, null=True) 'topologie.Stack',
blank=True,
null=True,
on_delete=models.SET_NULL
)
stack_member_id = models.PositiveIntegerField(blank=True, null=True)
model = models.ForeignKey(
'topologie.ModelSwitch',
blank=True,
null=True,
on_delete=models.SET_NULL
)
class Meta: class Meta:
unique_together = ('stack', 'stack_member_id') unique_together = ('stack', 'stack_member_id')
@ -89,10 +116,38 @@ 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 ModelSwitch(models.Model):
"""Un modèle (au sens constructeur) de switch"""
PRETTY_NAME = "Modèle de switch"
reference = models.CharField(max_length=255)
constructor = models.ForeignKey(
'topologie.ConstructorSwitch',
on_delete=models.PROTECT
)
def __str__(self):
return str(self.constructor) + ' ' + str(self.reference)
class ConstructorSwitch(models.Model):
"""Un constructeur de switch"""
PRETTY_NAME = "Constructeur de switch"
name = models.CharField(max_length=255)
def __str__(self):
return str(self.name)
class Port(models.Model): class Port(models.Model):
""" Definition d'un port. Relié à un switch(foreign_key), """ Definition d'un port. Relié à un switch(foreign_key),
@ -102,7 +157,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
@ -117,13 +173,37 @@ class Port(models.Model):
('COMMON', 'COMMON'), ('COMMON', 'COMMON'),
) )
switch = models.ForeignKey('Switch', related_name="ports") switch = models.ForeignKey(
port = models.IntegerField() 'Switch',
room = models.ForeignKey('Room', on_delete=models.PROTECT, blank=True, null=True) related_name="ports",
machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True) on_delete=models.CASCADE
related = models.OneToOneField('self', null=True, blank=True, related_name='related_port') )
port = models.PositiveIntegerField()
room = models.ForeignKey(
'Room',
on_delete=models.PROTECT,
blank=True,
null=True
)
machine_interface = models.ForeignKey(
'machines.Interface',
on_delete=models.SET_NULL,
blank=True,
null=True
)
related = models.OneToOneField(
'self',
null=True,
blank=True,
related_name='related_port'
)
radius = models.CharField(max_length=32, choices=STATES, default='NO') 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 +222,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\
self.related or self.machine_interface and self.related:
raise ValidationError("Chambre, interface et related_port sont\
mutuellement exclusifs")
if self.related == self: if self.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,6 +252,7 @@ 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"
@ -180,6 +266,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):
"""Vide les id des switches membres d'une stack supprimée"""
Switch.objects.filter(stack=None).update(stack_member_id=None) Switch.objects.filter(stack=None).update(stack_member_id=None)

View file

@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Chambre</th> <th>{% include "buttons/sort.html" with prefix='room' col='name' text='Chambre' %}</th>
<th>Commentaire</th> <th>Commentaire</th>
<th></th> <th></th>
</tr> </tr>

View file

@ -0,0 +1,54 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% if constructor_switch_list.paginator %}
{% include "pagination.html" with list=constructor_switch_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>{% include "buttons/sort.html" with prefix='constructor-switch' col='name' text='Constructeur' %}</th>
<th></th>
</tr>
</thead>
{% for constructor_switch in constructor_switch_list %}
<tr>
<td>{{constructor_switch}}</td>
<td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'constructor_switch' constructor_switch.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-constructor-switch' constructor_switch.id %}">
<i class="glyphicon glyphicon-edit"></i>
</a>
<a class="btn btn-danger btn-sm" role="button" title="Supprimer" href="{% url 'topologie:del-constructor-switch' constructor_switch.id %}">
<i class="glyphicon glyphicon-trash"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>

View file

@ -0,0 +1,56 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% if model_switch_list.paginator %}
{% include "pagination.html" with list=model_switch_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>{% include "buttons/sort.html" with prefix='model-switch' col='reference' text='Référence' %}</th>
<th>{% include "buttons/sort.html" with prefix='model-switch' col='constructor' text='Constructeur' %}</th>
<th></th>
</tr>
</thead>
{% for model_switch in model_switch_list %}
<tr>
<td>{{model_switch.reference}}</td>
<td>{{model_switch.constructor}}</td>
<td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'model_switch' model_switch.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-model-switch' model_switch.id %}">
<i class="glyphicon glyphicon-edit"></i>
</a>
<a class="btn btn-danger btn-sm" role="button" title="Supprimer" href="{% url 'topologie:del-model-switch' model_switch.id %}">
<i class="glyphicon glyphicon-trash"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>

View file

@ -25,12 +25,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Port</th> <th>{% include "buttons/sort.html" with prefix='port' col='port' text='Port' %}</th>
<th>Room</th> <th>{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %}</th>
<th>Interface machine</th> <th>{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %}</th>
<th>Related</th> <th>{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %}</th>
<th>Radius</th> <th>{% include "buttons/sort.html" with prefix='port' col='radius' text='Radius' %}</th>
<th>Vlan forcé</th> <th>{% include "buttons/sort.html" with prefix='port' col='vlan' text='Vlan forcé' %}</th>
<th>Détails</th> <th>Détails</th>
<th></th> <th></th>
</tr> </tr>

View file

@ -25,14 +25,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Stack</th> <th>{% include "buttons/sort.html" with prefix='stack' col='name' text='Stack' %}</th>
<th>ID</th> <th>{% include "buttons/sort.html" with prefix='stack' col='id' text='ID' %}</th>
<th>Details</th> <th>Détails</th>
<th>Membres</th> <th>Membres</th>
</tr> </tr>
</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

@ -22,15 +22,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% if switch_list.paginator %}
{% include "pagination.html" with list=switch_list %}
{% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Dns</th> <th>{% include "buttons/sort.html" with prefix='switch' col='dns' text='Dns' %}</th>
<th>Ipv4</th> <th>{% include "buttons/sort.html" with prefix='switch' col='ip' text='Ipv4' %}</th>
<th>Localisation</th> <th>{% include "buttons/sort.html" with prefix='switch' col='loc' text='Localisation' %}</th>
<th>Ports</th> <th>{% include "buttons/sort.html" with prefix='switch' col='ports' text='Ports' %}</th>
<th>Stack</th> <th>{% include "buttons/sort.html" with prefix='switch' col='stack' text='Stack' %}</th>
<th>id interne Stack</th> <th>Id stack</th>
<th>Modèle</th>
<th>Détails</th> <th>Détails</th>
<th></th> <th></th>
</tr> </tr>
@ -47,11 +52,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{switch.number}}</td> <td>{{switch.number}}</td>
<td>{{switch.stack.name}}</td> <td>{{switch.stack.name}}</td>
<td>{{switch.stack_member_id}}</td> <td>{{switch.stack_member_id}}</td>
<td>{{switch.model}}</td>
<td>{{switch.details}}</td> <td>{{switch.details}}</td>
<td class="text-right"> <td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'switch' switch.pk %}"> {% include 'buttons/history.html' with href='topologie:history' name='switch' id=switch.pk%}
<i class="glyphicon glyphicon-time"></i> {% if is_infra %}
</a> {% include 'buttons/edit.html' with href='topologie:edit-switch' id=switch.pk %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=switch.switch_interface.id %}
{% include 'buttons/add.html' with href='topologie:create-ports' id=switch.pk desc='Création de ports'%}
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h2>Switchs</h2> <h2>Switchs</h2>
{% if is_infra %} {% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:new-switch' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un switch</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:new-switch' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un switch</a>
<hr>
{% endif %} {% endif %}
{% include "topologie/aff_switch.html" with switch_list=switch_list %} {% include "topologie/aff_switch.html" with switch_list=switch_list %}
<br /> <br />

Some files were not shown because too many files have changed in this diff Show more