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

Merge branch 'master' into ouverture_des_ports

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

1
.gitignore vendored
View file

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

View file

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

View file

@ -19,24 +19,44 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Forms de l'application cotisation de re2o. Dépendance avec les models,
importé par les views.
Permet de créer une nouvelle facture pour un user (NewFactureForm),
et de l'editer (soit l'user avec EditFactureForm,
soit le trésorier avec TrezEdit qui a plus de possibilités que self
notamment sur le controle trésorier)
SelectArticleForm est utilisée lors de la creation d'une facture en
parrallèle de NewFacture pour le choix des articles désirés.
(la vue correspondante est unique)
ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter,
éditer ou supprimer une banque/moyen de paiement ou un article
"""
from __future__ import unicode_literals
from django import forms
from django.forms import ModelForm, Form
from django import forms
from django.core.validators import MinValueValidator
from .models import Article, Paiement, Facture, Banque, Vente
from .models import Article, Paiement, Facture, Banque
class NewFactureForm(ModelForm):
"""Creation d'une facture, moyen de paiement, banque et numero
de cheque"""
def __init__(self, *args, **kwargs):
super(NewFactureForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['cheque'].required = False
self.fields['banque'].required = False
self.fields['cheque'].label = 'Numero de chèque'
self.fields['banque'].empty_label = "Non renseigné"
self.fields['paiement'].empty_label = "Séléctionner un moyen de paiement"
self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects.filter(type_paiement=1).first().id
self.fields['paiement'].empty_label = "Séléctionner\
un moyen de paiement"
self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects\
.filter(type_paiement=1).first().id
class Meta:
model = Facture
@ -48,45 +68,76 @@ class NewFactureForm(ModelForm):
cheque = cleaned_data.get("cheque")
banque = cleaned_data.get("banque")
if not paiement:
raise forms.ValidationError("Le moyen de paiement est obligatoire.")
raise forms.ValidationError("Le moyen de paiement est obligatoire")
elif paiement.type_paiement == "check" and not (cheque and banque):
raise forms.ValidationError("Le numéro de chèque et la banque sont obligatoires.")
raise forms.ValidationError("Le numéro de chèque et\
la banque sont obligatoires.")
return cleaned_data
class CreditSoldeForm(NewFactureForm):
"""Permet de faire des opérations sur le solde si il est activé"""
class Meta(NewFactureForm.Meta):
model = Facture
fields = ['paiement', 'banque', 'cheque']
def __init__(self, *args, **kwargs):
super(CreditSoldeForm, self).__init__(*args, **kwargs)
self.fields['paiement'].queryset = Paiement.objects.exclude(moyen='solde').exclude(moyen="Solde")
self.fields['paiement'].queryset = Paiement.objects.exclude(
moyen='solde'
).exclude(moyen="Solde")
montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True)
class SelectArticleForm(Form):
article = forms.ModelChoiceField(queryset=Article.objects.all(), label="Article", required=True)
quantity = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)], required=True)
"""Selection d'un article lors de la creation d'une facture"""
article = forms.ModelChoiceField(
queryset=Article.objects.all(),
label="Article",
required=True
)
quantity = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)],
required=True
)
class NewFactureFormPdf(Form):
article = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Article")
number = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)])
"""Creation d'un pdf facture par le trésorier"""
article = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(),
label="Article"
)
number = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)]
)
paid = forms.BooleanField(label="Payé", required=False)
dest = forms.CharField(required=True, max_length=255, label="Destinataire")
chambre = forms.CharField(required=False, max_length=10, label="Adresse")
fid = forms.CharField(required=True, max_length=10, label="Numéro de la facture")
fid = forms.CharField(
required=True,
max_length=10,
label="Numéro de la facture"
)
class EditFactureForm(NewFactureForm):
"""Edition d'une facture : moyen de paiement, banque, user parent"""
class Meta(NewFactureForm.Meta):
fields = ['paiement', 'banque', 'cheque', 'user']
def __init__(self, *args, **kwargs):
super(EditFactureForm, self).__init__(*args, **kwargs)
self.fields['user'].label = 'Adherent'
self.fields['user'].empty_label = "Séléctionner l'adhérent propriétaire"
self.fields['user'].empty_label = "Séléctionner\
l'adhérent propriétaire"
class TrezEditFactureForm(EditFactureForm):
"""Vue pour édition controle trésorier"""
class Meta(EditFactureForm.Meta):
fields = '__all__'
@ -97,38 +148,67 @@ class TrezEditFactureForm(EditFactureForm):
class ArticleForm(ModelForm):
"""Creation d'un article. Champs : nom, cotisation, durée"""
class Meta:
model = Article
fields = '__all__'
def __init__(self, *args, **kwargs):
super(ArticleForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = "Désignation de l'article"
class DelArticleForm(Form):
articles = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Articles actuels", widget=forms.CheckboxSelectMultiple)
"""Suppression d'un ou plusieurs articles en vente. Choix
parmis les modèles"""
articles = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(),
label="Articles actuels",
widget=forms.CheckboxSelectMultiple
)
class PaiementForm(ModelForm):
"""Creation d'un moyen de paiement, champ text moyen et type
permettant d'indiquer si il s'agit d'un chèque ou non pour le form"""
class Meta:
model = Paiement
fields = ['moyen', 'type_paiement']
def __init__(self, *args, **kwargs):
super(PaiementForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['moyen'].label = 'Moyen de paiement à ajouter'
self.fields['type_paiement'].label = 'Type de paiement à ajouter'
class DelPaiementForm(Form):
paiements = forms.ModelMultipleChoiceField(queryset=Paiement.objects.all(), label="Moyens de paiement actuels", widget=forms.CheckboxSelectMultiple)
"""Suppression d'un ou plusieurs moyens de paiements, selection
parmis les models"""
paiements = forms.ModelMultipleChoiceField(
queryset=Paiement.objects.all(),
label="Moyens de paiement actuels",
widget=forms.CheckboxSelectMultiple
)
class BanqueForm(ModelForm):
"""Creation d'une banque, field name"""
class Meta:
model = Banque
fields = ['name']
def __init__(self, *args, **kwargs):
super(BanqueForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Banque à ajouter'
class DelBanqueForm(Form):
banques = forms.ModelMultipleChoiceField(queryset=Banque.objects.all(), label="Banques actuelles", widget=forms.CheckboxSelectMultiple)
"""Selection d'une ou plusieurs banques, pour suppression"""
banques = forms.ModelMultipleChoiceField(
queryset=Banque.objects.all(),
label="Banques actuelles",
widget=forms.CheckboxSelectMultiple
)

View file

@ -20,59 +20,111 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Definition des models bdd pour les factures et cotisation.
Pièce maitresse : l'ensemble du code intelligent se trouve ici,
dans les clean et save des models ainsi que de leur methodes supplémentaires.
Facture : reliée à un user, elle a un moyen de paiement, une banque (option),
une ou plusieurs ventes
Article : liste des articles en vente, leur prix, etc
Vente : ensemble des ventes effectuées, reliées à une facture (foreignkey)
Banque : liste des banques existantes
Cotisation : objets de cotisation, contenant un début et une fin. Reliées
aux ventes, en onetoone entre une vente et une cotisation.
Crées automatiquement au save des ventes.
Post_save et Post_delete : sychronisation des services et régénération
des services d'accès réseau (ex dhcp) lors de la vente d'une cotisation
par exemple
"""
from __future__ import unicode_literals
from dateutil.relativedelta import relativedelta
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from dateutil.relativedelta import relativedelta
from django.forms import ValidationError
from django.core.validators import MinValueValidator
from django.db.models import Max
from django.utils import timezone
from machines.models import regen
class Facture(models.Model):
""" Définition du modèle des factures. Une facture regroupe une ou
plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement
et si il y a lieu un numero pour les chèques. Possède les valeurs
valides et controle (trésorerie)"""
PRETTY_NAME = "Factures émises"
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
banque = models.ForeignKey('Banque', on_delete=models.PROTECT, blank=True, null=True)
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True)
cheque = models.CharField(max_length=255, blank=True)
date = models.DateTimeField(auto_now_add=True)
valid = models.BooleanField(default=True)
control = models.BooleanField(default=False)
def prix(self):
prix = Vente.objects.filter(facture=self).aggregate(models.Sum('prix'))['prix__sum']
"""Renvoie le prix brut sans les quantités. Méthode
dépréciée"""
prix = Vente.objects.filter(
facture=self
).aggregate(models.Sum('prix'))['prix__sum']
return prix
def prix_total(self):
return Vente.objects.filter(facture=self).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total']
"""Prix total : somme des produits prix_unitaire et quantité des
ventes de l'objet"""
return Vente.objects.filter(
facture=self
).aggregate(
total=models.Sum(
models.F('prix')*models.F('number'),
output_field=models.FloatField()
)
)['total']
def name(self):
name = ' - '.join(Vente.objects.filter(facture=self).values_list('name', flat=True))
"""String, somme des name des ventes de self"""
name = ' - '.join(Vente.objects.filter(
facture=self
).values_list('name', flat=True))
return name
def __str__(self):
return str(self.user) + ' ' + str(self.date)
@receiver(post_save, sender=Facture)
def facture_post_save(sender, **kwargs):
"""Post save d'une facture, synchronise l'user ldap"""
facture = kwargs['instance']
user = facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Facture)
def facture_post_delete(sender, **kwargs):
"""Après la suppression d'une facture, on synchronise l'user ldap"""
user = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Vente(models.Model):
"""Objet vente, contient une quantité, une facture parente, un nom,
un prix. Peut-être relié à un objet cotisation, via le boolean
iscotisation"""
PRETTY_NAME = "Ventes effectuées"
facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
@ -80,44 +132,67 @@ class Vente(models.Model):
name = models.CharField(max_length=255)
prix = models.DecimalField(max_digits=5, decimal_places=2)
iscotisation = models.BooleanField()
duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True)
duration = models.IntegerField(
help_text="Durée exprimée en mois entiers",
blank=True,
null=True)
def prix_total(self):
"""Renvoie le prix_total de self (nombre*prix)"""
return self.prix*self.number
def update_cotisation(self):
"""Mets à jour l'objet related cotisation de la vente, si
il existe : update la date de fin à partir de la durée de
la vente"""
if hasattr(self, 'cotisation'):
cotisation = self.cotisation
cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number)
cotisation.date_end = cotisation.date_start + relativedelta(
months=self.duration*self.number)
return
def create_cotis(self, date_start=False):
""" Update et crée l'objet cotisation associé à une facture, prend en argument l'user, la facture pour la quantitéi, et l'article pour la durée"""
"""Update et crée l'objet cotisation associé à une facture, prend
en argument l'user, la facture pour la quantitéi, et l'article pour
la durée"""
if not hasattr(self, 'cotisation'):
cotisation = Cotisation(vente=self)
if date_start:
end_adhesion = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self.facture.user).exclude(valid=False))).filter(date_start__lt=date_start).aggregate(Max('date_end'))['date_end__max']
end_adhesion = Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self.facture.user
).exclude(valid=False))
).filter(
date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max']
else:
end_adhesion = self.facture.user.end_adhesion()
date_start = date_start or timezone.now()
end_adhesion = end_adhesion or date_start
date_max = max(end_adhesion, date_start)
cotisation.date_start = date_max
cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number)
cotisation.date_end = cotisation.date_start + relativedelta(
months=self.duration*self.number
)
return
def save(self, *args, **kwargs):
# On verifie que si iscotisation, duration est présent
if self.iscotisation and not self.duration:
raise ValidationError("Cotisation et durée doivent être présents ensembles")
raise ValidationError("Cotisation et durée doivent être présents\
ensembles")
self.update_cotisation()
super(Vente, self).save(*args, **kwargs)
def __str__(self):
return str(self.name) + ' ' + str(self.facture)
@receiver(post_save, sender=Vente)
def vente_post_save(sender, **kwargs):
"""Post save d'une vente, déclencge la création de l'objet cotisation
si il y a lieu(si iscotisation) """
vente = kwargs['instance']
if hasattr(vente, 'cotisation'):
vente.cotisation.vente = vente
@ -128,14 +203,20 @@ def vente_post_save(sender, **kwargs):
user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Vente)
def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, on synchronise l'user ldap (ex
suppression d'une cotisation"""
vente = kwargs['instance']
if vente.iscotisation:
user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Article(models.Model):
"""Liste des articles en vente : prix, nom, et attribut iscotisation
et duree si c'est une cotisation"""
PRETTY_NAME = "Articles en vente"
name = models.CharField(max_length=255, unique=True)
@ -154,7 +235,9 @@ class Article(models.Model):
def __str__(self):
return self.name
class Banque(models.Model):
"""Liste des banques"""
PRETTY_NAME = "Banques enregistrées"
name = models.CharField(max_length=255)
@ -162,7 +245,9 @@ class Banque(models.Model):
def __str__(self):
return self.name
class Paiement(models.Model):
"""Moyens de paiement"""
PRETTY_NAME = "Moyens de paiement"
PAYMENT_TYPES = (
(0, 'Autre'),
@ -179,11 +264,15 @@ class Paiement(models.Model):
self.moyen = self.moyen.title()
def save(self, *args, **kwargs):
"""Un seul type de paiement peut-etre cheque..."""
if Paiement.objects.filter(type_paiement=1).count() > 1:
raise ValidationError("On ne peut avoir plusieurs mode de paiement chèque")
raise ValidationError("On ne peut avoir plusieurs mode de paiement\
chèque")
super(Paiement, self).save(*args, **kwargs)
class Cotisation(models.Model):
"""Objet cotisation, debut et fin, relié en onetoone à une vente"""
PRETTY_NAME = "Cotisations"
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
@ -193,15 +282,19 @@ class Cotisation(models.Model):
def __str__(self):
return str(self.vente)
@receiver(post_save, sender=Cotisation)
def cotisation_post_save(sender, **kwargs):
"""Après modification d'une cotisation, regeneration des services"""
regen('dns')
regen('dhcp')
regen('mac_ip_list')
regen('mailing')
@receiver(post_delete, sender=Cotisation)
def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, régénération des services"""
cotisation = kwargs['instance']
regen('mac_ip_list')
regen('mailing')

View file

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

View file

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

View file

@ -24,40 +24,41 @@
# Goulven Kermarec, Gabriel Détraz
# Gplv2
from __future__ import unicode_literals
import os
from django.shortcuts import render, redirect
from django.shortcuts import get_object_or_404
from django.template.context_processors import csrf
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages
from django.db.models import Max, ProtectedError
from django.db.models import ProtectedError
from django.db import transaction
from django.forms import modelformset_factory, formset_factory
import os
from django.utils import timezone
from reversion import revisions as reversion
from reversion.models import Version
from .models import Facture, Article, Vente, Cotisation, Paiement, Banque
from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, CreditSoldeForm, SelectArticleForm
# Import des models, forms et fonctions re2o
from users.models import User
from .tex import render_tex
from re2o.settings import LOGO_PATH
from re2o import settings
from re2o.views import form
from preferences.models import OptionalUser, AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque
from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm
from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm
from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf
from .forms import SelectArticleForm, CreditSoldeForm
from .tex import render_tex
from dateutil.relativedelta import relativedelta
from django.utils import timezone
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required
@permission_required('cableur')
def new_facture(request, userid):
"""Creation d'une facture pour un user. Renvoie la liste des articles
et crée des factures dans un formset. Utilise un peu de js coté template
pour ajouter des articles.
Parse les article et boucle dans le formset puis save les ventes,
enfin sauve la facture parente.
TODO : simplifier cette fonction, déplacer l'intelligence coté models
Facture et Vente."""
try:
user = User.objects.get(pk=userid)
except User.DoesNotExist:
@ -70,50 +71,80 @@ def new_facture(request, userid):
facture_form = NewFactureForm(request.POST or None, instance=facture)
article_formset = formset_factory(SelectArticleForm)(request.POST or None)
if facture_form.is_valid() and article_formset.is_valid():
new_facture = facture_form.save(commit=False)
new_facture_instance = facture_form.save(commit=False)
articles = article_formset
# Si au moins un article est rempli
if any(art.cleaned_data for art in articles):
options, created = OptionalUser.objects.get_or_create()
options, _created = OptionalUser.objects.get_or_create()
user_solde = options.user_solde
solde_negatif = options.solde_negatif
# Si on paye par solde, que l'option est activée, on vérifie que le négatif n'est pas atteint
# Si on paye par solde, que l'option est activée,
# on vérifie que le négatif n'est pas atteint
if user_solde:
if new_facture.paiement == Paiement.objects.get_or_create(moyen='solde')[0]:
if new_facture_instance.paiement == Paiement.objects.get_or_create(
moyen='solde'
)[0]:
prix_total = 0
for art_item in articles:
if art_item.cleaned_data:
prix_total += art_item.cleaned_data['article'].prix*art_item.cleaned_data['quantity']
prix_total += art_item.cleaned_data['article']\
.prix*art_item.cleaned_data['quantity']
if float(user.solde) - float(prix_total) < solde_negatif:
messages.error(request, "Le solde est insuffisant pour effectuer l'opération")
messages.error(request, "Le solde est insuffisant pour\
effectuer l'opération")
return redirect("/users/profil/" + userid)
with transaction.atomic(), reversion.create_revision():
new_facture.save()
new_facture_instance.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
for art_item in articles:
if art_item.cleaned_data:
article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity']
new_vente = Vente.objects.create(facture=new_facture, name=article.name, prix=article.prix, iscotisation=article.iscotisation, duration=article.duration, number=quantity)
new_vente = Vente.objects.create(
facture=new_facture_instance,
name=article.name,
prix=article.prix,
iscotisation=article.iscotisation,
duration=article.duration,
number=quantity
)
with transaction.atomic(), reversion.create_revision():
new_vente.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
if any(art_item.cleaned_data['article'].iscotisation for art_item in articles if art_item.cleaned_data):
messages.success(request, "La cotisation a été prolongée pour l'adhérent %s jusqu'au %s" % (user.pseudo, user.end_adhesion()) )
if any(art_item.cleaned_data['article'].iscotisation
for art_item in articles if art_item.cleaned_data):
messages.success(
request,
"La cotisation a été prolongée\
pour l'adhérent %s jusqu'au %s" % (
user.pseudo, user.end_adhesion()
)
)
else:
messages.success(request, "La facture a été crée")
return redirect("/users/profil/" + userid)
messages.error(request, u"Il faut au moins un article valide pour créer une facture" )
return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request)
messages.error(
request,
u"Il faut au moins un article valide pour créer une facture"
)
return form({
'factureform': facture_form,
'venteform': article_formset,
'articlelist': article_list
}, 'cotisations/new_facture.html', request)
@login_required
@permission_required('tresorier')
def new_facture_pdf(request):
"""Permet de générer un pdf d'une facture. Réservée
au trésorier, permet d'emettre des factures sans objet
Vente ou Facture correspondant en bdd"""
facture_form = NewFactureFormPdf(request.POST or None)
if facture_form.is_valid():
options, created = AssoOption.objects.get_or_create()
options, _created = AssoOption.objects.get_or_create()
tbl = []
article = facture_form.cleaned_data['article']
quantite = facture_form.cleaned_data['number']
@ -121,71 +152,131 @@ def new_facture_pdf(request):
destinataire = facture_form.cleaned_data['dest']
chambre = facture_form.cleaned_data['chambre']
fid = facture_form.cleaned_data['fid']
for a in article:
tbl.append([a, quantite, a.prix * quantite])
for art in article:
tbl.append([art, quantite, art.prix * quantite])
prix_total = sum(a[2] for a in tbl)
user = {'name': destinataire, 'room': chambre}
return render_tex(request, 'cotisations/factures.tex', {'DATE' : timezone.now(),'dest':user,'fid':fid, 'article':tbl, 'total':prix_total, 'paid':paid, 'asso_name':options.name, 'line1':options.adresse1, 'line2':options.adresse2, 'siret':options.siret, 'email':options.contact, 'phone':options.telephone, 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)})
return form({'factureform': facture_form}, 'cotisations/facture.html', request)
return render_tex(request, 'cotisations/factures.tex', {
'DATE': timezone.now(),
'dest': user,
'fid': fid,
'article': tbl,
'total': prix_total,
'paid': paid,
'asso_name': options.name,
'line1': options.adresse1,
'line2': options.adresse2,
'siret': options.siret,
'email': options.contact,
'phone': options.telephone,
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
return form({
'factureform': facture_form
}, 'cotisations/facture.html', request)
@login_required
def facture_pdf(request, factureid):
"""Affiche en pdf une facture. Cree une ligne par Vente de la facture,
et génére une facture avec le total, le moyen de paiement, l'adresse
de l'adhérent, etc. Réservée à self pour un user sans droits,
les droits cableurs permettent d'afficher toute facture"""
try:
facture = Facture.objects.get(pk=factureid)
except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante")
return redirect("/cotisations/")
if not request.user.has_perms(('cableur',)) and facture.user != request.user:
messages.error(request, "Vous ne pouvez pas afficher une facture ne vous appartenant pas sans droit cableur")
if not request.user.has_perms(('cableur',))\
and facture.user != request.user:
messages.error(request, "Vous ne pouvez pas afficher une facture ne vous\
appartenant pas sans droit cableur")
return redirect("/users/profil/" + str(request.user.id))
if not facture.valid:
messages.error(request, "Vous ne pouvez pas afficher une facture non valide")
messages.error(request, "Vous ne pouvez pas afficher\
une facture non valide")
return redirect("/users/profil/" + str(request.user.id))
vente = Vente.objects.all().filter(facture=facture)
ventes_objects = Vente.objects.all().filter(facture=facture)
ventes = []
options, created = AssoOption.objects.get_or_create()
for v in vente:
ventes.append([v, v.number, v.prix_total])
return render_tex(request, 'cotisations/factures.tex', {'paid':True, 'fid':facture.id, 'DATE':facture.date,'dest':facture.user, 'article':ventes, 'total': facture.prix_total(), 'asso_name':options.name, 'line1': options.adresse1, 'line2':options.adresse2, 'siret':options.siret, 'email':options.contact, 'phone':options.telephone, 'tpl_path':os.path.join(settings.BASE_DIR, LOGO_PATH)})
options, _created = AssoOption.objects.get_or_create()
for vente in ventes_objects:
ventes.append([vente, vente.number, vente.prix_total])
return render_tex(request, 'cotisations/factures.tex', {
'paid': True,
'fid': facture.id,
'DATE': facture.date,
'dest': facture.user,
'article': ventes,
'total': facture.prix_total(),
'asso_name': options.name,
'line1': options.adresse1,
'line2': options.adresse2,
'siret': options.siret,
'email': options.contact,
'phone': options.telephone,
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
@login_required
@permission_required('cableur')
def edit_facture(request, factureid):
"""Permet l'édition d'une facture. On peut y éditer les ventes
déjà effectuer, ou rendre une facture invalide (non payées, chèque
en bois etc). Mets à jour les durée de cotisation attenantes"""
try:
facture = Facture.objects.get(pk=factureid)
except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante")
return redirect("/cotisations/")
if request.user.has_perms(['tresorier']):
facture_form = TrezEditFactureForm(request.POST or None, instance=facture)
facture_form = TrezEditFactureForm(
request.POST or None,
instance=facture
)
elif facture.control or not facture.valid:
messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier")
messages.error(request, "Vous ne pouvez pas editer une facture\
controlée ou invalidée par le trésorier")
return redirect("/cotisations/")
else:
facture_form = EditFactureForm(request.POST or None, instance=facture)
ventes_objects = Vente.objects.filter(facture=facture)
vente_form_set = modelformset_factory(Vente, fields=('name','number'), extra=0, max_num=len(ventes_objects))
vente_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(ventes_objects)
)
vente_form = vente_form_set(request.POST or None, queryset=ventes_objects)
if facture_form.is_valid() and vente_form.is_valid():
with transaction.atomic(), reversion.create_revision():
facture_form.save()
vente_form.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for form in vente_form for field in facture_form.changed_data + form.changed_data))
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(
field for form in vente_form for field
in facture_form.changed_data + form.changed_data))
messages.success(request, "La facture a bien été modifiée")
return redirect("/cotisations/")
return form({'factureform': facture_form, 'venteform': vente_form}, 'cotisations/edit_facture.html', request)
return form({
'factureform': facture_form,
'venteform': vente_form
}, 'cotisations/edit_facture.html', request)
@login_required
@permission_required('cableur')
def del_facture(request, factureid):
"""Suppression d'une facture. Supprime en cascade les ventes
et cotisations filles"""
try:
facture = Facture.objects.get(pk=factureid)
except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante")
return redirect("/cotisations/")
if (facture.control or not facture.valid):
messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier")
if facture.control or not facture.valid:
messages.error(request, "Vous ne pouvez pas editer une facture\
controlée ou invalidée par le trésorier")
return redirect("/cotisations/")
if request.method == "POST":
with transaction.atomic(), reversion.create_revision():
@ -193,7 +284,11 @@ def del_facture(request, factureid):
reversion.set_user(request.user)
messages.success(request, "La facture a été détruite")
return redirect("/cotisations/")
return form({'objet': facture, 'objet_name': 'facture'}, 'cotisations/delete.html', request)
return form({
'objet': facture,
'objet_name': 'facture'
}, 'cotisations/delete.html', request)
@login_required
@permission_required('cableur')
@ -212,7 +307,14 @@ def credit_solde(request, userid):
facture_instance.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
new_vente = Vente.objects.create(facture=facture_instance, name="solde", prix=facture.cleaned_data['montant'], iscotisation=False, duration=0, number=1)
new_vente = Vente.objects.create(
facture=facture_instance,
name="solde",
prix=facture.cleaned_data['montant'],
iscotisation=False,
duration=0,
number=1
)
with transaction.atomic(), reversion.create_revision():
new_vente.save()
reversion.set_user(request.user)
@ -225,6 +327,13 @@ def credit_solde(request, userid):
@login_required
@permission_required('tresorier')
def add_article(request):
"""Ajoute un article. Champs : désignation,
prix, est-ce une cotisation et si oui sa durée
Réservé au trésorier
Nota bene : les ventes déjà effectuées ne sont pas reliées
aux articles en vente. La désignation, le prix... sont
copiés à la création de la facture. Un changement de prix n'a
PAS de conséquence sur les ventes déjà faites"""
article = ArticleForm(request.POST or None)
if article.is_valid():
with transaction.atomic(), reversion.create_revision():
@ -235,9 +344,12 @@ def add_article(request):
return redirect("/cotisations/index_article/")
return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def edit_article(request, articleid):
"""Edition d'un article (designation, prix, etc)
Réservé au trésorier"""
try:
article_instance = Article.objects.get(pk=articleid)
except Article.DoesNotExist:
@ -248,14 +360,20 @@ def edit_article(request, articleid):
with transaction.atomic(), reversion.create_revision():
article.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in article.changed_data))
reversion.set_comment(
"Champs modifié(s) : %s" % ', '.join(
field for field in article.changed_data
)
)
messages.success(request, "Type d'article modifié")
return redirect("/cotisations/index_article/")
return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def del_article(request):
"""Suppression d'un article en vente"""
article = DelArticleForm(request.POST or None)
if article.is_valid():
article_del = article.cleaned_data['articles']
@ -266,9 +384,12 @@ def del_article(request):
return redirect("/cotisations/index_article")
return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def add_paiement(request):
"""Ajoute un moyen de paiement. Relié aux factures
via foreign key"""
paiement = PaiementForm(request.POST or None)
if paiement.is_valid():
with transaction.atomic(), reversion.create_revision():
@ -279,9 +400,11 @@ def add_paiement(request):
return redirect("/cotisations/index_paiement/")
return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def edit_paiement(request, paiementid):
"""Edition d'un moyen de paiement"""
try:
paiement_instance = Paiement.objects.get(pk=paiementid)
except Paiement.DoesNotExist:
@ -292,14 +415,20 @@ def edit_paiement(request, paiementid):
with transaction.atomic(), reversion.create_revision():
paiement.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in paiement.changed_data))
reversion.set_comment(
"Champs modifié(s) : %s" % ', '.join(
field for field in paiement.changed_data
)
)
messages.success(request, "Type de paiement modifié")
return redirect("/cotisations/index_paiement/")
return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def del_paiement(request):
"""Suppression d'un moyen de paiement"""
paiement = DelPaiementForm(request.POST or None)
if paiement.is_valid():
paiement_dels = paiement.cleaned_data['paiements']
@ -309,15 +438,24 @@ def del_paiement(request):
paiement_del.delete()
reversion.set_user(request.user)
reversion.set_comment("Destruction")
messages.success(request, "Le moyen de paiement a été supprimé")
messages.success(
request,
"Le moyen de paiement a été supprimé"
)
except ProtectedError:
messages.error(request, "Le moyen de paiement %s est affecté à au moins une facture, vous ne pouvez pas le supprimer" % paiement_del)
messages.error(
request,
"Le moyen de paiement %s est affecté à au moins une\
facture, vous ne pouvez pas le supprimer" % paiement_del
)
return redirect("/cotisations/index_paiement/")
return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required
@permission_required('cableur')
def add_banque(request):
"""Ajoute une banque à la liste des banques"""
banque = BanqueForm(request.POST or None)
if banque.is_valid():
with transaction.atomic(), reversion.create_revision():
@ -328,9 +466,11 @@ def add_banque(request):
return redirect("/cotisations/index_banque/")
return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def edit_banque(request, banqueid):
"""Edite le nom d'une banque"""
try:
banque_instance = Banque.objects.get(pk=banqueid)
except Banque.DoesNotExist:
@ -341,14 +481,20 @@ def edit_banque(request, banqueid):
with transaction.atomic(), reversion.create_revision():
banque.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in banque.changed_data))
reversion.set_comment(
"Champs modifié(s) : %s" % ', '.join(
field for field in banque.changed_data
)
)
messages.success(request, "Banque modifiée")
return redirect("/cotisations/index_banque/")
return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def del_banque(request):
"""Supprime une banque"""
banque = DelBanqueForm(request.POST or None)
if banque.is_valid():
banque_dels = banque.cleaned_data['banques']
@ -360,17 +506,25 @@ def del_banque(request):
reversion.set_comment("Destruction")
messages.success(request, "La banque a été supprimée")
except ProtectedError:
messages.error(request, "La banque %s est affectée à au moins une facture, vous ne pouvez pas la supprimer" % banque_del)
messages.error(request, "La banque %s est affectée à au moins\
une facture, vous ne pouvez pas la supprimer" % banque_del)
return redirect("/cotisations/index_banque/")
return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required
@permission_required('tresorier')
def control(request):
options, created = GeneralOption.objects.get_or_create()
"""Pour le trésorier, vue pour controler en masse les
factures.Case à cocher, pratique"""
options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
facture_list = Facture.objects.order_by('date').reverse()
controlform_set = modelformset_factory(Facture, fields=('control','valid'), extra=0)
controlform_set = modelformset_factory(
Facture,
fields=('control', 'valid'),
extra=0
)
paginator = Paginator(facture_list, pagination_number)
page = request.GET.get('page')
try:
@ -379,7 +533,9 @@ def control(request):
facture_list = paginator.page(1)
except EmptyPage:
facture_list = paginator.page(paginator.num.pages)
page_query = Facture.objects.order_by('date').reverse().filter(id__in=[facture.id for facture in facture_list])
page_query = Facture.objects.order_by('date').reverse().filter(
id__in=[facture.id for facture in facture_list]
)
controlform = controlform_set(request.POST or None, queryset=page_query)
if controlform.is_valid():
with transaction.atomic(), reversion.create_revision():
@ -387,32 +543,50 @@ def control(request):
reversion.set_user(request.user)
reversion.set_comment("Controle trésorier")
return redirect("/cotisations/control/")
return render(request, 'cotisations/control.html', {'facture_list': facture_list, 'controlform': controlform})
return render(request, 'cotisations/control.html', {
'facture_list': facture_list,
'controlform': controlform
})
@login_required
@permission_required('cableur')
def index_article(request):
"""Affiche l'ensemble des articles en vente"""
article_list = Article.objects.order_by('name')
return render(request, 'cotisations/index_article.html', {'article_list':article_list})
return render(request, 'cotisations/index_article.html', {
'article_list': article_list
})
@login_required
@permission_required('cableur')
def index_paiement(request):
"""Affiche l'ensemble des moyens de paiement en vente"""
paiement_list = Paiement.objects.order_by('moyen')
return render(request, 'cotisations/index_paiement.html', {'paiement_list':paiement_list})
return render(request, 'cotisations/index_paiement.html', {
'paiement_list': paiement_list
})
@login_required
@permission_required('cableur')
def index_banque(request):
"""Affiche l'ensemble des banques"""
banque_list = Banque.objects.order_by('name')
return render(request, 'cotisations/index_banque.html', {'banque_list':banque_list})
return render(request, 'cotisations/index_banque.html', {
'banque_list': banque_list
})
@login_required
@permission_required('cableur')
def index(request):
options, created = GeneralOption.objects.get_or_create()
"""Affiche l'ensemble des factures, pour les cableurs et +"""
options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
facture_list = Facture.objects.order_by('date').select_related('user').select_related('paiement').prefetch_related('vente_set').reverse()
facture_list = Facture.objects.order_by('date').select_related('user')\
.select_related('paiement').prefetch_related('vente_set').reverse()
paginator = Paginator(facture_list, pagination_number)
page = request.GET.get('page')
try:
@ -423,41 +597,47 @@ def index(request):
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
facture_list = paginator.page(paginator.num_pages)
return render(request, 'cotisations/index.html', {'facture_list': facture_list})
return render(request, 'cotisations/index.html', {
'facture_list': facture_list
})
@login_required
def history(request, object, id):
def history(request, object, object_id):
"""Affiche l'historique de chaque objet"""
if object == 'facture':
try:
object_instance = Facture.objects.get(pk=id)
object_instance = Facture.objects.get(pk=object_id)
except Facture.DoesNotExist:
messages.error(request, "Facture inexistante")
return redirect("/cotisations/")
if not request.user.has_perms(('cableur',)) and object_instance.user != request.user:
messages.error(request, "Vous ne pouvez pas afficher l'historique d'une facture d'un autre user que vous sans droit cableur")
if not request.user.has_perms(('cableur',))\
and object_instance.user != request.user:
messages.error(request, "Vous ne pouvez pas afficher l'historique\
d'une facture d'un autre user que vous sans droit cableur")
return redirect("/users/profil/" + str(request.user.id))
elif object == 'paiement' and request.user.has_perms(('cableur',)):
try:
object_instance = Paiement.objects.get(pk=id)
object_instance = Paiement.objects.get(pk=object_id)
except Paiement.DoesNotExist:
messages.error(request, "Paiement inexistant")
return redirect("/cotisations/")
elif object == 'article' and request.user.has_perms(('cableur',)):
try:
object_instance = Article.objects.get(pk=id)
object_instance = Article.objects.get(pk=object_id)
except Article.DoesNotExist:
messages.error(request, "Article inexistante")
return redirect("/cotisations/")
elif object == 'banque' and request.user.has_perms(('cableur',)):
try:
object_instance = Banque.objects.get(pk=id)
object_instance = Banque.objects.get(pk=object_id)
except Banque.DoesNotExist:
messages.error(request, "Banque inexistante")
return redirect("/cotisations/")
else:
messages.error(request, "Objet inconnu")
return redirect("/cotisations/")
options, created = GeneralOption.objects.get_or_create()
options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number)
@ -470,4 +650,7 @@ def history(request, object, id):
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
reversions = paginator.page(paginator.num_pages)
return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance})
return render(request, 're2o/history.html', {
'reversions': reversions,
'object': object_instance
})

