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

Merge branch 'master' into reverse_url

This commit is contained in:
root 2017-11-04 19:52:08 +01:00
commit 5dc44a7136
19 changed files with 458 additions and 74 deletions

View file

@ -38,6 +38,7 @@ ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter,
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Q
from django.forms import ModelForm, Form from django.forms import ModelForm, Form
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from .models import Article, Paiement, Facture, Banque from .models import Article, Paiement, Facture, Banque
@ -90,10 +91,24 @@ class CreditSoldeForm(NewFactureForm):
montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True)
class SelectArticleForm(Form): class SelectUserArticleForm(Form):
"""Selection d'un article lors de la creation d'une facture""" """Selection d'un article lors de la creation d'une facture"""
article = forms.ModelChoiceField( article = forms.ModelChoiceField(
queryset=Article.objects.all(), queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')),
label="Article",
required=True
)
quantity = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)],
required=True
)
class SelectClubArticleForm(Form):
"""Selection d'un article lors de la creation d'une facture"""
article = forms.ModelChoiceField(
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')),
label="Article", label="Article",
required=True required=True
) )

View file

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

View file

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

View file

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

View file

@ -47,6 +47,7 @@ from __future__ import unicode_literals
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.db import models from django.db import models
from django.db.models import Q
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.forms import ValidationError from django.forms import ValidationError
@ -127,15 +128,26 @@ class Vente(models.Model):
iscotisation""" iscotisation"""
PRETTY_NAME = "Ventes effectuées" PRETTY_NAME = "Ventes effectuées"
COTISATION_TYPE = (
('Connexion', 'Connexion'),
('Adhesion', 'Adhesion'),
('All', 'All'),
)
facture = models.ForeignKey('Facture', on_delete=models.CASCADE) facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
number = models.IntegerField(validators=[MinValueValidator(1)]) number = models.IntegerField(validators=[MinValueValidator(1)])
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
prix = models.DecimalField(max_digits=5, decimal_places=2) prix = models.DecimalField(max_digits=5, decimal_places=2)
iscotisation = models.BooleanField()
duration = models.PositiveIntegerField( duration = models.PositiveIntegerField(
help_text="Durée exprimée en mois entiers", help_text="Durée exprimée en mois entiers",
blank=True, blank=True,
null=True) null=True)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
blank=True,
null=True,
max_length=255
)
def prix_total(self): def prix_total(self):
"""Renvoie le prix_total de self (nombre*prix)""" """Renvoie le prix_total de self (nombre*prix)"""
@ -155,22 +167,26 @@ class Vente(models.Model):
"""Update et crée l'objet cotisation associé à une facture, prend """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 en argument l'user, la facture pour la quantitéi, et l'article pour
la durée""" la durée"""
if not hasattr(self, 'cotisation'): if not hasattr(self, 'cotisation') and self.type_cotisation:
cotisation = Cotisation(vente=self) cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation
if date_start: if date_start:
end_adhesion = Cotisation.objects.filter( end_cotisation = Cotisation.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter( facture__in=Facture.objects.filter(
user=self.facture.user user=self.facture.user
).exclude(valid=False)) ).exclude(valid=False))
).filter(Q(type_cotisation='All') | Q(type_cotisation=self.type_cotisation)
).filter( ).filter(
date_start__lt=date_start date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max'] ).aggregate(Max('date_end'))['date_end__max']
elif self.type_cotisation=="Adhesion":
end_cotisation = self.facture.user.end_adhesion()
else: else:
end_adhesion = self.facture.user.end_adhesion() end_cotisation = self.facture.user.end_connexion()
date_start = date_start or timezone.now() date_start = date_start or timezone.now()
end_adhesion = end_adhesion or date_start end_cotisation = end_cotisation or date_start
date_max = max(end_adhesion, date_start) date_max = max(end_cotisation, date_start)
cotisation.date_start = date_max cotisation.date_start = date_max
cotisation.date_end = cotisation.date_start + relativedelta( cotisation.date_end = cotisation.date_start + relativedelta(
months=self.duration*self.number months=self.duration*self.number
@ -179,7 +195,7 @@ class Vente(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# On verifie que si iscotisation, duration est présent # On verifie que si iscotisation, duration est présent
if self.iscotisation and not self.duration: if self.type_cotisation and not self.duration:
raise ValidationError("Cotisation et durée doivent être présents\ raise ValidationError("Cotisation et durée doivent être présents\
ensembles") ensembles")
self.update_cotisation() self.update_cotisation()
@ -197,7 +213,7 @@ def vente_post_save(sender, **kwargs):
if hasattr(vente, 'cotisation'): if hasattr(vente, 'cotisation'):
vente.cotisation.vente = vente vente.cotisation.vente = vente
vente.cotisation.save() vente.cotisation.save()
if vente.iscotisation: if vente.type_cotisation:
vente.create_cotis() vente.create_cotis()
vente.cotisation.save() vente.cotisation.save()
user = vente.facture.user user = vente.facture.user
@ -209,7 +225,7 @@ def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, on synchronise l'user ldap (ex """Après suppression d'une vente, on synchronise l'user ldap (ex
suppression d'une cotisation""" suppression d'une cotisation"""
vente = kwargs['instance'] vente = kwargs['instance']
if vente.iscotisation: if vente.type_cotisation:
user = vente.facture.user user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@ -219,18 +235,47 @@ class Article(models.Model):
et duree si c'est une cotisation""" et duree si c'est une cotisation"""
PRETTY_NAME = "Articles en vente" PRETTY_NAME = "Articles en vente"
name = models.CharField(max_length=255, unique=True) USER_TYPES = (
('Adherent', 'Adherent'),
('Club', 'Club'),
('All', 'All'),
)
COTISATION_TYPE = (
('Connexion', 'Connexion'),
('Adhesion', 'Adhesion'),
('All', 'All'),
)
name = models.CharField(max_length=255)
prix = models.DecimalField(max_digits=5, decimal_places=2) prix = models.DecimalField(max_digits=5, decimal_places=2)
iscotisation = models.BooleanField()
duration = models.PositiveIntegerField( duration = models.PositiveIntegerField(
help_text="Durée exprimée en mois entiers", help_text="Durée exprimée en mois entiers",
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)]) validators=[MinValueValidator(0)])
type_user = models.CharField(
choices=USER_TYPES,
default='All',
max_length=255
)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
default=None,
blank=True,
null=True,
max_length=255
)
unique_together = ('name', 'type_user')
def clean(self): def clean(self):
if self.name.lower() == "solde": if self.name.lower() == "solde":
raise ValidationError("Solde est un nom d'article invalide") raise ValidationError("Solde est un nom d'article invalide")
if self.type_cotisation and not self.duration:
raise ValidationError(
"La durée est obligatoire si il s'agit d'une cotisation"
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -275,7 +320,17 @@ class Cotisation(models.Model):
"""Objet cotisation, debut et fin, relié en onetoone à une vente""" """Objet cotisation, debut et fin, relié en onetoone à une vente"""
PRETTY_NAME = "Cotisations" PRETTY_NAME = "Cotisations"
COTISATION_TYPE = (
('Connexion', 'Connexion'),
('Adhesion', 'Adhesion'),
('All', 'All'),
)
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True) vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
max_length=255,
)
date_start = models.DateTimeField() date_start = models.DateTimeField()
date_end = models.DateTimeField() date_end = models.DateTimeField()

