mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-22 03:13:12 +00:00
Merge branch 'master' into reverse_url
This commit is contained in:
commit
5dc44a7136
19 changed files with 458 additions and 74 deletions
|
@ -38,6 +38,7 @@ ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter,
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.forms import ModelForm, Form
|
||||
from django.core.validators import MinValueValidator
|
||||
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)
|
||||
|
||||
|
||||
class SelectArticleForm(Form):
|
||||
class SelectUserArticleForm(Form):
|
||||
"""Selection d'un article lors de la creation d'une facture"""
|
||||
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",
|
||||
required=True
|
||||
)
|
||||
|
|
20
cotisations/migrations/0025_article_type_user.py
Normal file
20
cotisations/migrations/0025_article_type_user.py
Normal 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),
|
||||
),
|
||||
]
|
78
cotisations/migrations/0026_auto_20171028_0126.py
Normal file
78
cotisations/migrations/0026_auto_20171028_0126.py
Normal 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',
|
||||
),
|
||||
]
|
20
cotisations/migrations/0027_auto_20171029_1156.py
Normal file
20
cotisations/migrations/0027_auto_20171029_1156.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -47,6 +47,7 @@ from __future__ import unicode_literals
|
|||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.forms import ValidationError
|
||||
|
@ -127,15 +128,26 @@ class Vente(models.Model):
|
|||
iscotisation"""
|
||||
PRETTY_NAME = "Ventes effectuées"
|
||||
|
||||
COTISATION_TYPE = (
|
||||
('Connexion', 'Connexion'),
|
||||
('Adhesion', 'Adhesion'),
|
||||
('All', 'All'),
|
||||
)
|
||||
|
||||
facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
|
||||
number = models.IntegerField(validators=[MinValueValidator(1)])
|
||||
name = models.CharField(max_length=255)
|
||||
prix = models.DecimalField(max_digits=5, decimal_places=2)
|
||||
iscotisation = models.BooleanField()
|
||||
duration = models.PositiveIntegerField(
|
||||
help_text="Durée exprimée en mois entiers",
|
||||
blank=True,
|
||||
null=True)
|
||||
type_cotisation = models.CharField(
|
||||
choices=COTISATION_TYPE,
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=255
|
||||
)
|
||||
|
||||
def prix_total(self):
|
||||
"""Renvoie le prix_total de self (nombre*prix)"""
|
||||
|
@ -155,22 +167,26 @@ class Vente(models.Model):
|
|||
"""Update et crée l'objet cotisation associé à une facture, prend
|
||||
en argument l'user, la facture pour la quantitéi, et l'article pour
|
||||
la durée"""
|
||||
if not hasattr(self, 'cotisation'):
|
||||
if not hasattr(self, 'cotisation') and self.type_cotisation:
|
||||
cotisation = Cotisation(vente=self)
|
||||
cotisation.type_cotisation = self.type_cotisation
|
||||
if date_start:
|
||||
end_adhesion = Cotisation.objects.filter(
|
||||
end_cotisation = Cotisation.objects.filter(
|
||||
vente__in=Vente.objects.filter(
|
||||
facture__in=Facture.objects.filter(
|
||||
user=self.facture.user
|
||||
).exclude(valid=False))
|
||||
).filter(Q(type_cotisation='All') | Q(type_cotisation=self.type_cotisation)
|
||||
).filter(
|
||||
date_start__lt=date_start
|
||||
).aggregate(Max('date_end'))['date_end__max']
|
||||
elif self.type_cotisation=="Adhesion":
|
||||
end_cotisation = self.facture.user.end_adhesion()
|
||||
else:
|
||||
end_adhesion = self.facture.user.end_adhesion()
|
||||
end_cotisation = self.facture.user.end_connexion()
|
||||
date_start = date_start or timezone.now()
|
||||
end_adhesion = end_adhesion or date_start
|
||||
date_max = max(end_adhesion, date_start)
|
||||
end_cotisation = end_cotisation or date_start
|
||||
date_max = max(end_cotisation, date_start)
|
||||
cotisation.date_start = date_max
|
||||
cotisation.date_end = cotisation.date_start + relativedelta(
|
||||
months=self.duration*self.number
|
||||
|
@ -179,7 +195,7 @@ class Vente(models.Model):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
# On verifie que si iscotisation, duration est présent
|
||||
if self.iscotisation and not self.duration:
|
||||
if self.type_cotisation and not self.duration:
|
||||
raise ValidationError("Cotisation et durée doivent être présents\
|
||||
ensembles")
|
||||
self.update_cotisation()
|
||||
|
@ -197,7 +213,7 @@ def vente_post_save(sender, **kwargs):
|
|||
if hasattr(vente, 'cotisation'):
|
||||
vente.cotisation.vente = vente
|
||||
vente.cotisation.save()
|
||||
if vente.iscotisation:
|
||||
if vente.type_cotisation:
|
||||
vente.create_cotis()
|
||||
vente.cotisation.save()
|
||||
user = vente.facture.user
|
||||
|
@ -209,7 +225,7 @@ def vente_post_delete(sender, **kwargs):
|
|||
"""Après suppression d'une vente, on synchronise l'user ldap (ex
|
||||
suppression d'une cotisation"""
|
||||
vente = kwargs['instance']
|
||||
if vente.iscotisation:
|
||||
if vente.type_cotisation:
|
||||
user = vente.facture.user
|
||||
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
|
||||
|
||||
|
@ -219,18 +235,47 @@ class Article(models.Model):
|
|||
et duree si c'est une cotisation"""
|
||||
PRETTY_NAME = "Articles en vente"
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
USER_TYPES = (
|
||||
('Adherent', 'Adherent'),
|
||||
('Club', 'Club'),
|
||||
('All', 'All'),
|
||||
)
|
||||
|
||||
COTISATION_TYPE = (
|
||||
('Connexion', 'Connexion'),
|
||||
('Adhesion', 'Adhesion'),
|
||||
('All', 'All'),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
prix = models.DecimalField(max_digits=5, decimal_places=2)
|
||||
iscotisation = models.BooleanField()
|
||||
duration = models.PositiveIntegerField(
|
||||
help_text="Durée exprimée en mois entiers",
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0)])
|
||||
type_user = models.CharField(
|
||||
choices=USER_TYPES,
|
||||
default='All',
|
||||
max_length=255
|
||||
)
|
||||
type_cotisation = models.CharField(
|
||||
choices=COTISATION_TYPE,
|
||||
default=None,
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=255
|
||||
)
|
||||
|
||||
unique_together = ('name', 'type_user')
|
||||
|
||||
def clean(self):
|
||||
if self.name.lower() == "solde":
|
||||
raise ValidationError("Solde est un nom d'article invalide")
|
||||
if self.type_cotisation and not self.duration:
|
||||
raise ValidationError(
|
||||
"La durée est obligatoire si il s'agit d'une cotisation"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -275,7 +320,17 @@ class Cotisation(models.Model):
|
|||
"""Objet cotisation, debut et fin, relié en onetoone à une vente"""
|
||||
PRETTY_NAME = "Cotisations"
|
||||
|
||||
COTISATION_TYPE = (
|
||||
('Connexion', 'Connexion'),
|
||||
('Adhesion', 'Adhesion'),
|
||||
('All', 'All'),
|
||||
)
|
||||
|
||||
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
|
||||
type_cotisation = models.CharField(
|
||||
choices=COTISATION_TYPE,
|
||||
max_length=255,
|
||||
)
|
||||
date_start = models.DateTimeField()
|
||||
date_end = models.DateTimeField()
|
||||
|
||||
|
|
|
@ -27,8 +27,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th>Article</th>
|
||||
<th>Prix</th>
|
||||
<th>Cotisation</th>
|
||||
<th>Type Cotisation</th>
|
||||
<th>Durée (mois)</th>
|
||||
<th>Article pour</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -36,8 +37,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<td>{{ article.name }}</td>
|
||||
<td>{{ article.prix }}</td>
|
||||
<td>{{ article.iscotisation }}</td>
|
||||
<td>{{ article.type_cotisation }}</td>
|
||||
<td>{{ article.duration }}</td>
|
||||
<td>{{ article.type_user }}</td>
|
||||
<td class="text-right">
|
||||
{% if is_trez %}
|
||||
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'cotisations:edit-article' article.id %}">
|
||||
|
|
|
@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<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='date' text='Date' %}</th>
|
||||
<th></th>
|
||||
<th>{% include "buttons/sort.html" with prefix='cotis' col='id' text='Id facture' %}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
@ -46,17 +46,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ facture.prix_total }}</td>
|
||||
<td>{{ facture.paiement }}</td>
|
||||
<td>{{ facture.date }}</td>
|
||||
<td>{{ facture.id }}</td>
|
||||
{% if is_cableur %}
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="editionfacture">
|
||||
{% 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:history' 'facture' facture.id %}"><i class="glyphicon glyphicon-time"></i> Historique</a></li>
|
||||
{% else %}
|
||||
<li>Facture controlée</li>
|
||||
{% endif %}
|
||||
|
@ -74,11 +76,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<font color="red">Facture invalide</font>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
|
@ -42,6 +42,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<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='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>Prix total</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>{{ form.instance.user.name }}</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.prix_total }}</td>
|
||||
<td>{{ form.instance.paiement }}</td>
|
||||
|
|
|
@ -33,6 +33,7 @@ from django.contrib.auth.decorators import login_required, permission_required
|
|||
from django.contrib import messages
|
||||
from django.db.models import ProtectedError
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.forms import modelformset_factory, formset_factory
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
@ -45,10 +46,21 @@ from re2o.views import form
|
|||
from re2o.utils import SortTable
|
||||
from preferences.models import OptionalUser, AssoOption, GeneralOption
|
||||
from .models import Facture, Article, Vente, Paiement, Banque
|
||||
from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm
|
||||
from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm
|
||||
from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf
|
||||
from .forms import SelectArticleForm, CreditSoldeForm
|
||||
from .forms import (
|
||||
NewFactureForm,
|
||||
TrezEditFactureForm,
|
||||
EditFactureForm,
|
||||
ArticleForm,
|
||||
DelArticleForm,
|
||||
PaiementForm,
|
||||
DelPaiementForm,
|
||||
BanqueForm,
|
||||
DelBanqueForm,
|
||||
NewFactureFormPdf,
|
||||
SelectUserArticleForm,
|
||||
SelectClubArticleForm,
|
||||
CreditSoldeForm
|
||||
)
|
||||
from .tex import render_tex
|
||||
|
||||
|
||||
|
@ -69,10 +81,15 @@ def new_facture(request, userid):
|
|||
return redirect(reverse('cotisations:index'))
|
||||
facture = Facture(user=user)
|
||||
# Le template a besoin de connaitre les articles pour le js
|
||||
article_list = Article.objects.all()
|
||||
article_list = Article.objects.filter(
|
||||
Q(type_user='All') | Q(type_user=request.user.class_name)
|
||||
)
|
||||
# On envoie la form fature et un formset d'articles
|
||||
facture_form = NewFactureForm(request.POST or None, instance=facture)
|
||||
article_formset = formset_factory(SelectArticleForm)(request.POST or None)
|
||||
if request.user.is_class_club:
|
||||
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
|
||||
else:
|
||||
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
|
||||
if facture_form.is_valid() and article_formset.is_valid():
|
||||
new_facture_instance = facture_form.save(commit=False)
|
||||
articles = article_formset
|
||||
|
@ -111,7 +128,7 @@ def new_facture(request, userid):
|
|||
facture=new_facture_instance,
|
||||
name=article.name,
|
||||
prix=article.prix,
|
||||
iscotisation=article.iscotisation,
|
||||
type_cotisation=article.type_cotisation,
|
||||
duration=article.duration,
|
||||
number=quantity
|
||||
)
|
||||
|
@ -119,7 +136,7 @@ def new_facture(request, userid):
|
|||
new_vente.save()
|
||||
reversion.set_user(request.user)
|
||||
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):
|
||||
messages.success(
|
||||
request,
|
||||
|
@ -326,8 +343,6 @@ def credit_solde(request, userid):
|
|||
facture=facture_instance,
|
||||
name="solde",
|
||||
prix=facture.cleaned_data['montant'],
|
||||
iscotisation=False,
|
||||
duration=0,
|
||||
number=1
|
||||
)
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
|
|
|
@ -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...", '')
|
||||
elif not interface.is_active:
|
||||
return (False, u"Machine desactivée", '')
|
||||
elif not interface.ipv4:
|
||||
interface.assign_ipv4()
|
||||
return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm)
|
||||
else:
|
||||
return (True, u"Access ok", user.pwd_ntlm)
|
||||
elif nas_type:
|
||||
|
@ -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)
|
||||
else:
|
||||
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)
|
||||
elif not interface.ipv4:
|
||||
interface.assign_ipv4()
|
||||
return (sw_name, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN)
|
||||
else:
|
||||
return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN)
|
||||
|
||||
|
|
132
logs/views.py
132
logs/views.py
|
@ -47,13 +47,50 @@ 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
|
||||
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 users.models import (
|
||||
User,
|
||||
ServiceUser,
|
||||
Right,
|
||||
School,
|
||||
ListRight,
|
||||
ListShell,
|
||||
Ban,
|
||||
Whitelist,
|
||||
Adherent,
|
||||
Club
|
||||
)
|
||||
from cotisations.models import (
|
||||
Facture,
|
||||
Vente,
|
||||
Article,
|
||||
Banque,
|
||||
Paiement,
|
||||
Cotisation
|
||||
)
|
||||
from machines.models import (
|
||||
Machine,
|
||||
MachineType,
|
||||
IpType,
|
||||
Extension,
|
||||
Interface,
|
||||
Domain,
|
||||
IpList,
|
||||
OuverturePortList,
|
||||
Service,
|
||||
Vlan,
|
||||
Nas,
|
||||
SOA,
|
||||
Mx,
|
||||
Ns
|
||||
)
|
||||
from topologie.models import (
|
||||
Switch,
|
||||
Port,
|
||||
Room,
|
||||
Stack,
|
||||
ModelSwitch,
|
||||
ConstructorSwitch
|
||||
)
|
||||
from preferences.models import GeneralOption
|
||||
from re2o.views import form
|
||||
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,
|
||||
cotisants, activés, archivés, etc"""
|
||||
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)
|
||||
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_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]
|
||||
_all_adherent = all_adherent()
|
||||
_all_has_access = all_has_access()
|
||||
_all_baned = all_baned()
|
||||
_all_whitelisted = all_whitelisted()
|
||||
_all_active_interfaces_count = all_active_interfaces_count()
|
||||
_all_active_assigned_interfaces_count = all_active_assigned_interfaces_count()
|
||||
stats = [
|
||||
[["Categorie", "Nombre d'utilisateurs"], {
|
||||
[["Categorie", "Nombre d'utilisateurs (total club et adhérents)", "Nombre d'adhérents", "Nombre de clubs"], {
|
||||
'active_users': [
|
||||
"Users actifs",
|
||||
User.objects.filter(state=User.STATE_ACTIVE).count()],
|
||||
User.objects.filter(state=User.STATE_ACTIVE).count(),
|
||||
Adherent.objects.filter(state=Adherent.STATE_ACTIVE).count(),
|
||||
Club.objects.filter(state=Club.STATE_ACTIVE).count()],
|
||||
'inactive_users': [
|
||||
"Users désactivés",
|
||||
User.objects.filter(state=User.STATE_DISABLED).count()],
|
||||
User.objects.filter(state=User.STATE_DISABLED).count(),
|
||||
Adherent.objects.filter(state=Adherent.STATE_DISABLED).count(),
|
||||
Club.objects.filter(state=Club.STATE_DISABLED).count()],
|
||||
'archive_users': [
|
||||
"Users archivés",
|
||||
User.objects.filter(state=User.STATE_ARCHIVE).count()],
|
||||
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': [
|
||||
"Adhérents à l'association",
|
||||
all_adherent().count()],
|
||||
"Cotisant à l'association",
|
||||
_all_adherent.count(),
|
||||
_all_adherent.exclude(adherent__isnull=True).count(),
|
||||
_all_adherent.exclude(club__isnull=True).count()],
|
||||
'connexion_users': [
|
||||
"Utilisateurs bénéficiant d'une connexion",
|
||||
all_has_access().count()],
|
||||
_all_has_access.count(),
|
||||
_all_has_access.exclude(adherent__isnull=True).count(),
|
||||
_all_has_access.exclude(club__isnull=True).count()],
|
||||
'ban_users': [
|
||||
"Utilisateurs bannis",
|
||||
all_baned().count()],
|
||||
_all_baned.count(),
|
||||
_all_baned.exclude(adherent__isnull=True).count(),
|
||||
_all_baned.exclude(club__isnull=True).count()],
|
||||
'whitelisted_user': [
|
||||
"Utilisateurs bénéficiant d'une connexion gracieuse",
|
||||
all_whitelisted().count()],
|
||||
_all_whitelisted.count(),
|
||||
_all_whitelisted.exclude(adherent__isnull=True).count(),
|
||||
_all_whitelisted.exclude(club__isnull=True).count()],
|
||||
'actives_interfaces': [
|
||||
"Interfaces actives (ayant accès au reseau)",
|
||||
all_active_interfaces_count().count()],
|
||||
_all_active_interfaces_count.count(),
|
||||
_all_active_interfaces_count.exclude(
|
||||
machine__user__adherent__isnull=True
|
||||
).count(),
|
||||
_all_active_interfaces_count.exclude(
|
||||
machine__user__club__isnull=True
|
||||
).count()],
|
||||
'actives_assigned_interfaces': [
|
||||
"Interfaces actives et assignées ipv4",
|
||||
all_active_assigned_interfaces_count().count()]
|
||||
_all_active_assigned_interfaces_count.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]
|
||||
]
|
||||
return render(request, 'logs/stats_general.html', {'stats_list': stats})
|
||||
|
@ -237,6 +306,8 @@ def stats_models(request):
|
|||
stats = {
|
||||
'Users': {
|
||||
'users': [User.PRETTY_NAME, User.objects.count()],
|
||||
'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()],
|
||||
'clubs': [Club.PRETTY_NAME, Club.objects.count()],
|
||||
'serviceuser': [ServiceUser.PRETTY_NAME,
|
||||
ServiceUser.objects.count()],
|
||||
'right': [Right.PRETTY_NAME, Right.objects.count()],
|
||||
|
@ -263,11 +334,30 @@ def stats_models(request):
|
|||
'alias': [Domain.PRETTY_NAME,
|
||||
Domain.objects.exclude(cname=None).count()],
|
||||
'iplist': [IpList.PRETTY_NAME, IpList.objects.count()],
|
||||
'service': [Service.PRETTY_NAME, Service.objects.count()],
|
||||
'ouvertureportlist': [
|
||||
OuverturePortList.PRETTY_NAME,
|
||||
OuverturePortList.objects.count()
|
||||
],
|
||||
'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()],
|
||||
'SOA': [Mx.PRETTY_NAME, Mx.objects.count()],
|
||||
'Mx': [Mx.PRETTY_NAME, Mx.objects.count()],
|
||||
'Ns': [Ns.PRETTY_NAME, Ns.objects.count()],
|
||||
'nas': [Nas.PRETTY_NAME, Nas.objects.count()],
|
||||
},
|
||||
'Topologie': {
|
||||
'switch': [Switch.PRETTY_NAME, Switch.objects.count()],
|
||||
'port': [Port.PRETTY_NAME, Port.objects.count()],
|
||||
'chambre': [Room.PRETTY_NAME, Room.objects.count()],
|
||||
'stack': [Stack.PRETTY_NAME, Stack.objects.count()],
|
||||
'modelswitch': [
|
||||
ModelSwitch.PRETTY_NAME,
|
||||
ModelSwitch.objects.count()
|
||||
],
|
||||
'constructorswitch': [
|
||||
ConstructorSwitch.PRETTY_NAME,
|
||||
ConstructorSwitch.objects.count()
|
||||
],
|
||||
},
|
||||
'Actions effectuées sur la base':
|
||||
{
|
||||
|
|
|
@ -460,6 +460,15 @@ class Interface(models.Model):
|
|||
def clean(self, *args, **kwargs):
|
||||
""" Formate l'addresse mac en mac_bare (fonction filter_mac)
|
||||
et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
|
||||
# If type was an invalid value, django won't create an attribute type
|
||||
# but try clean() as we may be able to create it from another value
|
||||
# so even if the error as yet been detected at this point, django
|
||||
# continues because the error might not prevent us from creating the
|
||||
# instance.
|
||||
# But in our case, it's impossible to create a type value so we raise
|
||||
# the error.
|
||||
if not hasattr(self, 'type') :
|
||||
raise ValidationError("Le type d'ip choisi n'est pas valide")
|
||||
self.filter_macaddress()
|
||||
self.mac_address = str(EUI(self.mac_address)) or None
|
||||
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
|
||||
|
@ -626,6 +635,8 @@ class IpList(models.Model):
|
|||
|
||||
class Service(models.Model):
|
||||
""" Definition d'un service (dhcp, dns, etc)"""
|
||||
PRETTY_NAME = "Services à générer (dhcp, dns, etc)"
|
||||
|
||||
service_type = models.CharField(max_length=255, blank=True, unique=True)
|
||||
min_time_regen = models.DurationField(
|
||||
default=timedelta(minutes=1),
|
||||
|
@ -673,6 +684,8 @@ def regen(service):
|
|||
|
||||
class Service_link(models.Model):
|
||||
""" Definition du lien entre serveurs et services"""
|
||||
PRETTY_NAME = "Relation entre service et serveur"
|
||||
|
||||
service = models.ForeignKey('Service', on_delete=models.CASCADE)
|
||||
server = models.ForeignKey('Interface', on_delete=models.CASCADE)
|
||||
last_regen = models.DateTimeField(auto_now_add=True)
|
||||
|
@ -702,6 +715,8 @@ class Service_link(models.Model):
|
|||
|
||||
class OuverturePortList(models.Model):
|
||||
"""Liste des ports ouverts sur une interface."""
|
||||
PRETTY_NAME = "Profil d'ouverture de ports"
|
||||
|
||||
name = models.CharField(
|
||||
help_text="Nom de la configuration des ports.",
|
||||
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
|
||||
"""
|
||||
PRETTY_NAME = "Plage de port ouverte"
|
||||
|
||||
TCP = 'T'
|
||||
UDP = 'U'
|
||||
IN = 'I'
|
||||
|
|
|
@ -56,6 +56,7 @@ def all_adherent(search_time=DT_NOW):
|
|||
return User.objects.filter(
|
||||
facture__in=Facture.objects.filter(
|
||||
vente__in=Vente.objects.filter(
|
||||
Q(type_cotisation='All') | Q(type_cotisation='Adhesion'),
|
||||
cotisation__in=Cotisation.objects.filter(
|
||||
vente__in=Vente.objects.filter(
|
||||
facture__in=Facture.objects.all().exclude(valid=False)
|
||||
|
@ -94,6 +95,7 @@ def all_has_access(search_time=DT_NOW):
|
|||
Q(facture__in=Facture.objects.filter(
|
||||
vente__in=Vente.objects.filter(
|
||||
cotisation__in=Cotisation.objects.filter(
|
||||
Q(type_cotisation='All') | Q(type_cotisation='Connexion'),
|
||||
vente__in=Vente.objects.filter(
|
||||
facture__in=Facture.objects.all()
|
||||
.exclude(valid=False)
|
||||
|
@ -179,15 +181,18 @@ class SortTable:
|
|||
'cotis_user': ['user__pseudo'],
|
||||
'cotis_paiement': ['paiement__moyen'],
|
||||
'cotis_date': ['date'],
|
||||
'cotis_id': ['id'],
|
||||
'default': ['-date']
|
||||
}
|
||||
COTISATIONS_CONTROL = {
|
||||
'control_name': ['user__name'],
|
||||
'control_name': ['user__adherent__name'],
|
||||
'control_surname': ['user__surname'],
|
||||
'control_paiement': ['paiement'],
|
||||
'control_date': ['date'],
|
||||
'control_valid': ['valid'],
|
||||
'control_control': ['control'],
|
||||
'control_id': ['id'],
|
||||
'control_user-id': ['user__id'],
|
||||
'default': ['-date']
|
||||
}
|
||||
TOPOLOGIE_INDEX = {
|
||||
|
|
|
@ -49,6 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</head>
|
||||
|
||||
<body>
|
||||
{% include "cookie_banner.html" %}
|
||||
<div id="wrap">
|
||||
<nav class="navbar navbar-inverse">
|
||||
<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>
|
||||
<td class="text-right">
|
||||
{% 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 %}
|
||||
<font color="red">Désactivée</font>
|
||||
{% endif %}
|
||||
|
@ -155,8 +156,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th scope="row">Adhésion</th>
|
||||
<td class="text-right">
|
||||
{% if request_user.end_adhesion != None %}
|
||||
<font color="green">{{ request_user.end_adhesion }}</font>
|
||||
{% if request_user.is_adherent %}
|
||||
<font color="green">jusqu'au {{ request_user.end_adhesion|date:"d b Y" }}</font>
|
||||
{% else %}
|
||||
<font color="red">Non adhérent</font>
|
||||
{% endif %}
|
||||
|
|
19
templates/cookie_banner.html
Normal file
19
templates/cookie_banner.html
Normal 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 %}
|
|
@ -140,7 +140,7 @@ class EditRoomForm(ModelForm):
|
|||
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."""
|
||||
begin = forms.IntegerField(label="Début :", min_value=0)
|
||||
end = forms.IntegerField(label="Fin :", min_value=0)
|
||||
|
|
|
@ -160,6 +160,7 @@ class Switch(models.Model):
|
|||
|
||||
class ModelSwitch(models.Model):
|
||||
"""Un modèle (au sens constructeur) de switch"""
|
||||
PRETTY_NAME = "Modèle de switch"
|
||||
reference = models.CharField(max_length=255)
|
||||
constructor = models.ForeignKey(
|
||||
'topologie.ConstructorSwitch',
|
||||
|
@ -172,6 +173,7 @@ class ModelSwitch(models.Model):
|
|||
|
||||
class ConstructorSwitch(models.Model):
|
||||
"""Un constructeur de switch"""
|
||||
PRETTY_NAME = "Constructeur de switch"
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -182,7 +182,7 @@ class User(AbstractBaseUser):
|
|||
""" Definition de l'utilisateur de base.
|
||||
Champs principaux : name, surnname, pseudo, email, room, password
|
||||
Herite du django BaseUser et du système d'auth django"""
|
||||
PRETTY_NAME = "Utilisateurs"
|
||||
PRETTY_NAME = "Utilisateurs (clubs et adhérents)"
|
||||
STATE_ACTIVE = 0
|
||||
STATE_DISABLED = 1
|
||||
STATE_ARCHIVE = 2
|
||||
|
@ -255,7 +255,7 @@ class User(AbstractBaseUser):
|
|||
def class_name(self):
|
||||
"""Renvoie si il s'agit d'un adhérent ou d'un club"""
|
||||
if hasattr(self, 'adherent'):
|
||||
return "Adhérent"
|
||||
return "Adherent"
|
||||
elif hasattr(self, 'club'):
|
||||
return "Club"
|
||||
else:
|
||||
|
@ -378,6 +378,22 @@ class User(AbstractBaseUser):
|
|||
user=self
|
||||
).exclude(valid=False)
|
||||
)
|
||||
).filter(
|
||||
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
|
||||
).aggregate(models.Max('date_end'))['date_end__max']
|
||||
return date_max
|
||||
|
||||
def end_connexion(self):
|
||||
""" Renvoie la date de fin de connexion d'un user. Examine les objets
|
||||
cotisation"""
|
||||
date_max = Cotisation.objects.filter(
|
||||
vente__in=Vente.objects.filter(
|
||||
facture__in=Facture.objects.filter(
|
||||
user=self
|
||||
).exclude(valid=False)
|
||||
)
|
||||
).filter(
|
||||
Q(type_cotisation='All') | Q(type_cotisation='Connexion')
|
||||
).aggregate(models.Max('date_end'))['date_end__max']
|
||||
return date_max
|
||||
|
||||
|
@ -392,6 +408,17 @@ class User(AbstractBaseUser):
|
|||
else:
|
||||
return True
|
||||
|
||||
def is_connected(self):
|
||||
""" Renvoie True si l'user est adhérent : si
|
||||
self.end_adhesion()>now et end_connexion>now"""
|
||||
end = self.end_connexion()
|
||||
if not end:
|
||||
return False
|
||||
elif end < DT_NOW:
|
||||
return False
|
||||
else:
|
||||
return self.is_adherent()
|
||||
|
||||
@cached_property
|
||||
def end_ban(self):
|
||||
""" Renvoie la date de fin de ban d'un user, False sinon """
|
||||
|
@ -433,20 +460,20 @@ class User(AbstractBaseUser):
|
|||
def has_access(self):
|
||||
""" Renvoie si un utilisateur a accès à internet """
|
||||
return self.state == User.STATE_ACTIVE\
|
||||
and not self.is_ban and (self.is_adherent() or self.is_whitelisted)
|
||||
and not self.is_ban and (self.is_connected() or self.is_whitelisted)
|
||||
|
||||
def end_access(self):
|
||||
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
|
||||
if not self.end_adhesion():
|
||||
if not self.end_connexion():
|
||||
if not self.end_whitelist:
|
||||
return None
|
||||
else:
|
||||
return self.end_whitelist
|
||||
else:
|
||||
if not self.end_whitelist:
|
||||
return self.end_adhesion()
|
||||
return self.end_connexion()
|
||||
else:
|
||||
return max(self.end_adhesion(), self.end_whitelist)
|
||||
return max(self.end_connexion(), self.end_whitelist)
|
||||
|
||||
@cached_property
|
||||
def solde(self):
|
||||
|
@ -545,12 +572,16 @@ class User(AbstractBaseUser):
|
|||
mail, password, shell, home
|
||||
access_refresh : synchronise le dialup_access notant si l'user a accès
|
||||
aux services
|
||||
mac_refresh : synchronise les machines de l'user"""
|
||||
mac_refresh : synchronise les machines de l'user
|
||||
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
|
||||
self.refresh_from_db()
|
||||
try:
|
||||
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
|
||||
except LdapUser.DoesNotExist:
|
||||
user_ldap = LdapUser(uidNumber=self.uid_number)
|
||||
base = True
|
||||
access_refresh = True
|
||||
mac_refresh = True
|
||||
if base:
|
||||
user_ldap.name = self.pseudo
|
||||
user_ldap.sn = self.pseudo
|
||||
|
@ -574,7 +605,6 @@ class User(AbstractBaseUser):
|
|||
user_ldap.macs = [str(mac) for mac in Interface.objects.filter(
|
||||
machine__user=self
|
||||
).values_list('mac_address', flat=True).distinct()]
|
||||
|
||||
user_ldap.save()
|
||||
|
||||
def ldap_del(self):
|
||||
|
@ -730,6 +760,7 @@ class User(AbstractBaseUser):
|
|||
|
||||
|
||||
class Adherent(User):
|
||||
PRETTY_NAME = "Adhérents"
|
||||
name = models.CharField(max_length=255)
|
||||
room = models.OneToOneField(
|
||||
'topologie.Room',
|
||||
|
@ -741,6 +772,7 @@ class Adherent(User):
|
|||
|
||||
|
||||
class Club(User):
|
||||
PRETTY_NAME = "Clubs"
|
||||
room = models.ForeignKey(
|
||||
'topologie.Room',
|
||||
on_delete=models.PROTECT,
|
||||
|
@ -750,6 +782,8 @@ class Club(User):
|
|||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=Adherent)
|
||||
@receiver(post_save, sender=Club)
|
||||
@receiver(post_save, sender=User)
|
||||
def user_post_save(sender, **kwargs):
|
||||
""" Synchronisation post_save : envoie le mail de bienvenue si creation
|
||||
|
@ -762,6 +796,8 @@ def user_post_save(sender, **kwargs):
|
|||
regen('mailing')
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Adherent)
|
||||
@receiver(post_delete, sender=Club)
|
||||
@receiver(post_delete, sender=User)
|
||||
def user_post_delete(sender, **kwargs):
|
||||
"""Post delete d'un user, on supprime son instance ldap"""
|
||||
|
|
|
@ -119,7 +119,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th>Accès internet</th>
|
||||
{% 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 %}
|
||||
<td><font color="red">Désactivé</font></td>
|
||||
{% endif %}
|
||||
|
|
Loading…
Reference in a new issue