View file

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

View file

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

View file

@ -23,35 +23,41 @@
# App de gestion des statistiques pour re2o
# Gabriel Détraz
# Gplv2
"""
Vues des logs et statistiques générales.
La vue index générale affiche une selection des dernières actions,
classées selon l'importance, avec date, et user formatés.
Stats_logs renvoie l'ensemble des logs.
Les autres vues sont thématiques, ensemble des statistiques et du
nombre d'objets par models, nombre d'actions par user, etc
"""
from __future__ import unicode_literals
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.shortcuts import get_object_or_404
from django.template.context_processors import csrf
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import ProtectedError
from django.forms import ValidationError
from django.db import transaction
from django.db.models import Count
from reversion.models import Revision
from reversion.models import Version, ContentType
from users.models import User, ServiceUser, Right, School, ListRight, ListShell, Ban, Whitelist
from users.models import all_has_access, all_whitelisted, all_baned, all_adherent
from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation
from machines.models import Machine, MachineType, IpType, Extension, Interface, Domain, IpList
from machines.views import all_active_assigned_interfaces_count, all_active_interfaces_count
from users.models import User, ServiceUser, Right, School, ListRight, ListShell
from users.models import Ban, Whitelist
from cotisations.models import Facture, Vente, Article, Banque, Paiement
from cotisations.models import Cotisation
from machines.models import Machine, MachineType, IpType, Extension, Interface
from machines.models import Domain, IpList
from topologie.models import Switch, Port, Room
from preferences.models import GeneralOption
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from re2o.views import form
from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent
from re2o.utils import all_active_assigned_interfaces_count
from re2o.utils import all_active_interfaces_count
STATS_DICT = {
0: ["Tout", 36],
@ -62,23 +68,22 @@ STATS_DICT = {
5: ["2 an", 24],
}
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required
@permission_required('cableur')
def index(request):
options, created = GeneralOption.objects.get_or_create()
"""Affiche les logs affinés, date reformatées, selectionne
les event importants (ajout de droits, ajout de ban/whitelist)"""
options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
# The types of content kept for display
content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user']
# Select only wanted versions
versions = Version.objects.filter(content_type__in=ContentType.objects.filter(model__in=content_type_filter)).order_by('revision__date_created').reverse().select_related('revision')
versions = Version.objects.filter(
content_type__in=ContentType.objects.filter(
model__in=content_type_filter
)
).order_by('revision__date_created').reverse().select_related('revision')
paginator = Paginator(versions, pagination_number)
page = request.GET.get('page')
try:
@ -97,28 +102,36 @@ def index(request):
# Parse every item (max = pagination_number)
for i in range(len(versions.object_list)):
if versions.object_list[i].object:
v = versions.object_list[i]
version = versions.object_list[i]
versions.object_list[i] = {
'rev_id' : v.revision.id,
'comment': v.revision.comment,
'datetime': v.revision.date_created.strftime('%d/%m/%y %H:%M:%S'),
'username': v.revision.user.get_username() if v.revision.user else '?',
'user_id': v.revision.user_id,
'version': v }
'rev_id': version.revision.id,
'comment': version.revision.comment,
'datetime': version.revision.date_created.strftime(
'%d/%m/%y %H:%M:%S'
),
'username':
version.revision.user.get_username()
if version.revision.user else '?',
'user_id': version.revision.user_id,
'version': version}
else:
to_remove.insert(0, i)
# Remove all tagged invalid items
for i in to_remove:
versions.object_list.pop(i)
return render(request, 'logs/index.html', {'versions_list': versions})
@login_required
@permission_required('cableur')
def stats_logs(request):
options, created = GeneralOption.objects.get_or_create()
"""Affiche l'ensemble des logs et des modifications sur les objets,
classés par date croissante, en vrac"""
options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
revisions = Revision.objects.all().order_by('date_created').reverse().select_related('user').prefetch_related('version_set__object')
revisions = Revision.objects.all().order_by('date_created')\
.reverse().select_related('user')\
.prefetch_related('version_set__object')
paginator = Paginator(revisions, pagination_number)
page = request.GET.get('page')
try:
@ -129,7 +142,10 @@ def stats_logs(request):
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
revisions = paginator.page(paginator.num_pages)
return render(request, 'logs/stats_logs.html', {'revisions_list': revisions})
return render(request, 'logs/stats_logs.html', {
'revisions_list': revisions
})
@login_required
@permission_required('bureau')
@ -143,31 +159,59 @@ def revert_action(request, revision_id):
revision.revert()
messages.success(request, "L'action a été supprimée")
return redirect("/logs/")
return form({'objet': revision, 'objet_name': revision.__class__.__name__ }, 'logs/delete.html', request)
return form({
'objet': revision,
'objet_name': revision.__class__.__name__
}, 'logs/delete.html', request)
@login_required
@permission_required('cableur')
def stats_general(request):
all_active_users = User.objects.filter(state=User.STATE_ACTIVE)
ip = dict()
"""Statistiques générales affinées sur les ip, activées, utilisées par
range, et les statistiques générales sur les users : users actifs,
cotisants, activés, archivés, etc"""
ip_dict = dict()
for ip_range in IpType.objects.all():
all_ip = IpList.objects.filter(ip_type=ip_range)
used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
active_ip = all_active_assigned_interfaces_count().filter(ipv4__in=IpList.objects.filter(ip_type=ip_range)).count()
ip[ip_range] = [ip_range, all_ip.count(), used_ip, active_ip, all_ip.count()-used_ip]
active_ip = all_active_assigned_interfaces_count().filter(
ipv4__in=IpList.objects.filter(ip_type=ip_range)
).count()
ip_dict[ip_range] = [ip_range, all_ip.count(),
used_ip, active_ip, all_ip.count()-used_ip]
stats = [
[["Categorie", "Nombre d'utilisateurs"], {
'active_users' : ["Users actifs", User.objects.filter(state=User.STATE_ACTIVE).count()],
'inactive_users' : ["Users désactivés", User.objects.filter(state=User.STATE_DISABLED).count()],
'archive_users' : ["Users archivés", User.objects.filter(state=User.STATE_ARCHIVE).count()],
'adherent_users' : ["Adhérents à l'association", all_adherent().count()],
'connexion_users' : ["Utilisateurs bénéficiant d'une connexion", all_has_access().count()],
'ban_users' : ["Utilisateurs bannis", all_baned().count()],
'whitelisted_user' : ["Utilisateurs bénéficiant d'une connexion gracieuse", all_whitelisted().count()],
'actives_interfaces' : ["Interfaces actives (ayant accès au reseau)", all_active_interfaces_count().count()],
'actives_assigned_interfaces' : ["Interfaces actives et assignées ipv4", all_active_assigned_interfaces_count().count()]
'active_users': [
"Users actifs",
User.objects.filter(state=User.STATE_ACTIVE).count()],
'inactive_users': [
"Users désactivés",
User.objects.filter(state=User.STATE_DISABLED).count()],
'archive_users': [
"Users archivés",
User.objects.filter(state=User.STATE_ARCHIVE).count()],
'adherent_users': [
"Adhérents à l'association",
all_adherent().count()],
'connexion_users': [
"Utilisateurs bénéficiant d'une connexion",
all_has_access().count()],
'ban_users': [
"Utilisateurs bannis",
all_baned().count()],
'whitelisted_user': [
"Utilisateurs bénéficiant d'une connexion gracieuse",
all_whitelisted().count()],
'actives_interfaces': [
"Interfaces actives (ayant accès au reseau)",
all_active_interfaces_count().count()],
'actives_assigned_interfaces': [
"Interfaces actives et assignées ipv4",
all_active_assigned_interfaces_count().count()]
}],
[["Range d'ip", "Nombre d'ip totales", "Ip assignées", "Ip assignées à une machine active", "Ip non assignées"] ,ip]
[["Range d'ip", "Nombre d'ip totales", "Ip assignées",
"Ip assignées à une machine active", "Ip non assignées"], ip_dict]
]
return render(request, 'logs/stats_general.html', {'stats_list': stats})
@ -175,11 +219,14 @@ def stats_general(request):
@login_required
@permission_required('cableur')
def stats_models(request):
all_active_users = User.objects.filter(state=User.STATE_ACTIVE)
"""Statistiques générales, affiche les comptages par models:
nombre d'users, d'écoles, de droits, de bannissements,
de factures, de ventes, de banque, de machines, etc"""
stats = {
'Users': {
'users': [User.PRETTY_NAME, User.objects.count()],
'serviceuser' : [ServiceUser.PRETTY_NAME, ServiceUser.objects.count()],
'serviceuser': [ServiceUser.PRETTY_NAME,
ServiceUser.objects.count()],
'right': [Right.PRETTY_NAME, Right.objects.count()],
'school': [School.PRETTY_NAME, School.objects.count()],
'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()],
@ -193,15 +240,16 @@ def stats_models(request):
'cotisation': [Cotisation.PRETTY_NAME, Cotisation.objects.count()],
'article': [Article.PRETTY_NAME, Article.objects.count()],
'banque': [Banque.PRETTY_NAME, Banque.objects.count()],
'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()],
},
'Machines': {
'machine': [Machine.PRETTY_NAME, Machine.objects.count()],
'typemachine' : [MachineType.PRETTY_NAME, MachineType.objects.count()],
'typemachine': [MachineType.PRETTY_NAME,
MachineType.objects.count()],
'typeip': [IpType.PRETTY_NAME, IpType.objects.count()],
'extension': [Extension.PRETTY_NAME, Extension.objects.count()],
'interface': [Interface.PRETTY_NAME, Interface.objects.count()],
'alias' : [Domain.PRETTY_NAME, Domain.objects.exclude(cname=None).count()],
'alias': [Domain.PRETTY_NAME,
Domain.objects.exclude(cname=None).count()],
'iplist': [IpList.PRETTY_NAME, IpList.objects.count()],
},
'Topologie': {
@ -216,43 +264,72 @@ def stats_models(request):
}
return render(request, 'logs/stats_models.html', {'stats_list': stats})
@login_required
@permission_required('cableur')
def stats_users(request):
"""Affiche les statistiques base de données aggrégées par user :
nombre de machines par user, d'etablissements par user,
de moyens de paiements par user, de banque par user,
de bannissement par user, etc"""
onglet = request.GET.get('onglet')
try:
search_field = STATS_DICT[onglet]
except:
search_field = STATS_DICT[0]
_search_field = STATS_DICT[onglet]
except KeyError:
_search_field = STATS_DICT[0]
onglet = 0
start_date = timezone.now() + relativedelta(months=-search_field[1])
stats = {
'Utilisateur': {
'Machines' : User.objects.annotate(num=Count('machine')).order_by('-num')[:10],
'Facture' : User.objects.annotate(num=Count('facture')).order_by('-num')[:10],
'Bannissement' : User.objects.annotate(num=Count('ban')).order_by('-num')[:10],
'Accès gracieux' : User.objects.annotate(num=Count('whitelist')).order_by('-num')[:10],
'Droits' : User.objects.annotate(num=Count('right')).order_by('-num')[:10],
'Machines': User.objects.annotate(
num=Count('machine')
).order_by('-num')[:10],
'Facture': User.objects.annotate(
num=Count('facture')
).order_by('-num')[:10],
'Bannissement': User.objects.annotate(
num=Count('ban')
).order_by('-num')[:10],
'Accès gracieux': User.objects.annotate(
num=Count('whitelist')
).order_by('-num')[:10],
'Droits': User.objects.annotate(
num=Count('right')
).order_by('-num')[:10],
},
'Etablissement': {
'Utilisateur' : School.objects.annotate(num=Count('user')).order_by('-num')[:10],
'Utilisateur': School.objects.annotate(
num=Count('user')
).order_by('-num')[:10],
},
'Moyen de paiement': {
'Utilisateur' : Paiement.objects.annotate(num=Count('facture')).order_by('-num')[:10],
'Utilisateur': Paiement.objects.annotate(
num=Count('facture')
).order_by('-num')[:10],
},
'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
@permission_required('cableur')
def stats_actions(request):
onglet = request.GET.get('onglet')
"""Vue qui affiche les statistiques de modifications d'objets par
utilisateurs.
Affiche le nombre de modifications aggrégées par utilisateurs"""
stats = {
'Utilisateur': {
'Action' : User.objects.annotate(num=Count('revision')).order_by('-num')[:40],
'Action': User.objects.annotate(
num=Count('revision')
).order_by('-num')[:40],
},
}
return render(request, 'logs/stats_users.html', {'stats_list': stats})

View file

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

View file

@ -31,15 +31,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th></th>
</tr>
</thead>
{% for text in text_list %}
{% for txt in txt_list %}
<tr>
<td>{{ text.zone }}</td>
<td>{{ text.dns_entry }}</td>
<td>{{ txt.zone }}</td>
<td>{{ txt.dns_entry }}</td>
<td class="text-right">
{% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-text' id=text.id %}
{% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
{% endif %}
{% include 'buttons/history.html' with href='machines:history' name='text' id=text.id %}
{% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %}
</td>
</tr>
{% endfor %}

View file

@ -47,12 +47,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-ns' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement NS</a>
{% endif %}
{% 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 %}
<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-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-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-txt' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement TXT</a>
{% endif %}
{% include "machines/aff_text.html" with text_list=text_list %}
{% include "machines/aff_txt.html" with txt_list=txt_list %}
<br />
<br />
<br />

View file

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

View file

@ -1,386 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django import template
from django.utils.safestring import mark_safe
from django.forms import TextInput
from bootstrap3.templatetags.bootstrap3 import bootstrap_form
from bootstrap3.utils import render_tag
from bootstrap3.forms import render_field
register = template.Library()
@register.simple_tag
def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs):
"""
Render a form where some specific fields are rendered using Typeahead.
Using Typeahead really improves the performance, the speed and UX when
dealing with very large datasets (select with 50k+ elts for instance).
For convenience, it accepts the same parameters as a standard bootstrap
can accept.
**Tag name**::
bootstrap_form_typeahead
**Parameters**:
form
The form that is to be rendered
typeahead_fields
A list of field names (comma separated) that should be rendered
with typeahead instead of the default bootstrap renderer.
bft_param
A dict of parameters for the bootstrap_form_typeahead tag. The
possible parameters are the following.
choices
A dict of strings representing the choices in JS. The keys of
the dict are the names of the concerned fields. The choices
must be an array of objects. Each of those objects must at
least have the fields 'key' (value to send) and 'value' (value
to display). Other fields can be added as desired.
For a more complex structure you should also consider
reimplementing the engine and the match_func.
If not specified, the key is the id of the object and the value
is its string representation as in a normal bootstrap form.
Example :
'choices' : {
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]',
'field_B':...,
...
}
engine
A dict of strings representating the engine used for matching
queries and possible values with typeahead. The keys of the
dict are the names of the concerned fields. The string is valid
JS code.
If not specified, BloodHound with relevant basic properties is
used.
Example :
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...}
match_func
A dict of strings representing a valid JS function used in the
dataset to overload the matching engine. The keys of the dict
are the names of the concerned fields. This function is used
the source of the dataset. This function receives 2 parameters,
the query and the synchronize function as specified in
typeahead.js documentation. If needed, the local variables
'choices_<fieldname>' and 'engine_<fieldname>' contains
respectively the array of all possible values and the engine
to match queries with possible values.
If not specified, the function used display up to the 10 first
elements if the query is empty and else the matching results.
Example :
'match_func' : {
'field_A': 'function(q, sync) { engine.search(q, sync); }',
'field_B': ...,
...
}
update_on
A dict of list of ids that the values depends on. The engine
and the typeahead properties are recalculated and reapplied.
Example :
'addition' : {
'field_A' : [ 'id0', 'id1', ... ] ,
'field_B' : ... ,
...
}
See boostrap_form_ for other arguments
**Usage**::
{% bootstrap_form_typeahead
form
[ '<field1>[,<field2>[,...]]' ]
[ {
[ 'choices': {
[ '<field1>': '<choices1>'
[, '<field2>': '<choices2>'
[, ... ] ] ]
} ]
[, 'engine': {
[ '<field1>': '<engine1>'
[, '<field2>': '<engine2>'
[, ... ] ] ]
} ]
[, 'match_func': {
[ '<field1>': '<match_func1>'
[, '<field2>': '<match_func2>'
[, ... ] ] ]
} ]
[, 'update_on': {
[ '<field1>': '<update_on1>'
[, '<field2>': '<update_on2>'
[, ... ] ] ]
} ]
} ]
[ <standard boostrap_form parameters> ]
%}
**Example**:
{% bootstrap_form_typeahead form 'ipv4' choices='[...]' %}
"""
t_fields = typeahead_fields.split(',')
params = kwargs.get('bft_param', {})
exclude = params.get('exclude', None)
exclude = exclude.split(',') if exclude else []
t_choices = params.get('choices', {})
t_engine = params.get('engine', {})
t_match_func = params.get('match_func', {})
t_update_on = params.get('update_on', {})
hidden = [h.name for h in django_form.hidden_fields()]
form = ''
for f_name, f_value in django_form.fields.items() :
if not f_name in exclude :
if f_name in t_fields and not f_name in hidden :
f_bound = f_value.get_bound_field( django_form, f_name )
f_value.widget = TextInput(
attrs={
'name': 'typeahead_'+f_name,
'placeholder': f_value.empty_label
}
)
form += render_field(
f_value.get_bound_field( django_form, f_name ),
*args,
**kwargs
)
form += render_tag(
'div',
content = hidden_tag( f_bound, f_name ) +
typeahead_js(
f_name,
f_value,
f_bound,
t_choices,
t_engine,
t_match_func,
t_update_on
)
)
else:
form += render_field(
f_value.get_bound_field(django_form, f_name),
*args,
**kwargs
)
return mark_safe( form )
def input_id( f_name ) :
""" The id of the HTML input element """
return 'id_'+f_name
def hidden_id( f_name ):
""" The id of the HTML hidden input element """
return 'typeahead_hidden_'+f_name
def hidden_tag( f_bound, f_name ):
""" The HTML hidden input element """
return render_tag(
'input',
attrs={
'id': hidden_id(f_name),
'name': f_name,
'type': 'hidden',
'value': f_bound.value() or ""
}
)
def typeahead_js( f_name, f_value, f_bound,
t_choices, t_engine, t_match_func, t_update_on ) :
""" The whole script to use """
choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \
else default_choices( f_value )
engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \
else default_engine ( f_name )
match_func = mark_safe(t_match_func[f_name]) \
if f_name in t_match_func.keys() else default_match_func( f_name )
update_on = t_update_on[f_name] if f_name in t_update_on.keys() else []
js_content = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
'engine_{f_name} = {engine};'
'$( "#{input_id}" ).typeahead( "destroy" );'
'$( "#{input_id}" ).typeahead( {datasets} );'
'}};'
'$( "#{input_id}" ).bind( "typeahead:select", {updater} );'
'$( "#{input_id}" ).bind( "typeahead:change", {change} );'
'{updates}'
'$( "#{input_id}" ).ready( function() {{'
'setup_{f_name}();'
'{init_input}'
'}} );'
).format(
f_name = f_name,
choices = choices,
engine = engine,
input_id = input_id( f_name ),
datasets = default_datasets( f_name, match_func ),
updater = typeahead_updater( f_name ),
change = typeahead_change( f_name ),
updates = ''.join( [ (
'$( "#{u_id}" ).change( function() {{'
'setup_{f_name}();'
'{reset_input}'
'}} );'
).format(
u_id = u_id,
reset_input = reset_input( f_name ),
f_name = f_name
) for u_id in update_on ]
),
init_input = init_input( f_name, f_bound ),
)
return render_tag( 'script', content=mark_safe( js_content ) )
def init_input( f_name, f_bound ) :
""" The JS script to init the fields values """
init_key = f_bound.value() or '""'
return (
'$( "#{input_id}" ).typeahead("val", {init_val});'
'$( "#{hidden_id}" ).val( {init_key} );'
).format(
input_id = input_id( f_name ),
init_val = '""' if init_key == '""' else
'engine_{f_name}.get( {init_key} )[0].value'.format(
f_name = f_name,
init_key = init_key
),
init_key = init_key,
hidden_id = hidden_id( f_name )
)
def reset_input( f_name ) :
""" The JS script to reset the fields values """
return (
'$( "#{input_id}" ).typeahead("val", "");'
'$( "#{hidden_id}" ).val( "" );'
).format(
input_id = input_id( f_name ),
hidden_id = hidden_id( f_name )
)
def default_choices( f_value ) :
""" The JS script creating the variable choices_<fieldname> """
return '[{objects}]'.format(
objects = ','.join(
[ '{{key:{k},value:"{v}"}}'.format(
k = choice[0] if choice[0] != '' else '""',
v = choice[1]
) for choice in f_value.choices ]
)
)
def default_engine ( f_name ) :
""" The JS script creating the variable engine_<field_name> """
return (
'new Bloodhound({{'
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
'queryTokenizer: Bloodhound.tokenizers.whitespace,'
'local: choices_{f_name},'
'identify: function(obj) {{ return obj.key; }}'
'}})'
).format(
f_name = f_name
)
def default_datasets( f_name, match_func ) :
""" The JS script creating the datasets to use with typeahead """
return (
'{{'
'hint: true,'
'highlight: true,'
'minLength: 0'
'}},'
'{{'
'display: "value",'
'name: "{f_name}",'
'source: {match_func}'
'}}'
).format(
f_name = f_name,
match_func = match_func
)
def default_match_func ( f_name ) :
""" The JS script creating the matching function to use with typeahed """
return (
'function ( q, sync ) {{'
'if ( q === "" ) {{'
'var first = choices_{f_name}.slice( 0, 5 ).map('
'function ( obj ) {{ return obj.key; }}'
');'
'sync( engine_{f_name}.get( first ) );'
'}} else {{'
'engine_{f_name}.search( q, sync );'
'}}'
'}}'
).format(
f_name = f_name
)
def typeahead_updater( f_name ):
""" The JS script creating the function triggered when an item is
selected through typeahead """
return (
'function(evt, item) {{'
'$( "#{hidden_id}" ).val( item.key );'
'$( "#{hidden_id}" ).change();'
'return item;'
'}}'
).format(
hidden_id = hidden_id( f_name )
)
def typeahead_change( f_name ):
""" The JS script creating the function triggered when an item is changed
(i.e. looses focus and value has changed since the moment it gained focus
"""
return (
'function(evt) {{'
'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{'
'$( "#{hidden_id}" ).val( "" );'
'$( "#{hidden_id}" ).change();'
'}}'
'}}'
).format(
input_id = input_id( f_name ),
hidden_id = hidden_id( f_name )
)

View file

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

View file

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

View file

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

View file

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

View file

@ -20,26 +20,38 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Reglages généraux, machines, utilisateurs, mail, general pour l'application.
"""
from __future__ import unicode_literals
from django.db import models
from cotisations.models import Paiement
from machines.models import Vlan
class OptionalUser(models.Model):
"""Options pour l'user : obligation ou nom du telephone,
activation ou non du solde, autorisation du negatif, fingerprint etc"""
PRETTY_NAME = "Options utilisateur"
is_tel_mandatory = models.BooleanField(default=True)
user_solde = models.BooleanField(default=False)
solde_negatif = models.DecimalField(max_digits=5, decimal_places=2, default=0)
solde_negatif = models.DecimalField(
max_digits=5,
decimal_places=2,
default=0
)
gpg_fingerprint = models.BooleanField(default=True)
def clean(self):
"""Creation du mode de paiement par solde"""
if self.user_solde:
Paiement.objects.get_or_create(moyen="Solde")
class OptionalMachine(models.Model):
"""Options pour les machines : maximum de machines ou d'alias par user
sans droit, activation de l'ipv6"""
PRETTY_NAME = "Options machines"
password_machine = models.BooleanField(default=False)
@ -47,21 +59,43 @@ class OptionalMachine(models.Model):
max_lambdauser_aliases = models.IntegerField(default=10)
ipv6 = models.BooleanField(default=False)
class OptionalTopologie(models.Model):
"""Reglages pour la topologie : mode d'accès radius, vlan où placer
les machines en accept ou reject"""
PRETTY_NAME = "Options topologie"
MACHINE = 'MACHINE'
DEFINED = 'DEFINED'
CHOICE_RADIUS = (
(MACHINE, 'Sur le vlan de la plage ip machine'),
(DEFINED, 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"'),
(DEFINED, 'Prédéfini dans "Vlan où placer les machines\
après acceptation RADIUS"'),
)
radius_general_policy = models.CharField(
max_length=32,
choices=CHOICE_RADIUS,
default='DEFINED'
)
vlan_decision_ok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='decision_ok',
blank=True,
null=True
)
vlan_decision_nok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='decision_nok',
blank=True,
null=True
)
radius_general_policy = models.CharField(max_length=32, choices=CHOICE_RADIUS, default='DEFINED')
vlan_decision_ok = models.OneToOneField('machines.Vlan', on_delete=models.PROTECT, related_name='decision_ok', blank=True, null=True)
vlan_decision_nok = models.OneToOneField('machines.Vlan', on_delete=models.PROTECT, related_name='decision_nok', blank=True, null=True)
class GeneralOption(models.Model):
"""Options générales : nombre de resultats par page, nom du site,
temps les liens sont valides"""
PRETTY_NAME = "Options générales"
search_display_page = models.IntegerField(default=15)
@ -71,7 +105,10 @@ class GeneralOption(models.Model):
site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@serveur.net")
class Service(models.Model):
"""Liste des services affichés sur la page d'accueil : url, description,
image et nom"""
name = models.CharField(max_length=32)
url = models.URLField()
description = models.TextField()
@ -80,21 +117,32 @@ class Service(models.Model):
def __str__(self):
return str(self.name)
class AssoOption(models.Model):
"""Options générales de l'asso : siret, addresse, nom, etc"""
PRETTY_NAME = "Options de l'association"
name = models.CharField(default="Association réseau école machin", max_length=256)
name = models.CharField(
default="Association réseau école machin",
max_length=256
)
siret = models.CharField(default="00000000000000", max_length=32)
adresse1 = models.CharField(default="1 Rue de exemple", max_length=128)
adresse2 = models.CharField(default="94230 Cachan", max_length=128)
contact = models.EmailField(default="contact@example.org")
telephone = models.CharField(max_length=15, default="0000000000")
pseudo = models.CharField(default="Asso", max_length=32)
utilisateur_asso = models.OneToOneField('users.User', on_delete=models.PROTECT, blank=True, null=True)
utilisateur_asso = models.OneToOneField(
'users.User',
on_delete=models.PROTECT,
blank=True,
null=True
)
class MailMessageOption(models.Model):
"""Reglages, mail de bienvenue et autre"""
PRETTY_NAME = "Options de corps de mail"
welcome_mail_fr = models.TextField(default="")
welcome_mail_en = models.TextField(default="")

View file

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

View file

@ -19,6 +19,9 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Urls de l'application preferences, pointant vers les fonctions de views
"""
from __future__ import unicode_literals
@ -28,15 +31,47 @@ from . import views
urlpatterns = [
url(r'^edit_options/(?P<section>OptionalUser)$', 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'^edit_options/(?P<section>OptionalUser)$',
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'^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'^history/(?P<object>service)/(?P<id>[0-9]+)$', views.history, name='history'),
url(
r'^history/(?P<object>service)/(?P<id>[0-9]+)$',
views.history,
name='history'
),
url(r'^$', views.display_options, name='display-options'),
]

View file

@ -23,48 +23,53 @@
# App de gestion des machines pour re2o
# Gabriel Détraz, Augustin Lemesle
# Gplv2
"""
Vue d'affichage, et de modification des réglages (réglages machine,
topologie, users, service...)
"""
from __future__ import unicode_literals
from django.shortcuts import render
from django.shortcuts import get_object_or_404, render, redirect
from django.template.context_processors import csrf
from django.shortcuts import render, redirect
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import Max, ProtectedError
from django.db import IntegrityError
from django.core.mail import send_mail
from django.utils import timezone
from django.core.urlresolvers import reverse
from django.db.models import ProtectedError
from django.db import transaction
from reversion.models import Version
from reversion import revisions as reversion
from re2o.views import form
from .forms import ServiceForm, DelServiceForm
from .models import Service, OptionalUser, OptionalMachine, AssoOption, MailMessageOption, GeneralOption, OptionalTopologie
from .models import Service, OptionalUser, OptionalMachine, AssoOption
from .models import MailMessageOption, GeneralOption, OptionalTopologie
from . import models
from . import forms
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
@login_required
@permission_required('cableur')
def display_options(request):
useroptions, created = OptionalUser.objects.get_or_create()
machineoptions, created = OptionalMachine.objects.get_or_create()
topologieoptions, created = OptionalTopologie.objects.get_or_create()
generaloptions, created = GeneralOption.objects.get_or_create()
assooptions, created = AssoOption.objects.get_or_create()
mailmessageoptions, created = MailMessageOption.objects.get_or_create()
"""Vue pour affichage des options (en vrac) classé selon les models
correspondants dans un tableau"""
useroptions, _created = OptionalUser.objects.get_or_create()
machineoptions, _created = OptionalMachine.objects.get_or_create()
topologieoptions, _created = OptionalTopologie.objects.get_or_create()
generaloptions, _created = GeneralOption.objects.get_or_create()
assooptions, _created = AssoOption.objects.get_or_create()
mailmessageoptions, _created = MailMessageOption.objects.get_or_create()
service_list = Service.objects.all()
return form({'useroptions': useroptions, 'machineoptions': machineoptions, 'topologieoptions': topologieoptions, 'generaloptions': generaloptions, 'assooptions' : assooptions, 'mailmessageoptions' : mailmessageoptions, 'service_list':service_list}, 'preferences/display_preferences.html', request)
return form({
'useroptions': useroptions,
'machineoptions': machineoptions,
'topologieoptions': topologieoptions,
'generaloptions': generaloptions,
'assooptions': assooptions,
'mailmessageoptions': mailmessageoptions,
'service_list': service_list
}, 'preferences/display_preferences.html', request)
@login_required
@permission_required('admin')
@ -73,23 +78,36 @@ def edit_options(request, section):
model = getattr(models, section, None)
form_instance = getattr(forms, 'Edit' + section + 'Form', None)
if model and form:
options_instance, created = model.objects.get_or_create()
options = form_instance(request.POST or None, instance=options_instance)
options_instance, _created = model.objects.get_or_create()
options = form_instance(
request.POST or None,
instance=options_instance
)
if options.is_valid():
with transaction.atomic(), reversion.create_revision():
options.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in options.changed_data))
reversion.set_comment(
"Champs modifié(s) : %s" % ', '.join(
field for field in options.changed_data
)
)
messages.success(request, "Préférences modifiées")
return redirect("/preferences/")
return form({'options': options}, 'preferences/edit_preferences.html', request)
return form(
{'options': options},
'preferences/edit_preferences.html',
request
)
else:
messages.error(request, "Objet inconnu")
return redirect("/preferences/")
@login_required
@permission_required('admin')
def add_services(request):
"""Ajout d'un service de la page d'accueil"""
services = ServiceForm(request.POST or None)
if services.is_valid():
with transaction.atomic(), reversion.create_revision():
@ -98,11 +116,17 @@ def add_services(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement ns a été ajouté")
return redirect("/preferences/")
return form({'preferenceform': services}, 'preferences/preferences.html', request)
return form(
{'preferenceform': services},
'preferences/preferences.html',
request
)
@login_required
@permission_required('admin')
def edit_services(request, servicesid):
"""Edition des services affichés sur la page d'accueil"""
try:
services_instance = Service.objects.get(pk=servicesid)
except Service.DoesNotExist:
@ -113,14 +137,24 @@ def edit_services(request, servicesid):
with transaction.atomic(), reversion.create_revision():
services.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in services.changed_data))
reversion.set_comment(
"Champs modifié(s) : %s" % ', '.join(
field for field in services.changed_data
)
)
messages.success(request, "Service modifié")
return redirect("/preferences/")
return form({'preferenceform': services}, 'preferences/preferences.html', request)
return form(
{'preferenceform': services},
'preferences/preferences.html',
request
)
@login_required
@permission_required('admin')
def del_services(request):
"""Suppression d'un service de la page d'accueil"""
services = DelServiceForm(request.POST or None)
if services.is_valid():
services_dels = services.cleaned_data['services']
@ -131,20 +165,28 @@ def del_services(request):
reversion.set_user(request.user)
messages.success(request, "Le services a été supprimée")
except ProtectedError:
messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % services_del)
messages.error(request, "Erreur le service\
suivant %s ne peut être supprimé" % services_del)
return redirect("/preferences/")
return form({'preferenceform': services}, 'preferences/preferences.html', request)
return form(
{'preferenceform': services},
'preferences/preferences.html',
request
)
@login_required
@permission_required('cableur')
def history(request, object, id):
if object == 'service':
def history(request, object_name, object_id):
"""Historique de creation et de modification d'un service affiché sur
la page d'accueil"""
if object_name == 'service':
try:
object_instance = Service.objects.get(pk=id)
object_instance = Service.objects.get(pk=object_id)
except Service.DoesNotExist:
messages.error(request, "Service inexistant")
return redirect("/preferences/")
options, created = GeneralOption.objects.get_or_create()
options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number)
@ -157,4 +199,7 @@ def history(request, object, id):
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
reversions = paginator.page(paginator.num_pages)
return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance})
return render(request, 're2o/history.html', {
'reversions': reversions,
'object': object_instance
})