View file

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

View file

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

View file

@ -42,6 +42,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Profil</th> <th>Profil</th>
<th>{% include "buttons/sort.html" with prefix='control' col='name' text='Nom' %}</th> <th>{% include "buttons/sort.html" with prefix='control' col='name' text='Nom' %}</th>
<th>{% include "buttons/sort.html" with prefix='control' col='surname' text='Prénom' %}</th> <th>{% include "buttons/sort.html" with prefix='control' col='surname' text='Prénom' %}</th>
<th>{% include "buttons/sort.html" with prefix='control' col='id' text='Id facture' %}</th>
<th>{% include "buttons/sort.html" with prefix='control' col='user-id' text='Id user' %}</th>
<th>Designation</th> <th>Designation</th>
<th>Prix total</th> <th>Prix total</th>
<th>{% include "buttons/sort.html" with prefix='control' col='paiement' text='Moyen de paiement' %}</th> <th>{% include "buttons/sort.html" with prefix='control' col='paiement' text='Moyen de paiement' %}</th>
@ -58,6 +60,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td> </td>
<td>{{ form.instance.user.name }}</td> <td>{{ form.instance.user.name }}</td>
<td>{{ form.instance.user.surname }}</td> <td>{{ form.instance.user.surname }}</td>
<td>{{ form.instance.id }}</td>
<td>{{ form.instance.user.id }}</td>
<td>{{ form.instance.name }}</td> <td>{{ form.instance.name }}</td>
<td>{{ form.instance.prix_total }}</td> <td>{{ form.instance.prix_total }}</td>
<td>{{ form.instance.paiement }}</td> <td>{{ form.instance.paiement }}</td>