View file

@ -19,15 +19,19 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Fonction de context, variables renvoyées à toutes les vues"""
from __future__ import unicode_literals
from machines.models import Interface, Machine
from preferences.models import GeneralOption, OptionalMachine
def context_user(request):
general_options, created = GeneralOption.objects.get_or_create()
machine_options, created = OptionalMachine.objects.get_or_create()
"""Fonction de context lorsqu'un user est logué (ou non),
renvoie les infos sur l'user, la liste de ses droits, ses machines"""
general_options, _created = GeneralOption.objects.get_or_create()
machine_options, _created = OptionalMachine.objects.get_or_create()
user = request.user
if user.is_authenticated():
interfaces = user.user_interfaces()

View file

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

View file

@ -49,10 +49,16 @@ urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^users/', include('users.urls', namespace='users')),
url(r'^search/', include('search.urls', namespace='search')),
url(r'^cotisations/', include('cotisations.urls', namespace='cotisations')),
url(
r'^cotisations/',
include('cotisations.urls', namespace='cotisations')
),
url(r'^machines/', include('machines.urls', namespace='machines')),
url(r'^topologie/', include('topologie.urls', namespace='topologie')),
url(r'^logs/', include('logs.urls', namespace='logs')),
url(r'^preferences/', include('preferences.urls', namespace='preferences')),
url(
r'^preferences/',
include('preferences.urls', namespace='preferences')
),
]

141
re2o/utils.py Normal file
View file

@ -0,0 +1,141 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# -*- coding: utf-8 -*-
# David Sinquin, Gabriel Détraz, Goulven Kermarec
"""
Regroupe les fonctions transversales utiles
Fonction :
- récupérer tous les utilisateurs actifs
- récupérer toutes les machines
- récupérer tous les bans
etc
"""
from __future__ import unicode_literals
from django.utils import timezone
from django.db.models import Q
from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine
from users.models import User, Ban, Whitelist
from preferences.models import Service
DT_NOW = timezone.now()
def all_adherent(search_time=DT_NOW):
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est
qu'une seule requete sql
Inspecte les factures de l'user et ses cotisation, regarde si elles
sont posterieur à now (end_time)"""
return User.objects.filter(
facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.all().exclude(valid=False)
)
).filter(date_end__gt=search_time)
)
)
).distinct()
def all_baned(search_time=DT_NOW):
""" Fonction renvoyant tous les users bannis """
return User.objects.filter(
ban__in=Ban.objects.filter(
date_end__gt=search_time
)
).distinct()
def all_whitelisted(search_time=DT_NOW):
""" Fonction renvoyant tous les users whitelistes """
return User.objects.filter(
whitelist__in=Whitelist.objects.filter(
date_end__gt=search_time
)
).distinct()
def all_has_access(search_time=DT_NOW):
""" Renvoie tous les users beneficiant d'une connexion
: user adherent ou whiteliste et non banni """
return User.objects.filter(
Q(state=User.STATE_ACTIVE) &
~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
(Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
Q(facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.all()
.exclude(valid=False)
)
).filter(date_end__gt=search_time)
)
)))
).distinct()
def filter_active_interfaces(interface_set):
"""Filtre les machines autorisées à sortir sur internet dans une requête"""
return interface_set.filter(
machine__in=Machine.objects.filter(
user__in=all_has_access()
).filter(active=True)
).select_related('domain').select_related('machine')\
.select_related('type').select_related('ipv4')\
.select_related('domain__extension').select_related('ipv4__ip_type')\
.distinct()
def all_active_interfaces():
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
return filter_active_interfaces(Interface.objects)
def all_active_assigned_interfaces():
""" Renvoie l'ensemble des machines qui ont une ipv4 assignées et
disposant de l'accès internet"""
return all_active_interfaces().filter(ipv4__isnull=False)
def all_active_interfaces_count():
""" Version light seulement pour compter"""
return Interface.objects.filter(
machine__in=Machine.objects.filter(
user__in=all_has_access()
).filter(active=True)
)
def all_active_assigned_interfaces_count():
""" Version light seulement pour compter"""
return all_active_interfaces_count().filter(ipv4__isnull=False)