View file

@ -33,6 +33,7 @@ from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages from django.contrib import messages
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.forms import modelformset_factory, formset_factory from django.forms import modelformset_factory, formset_factory
from django.utils import timezone from django.utils import timezone
from reversion import revisions as reversion from reversion import revisions as reversion
@ -45,10 +46,21 @@ from re2o.views import form
from re2o.utils import SortTable from re2o.utils import SortTable
from preferences.models import OptionalUser, AssoOption, GeneralOption from preferences.models import OptionalUser, AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque from .models import Facture, Article, Vente, Paiement, Banque
from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm from .forms import (
from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm NewFactureForm,
from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf TrezEditFactureForm,
from .forms import SelectArticleForm, CreditSoldeForm EditFactureForm,
ArticleForm,
DelArticleForm,
PaiementForm,
DelPaiementForm,
BanqueForm,
DelBanqueForm,
NewFactureFormPdf,
SelectUserArticleForm,
SelectClubArticleForm,
CreditSoldeForm
)
from .tex import render_tex from .tex import render_tex
@ -69,10 +81,15 @@ def new_facture(request, userid):
return redirect(reverse('cotisations:index')) return redirect(reverse('cotisations:index'))
facture = Facture(user=user) facture = Facture(user=user)
# Le template a besoin de connaitre les articles pour le js # Le template a besoin de connaitre les articles pour le js
article_list = Article.objects.all() article_list = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
# On envoie la form fature et un formset d'articles # On envoie la form fature et un formset d'articles
facture_form = NewFactureForm(request.POST or None, instance=facture) facture_form = NewFactureForm(request.POST or None, instance=facture)
article_formset = formset_factory(SelectArticleForm)(request.POST or None) if request.user.is_class_club:
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
else:
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
if facture_form.is_valid() and article_formset.is_valid(): if facture_form.is_valid() and article_formset.is_valid():
new_facture_instance = facture_form.save(commit=False) new_facture_instance = facture_form.save(commit=False)
articles = article_formset articles = article_formset
@ -111,7 +128,7 @@ def new_facture(request, userid):
facture=new_facture_instance, facture=new_facture_instance,
name=article.name, name=article.name,
prix=article.prix, prix=article.prix,
iscotisation=article.iscotisation, type_cotisation=article.type_cotisation,
duration=article.duration, duration=article.duration,
number=quantity number=quantity
) )
@ -119,7 +136,7 @@ def new_facture(request, userid):
new_vente.save() new_vente.save()
reversion.set_user(request.user) reversion.set_user(request.user)
reversion.set_comment("Création") reversion.set_comment("Création")
if any(art_item.cleaned_data['article'].iscotisation if any(art_item.cleaned_data['article'].type_cotisation
for art_item in articles if art_item.cleaned_data): for art_item in articles if art_item.cleaned_data):
messages.success( messages.success(
request, request,
@ -326,8 +343,6 @@ def credit_solde(request, userid):
facture=facture_instance, facture=facture_instance,
name="solde", name="solde",
prix=facture.cleaned_data['montant'], prix=facture.cleaned_data['montant'],
iscotisation=False,
duration=0,
number=1 number=1
) )
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():

View file

@ -247,6 +247,9 @@ def check_user_machine_and_register(nas_type, username, mac_address):
return (False, u"Machine enregistrée sur le compte d'un autre user...", '') return (False, u"Machine enregistrée sur le compte d'un autre user...", '')
elif not interface.is_active: elif not interface.is_active:
return (False, u"Machine desactivée", '') return (False, u"Machine desactivée", '')
elif not interface.ipv4:
interface.assign_ipv4()
return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm)
else: else:
return (True, u"Access ok", user.pwd_ntlm) return (True, u"Access ok", user.pwd_ntlm)
elif nas_type: elif nas_type:
@ -324,8 +327,13 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN) return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN)
else: else:
return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK)
elif not interface.first().is_active: else:
interface = interface.first()
if not interface.is_active:
return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK)
elif not interface.ipv4:
interface.assign_ipv4()
return (sw_name, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN)
else: else:
return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN) return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN)

View file