View file

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

View file

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

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

@ -0,0 +1,210 @@
/*!
* bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/
@-webkit-keyframes blink {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
@-moz-keyframes blink {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
@keyframes blink {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
.tokenfield {
height: auto;
min-height: 34px;
padding-bottom: 0px;
}
.tokenfield.focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.tokenfield .token {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: -1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.tokenfield .token:hover {
border-color: #b9b9b9;
}
.tokenfield .token.active {
border-color: #52a8ec;
border-color: rgba(82, 168, 236, 0.8);
}
.tokenfield .token.duplicate {
border-color: #ebccd1;
-webkit-animation-name: blink;
animation-name: blink;
-webkit-animation-duration: 0.1s;
animation-duration: 0.1s;
-webkit-animation-direction: normal;
animation-direction: normal;
-webkit-animation-timing-function: ease;
animation-timing-function: ease;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.tokenfield .token.invalid {
background: none;
border: 1px solid transparent;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
border-bottom: 1px dotted #d9534f;
}
.tokenfield .token.invalid.active {
background: #ededed;
border: 1px solid #ededed;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.tokenfield .token .token-label {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.tokenfield .token .close {
font-family: Arial;
display: inline-block;
line-height: 100%;
font-size: 1.1em;
line-height: 1.49em;
margin-left: 5px;
float: none;
height: 100%;
vertical-align: top;
padding-right: 4px;
}
.tokenfield .token-input {
background: none;
width: 60px;
min-width: 60px;
border: 0;
height: 20px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.tokenfield .token-input:focus {
border-color: transparent;
outline: 0;
/* IE6-9 */
-webkit-box-shadow: none;
box-shadow: none;
}
.tokenfield.disabled {
cursor: not-allowed;
background-color: #eeeeee;
}
.tokenfield.disabled .token-input {
cursor: not-allowed;
}
.tokenfield.disabled .token:hover {
cursor: not-allowed;
border-color: #d9d9d9;
}
.tokenfield.disabled .token:hover .close {
cursor: not-allowed;
opacity: 0.2;
filter: alpha(opacity=20);
}
.has-warning .tokenfield.focus {
border-color: #66512c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
}
.has-error .tokenfield.focus {
border-color: #843534;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
}
.has-success .tokenfield.focus {
border-color: #2b542c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
}
.tokenfield.input-sm,
.input-group-sm .tokenfield {
min-height: 30px;
padding-bottom: 0px;
}
.input-group-sm .token,
.tokenfield.input-sm .token {
height: 20px;
margin-bottom: 4px;
}
.input-group-sm .token-input,
.tokenfield.input-sm .token-input {
height: 18px;
margin-bottom: 5px;
}
.tokenfield.input-lg,
.input-group-lg .tokenfield {
height: auto;
min-height: 45px;
padding-bottom: 4px;
}
.input-group-lg .token,
.tokenfield.input-lg .token {
height: 25px;
}
.input-group-lg .token-label,
.tokenfield.input-lg .token-label {
line-height: 23px;
}
.input-group-lg .token .close,
.tokenfield.input-lg .token .close {
line-height: 1.3em;
}
.input-group-lg .token-input,
.tokenfield.input-lg .token-input {
height: 23px;
line-height: 23px;
margin-bottom: 6px;
vertical-align: top;
}
.tokenfield.rtl {
direction: rtl;
text-align: right;
}
.tokenfield.rtl .token {
margin: -1px 0 5px 5px;
}
.tokenfield.rtl .token .token-label {
padding-left: 0px;
padding-right: 4px;
}

View file

@ -0,0 +1,23 @@
#### Sliptree
- by Illimar Tambek for [Sliptree](http://sliptree.com)
- Copyright (c) 2013 by Sliptree
Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
Copyright (C) 2011-2017 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Snaptortoise
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

@ -0,0 +1,139 @@
/*
* Konami-JS ~
* :: Now with support for touch events and multiple instances for
* :: those situations that call for multiple easter eggs!
* Code: https://github.com/snaptortoise/konami-js
* Examples: http://www.snaptortoise.com/konami-js
* Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
* Version: 1.5.1 (9/4/2017)
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
*/
var Konami = function (callback) {
var konami = {
addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener)
obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event, ref_obj);
}
obj.attachEvent("on" + type, obj[type + fn]);
}
},
removeEvent: function (obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: "",
pattern: "38384040373937396665",
keydownHandler: function (e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
}
if (konami.input === konami.pattern) {
konami.code(this._currentlink);
konami.input = '';
e.preventDefault();
return false;
}
},
load: function (link) {
this.addEvent(document, "keydown", this.keydownHandler, this);
this.iphone.load(link);
},
unload: function () {
this.removeEvent(document, 'keydown', this.keydownHandler);
this.iphone.unload();
},
code: function (link) {
window.location = link
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: "",
keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
input: [],
code: function (link) {
konami.code(link);
},
touchmoveHandler: function (e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
},
toucheendHandler: function () {
if (konami.iphone.tap === true) {
konami.iphone.check_direction(this._currentLink);
}
},
touchstartHandler: function (e) {
konami.iphone.start_x = e.changedTouches[0].pageX;
konami.iphone.start_y = e.changedTouches[0].pageY;
konami.iphone.tap = true;
konami.iphone.capture = true;
},
load: function (link) {
this.orig_keys = this.keys;
konami.addEvent(document, "touchmove", this.touchmoveHandler);
konami.addEvent(document, "touchend", this.toucheendHandler, false);
konami.addEvent(document, "touchstart", this.touchstartHandler);
},
unload: function () {
konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
konami.removeEvent(document, 'touchend', this.toucheendHandler);
konami.removeEvent(document, 'touchstart', this.touchstartHandler);
},
check_direction: function () {
x_magnitude = Math.abs(this.start_x - this.stop_x);
y_magnitude = Math.abs(this.start_y - this.stop_y);
x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
result = (x_magnitude > y_magnitude) ? x : y;
result = (this.tap === true) ? "TAP" : result;
return result;
}
}
}
typeof callback === "string" && konami.load(callback);
if (typeof callback === "function") {
konami.code = callback;
konami.load();
}
return konami;
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = Konami;
} else {
if (typeof define === 'function' && define.amd) {
define([], function() {
return Konami;
});
} else {
window.Konami = Konami;
}
}

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