@ -47,13 +47,50 @@ from django.db.models import Count
from reversion.models import Revision from reversion.models import Revision
from reversion.models import Version, ContentType from reversion.models import Version, ContentType
from users.models import User, ServiceUser, Right, School, ListRight, ListShell from users.models import (
from users.models import Ban, Whitelist User,
from cotisations.models import Facture, Vente, Article, Banque, Paiement ServiceUser,
from cotisations.models import Cotisation Right,
from machines.models import Machine, MachineType, IpType, Extension, Interface School,
from machines.models import Domain, IpList ListRight,
from topologie.models import Switch, Port, Room ListShell,
Ban,
Whitelist,
Adherent,
Club
)
from cotisations.models import (
Facture,
Vente,
Article,
Banque,
Paiement,
Cotisation
)
from machines.models import (
Machine,
MachineType,
IpType,
Extension,
Interface,
Domain,
IpList,
OuverturePortList,
Service,
Vlan,
Nas,
SOA,
Mx,
Ns
)
from topologie.models import (
Switch,
Port,
Room,
Stack,
ModelSwitch,
ConstructorSwitch
)
from preferences.models import GeneralOption from preferences.models import GeneralOption
from re2o.views import form from re2o.views import form
from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent
@ -184,45 +221,77 @@ def stats_general(request):
range, et les statistiques générales sur les users : users actifs, range, et les statistiques générales sur les users : users actifs,
cotisants, activés, archivés, etc""" cotisants, activés, archivés, etc"""
ip_dict = dict() ip_dict = dict()
for ip_range in IpType.objects.all(): for ip_range in IpType.objects.select_related('vlan').all():
all_ip = IpList.objects.filter(ip_type=ip_range) all_ip = IpList.objects.filter(ip_type=ip_range)
used_ip = Interface.objects.filter(ipv4__in=all_ip).count() used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
active_ip = all_active_assigned_interfaces_count().filter( active_ip = all_active_assigned_interfaces_count().filter(
ipv4__in=IpList.objects.filter(ip_type=ip_range) ipv4__in=IpList.objects.filter(ip_type=ip_range)
).count() ).count()
ip_dict[ip_range] = [ip_range, all_ip.count(), ip_dict[ip_range] = [ip_range, ip_range.vlan, all_ip.count(),
used_ip, active_ip, all_ip.count()-used_ip] used_ip, active_ip, all_ip.count()-used_ip]
_all_adherent = all_adherent()
_all_has_access = all_has_access()
_all_baned = all_baned()
_all_whitelisted = all_whitelisted()
_all_active_interfaces_count = all_active_interfaces_count()
_all_active_assigned_interfaces_count = all_active_assigned_interfaces_count()
stats = [ stats = [
[["Categorie", "Nombre d'utilisateurs"], { [["Categorie", "Nombre d'utilisateurs (total club et adhérents)", "Nombre d'adhérents", "Nombre de clubs"], {
'active_users': [ 'active_users': [
"Users actifs", "Users actifs",
User.objects.filter(state=User.STATE_ACTIVE).count()], User.objects.filter(state=User.STATE_ACTIVE).count(),
Adherent.objects.filter(state=Adherent.STATE_ACTIVE).count(),
Club.objects.filter(state=Club.STATE_ACTIVE).count()],
'inactive_users': [ 'inactive_users': [
"Users désactivés", "Users désactivés",
User.objects.filter(state=User.STATE_DISABLED).count()], User.objects.filter(state=User.STATE_DISABLED).count(),
Adherent.objects.filter(state=Adherent.STATE_DISABLED).count(),
Club.objects.filter(state=Club.STATE_DISABLED).count()],
'archive_users': [ 'archive_users': [
"Users archivés", "Users archivés",
User.objects.filter(state=User.STATE_ARCHIVE).count()], User.objects.filter(state=User.STATE_ARCHIVE).count(),
Adherent.objects.filter(state=Adherent.STATE_ARCHIVE).count(),
Club.objects.filter(state=Club.STATE_ARCHIVE).count()],
'adherent_users': [ 'adherent_users': [
"Adhérents à l'association", "Cotisant à l'association",
all_adherent().count()], _all_adherent.count(),
_all_adherent.exclude(adherent__isnull=True).count(),
_all_adherent.exclude(club__isnull=True).count()],
'connexion_users': [ 'connexion_users': [
"Utilisateurs bénéficiant d'une connexion", "Utilisateurs bénéficiant d'une connexion",
all_has_access().count()], _all_has_access.count(),
_all_has_access.exclude(adherent__isnull=True).count(),
_all_has_access.exclude(club__isnull=True).count()],
'ban_users': [ 'ban_users': [
"Utilisateurs bannis", "Utilisateurs bannis",
all_baned().count()], _all_baned.count(),
_all_baned.exclude(adherent__isnull=True).count(),
_all_baned.exclude(club__isnull=True).count()],
'whitelisted_user': [ 'whitelisted_user': [
"Utilisateurs bénéficiant d'une connexion gracieuse", "Utilisateurs bénéficiant d'une connexion gracieuse",
all_whitelisted().count()], _all_whitelisted.count(),
_all_whitelisted.exclude(adherent__isnull=True).count(),
_all_whitelisted.exclude(club__isnull=True).count()],
'actives_interfaces': [ 'actives_interfaces': [
"Interfaces actives (ayant accès au reseau)", "Interfaces actives (ayant accès au reseau)",
all_active_interfaces_count().count()], _all_active_interfaces_count.count(),
_all_active_interfaces_count.exclude(
machine__user__adherent__isnull=True
).count(),
_all_active_interfaces_count.exclude(
machine__user__club__isnull=True
).count()],
'actives_assigned_interfaces': [ 'actives_assigned_interfaces': [
"Interfaces actives et assignées ipv4", "Interfaces actives et assignées ipv4",
all_active_assigned_interfaces_count().count()] _all_active_assigned_interfaces_count.count(),
_all_active_assigned_interfaces_count.exclude(
machine__user__adherent__isnull=True
).count(),
_all_active_assigned_interfaces_count.exclude(
machine__user__club__isnull=True
).count()]
}], }],
[["Range d'ip", "Nombre d'ip totales", "Ip assignées", [["Range d'ip", "Vlan", "Nombre d'ip totales", "Ip assignées",
"Ip assignées à une machine active", "Ip non assignées"], ip_dict] "Ip assignées à une machine active", "Ip non assignées"], ip_dict]
] ]
return render(request, 'logs/stats_general.html', {'stats_list': stats}) return render(request, 'logs/stats_general.html', {'stats_list': stats})
@ -237,6 +306,8 @@ def stats_models(request):
stats = { stats = {
'Users': { 'Users': {
'users': [User.PRETTY_NAME, User.objects.count()], 'users': [User.PRETTY_NAME, User.objects.count()],
'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()],
'clubs': [Club.PRETTY_NAME, Club.objects.count()],
'serviceuser': [ServiceUser.PRETTY_NAME, 'serviceuser': [ServiceUser.PRETTY_NAME,
ServiceUser.objects.count()], ServiceUser.objects.count()],
'right': [Right.PRETTY_NAME, Right.objects.count()], 'right': [Right.PRETTY_NAME, Right.objects.count()],
@ -263,11 +334,30 @@ def stats_models(request):
'alias': [Domain.PRETTY_NAME, 'alias': [Domain.PRETTY_NAME,
Domain.objects.exclude(cname=None).count()], Domain.objects.exclude(cname=None).count()],
'iplist': [IpList.PRETTY_NAME, IpList.objects.count()], 'iplist': [IpList.PRETTY_NAME, IpList.objects.count()],
'service': [Service.PRETTY_NAME, Service.objects.count()],
'ouvertureportlist': [
OuverturePortList.PRETTY_NAME,
OuverturePortList.objects.count()
],
'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()],
'SOA': [Mx.PRETTY_NAME, Mx.objects.count()],
'Mx': [Mx.PRETTY_NAME, Mx.objects.count()],
'Ns': [Ns.PRETTY_NAME, Ns.objects.count()],
'nas': [Nas.PRETTY_NAME, Nas.objects.count()],
}, },
'Topologie': { 'Topologie': {
'switch': [Switch.PRETTY_NAME, Switch.objects.count()], 'switch': [Switch.PRETTY_NAME, Switch.objects.count()],
'port': [Port.PRETTY_NAME, Port.objects.count()], 'port': [Port.PRETTY_NAME, Port.objects.count()],
'chambre': [Room.PRETTY_NAME, Room.objects.count()], 'chambre': [Room.PRETTY_NAME, Room.objects.count()],
'stack': [Stack.PRETTY_NAME, Stack.objects.count()],
'modelswitch': [
ModelSwitch.PRETTY_NAME,
ModelSwitch.objects.count()
],
'constructorswitch': [
ConstructorSwitch.PRETTY_NAME,
ConstructorSwitch.objects.count()
],
}, },
'Actions effectuées sur la base': 'Actions effectuées sur la base':
{ {

View file

@ -460,6 +460,15 @@ class Interface(models.Model):
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
""" Formate l'addresse mac en mac_bare (fonction filter_mac) """ Formate l'addresse mac en mac_bare (fonction filter_mac)
et assigne une ipv4 dans le bon range si inexistante ou incohérente""" et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
# If type was an invalid value, django won't create an attribute type
# but try clean() as we may be able to create it from another value
# so even if the error as yet been detected at this point, django
# continues because the error might not prevent us from creating the
# instance.
# But in our case, it's impossible to create a type value so we raise
# the error.
if not hasattr(self, 'type') :
raise ValidationError("Le type d'ip choisi n'est pas valide")
self.filter_macaddress() self.filter_macaddress()
self.mac_address = str(EUI(self.mac_address)) or None self.mac_address = str(EUI(self.mac_address)) or None
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
@ -626,6 +635,8 @@ class IpList(models.Model):
class Service(models.Model): class Service(models.Model):
""" Definition d'un service (dhcp, dns, etc)""" """ Definition d'un service (dhcp, dns, etc)"""
PRETTY_NAME = "Services à générer (dhcp, dns, etc)"
service_type = models.CharField(max_length=255, blank=True, unique=True) service_type = models.CharField(max_length=255, blank=True, unique=True)
min_time_regen = models.DurationField( min_time_regen = models.DurationField(
default=timedelta(minutes=1), default=timedelta(minutes=1),
@ -673,6 +684,8 @@ def regen(service):
class Service_link(models.Model): class Service_link(models.Model):
""" Definition du lien entre serveurs et services""" """ Definition du lien entre serveurs et services"""
PRETTY_NAME = "Relation entre service et serveur"
service = models.ForeignKey('Service', on_delete=models.CASCADE) service = models.ForeignKey('Service', on_delete=models.CASCADE)
server = models.ForeignKey('Interface', on_delete=models.CASCADE) server = models.ForeignKey('Interface', on_delete=models.CASCADE)
last_regen = models.DateTimeField(auto_now_add=True) last_regen = models.DateTimeField(auto_now_add=True)
@ -702,6 +715,8 @@ class Service_link(models.Model):
class OuverturePortList(models.Model): class OuverturePortList(models.Model):
"""Liste des ports ouverts sur une interface.""" """Liste des ports ouverts sur une interface."""
PRETTY_NAME = "Profil d'ouverture de ports"
name = models.CharField( name = models.CharField(
help_text="Nom de la configuration des ports.", help_text="Nom de la configuration des ports.",
max_length=255 max_length=255
@ -748,6 +763,8 @@ class OuverturePort(models.Model):
On limite les ports entre 0 et 65535, tels que défini par la RFC On limite les ports entre 0 et 65535, tels que défini par la RFC
""" """
PRETTY_NAME = "Plage de port ouverte"
TCP = 'T' TCP = 'T'
UDP = 'U' UDP = 'U'
IN = 'I' IN = 'I'

View file

@ -56,6 +56,7 @@ def all_adherent(search_time=DT_NOW):
return User.objects.filter( return User.objects.filter(
facture__in=Facture.objects.filter( facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
Q(type_cotisation='All') | Q(type_cotisation='Adhesion'),
cotisation__in=Cotisation.objects.filter( cotisation__in=Cotisation.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.all().exclude(valid=False) facture__in=Facture.objects.all().exclude(valid=False)
@ -94,6 +95,7 @@ def all_has_access(search_time=DT_NOW):
Q(facture__in=Facture.objects.filter( Q(facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter( cotisation__in=Cotisation.objects.filter(
Q(type_cotisation='All') | Q(type_cotisation='Connexion'),
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.all() facture__in=Facture.objects.all()
.exclude(valid=False) .exclude(valid=False)
@ -179,15 +181,18 @@ class SortTable:
'cotis_user': ['user__pseudo'], 'cotis_user': ['user__pseudo'],
'cotis_paiement': ['paiement__moyen'], 'cotis_paiement': ['paiement__moyen'],
'cotis_date': ['date'], 'cotis_date': ['date'],
'cotis_id': ['id'],
'default': ['-date'] 'default': ['-date']
} }
COTISATIONS_CONTROL = { COTISATIONS_CONTROL = {
'control_name': ['user__name'], 'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'], 'control_surname': ['user__surname'],
'control_paiement': ['paiement'], 'control_paiement': ['paiement'],
'control_date': ['date'], 'control_date': ['date'],
'control_valid': ['valid'], 'control_valid': ['valid'],
'control_control': ['control'], 'control_control': ['control'],
'control_id': ['id'],
'control_user-id': ['user__id'],
'default': ['-date'] 'default': ['-date']
} }
TOPOLOGIE_INDEX = { TOPOLOGIE_INDEX = {

View file

@ -49,6 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</head> </head>
<body> <body>
{% include "cookie_banner.html" %}
<div id="wrap"> <div id="wrap">
<nav class="navbar navbar-inverse"> <nav class="navbar navbar-inverse">
<div class="container-fluid"> <div class="container-fluid">
@ -146,7 +147,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th scope="row">Connexion</th> <th scope="row">Connexion</th>
<td class="text-right"> <td class="text-right">
{% if request_user.has_access %} {% if request_user.has_access %}
<font color="green">Active</font> <font color="green">jusqu'au {{ request.user.end_access|date:"d b Y" }}</font>
{% else %} {% else %}
<font color="red">Désactivée</font> <font color="red">Désactivée</font>
{% endif %} {% endif %}
@ -155,8 +156,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th scope="row">Adhésion</th> <th scope="row">Adhésion</th>
<td class="text-right"> <td class="text-right">
{% if request_user.end_adhesion != None %} {% if request_user.is_adherent %}
<font color="green">{{ request_user.end_adhesion }}</font> <font color="green">jusqu'au {{ request_user.end_adhesion|date:"d b Y" }}</font>
{% else %} {% else %}
<font color="red">Non adhérent</font> <font color="red">Non adhérent</font>
{% endif %} {% endif %}

View file

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

View file

@ -140,7 +140,7 @@ class EditRoomForm(ModelForm):
super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs) super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs)
class CreatePortsForm(Form): class CreatePortsForm(forms.Form):
"""Permet de créer une liste de ports pour un switch.""" """Permet de créer une liste de ports pour un switch."""
begin = forms.IntegerField(label="Début :", min_value=0) begin = forms.IntegerField(label="Début :", min_value=0)
end = forms.IntegerField(label="Fin :", min_value=0) end = forms.IntegerField(label="Fin :", min_value=0)

View file

@ -160,6 +160,7 @@ class Switch(models.Model):
class ModelSwitch(models.Model): class ModelSwitch(models.Model):
"""Un modèle (au sens constructeur) de switch""" """Un modèle (au sens constructeur) de switch"""
PRETTY_NAME = "Modèle de switch"
reference = models.CharField(max_length=255) reference = models.CharField(max_length=255)
constructor = models.ForeignKey( constructor = models.ForeignKey(
'topologie.ConstructorSwitch', 'topologie.ConstructorSwitch',
@ -172,6 +173,7 @@ class ModelSwitch(models.Model):
class ConstructorSwitch(models.Model): class ConstructorSwitch(models.Model):
"""Un constructeur de switch""" """Un constructeur de switch"""
PRETTY_NAME = "Constructeur de switch"
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
def __str__(self): def __str__(self):

View file

@ -182,7 +182,7 @@ class User(AbstractBaseUser):
""" Definition de l'utilisateur de base. """ Definition de l'utilisateur de base.
Champs principaux : name, surnname, pseudo, email, room, password Champs principaux : name, surnname, pseudo, email, room, password
Herite du django BaseUser et du système d'auth django""" Herite du django BaseUser et du système d'auth django"""
PRETTY_NAME = "Utilisateurs" PRETTY_NAME = "Utilisateurs (clubs et adhérents)"
STATE_ACTIVE = 0 STATE_ACTIVE = 0
STATE_DISABLED = 1 STATE_DISABLED = 1
STATE_ARCHIVE = 2 STATE_ARCHIVE = 2
@ -255,7 +255,7 @@ class User(AbstractBaseUser):
def class_name(self): def class_name(self):
"""Renvoie si il s'agit d'un adhérent ou d'un club""" """Renvoie si il s'agit d'un adhérent ou d'un club"""
if hasattr(self, 'adherent'): if hasattr(self, 'adherent'):
return "Adhérent" return "Adherent"
elif hasattr(self, 'club'): elif hasattr(self, 'club'):
return "Club" return "Club"
else: else:
@ -378,6 +378,22 @@ class User(AbstractBaseUser):
user=self user=self
).exclude(valid=False) ).exclude(valid=False)
) )
).filter(
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
).aggregate(models.Max('date_end'))['date_end__max']
return date_max
def end_connexion(self):
""" Renvoie la date de fin de connexion d'un user. Examine les objets
cotisation"""
date_max = Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self
).exclude(valid=False)
)
).filter(
Q(type_cotisation='All') | Q(type_cotisation='Connexion')
).aggregate(models.Max('date_end'))['date_end__max'] ).aggregate(models.Max('date_end'))['date_end__max']
return date_max return date_max
@ -392,6 +408,17 @@ class User(AbstractBaseUser):
else: else:
return True return True
def is_connected(self):
""" Renvoie True si l'user est adhérent : si
self.end_adhesion()>now et end_connexion>now"""
end = self.end_connexion()
if not end:
return False
elif end < DT_NOW:
return False
else:
return self.is_adherent()
@cached_property @cached_property
def end_ban(self): def end_ban(self):
""" Renvoie la date de fin de ban d'un user, False sinon """ """ Renvoie la date de fin de ban d'un user, False sinon """
@ -433,20 +460,20 @@ class User(AbstractBaseUser):
def has_access(self): def has_access(self):
""" Renvoie si un utilisateur a accès à internet """ """ Renvoie si un utilisateur a accès à internet """
return self.state == User.STATE_ACTIVE\ return self.state == User.STATE_ACTIVE\
and not self.is_ban and (self.is_adherent() or self.is_whitelisted) and not self.is_ban and (self.is_connected() or self.is_whitelisted)
def end_access(self): def end_access(self):
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)""" """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
if not self.end_adhesion(): if not self.end_connexion():
if not self.end_whitelist: if not self.end_whitelist:
return None return None
else: else:
return self.end_whitelist return self.end_whitelist
else: else:
if not self.end_whitelist: if not self.end_whitelist:
return self.end_adhesion() return self.end_connexion()
else: else:
return max(self.end_adhesion(), self.end_whitelist) return max(self.end_connexion(), self.end_whitelist)
@cached_property @cached_property
def solde(self): def solde(self):
@ -545,12 +572,16 @@ class User(AbstractBaseUser):
mail, password, shell, home mail, password, shell, home
access_refresh : synchronise le dialup_access notant si l'user a accès access_refresh : synchronise le dialup_access notant si l'user a accès
aux services aux services
mac_refresh : synchronise les machines de l'user""" mac_refresh : synchronise les machines de l'user
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
self.refresh_from_db() self.refresh_from_db()
try: try:
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
except LdapUser.DoesNotExist: except LdapUser.DoesNotExist:
user_ldap = LdapUser(uidNumber=self.uid_number) user_ldap = LdapUser(uidNumber=self.uid_number)
base = True
access_refresh = True
mac_refresh = True
if base: if base:
user_ldap.name = self.pseudo user_ldap.name = self.pseudo
user_ldap.sn = self.pseudo user_ldap.sn = self.pseudo
@ -574,7 +605,6 @@ class User(AbstractBaseUser):
user_ldap.macs = [str(mac) for mac in Interface.objects.filter( user_ldap.macs = [str(mac) for mac in Interface.objects.filter(
machine__user=self machine__user=self
).values_list('mac_address', flat=True).distinct()] ).values_list('mac_address', flat=True).distinct()]
user_ldap.save() user_ldap.save()
def ldap_del(self): def ldap_del(self):
@ -730,6 +760,7 @@ class User(AbstractBaseUser):
class Adherent(User): class Adherent(User):
PRETTY_NAME = "Adhérents"
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
room = models.OneToOneField( room = models.OneToOneField(
'topologie.Room', 'topologie.Room',
@ -741,6 +772,7 @@ class Adherent(User):
class Club(User): class Club(User):
PRETTY_NAME = "Clubs"
room = models.ForeignKey( room = models.ForeignKey(
'topologie.Room', 'topologie.Room',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -750,6 +782,8 @@ class Club(User):
pass pass
@receiver(post_save, sender=Adherent)
@receiver(post_save, sender=Club)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def user_post_save(sender, **kwargs): def user_post_save(sender, **kwargs):
""" Synchronisation post_save : envoie le mail de bienvenue si creation """ Synchronisation post_save : envoie le mail de bienvenue si creation
@ -762,6 +796,8 @@ def user_post_save(sender, **kwargs):
regen('mailing') regen('mailing')
@receiver(post_delete, sender=Adherent)
@receiver(post_delete, sender=Club)
@receiver(post_delete, sender=User) @receiver(post_delete, sender=User)
def user_post_delete(sender, **kwargs): def user_post_delete(sender, **kwargs):
"""Post delete d'un user, on supprime son instance ldap""" """Post delete d'un user, on supprime son instance ldap"""

View file

@ -119,7 +119,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Accès internet</th> <th>Accès internet</th>
{% if user.has_access == True %} {% if user.has_access == True %}
<td><font color="green">Actif</font></td> <td><font color="green">Actif (jusqu'au {{ user.end_access }})</font></td>
{% else %} {% else %}
<td><font color="red">Désactivé</font></td> <td><font color="red">Désactivé</font></td>
{% endif %} {% endif %}