@ -0,0 +1,316 @@
// Re2o est un logiciel d'administration développé initiallement au rezometz. Il
// se veut agnostique au réseau considéré, de manière à être installable en
// quelques clics.
//
// Copyright © 2017 Maël Kervella
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
// General options
//=====================================
// Times the canvas is refreshed a second
var FPS = 30;
// Determine the length of the trail (0=instant disappear, maximum=window.innerHeight=no disappear)
var TRAIL_TIME = 5;
// The color of the characters
var RAIN_COLOR = "#00F";
// The characters displayed
var CHARACTERS = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split("");
// The font size used to display the characters
var FONT_SIZE = 10;
// The maximum number of characters displayed by column
var MAX_CHAR = 7;
var Sapphire = function () {
var sapphire = {
triggerHandle: undefined,
activated: false,
runOnce: false,
getClass: function(elt, main, name) { elt.obj = main.getElementsByClassName(name); },
getTag: function(elt, main, name) { elt.obj = main.getElementsByTagName(name); },
getProp: function(elt) {
for (var i=0 ; i<elt.obj.length ; i++) {
for (var p in elt.prop) {
if ( p === "color" ) { elt.prop[p][i] = elt.obj[i].style.color; }
else if ( p === "bgColor" ) { elt.prop[p][i] = elt.obj[i].style.backgroundColor; }
else if ( p === "display" ) { elt.prop[p][i] = elt.obj[i].style.display; }
}
}
},
alterProp: function(elt) {
for (var i=0 ; i<elt.obj.length ; i++) {
for (var p in elt.prop) {
if ( p === "color" ) { elt.obj[i].style.color = "white"; }
else if ( p === "bgColor" ) { elt.obj[i].style.backgroundColor = "transparent"; }
else if ( p === "display" ) { elt.obj[i].style.display = "none"; }
}
}
},
revertProp: function(elt) {
for (var i=0 ; i<elt.obj.length ; i++) {
for (var p in elt.prop) {
if ( p === "color" ) { elt.obj[i].style.color = elt.prop[p][i]; }
else if ( p === "bgColor" ) { elt.obj[i].style.backgroundColor = elt.prop[p][i]; }
else if ( p === "display" ) { elt.obj[i].style.display = elt.prop[p][i]; }
}
}
},
elts: {
alerts: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "alert"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
btns: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getClass(this, main, "btn"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
body: {
obj: undefined,
prop: {color: []},
get: function(main) {
this.obj = document.body;
for (var p in this.prop) { if ( p === "color" ) { this.prop[p] = this.obj.style.color; } }
},
alter: function() { for (var p in this.prop) { if ( p === "color" ) { this.obj.style.color = "white"; } } },
revert: function() { for (var p in this.prop) { if ( p === "color" ) { this.obj.style.color = this.prop[p]; } } }
},
captions: {
obj: undefined,
prop: {color: []},
get: function(main) { sapphire.getClass(this, main, "caption"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
helps: {
obj: undefined,
prop: {color: []},
get: function(main) { sapphire.getClass(this, main, "help-block"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
hrs: {
obj: undefined,
prop: {display: []},
get: function(main) { sapphire.getTag(this, main, "hr"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
inputs: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getTag(this, main, "input"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
listGroups: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getClass(this, main, "list-group-item"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
paginations: {
obj: [],
prop: {bgColor: []},
get: function(main) {
var a = main.getElementsByClassName("pagination");
for (var i=0 ; i<a.length ; i++) {
this.obj[i] = []; this.prop.bgColor[i] = [];
for (var j=0 ; j<a[i].children.length ; j++) {
this.obj[i][j] = a[i].children[j].children[0];
this.prop.bgColor[i][j] = this.obj[i][j].style.backgroundColor;
}
}
},
alter: function () {
for (var i=0 ; i<this.obj.length ; i++)
for (var j=0 ; j<this.obj[i].length ; j++)
for (var p in this.prop)
if ( p === "bgColor" ) { this.obj[i][j].style.backgroundColor = "transparent"; }
},
revert: function() {
for (var i=0 ; i<this.obj.length ; i++)
for (var j=0 ; j<this.obj[i].length ; j++)
for (var p in this.prop)
if ( p === "bgColor" ) { this.obj[i][j].style.backgroundColor = this.prop[p][i][j]; }
}
},
panelHeadings: {
obj: undefined,
prop: {bgColor: [], color: []},
get: function(main) { sapphire.getClass(this, main, "panel-heading"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
panels: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "panel"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
selects: {
obj: undefined,
prop: {color: [], bgColor: []},
get: function(main) { sapphire.getTag(this, main, "select"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
sidenavs: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "sidenav"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
tds: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getTag(this, main, "td"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
thumbnails: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getClass(this, main, "thumbnail"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
},
trs: {
obj: undefined,
prop: {bgColor: []},
get: function(main) { sapphire.getTag(this, main, "tr"); sapphire.getProp(this); },
alter: function() { sapphire.alterProp(this); },
revert: function() { sapphire.revertProp(this); }
}
},
columns: undefined,
alpha: undefined,
drops: undefined,
canvas: undefined,
init: function() {
var main = document.getElementById("main");
for (var e in sapphire.elts) { sapphire.elts[e].get(main); }
},
resize: function() {
var ctx = sapphire.canvas.getContext("2d");
var img = ctx.getImageData( 0, 0, sapphire.canvas.width, sapphire.canvas.height );
sapphire.canvas.width = window.innerWidth;
sapphire.canvas.height = window.innerHeight;
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height);
ctx.putImageData( img, 0, 0 );
sapphire.columns = sapphire.canvas.width/FONT_SIZE;
sapphire.alpha = Math.max( 0, Math.min( 1, TRAIL_TIME / ( sapphire.canvas.height/FONT_SIZE ) ) );
var newDrops = [];
for(var x = 0; x < sapphire.columns; x++) {
if ( sapphire.drops && sapphire.drops[x] ) { newDrops[x] = sapphire.drops[x] }
else {
newDrops[x] = [];
var nb = Math.floor(Math.random()*MAX_CHAR);
for (var y = 0; y < nb; y++)
newDrops[x][y] = 0;
}
}
sapphire.drops = newDrops;
},
run: function() {
sapphire.canvas = document.createElement("canvas");
document.body.appendChild(sapphire.canvas);
sapphire.canvas.style.position = "fixed";
sapphire.canvas.style.zIndex = -1;
sapphire.canvas.style.left = 0;
sapphire.canvas.style.top = 0;
var ctx = sapphire.canvas.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height);
function attenuateBackground() {
ctx.fillStyle = "rgba(0, 0, 0, "+sapphire.alpha+")";
ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height);
}
function drawMatrixRainDrop() {
ctx.fillStyle = RAIN_COLOR;
ctx.font = FONT_SIZE + "px arial";
for(var i = 0; i < sapphire.drops.length; i++) {
for (var j = 0; j < sapphire.drops[i].length; j++) {
var text = CHARACTERS[Math.floor(Math.random()*CHARACTERS.length)];
ctx.fillText(text, i*FONT_SIZE, sapphire.drops[i][j]*FONT_SIZE);
if(sapphire.drops[i][j]*FONT_SIZE > sapphire.canvas.height && Math.random() > 0.975)
sapphire.drops[i][j] = 0;
sapphire.drops[i][j]++;
}
}
}
function drawEverything() {
attenuateBackground();
drawMatrixRainDrop();
}
sapphire.resize();
window.addEventListener('resize', sapphire.resize);
sapphire.triggerHandle = setInterval(drawEverything, 1000/FPS);
},
stop: function() {
window.removeEventListener('resize', sapphire.resize);
clearInterval(sapphire.triggerHandle);
sapphire.canvas.parentNode.removeChild(sapphire.canvas);
},
alterElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].alter(main); } },
revertElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].revert(main); } },
activate: function() {
if (!sapphire.runOnce) {
sapphire.runOnce = true;
sapphire.init();
}
if (!sapphire.activated) {
sapphire.activated = true;
sapphire.alterElts();
sapphire.run()
}
else {
sapphire.activated = false;
sapphire.stop();
sapphire.revertElts();
}
}
}
return sapphire;
}
var s = Sapphire();
Konami(s.activate);

View file

@ -0,0 +1,19 @@
Copyright (c) 2013-2014 Twitter, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

View file

@ -33,10 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{# Load CSS and JavaScript #}
{% bootstrap_css %}
<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 %}
<script src="/static/js/typeahead.js"></script>
<script src="/static/js/handlebars.js"></script>
<script src="/static/js/typeahead/typeahead.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" %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>

View file

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

View file

@ -19,52 +19,113 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Un forms le plus simple possible pour les objets topologie de re2o.
Permet de créer et supprimer : un Port de switch, relié à un switch.
Permet de créer des stacks et d'y ajouter des switchs (StackForm)
Permet de créer, supprimer et editer un switch (EditSwitchForm,
NewSwitchForm)
"""
from __future__ import unicode_literals
from .models import Port, Switch, Room, Stack
from django.forms import ModelForm, Form
from machines.models import Interface
from django.forms import ModelForm
from .models import Port, Switch, Room, Stack
class PortForm(ModelForm):
"""Formulaire pour la création d'un port d'un switch
Relié directement au modèle port"""
class Meta:
model = Port
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(PortForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditPortForm(ModelForm):
"""Form pour l'édition d'un port de switche : changement des reglages
radius ou vlan, ou attribution d'une chambre, autre port ou machine
Un port est relié à une chambre, un autre port (uplink) ou une machine
(serveur ou borne), mutuellement exclusif
Optimisation sur les queryset pour machines et port_related pour
optimiser le temps de chargement avec select_related (vraiment
lent sans)"""
class Meta(PortForm.Meta):
fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details']
fields = ['room', 'related', 'machine_interface', 'radius',
'vlan_force', 'details']
def __init__(self, *args, **kwargs):
super(EditPortForm, self).__init__(*args, **kwargs)
self.fields['machine_interface'].queryset = Interface.objects.all().select_related('domain__extension')
self.fields['related'].queryset = Port.objects.all().select_related('switch__switch_interface__domain__extension').order_by('switch', 'port')
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['machine_interface'].queryset = Interface.objects.all()\
.select_related('domain__extension')
self.fields['related'].queryset = Port.objects.all()\
.select_related('switch__switch_interface__domain__extension')\
.order_by('switch', 'port')
class AddPortForm(ModelForm):
"""Permet d'ajouter un port de switch. Voir EditPortForm pour plus
d'informations"""
class Meta(PortForm.Meta):
fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details']
fields = ['port', 'room', 'machine_interface', 'related',
'radius', 'vlan_force', 'details']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs)
class StackForm(ModelForm):
"""Permet d'edition d'une stack : stack_id, et switches membres
de la stack"""
class Meta:
model = Stack
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(StackForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditSwitchForm(ModelForm):
"""Permet d'éditer un switch : nom et nombre de ports"""
class Meta:
model = Switch
fields = '__all__'
def __init__(self, *args, **kwargs):
super(EditSwitchForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditSwitchForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['location'].label = 'Localisation'
self.fields['number'].label = 'Nombre de ports'
class NewSwitchForm(ModelForm):
"""Permet de créer un switch : emplacement, paramètres machine,
membre d'un stack (option), nombre de ports (number)"""
class Meta(EditSwitchForm.Meta):
fields = ['location', 'number', 'details', 'stack', 'stack_member_id']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NewSwitchForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditRoomForm(ModelForm):
"""Permet d'éediter le nom et commentaire d'une prise murale"""
class Meta:
model = Room
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs)

View file

@ -20,19 +20,27 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Definition des modèles de l'application topologie.
On défini les models suivants :
- stack (id, id_min, id_max et nom) regrouppant les switches
- switch : nom, nombre de port, et interface
machine correspondante (mac, ip, etc) (voir machines.models.interface)
- Port: relié à un switch parent par foreign_key, numero du port,
relié de façon exclusive à un autre port, une machine
(serveur ou borne) ou une prise murale
- room : liste des prises murales, nom et commentaire de l'état de
la prise
"""
from __future__ import unicode_literals
from django.db import models
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.forms import ModelForm, Form
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
import reversion
from machines.models import Vlan
class Stack(models.Model):
@ -59,7 +67,9 @@ class Stack(models.Model):
def clean(self):
""" Verification que l'id_max < id_min"""
if self.member_id_max < self.member_id_min:
raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"})
raise ValidationError({'member_id_max': "L'id maximale est\
inférieure à l'id minimale"})
class Switch(models.Model):
""" Definition d'un switch. Contient un nombre de ports (number),
@ -69,14 +79,25 @@ class Switch(models.Model):
Pourquoi ne pas avoir fait hériter switch de interface ?
Principalement par méconnaissance de la puissance de cette façon de faire.
Ceci étant entendu, django crée en interne un onetoone, ce qui a un
effet identique avec ce que l'on fait ici"""
effet identique avec ce que l'on fait ici
Validation au save que l'id du stack est bien dans le range id_min
id_max de la stack parente"""
PRETTY_NAME = "Switch / Commutateur"
switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE)
switch_interface = models.OneToOneField(
'machines.Interface',
on_delete=models.CASCADE
)
location = models.CharField(max_length=255)
number = models.IntegerField()
details = models.CharField(max_length=255, blank=True)
stack = models.ForeignKey(Stack, blank=True, null=True, on_delete=models.SET_NULL)
stack = models.ForeignKey(
Stack,
blank=True,
null=True,
on_delete=models.SET_NULL
)
stack_member_id = models.IntegerField(blank=True, null=True)
class Meta:
@ -89,10 +110,16 @@ class Switch(models.Model):
""" Verifie que l'id stack est dans le bon range"""
if self.stack is not None:
if self.stack_member_id is not None:
if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min):
raise ValidationError({'stack_member_id': "L'id de ce switch est en dehors des bornes permises pas la stack"})
if (self.stack_member_id > self.stack.member_id_max) or\
(self.stack_member_id < self.stack.member_id_min):
raise ValidationError(
{'stack_member_id': "L'id de ce switch est en\
dehors des bornes permises pas la stack"}
)
else:
raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"})
raise ValidationError({'stack_member_id': "L'id dans la stack\
ne peut être nul"})
class Port(models.Model):
""" Definition d'un port. Relié à un switch(foreign_key),
@ -102,7 +129,8 @@ class Port(models.Model):
- un autre port (uplink) (related)
Champs supplémentaires :
- RADIUS (mode STRICT : connexion sur port uniquement si machine
d'un adhérent à jour de cotisation et que la chambre est également à jour de cotisation
d'un adhérent à jour de cotisation et que la chambre est également à
jour de cotisation
mode COMMON : vérification uniquement du statut de la machine
mode NO : accepte toute demande venant du port et place sur le vlan normal
mode BLOQ : rejet de toute authentification
@ -119,11 +147,31 @@ class Port(models.Model):
switch = models.ForeignKey('Switch', related_name="ports")
port = models.IntegerField()
room = models.ForeignKey('Room', on_delete=models.PROTECT, blank=True, null=True)
machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True)
related = models.OneToOneField('self', null=True, blank=True, related_name='related_port')
room = models.ForeignKey(
'Room',
on_delete=models.PROTECT,
blank=True,
null=True
)
machine_interface = models.ForeignKey(
'machines.Interface',
on_delete=models.SET_NULL,
blank=True,
null=True
)
related = models.OneToOneField(
'self',
null=True,
blank=True,
related_name='related_port'
)
radius = models.CharField(max_length=32, choices=STATES, default='NO')
vlan_force = models.ForeignKey('machines.Vlan', on_delete=models.SET_NULL, blank=True, null=True)
vlan_force = models.ForeignKey(
'machines.Vlan',
on_delete=models.SET_NULL,
blank=True,
null=True
)
details = models.CharField(max_length=255, blank=True)
class Meta:
@ -142,23 +190,28 @@ class Port(models.Model):
related_port.save()
def clean(self):
""" Verifie que un seul de chambre, interface_parent et related_port est rempli.
Verifie que le related n'est pas le port lui-même....
Verifie que le related n'est pas déjà occupé par une machine ou une chambre. Si
ce n'est pas le cas, applique la relation related
""" Verifie que un seul de chambre, interface_parent et related_port
est rempli. Verifie que le related n'est pas le port lui-même....
Verifie que le related n'est pas déjà occupé par une machine ou une
chambre. Si ce n'est pas le cas, applique la relation related
Si un port related point vers self, on nettoie la relation
A priori pas d'autre solution que de faire ça à la main. A priori tout cela est dans
un bloc transaction, donc pas de problème de cohérence"""
A priori pas d'autre solution que de faire ça à la main. A priori
tout cela est dans un bloc transaction, donc pas de problème de
cohérence"""
if hasattr(self, 'switch'):
if self.port > self.switch.number:
raise ValidationError("Ce port ne peut exister, numero trop élevé")
if self.room and self.machine_interface or self.room and self.related or self.machine_interface and self.related:
raise ValidationError("Chambre, interface et related_port sont mutuellement exclusifs")
raise ValidationError("Ce port ne peut exister,\
numero trop élevé")
if self.room and self.machine_interface or self.room and\
self.related or self.machine_interface and self.related:
raise ValidationError("Chambre, interface et related_port sont\
mutuellement exclusifs")
if self.related == self:
raise ValidationError("On ne peut relier un port à lui même")
if self.related and not self.related.related:
if self.related.machine_interface or self.related.room:
raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation")
raise ValidationError("Le port relié est déjà occupé, veuillez\
le libérer avant de créer une relation")
else:
self.make_port_related()
elif hasattr(self, 'related_port'):
@ -167,6 +220,7 @@ class Port(models.Model):
def __str__(self):
return str(self.switch) + " - " + str(self.port)
class Room(models.Model):
"""Une chambre/local contenant une prise murale"""
PRETTY_NAME = "Chambre/ Prise murale"
@ -180,6 +234,8 @@ class Room(models.Model):
def __str__(self):
return str(self.name)
@receiver(post_delete, sender=Stack)
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)

View file

@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</thead>
{% for stack in stack_list %}
{% for switch in stack.switch_set.all %}
<tbody>
<tr class="active">
{% if forloop.first %}
<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>
{% endif %}
</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 %}
</tbody>
{% endfor %}
</table>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,8 +20,16 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Definition des forms pour l'application users.
# -*- coding: utf-8 -*-
Modification, creation de :
- un user (informations personnelles)
- un bannissement
- le mot de passe d'un user
- une whiteliste
- un user de service
"""
from __future__ import unicode_literals
@ -29,17 +37,34 @@ from django import forms
from django.forms import ModelForm, Form
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.validators import MinLengthValidator
from preferences.models import OptionalUser
from django.utils import timezone
from .models import User, ServiceUser, Right, School, ListRight, Whitelist, Ban, Request, remove_user_room
from .models import get_admin_right
from preferences.models import OptionalUser
from .models import User, ServiceUser, Right, School, ListRight, Whitelist
from .models import Ban, remove_user_room
NOW = timezone.now()
class PassForm(forms.Form):
passwd1 = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput)
passwd2 = forms.CharField(label=u'Saisir à nouveau le mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput)
"""Formulaire de changement de mot de passe. Verifie que les 2
nouveaux mots de passe renseignés sont identiques et respectent
une norme"""
passwd1 = forms.CharField(
label=u'Nouveau mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput
)
passwd2 = forms.CharField(
label=u'Saisir à nouveau le mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput
)
def clean_passwd2(self):
"""Verifie que passwd1 et 2 sont identiques"""
# Check that the two password entries match
password1 = self.cleaned_data.get("passwd1")
password2 = self.cleaned_data.get("passwd2")
@ -47,18 +72,38 @@ class PassForm(forms.Form):
raise forms.ValidationError("Passwords don't match")
return password2
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label='Password', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255)
fields, plus a repeated password.
Formulaire pour la création d'un user. N'est utilisé que pour
l'admin, lors de la creation d'un user par admin. Inclu tous les
champs obligatoires"""
password1 = forms.CharField(
label='Password',
widget=forms.PasswordInput,
validators=[MinLengthValidator(8)],
max_length=255
)
password2 = forms.CharField(
label='Password confirmation',
widget=forms.PasswordInput,
validators=[MinLengthValidator(8)],
max_length=255
)
is_admin = forms.BooleanField(label='is admin')
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(UserCreationForm, self).__init__(*args, prefix=prefix, **kwargs)
class Meta:
model = User
fields = ('pseudo', 'name', 'surname', 'email')
def clean_password2(self):
"""Verifie que password1 et 2 sont identiques"""
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
@ -74,17 +119,40 @@ class UserCreationForm(forms.ModelForm):
user.is_admin = self.cleaned_data.get("is_admin")
return user
class ServiceUserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label='Password', widget=forms.PasswordInput, min_length=8, max_length=255)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, min_length=8, max_length=255)
fields, plus a repeated password.
Formulaire pour la creation de nouveaux serviceusers.
Requiert seulement un mot de passe; et un pseudo"""
password1 = forms.CharField(
label='Password',
widget=forms.PasswordInput,
min_length=8,
max_length=255
)
password2 = forms.CharField(
label='Password confirmation',
widget=forms.PasswordInput,
min_length=8,
max_length=255
)
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ServiceUserCreationForm, self).__init__(
*args,
prefix=prefix,
**kwargs
)
class Meta:
model = ServiceUser
fields = ('pseudo',)
def clean_password2(self):
"""Verifie que password1 et 2 sont indentiques"""
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
@ -99,10 +167,13 @@ class ServiceUserCreationForm(forms.ModelForm):
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
Formulaire pour la modification d'un user coté admin
"""
password = ReadOnlyPasswordHashField()
is_admin = forms.BooleanField(label='is admin', required=False)
@ -112,11 +183,13 @@ class UserChangeForm(forms.ModelForm):
fields = ('pseudo', 'password', 'name', 'surname', 'email')
def __init__(self, *args, **kwargs):
super(UserChangeForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(UserChangeForm, self).__init__(*args, prefix=prefix, **kwargs)
print("User is admin : %s" % kwargs['instance'].is_admin)
self.initial['is_admin'] = kwargs['instance'].is_admin
def clean_password(self):
"""Dummy fun"""
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
@ -130,40 +203,62 @@ class UserChangeForm(forms.ModelForm):
user.save()
return user
class ServiceUserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
Formulaire pour l'edition des service users coté admin
"""
password = ReadOnlyPasswordHashField()
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ServiceUserChangeForm, self).__init__(
*args,
prefix=prefix,
**kwargs
)
class Meta:
model = ServiceUser
fields = ('pseudo',)
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
"""Dummy fun"""
return self.initial["password"]
class ResetPasswordForm(forms.Form):
"""Formulaire de demande de reinitialisation de mot de passe,
mdp oublié"""
pseudo = forms.CharField(label=u'Pseudo', max_length=255)
email = forms.EmailField(max_length=255)
class MassArchiveForm(forms.Form):
"""Formulaire d'archivage des users inactif. Prend en argument
du formulaire la date de depart avant laquelle archiver les
users"""
date = forms.DateTimeField(help_text='%d/%m/%y')
def clean(self):
cleaned_data = super(MassArchiveForm, self).clean()
date = cleaned_data.get("date")
if date:
if date>timezone.now():
raise forms.ValidationError("Impossible d'archiver des utilisateurs dont la fin d'accès se situe dans le futur !")
if date > NOW:
raise forms.ValidationError("Impossible d'archiver des\
utilisateurs dont la fin d'accès se situe dans le futur !")
class BaseInfoForm(ModelForm):
"""Formulaire de base d'edition d'un user. Formulaire de base, utilisé
pour l'edition de self par self ou un cableur. On formate les champs
avec des label plus jolis"""
def __init__(self, *args, **kwargs):
super(BaseInfoForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(BaseInfoForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Prénom'
self.fields['surname'].label = 'Nom'
self.fields['school'].label = 'Établissement'
@ -186,13 +281,21 @@ class BaseInfoForm(ModelForm):
]
def clean_telephone(self):
"""Verifie que le tel est présent si 'option est validée
dans preferences"""
telephone = self.cleaned_data['telephone']
preferences, created = OptionalUser.objects.get_or_create()
preferences, _created = OptionalUser.objects.get_or_create()
if not telephone and preferences.is_tel_mandatory:
raise forms.ValidationError("Un numéro de téléphone valide est requis")
raise forms.ValidationError(
"Un numéro de téléphone valide est requis"
)
return telephone
class EditInfoForm(BaseInfoForm):
"""Edition complète d'un user. Utilisé par admin,
permet d'editer normalement la chambre, ou le shell
Herite de la base"""
class Meta(BaseInfoForm.Meta):
fields = [
'name',
@ -206,80 +309,138 @@ class EditInfoForm(BaseInfoForm):
'telephone',
]
class InfoForm(EditInfoForm):
""" Utile pour forcer un déménagement quand il y a déjà un user en place"""
force = forms.BooleanField(label="Forcer le déménagement ?", initial=False, required=False)
""" Utile pour forcer un déménagement quand il y a déjà un user en place
Formuaire utilisé pour la creation initiale"""
force = forms.BooleanField(
label="Forcer le déménagement ?",
initial=False,
required=False
)
def clean_force(self):
"""On supprime l'ancien user de la chambre si et seulement si la
case est cochée"""
if self.cleaned_data.get('force', False):
remove_user_room(self.cleaned_data.get('room'))
return
class UserForm(InfoForm):
""" Model form general"""
class Meta(InfoForm.Meta):
fields = '__all__'
class PasswordForm(ModelForm):
""" Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement"""
""" Formulaire de changement brut de mot de passe.
Ne pas utiliser sans traitement"""
class Meta:
model = User
fields = ['password', 'pwd_ntlm']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs)
class ServiceUserForm(ModelForm):
""" Modification d'un service user"""
password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False)
password = forms.CharField(
label=u'Nouveau mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
required=False
)
class Meta:
model = ServiceUser
fields = ('pseudo', 'access_group')
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditServiceUserForm(ServiceUserForm):
"""Formulaire d'edition de base d'un service user. Ne permet
d'editer que son group d'acl et son commentaire"""
class Meta(ServiceUserForm.Meta):
fields = ['access_group', 'comment']
class StateForm(ModelForm):
""" Changement de l'état d'un user"""
class Meta:
model = User
fields = ['state']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(StateForm, self).__init__(*args, prefix=prefix, **kwargs)
class SchoolForm(ModelForm):
"""Edition, creation d'un école"""
class Meta:
model = School
fields = ['name']
def __init__(self, *args, **kwargs):
super(SchoolForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(SchoolForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Établissement'
class ListRightForm(ModelForm):
"""Edition, d'un groupe , équivalent à un droit
Ne peremet pas d'editer le gid, car il sert de primary key"""
class Meta:
model = ListRight
fields = ['listright', 'details']
def __init__(self, *args, **kwargs):
super(ListRightForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['listright'].label = 'Nom du droit/groupe'
class NewListRightForm(ListRightForm):
"""Ajout d'un groupe/list de droit """
class Meta(ListRightForm.Meta):
fields = '__all__'
def __init__(self, *args, **kwargs):
super(NewListRightForm, self).__init__(*args, **kwargs)
self.fields['gid'].label = 'Gid, attention, cet attribut ne doit pas être modifié après création'
self.fields['gid'].label = 'Gid, attention, cet attribut ne doit\
pas être modifié après création'
class DelListRightForm(Form):
listrights = forms.ModelMultipleChoiceField(queryset=ListRight.objects.all(), label="Droits actuels", widget=forms.CheckboxSelectMultiple)
"""Suppression d'un ou plusieurs groupes"""
listrights = forms.ModelMultipleChoiceField(
queryset=ListRight.objects.all(),
label="Droits actuels",
widget=forms.CheckboxSelectMultiple
)
class DelSchoolForm(Form):
schools = forms.ModelMultipleChoiceField(queryset=School.objects.all(), label="Etablissements actuels", widget=forms.CheckboxSelectMultiple)
"""Suppression d'une ou plusieurs écoles"""
schools = forms.ModelMultipleChoiceField(
queryset=School.objects.all(),
label="Etablissements actuels",
widget=forms.CheckboxSelectMultiple
)
class RightForm(ModelForm):
"""Assignation d'un droit à un user"""
def __init__(self, *args, **kwargs):
super(RightForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(RightForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['right'].label = 'Droit'
self.fields['right'].empty_label = "Choisir un nouveau droit"
@ -289,15 +450,22 @@ class RightForm(ModelForm):
class DelRightForm(Form):
rights = forms.ModelMultipleChoiceField(queryset=Right.objects.all(), widget=forms.CheckboxSelectMultiple)
"""Suppression d'un droit d'un user"""
rights = forms.ModelMultipleChoiceField(
queryset=Right.objects.all(),
widget=forms.CheckboxSelectMultiple
)
def __init__(self, right, *args, **kwargs):
super(DelRightForm, self).__init__(*args, **kwargs)
self.fields['rights'].queryset = Right.objects.filter(right=right)
class BanForm(ModelForm):
"""Creation, edition d'un objet bannissement"""
def __init__(self, *args, **kwargs):
super(BanForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(BanForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['date_end'].label = 'Date de fin'
class Meta:
@ -305,15 +473,19 @@ class BanForm(ModelForm):
exclude = ['user']
def clean_date_end(self):
"""Verification que date_end est après now"""
date_end = self.cleaned_data['date_end']
if date_end < timezone.now():
raise forms.ValidationError("Triple buse, la date de fin ne peut pas être avant maintenant... Re2o ne voyage pas dans le temps")
if date_end < NOW:
raise forms.ValidationError("Triple buse, la date de fin ne peut\
pas être avant maintenant... Re2o ne voyage pas dans le temps")
return date_end
class WhitelistForm(ModelForm):
"""Creation, edition d'un objet whitelist"""
def __init__(self, *args, **kwargs):
super(WhitelistForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['date_end'].label = 'Date de fin'
class Meta:
@ -321,7 +493,9 @@ class WhitelistForm(ModelForm):
exclude = ['user']
def clean_date_end(self):
"""Verification que la date_end est posterieur à now"""
date_end = self.cleaned_data['date_end']
if date_end < timezone.now():
raise forms.ValidationError("Triple buse, la date de fin ne peut pas être avant maintenant... Re2o ne voyage pas dans le temps")
if date_end < NOW:
raise forms.ValidationError("Triple buse, la date de fin ne peut pas\
être avant maintenant... Re2o ne voyage pas dans le temps")
return date_end

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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