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

Merge branch 'crans' of https://gitlab.federez.net/federez/re2o into crans

This commit is contained in:
root 2018-07-01 13:55:52 +00:00
commit 093050e245
73 changed files with 2153 additions and 187 deletions

View file

@ -1,3 +1,4 @@
# -*- 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.

View file

@ -1,3 +1,4 @@
# -*- 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.
@ -43,6 +44,7 @@ class ExpiringTokenAuthentication(TokenAuthentication):
)
utc_now = datetime.datetime.now(datetime.timezone.utc)
if token.created < utc_now - token_duration:
raise ValueError('boom')
raise exceptions.AuthenticationFailed(_('Token has expired'))
return (token.user, token)

View file

@ -1,3 +1,4 @@
# -*- 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.

View file

@ -1,3 +1,4 @@
# -*- 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.

View file

@ -1,3 +1,4 @@
# -*- 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.

View file

@ -1,3 +1,4 @@
# -*- 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.
@ -484,10 +485,12 @@ class UserSerializer(NamespacedHMSerializer):
"""
access = serializers.BooleanField(source='has_access')
uid = serializers.IntegerField(source='uid_number')
email = serializers.CharField(source='get_mail')
class Meta:
model = users.User
fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment',
'external_mail', 'redirection', 'internal_address',
'state', 'registered', 'telephone', 'solde', 'access',
'end_access', 'uid', 'class_name', 'api_url')
extra_kwargs = {
@ -501,10 +504,12 @@ class ClubSerializer(NamespacedHMSerializer):
name = serializers.CharField(source='surname')
access = serializers.BooleanField(source='has_access')
uid = serializers.IntegerField(source='uid_number')
email = serializers.CharField(source='get_mail')
class Meta:
model = users.Club
fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment',
'external_mail', 'redirection', 'internal_address',
'state', 'registered', 'telephone', 'solde', 'room',
'access', 'end_access', 'administrators', 'members',
'mailing', 'uid', 'api_url')
@ -518,10 +523,12 @@ class AdherentSerializer(NamespacedHMSerializer):
"""
access = serializers.BooleanField(source='has_access')
uid = serializers.IntegerField(source='uid_number')
email = serializers.CharField(source='get_mail')
class Meta:
model = users.Adherent
fields = ('name', 'surname', 'pseudo', 'email', 'school', 'shell',
fields = ('name', 'surname', 'pseudo', 'email', 'redirection', 'internal_address',
'external_mail', 'school', 'shell',
'comment', 'state', 'registered', 'telephone', 'room',
'solde', 'access', 'end_access', 'uid', 'api_url')
extra_kwargs = {
@ -585,6 +592,15 @@ class WhitelistSerializer(NamespacedHMSerializer):
fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url')
class MailAliasSerializer(NamespacedHMSerializer):
"""Serialize `users.models.MailAlias` objects.
"""
class Meta:
model = users.MailAlias
fields = ('user', 'valeur', 'complete_mail')
# SERVICE REGEN

View file

@ -1,3 +1,4 @@
# -*- 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.

View file

@ -1,3 +1,4 @@
# -*- 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.

View file

@ -1,3 +1,4 @@
# -*- 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.
@ -91,6 +92,7 @@ router.register_viewset(r'users/listright', views.ListRightViewSet)
router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell')
router.register_viewset(r'users/ban', views.BanViewSet)
router.register_viewset(r'users/whitelist', views.WhitelistViewSet)
router.register_viewset(r'users/mailalias', views.MailAliasViewSet)
# SERVICE REGEN
router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen')
# DHCP

View file

@ -1,3 +1,4 @@
# -*- 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.
@ -461,6 +462,13 @@ class WhitelistViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.WhitelistSerializer
class MailAliasViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.MailAlias` objects.
"""
queryset = users.MailAlias.objects.all()
serializer_class = serializers.MailAliasSerializer
# SERVICE REGEN

View file

@ -355,27 +355,47 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
port=port_number
)
.first())
# Si le port est inconnu, on place sur le vlan defaut
# Aucune information particulière ne permet de déterminer quelle
# politique à appliquer sur ce port
if not port:
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK)
# Si un vlan a été précisé, on l'utilise pour VLAN_OK
if port.vlan_force:
DECISION_VLAN = int(port.vlan_force.vlan_id)
# On récupère le profil du port
port_profil = port.get_port_profil
# Si un vlan a été précisé dans la config du port,
# on l'utilise pour VLAN_OK
if port_profil.vlan_untagged:
DECISION_VLAN = int(port_profil.vlan_untagged.vlan_id)
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
else:
DECISION_VLAN = VLAN_OK
if port.radius == 'NO':
# Si le port est désactivé, on rejette sur le vlan de déconnexion
if not port.state:
return (sw_name, port.room, u'Port desactivé', VLAN_NOK)
# Si radius est désactivé, on laisse passer
if port_profil.radius_type == 'NO':
return (sw_name,
"",
u"Pas d'authentification sur ce port" + extra_log,
DECISION_VLAN)
if port.radius == 'BLOQ':
return (sw_name, port.room, u'Port desactive', VLAN_NOK)
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
# Par conséquent, on laisse passer sur le bon vlan
if nas_type.port_access_mode == '802.1X' and port_profil.radius_type == '802.1X':
room = port.room or "Chambre/local inconnu"
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN)
if port.radius == 'STRICT':
# Sinon, cela veut dire qu'on fait de l'auth radius par mac
# Si le port est en mode strict, on vérifie que tous les users
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
# dedans
if port_profil.radius_mode == 'STRICT':
room = port.room
if not room:
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK)
@ -390,7 +410,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
return (sw_name, room, u'Chambre resident desactive', VLAN_NOK)
# else: user OK, on passe à la verif MAC
if port.radius == 'COMMON' or port.radius == 'STRICT':
# Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
if port_profil.radius_mode == 'COMMON' or port_profil.radius_mode == 'STRICT':
# Authentification par mac
interface = (Interface.objects
.filter(mac_address=mac_address)
@ -399,15 +420,19 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
.first())
if not interface:
room = port.room
# On essaye de register la mac
# On essaye de register la mac, si l'autocapture a été activée
# Sinon on rejette sur vlan_nok
if not nas_type.autocapture_mac:
return (sw_name, "", u'Machine inconnue', VLAN_NOK)
# On ne peut autocapturer que si on connait la chambre et donc l'user correspondant
elif not room:
return (sw_name,
"Inconnue",
u'Chambre et machine inconnues',
VLAN_NOK)
else:
# Si la chambre est vide (local club, prises en libre services)
# Impossible d'autocapturer
if not room_user:
room_user = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room)
@ -418,6 +443,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
u'Machine et propriétaire de la chambre '
'inconnus',
VLAN_NOK)
# Si il y a plus d'un user dans la chambre, impossible de savoir à qui
# Ajouter la machine
elif room_user.count() > 1:
return (sw_name,
room,
@ -425,11 +452,13 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
'dans la chambre/local -> ajout de mac '
'automatique impossible',
VLAN_NOK)
# Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture
elif not room_user.first().has_access():
return (sw_name,
room,
u'Machine inconnue et adhérent non cotisant',
VLAN_NOK)
# Sinon on capture et on laisse passer sur le bon vlan
else:
result, reason = (room_user
.first()
@ -449,6 +478,9 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
reason + str(mac_address)
),
VLAN_NOK)
# L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
# Si elle n'a pas d'ipv4, on lui en met une
# Enfin on laisse passer sur le vlan pertinent
else:
room = port.room
if not interface.is_active:

View file

@ -266,7 +266,6 @@ class ExtensionForm(FormRevMixin, ModelForm):
self.fields['origin'].label = 'Enregistrement A origin'
self.fields['origin_v6'].label = 'Enregistrement AAAA origin'
self.fields['soa'].label = 'En-tête SOA à utiliser'
self.fielss['mail_extension'].label = 'Utilisable comme extension mail'
class DelExtensionForm(FormRevMixin, Form):

View file

@ -641,10 +641,6 @@ class Extension(RevMixin, AclMixin, models.Model):
'SOA',
on_delete=models.CASCADE
)
mail_extension = models.BooleanField(
default=False,
help_text="Determine si l'extension peut être utilisée comme extension mail interne"
)
class Meta:
permissions = (

View file

@ -34,6 +34,7 @@ from .models import (
OptionalTopologie,
GeneralOption,
Service,
MailContact,
AssoOption,
MailMessageOption,
HomeOption
@ -65,6 +66,11 @@ class ServiceAdmin(VersionAdmin):
pass
class MailContactAdmin(VersionAdmin):
"""Class admin gestion des adresses mail de contact"""
pass
class AssoOptionAdmin(VersionAdmin):
"""Class admin options de l'asso"""
pass
@ -86,5 +92,6 @@ admin.site.register(OptionalTopologie, OptionalTopologieAdmin)
admin.site.register(GeneralOption, GeneralOptionAdmin)
admin.site.register(HomeOption, HomeOptionAdmin)
admin.site.register(Service, ServiceAdmin)
admin.site.register(MailContact, MailContactAdmin)
admin.site.register(AssoOption, AssoOptionAdmin)
admin.site.register(MailMessageOption, MailMessageOptionAdmin)

View file

@ -35,7 +35,8 @@ from .models import (
AssoOption,
MailMessageOption,
HomeOption,
Service
Service,
MailContact
)
class EditOptionalUserForm(ModelForm):
@ -233,3 +234,30 @@ class DelServiceForm(Form):
self.fields['services'].queryset = instances
else:
self.fields['services'].queryset = Service.objects.all()
class MailContactForm(ModelForm):
"""Edition, ajout d'adresse de contact"""
class Meta:
model = MailContact
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(MailContactForm, self).__init__(*args, prefix=prefix, **kwargs)
class DelMailContactForm(Form):
"""Suppression d'adresse de contact"""
mailcontacts = forms.ModelMultipleChoiceField(
queryset=MailContact.objects.none(),
label="Enregistrements adresses actuels",
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
instances = kwargs.pop('instances', None)
super(DelMailContactForm, self).__init__(*args, **kwargs)
if instances:
self.fields['mailcontacts'].queryset = instances
else:
self.fields['mailcontacts'].queryset = MailContact.objects.all()

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 19:31
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0034_auto_20180416_1120'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='mail_extension',
field=models.CharField(default='@example.org', help_text='Extension principale pour les mails internes', max_length=32),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 16:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0035_optionaluser_mail_extension'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='mail_accounts',
field=models.BooleanField(default=False, help_text='Activation des comptes mails pour les utilisateurs'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 12:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0036_optionaluser_mail_accounts'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='max_mail_alias',
field=models.IntegerField(default=15, help_text="Nombre maximal d'alias pour un utilisateur lambda"),
),
]

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 15:27
from __future__ import unicode_literals
from django.db import migrations, models
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('preferences', '0037_optionaluser_max_mail_alias'),
]
operations = [
migrations.CreateModel(
name='MailContact',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('address', models.EmailField(default='contact@example.org', help_text='Adresse mail de contact', max_length=254)),
('commentary', models.CharField(blank=True, help_text="Description de l'utilisation de l'adresse mail associée", max_length=256, null=True)),
],
options={
'permissions': (('view_mailcontact', 'Peut voir les mails de contact'),),
},
bases=(re2o.mixins.AclMixin, models.Model),
),
]

View file

@ -31,6 +31,7 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from django.forms import ValidationError
import cotisations.models
import machines.models
from re2o.mixins import AclMixin
@ -102,6 +103,19 @@ class OptionalUser(AclMixin, PreferencesModel):
blank=True,
null=True
)
mail_accounts = models.BooleanField(
default=False,
help_text="Activation des comptes mails pour les utilisateurs"
)
mail_extension = models.CharField(
max_length = 32,
default = "@example.org",
help_text="Extension principale pour les mails internes",
)
max_mail_alias = models.IntegerField(
default = 15,
help_text = "Nombre maximal d'alias pour un utilisateur lambda"
)
class Meta:
permissions = (
@ -109,12 +123,17 @@ class OptionalUser(AclMixin, PreferencesModel):
)
def clean(self):
"""Creation du mode de paiement par solde"""
"""Clean du model:
Creation du mode de paiement par solde
Vérifie que l'extension mail commence bien par @
"""
if self.user_solde:
p = cotisations.models.Paiement.objects.filter(moyen="Solde")
if not len(p):
c = cotisations.models.Paiement(moyen="Solde")
c.save()
if self.mail_extension[0] != "@":
raise ValidationError("L'extension mail doit commencer par un @")
@receiver(post_save, sender=OptionalUser)
@ -273,6 +292,33 @@ class Service(AclMixin, models.Model):
def __str__(self):
return str(self.name)
class MailContact(AclMixin, models.Model):
"""Addresse mail de contact associée à un commentaire descriptif"""
address = models.EmailField(
default = "contact@example.org",
help_text = "Adresse mail de contact"
)
commentary = models.CharField(
blank = True,
null = True,
help_text = "Description de l'utilisation de l'adresse mail associée",
max_length = 256
)
@cached_property
def get_name(self):
return self.address.split("@")[0]
class Meta:
permissions = (
("view_mailcontact", "Peut voir les mails de contact"),
)
def __str__(self):
return(self.address)
class AssoOption(AclMixin, PreferencesModel):
"""Options générales de l'asso : siret, addresse, nom, etc"""

View file

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

View file

@ -36,20 +36,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a>
<p>
</p>
<h5>Généralités</h5>
<table class="table table-striped">
<tr>
<th>Téléphone obligatoirement requis</th>
<td>{{ useroptions.is_tel_mandatory }}</td>
<th>Activation du solde pour les utilisateurs</th>
<td>{{ useroptions.user_solde }}</td>
<th>Auto inscription</th>
<td>{{ useroptions.self_adhesion }}</td>
</tr>
<tr>
<th>Champ gpg fingerprint</th>
<td>{{ useroptions.gpg_fingerprint }}</td>
{% if useroptions.user_solde %}
<th>Solde négatif</th>
<td>{{ useroptions.solde_negatif }}</td>
{% endif %}
<th>Shell par défaut des utilisateurs</th>
<td>{{ useroptions.shell_default }}</td>
</tr>
<tr>
<th>Creations d'adhérents par tous</th>
@ -57,21 +56,37 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Creations de clubs par tous</th>
<td>{{ useroptions.all_can_create_club }}</td>
</tr>
{% if useroptions.user_solde %}
</table>
<h5>{% if useroptions.user_solde %}<span class="label label-success">Gestion du solde{% else %}<span class="label label-danger">Gesion du solde{% endif%}</span></h5>
<table class="table table-striped">
<tr>
<th>Activation du solde pour les utilisateurs</th>
<td>{{ useroptions.user_solde }}</td>
<th>Solde négatif</th>
<td>{{ useroptions.solde_negatif }}</td>
</tr>
<tr>
<th>Solde maximum</th>
<td>{{ useroptions.max_solde }}</td>
<th>Montant minimal de rechargement en ligne</th>
<td>{{ useroptions.min_online_payment }}</td>
</tr>
{% endif %}
</table>
<h5>{% if useroptions.mail_accounts %}<span class="label label-success">Comptes mails{% else %}<span class="label label-danger">Comptes mails{% endif%}</span></h5>
<table class="table table-striped">
<tr>
<th>Auto inscription</th>
<td>{{ useroptions.self_adhesion }}</td>
<th>Shell par défaut des utilisateurs</th>
<td>{{ useroptions.shell_default }}</td>
<th>Gestion des comptes mails</th>
<td>{{ useroptions.mail_accounts }}</td>
<th>Extension mail interne</th>
<td>{{ useroptions.mail_extension }}</td>
</tr>
<tr>
<th>Nombre d'alias maximum</th>
<td>{{ useroption.max_mail_alias }}<td>
</tr>
</table>
<h4>Préférences machines</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i>
@ -215,9 +230,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Service%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i> Ajouter un service</a>
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-services' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs service</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-service' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs services</a>
{% include "preferences/aff_service.html" with service_list=service_list %}
<h2>Liste des adresses mail de contact</h2>
{% can_create preferences.MailContact%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>Ajouter une adresse</a>
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>Supprimer une ou plusieurs adresses</a>
{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i>
Editer

View file

@ -73,7 +73,14 @@ urlpatterns = [
views.edit_service,
name='edit-service'
),
url(r'^del_services/$', views.del_services, name='del-services'),
url(r'^del_service/$', views.del_service, name='del-service'),
url(r'^add_mailcontact/$', views.add_mailcontact, name='add-mailcontact'),
url(
r'^edit_mailcontact/(?P<mailcontactid>[0-9]+)$',
views.edit_mailcontact,
name='edit-mailcontact'
),
url(r'^del_mailcontact/$', views.del_mailcontact, name='del-mailcontact'),
url(
r'^history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
re2o.views.history,

View file

@ -42,9 +42,10 @@ from reversion import revisions as reversion
from re2o.views import form
from re2o.acl import can_create, can_edit, can_delete_set, can_view_all
from .forms import ServiceForm, DelServiceForm
from .forms import ServiceForm, DelServiceForm, MailContactForm, DelMailContactForm
from .models import (
Service,
MailContact,
OptionalUser,
OptionalMachine,
AssoOption,
@ -71,6 +72,7 @@ def display_options(request):
homeoptions, _created = HomeOption.objects.get_or_create()
mailmessageoptions, _created = MailMessageOption.objects.get_or_create()
service_list = Service.objects.all()
mailcontact_list = MailContact.objects.all()
return form({
'useroptions': useroptions,
'machineoptions': machineoptions,
@ -79,7 +81,8 @@ def display_options(request):
'assooptions': assooptions,
'homeoptions': homeoptions,
'mailmessageoptions': mailmessageoptions,
'service_list': service_list
'service_list': service_list,
'mailcontact_list': mailcontact_list
}, 'preferences/display_preferences.html', request)
@ -169,7 +172,7 @@ def edit_service(request, service_instance, **_kwargs):
@login_required
@can_delete_set(Service)
def del_services(request, instances):
def del_service(request, instances):
"""Suppression d'un service de la page d'accueil"""
services = DelServiceForm(request.POST or None, instances=instances)
if services.is_valid():
@ -179,7 +182,7 @@ def del_services(request, instances):
with transaction.atomic(), reversion.create_revision():
services_del.delete()
reversion.set_user(request.user)
messages.success(request, "Le service a été supprimée")
messages.success(request, "Le service a été supprimé")
except ProtectedError:
messages.error(request, "Erreur le service\
suivant %s ne peut être supprimé" % services_del)
@ -189,3 +192,75 @@ def del_services(request, instances):
'preferences/preferences.html',
request
)
@login_required
@can_create(MailContact)
def add_mailcontact(request):
"""Ajout d'une adresse de contact"""
mailcontact = MailContactForm(
request.POST or None,
request.FILES or None
)
if mailcontact.is_valid():
with transaction.atomic(), reversion.create_revision():
mailcontact.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
messages.success(request, "Cette adresse a été ajoutée")
return redirect(reverse('preferences:display-options'))
return form(
{'preferenceform': mailcontact, 'action_name': 'Ajouter'},
'preferences/preferences.html',
request
)
@login_required
@can_edit(MailContact)
def edit_mailcontact(request, mailcontact_instance, **_kwargs):
"""Edition des adresses de contacte affichées"""
mailcontact = MailContactForm(
request.POST or None,
request.FILES or None,
instance=mailcontact_instance
)
if mailcontact.is_valid():
with transaction.atomic(), reversion.create_revision():
mailcontact.save()
reversion.set_user(request.user)
reversion.set_comment("Modification")
messages.success(request, "Adresse modifiée")
return redirect(reverse('preferences:display-options'))
return form(
{'preferenceform': mailcontact, 'action_name': 'Editer'},
'preferences/preferences.html',
request
)
@login_required
@can_delete_set(MailContact)
def del_mailcontact(request, instances):
"""Suppression d'une adresse de contact"""
mailcontacts = DelMailContactForm(
request.POST or None,
instances=instances
)
if mailcontacts.is_valid():
mailcontacts_dels = mailcontacts.cleaned_data['mailcontacts']
for mailcontacts_del in mailcontacts_dels:
try:
with transaction.atomic(), reversion.create_revision():
mailcontacts_del.delete()
reversion.set_user(request.user)
messages.success(request, "L'adresse a été supprimée")
except ProtectedError:
messages.error(request, "Erreur le service\
suivant %s ne peut être supprimé" % mailcontacts_del)
return redirect(reverse('preferences:display-options'))
return form(
{'preferenceform': mailcontacts, 'action_name': 'Supprimer'},
'preferences/preferences.html',
request
)

0
printer/__init__.py Normal file
View file

3
printer/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
printer/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PrinterConfig(AppConfig):
name = 'printer'

37
printer/forms.py Normal file
View file

@ -0,0 +1,37 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.forms
Form to add, edit, cancel printer jobs.
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from django import forms
from django.forms import (
Form,
ModelForm,
)
import itertools
from re2o.mixins import FormRevMixin
from .models import (
JobWithOptions,
)
class JobWithOptionsForm(FormRevMixin, ModelForm):
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(JobWithOptionsForm, self).__init__(*args, prefix=prefix, **kwargs)
class Meta:
model = JobWithOptions
fields = [
'file',
'color',
'disposition',
'count',
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 18:30
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Dummy',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 18:32
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import printer.models
import printer.validators
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('printer', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='JobWithOptions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=printer.models.user_printing_path, validators=[printer.validators.FileValidator(allowed_types=['application/pdf'], max_size=10485760)])),
('starttime', models.DateTimeField(auto_now_add=True)),
('endtime', models.DateTimeField(null=True)),
('status', models.CharField(choices=[('Printable', 'Printable'), ('Running', 'Running'), ('Cancelled', 'Cancelled'), ('Finished', 'Finished')], max_length=255)),
('price', models.IntegerField(default=0)),
('format', models.CharField(choices=[('A4', 'A4'), ('A3', 'A4')], default='A4', max_length=255)),
('color', models.CharField(choices=[('Greyscale', 'Greyscale'), ('Color', 'Color')], default='Greyscale', max_length=255)),
('disposition', models.CharField(choices=[('TwoSided', 'Two sided'), ('OneSided', 'One sided'), ('Booklet', 'Booklet')], default='TwoSided', max_length=255)),
('count', models.PositiveIntegerField(default=1)),
('stapling', models.CharField(choices=[('None', 'None'), ('TopLeft', 'One top left'), ('TopRight', 'One top right'), ('LeftSided', 'Two left sided'), ('RightSided', 'Two right sided')], default='None', max_length=255)),
('perforation', models.CharField(choices=[('None', 'None'), ('TwoLeftSidedHoles', 'Two left sided holes'), ('TwoRightSidedHoles', 'Two right sided holes'), ('TwoTopHoles', 'Two top holes'), ('TwoBottomHoles', 'Two bottom holes'), ('FourLeftSidedHoles', 'Four left sided holes'), ('FourRightSidedHoles', 'Four right sided holes')], default='None', max_length=255)),
('printAs', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='print_as_user', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
bases=(re2o.mixins.RevMixin, models.Model),
),
migrations.RemoveField(
model_name='dummy',
name='user',
),
migrations.DeleteModel(
name='Dummy',
),
]

View file

116
printer/models.py Normal file
View file

@ -0,0 +1,116 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.models
Models of the printer application
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from __future__ import unicode_literals
from django.db import models
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import filesizeformat
from re2o.mixins import RevMixin
import users.models
from .validators import (
FileValidator,
)
from .settings import (
MAX_PRINTFILE_SIZE,
ALLOWED_TYPES,
)
"""
- ```user_printing_path``` is a function that returns the path of the uploaded file, used with the FileField.
- ```Job``` is the main model of a printer job. His parent is the ```user``` model.
"""
def user_printing_path(instance, filename):
# File will be uploaded to MEDIA_ROOT/printings/user_<id>/<filename>
return 'printings/user_{0}/{1}'.format(instance.user.id, filename)
class JobWithOptions(RevMixin, models.Model):
"""
This is the main model of printer application :
- ```user``` is a ForeignKey to the User Application
- ```file``` is the file to print
- ```starttime``` is the time when the job was launched
- ```endtime``` is the time when the job was stopped.
A job is stopped when it is either finished or cancelled.
- ```status``` can be running, finished or cancelled.
- ```club``` is blank in general. If the job was launched as a club then
it is the id of the club.
- ```price``` is the total price of this printing.
Printing Options :
- ```format``` is the paper format. Example: A4.
- ```color``` is the colorization option. Either Color or Greyscale.
- ```disposition``` is the paper disposition.
- ```count``` is the number of copies to be printed.
- ```stapling``` is the stapling options.
- ```perforations``` is the perforation options.
Parent class : User
"""
STATUS_AVAILABLE = (
('Printable', 'Printable'),
('Running', 'Running'),
('Cancelled', 'Cancelled'),
('Finished', 'Finished')
)
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
file = models.FileField(upload_to=user_printing_path, validators=[FileValidator(allowed_types=ALLOWED_TYPES, max_size=MAX_PRINTFILE_SIZE)])
starttime = models.DateTimeField(auto_now_add=True)
endtime = models.DateTimeField(null=True)
status = models.CharField(max_length=255, choices=STATUS_AVAILABLE)
printAs = models.ForeignKey('users.User', on_delete=models.PROTECT, related_name='print_as_user', null=True)
price = models.IntegerField(default=0)
FORMAT_AVAILABLE = (
('A4', 'A4'),
('A3', 'A4'),
)
COLOR_CHOICES = (
('Greyscale', 'Greyscale'),
('Color', 'Color')
)
DISPOSITIONS_AVAILABLE = (
('TwoSided', 'Two sided'),
('OneSided', 'One sided'),
('Booklet', 'Booklet')
)
STAPLING_OPTIONS = (
('None', 'None'),
('TopLeft', 'One top left'),
('TopRight', 'One top right'),
('LeftSided', 'Two left sided'),
('RightSided', 'Two right sided')
)
PERFORATION_OPTIONS = (
('None', 'None'),
('TwoLeftSidedHoles', 'Two left sided holes'),
('TwoRightSidedHoles', 'Two right sided holes'),
('TwoTopHoles', 'Two top holes'),
('TwoBottomHoles', 'Two bottom holes'),
('FourLeftSidedHoles', 'Four left sided holes'),
('FourRightSidedHoles', 'Four right sided holes')
)
format = models.CharField(max_length=255, choices=FORMAT_AVAILABLE, default='A4')
color = models.CharField(max_length=255, choices=COLOR_CHOICES, default='Greyscale')
disposition = models.CharField(max_length=255, choices=DISPOSITIONS_AVAILABLE, default='TwoSided')
count = models.PositiveIntegerField(default=1)
stapling = models.CharField(max_length=255, choices=STAPLING_OPTIONS, default='None')
perforation = models.CharField(max_length=255, choices=PERFORATION_OPTIONS, default='None')

13
printer/settings.py Normal file
View file

@ -0,0 +1,13 @@
"""printer.settings
Define variables, to be changed into a configuration table.
"""
MAX_PRINTFILE_SIZE = 25 * 1024 * 1024 # 25 MB
ALLOWED_TYPES = ['application/pdf']

View file

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% block title %}Printing interface{% endblock %}
{% block content %}
<h3>{% trans "Failure" %}</h3>
{% endblock %}

View file

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% block title %}Printing interface{% endblock %}
{% block content %}
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<h3>{% trans "Printing Menu" %}</h3>
{{ jobform.management_form }}
{% bootstrap_formset_errors jobform %}
<div id="form_set" class="form-group">
{% for job in jobform.forms %}
<div class='file_to_print form-inline'>
{% bootstrap_form job label_class='sr-only' %}
<button class="btn btn-danger btn-sm" id="id_form-0-job-remove" type="button">
<span class="fa fa-times"></span>
</button>
</div>
{% endfor %}
</div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add a file"%}" id="add_one">
{% bootstrap_button action_name button_type="submit" icon="star" %}
</form>
<script type="text/javascript">
var template = `{% bootstrap_form jobform.empty_form label_class='sr-only' %}
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-job-remove" type="button">
<span class="fa fa-times"></span>
</button>`
function add_job() {
var new_index =
document.getElementsByClassName('file_to_print').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_job = document.createElement('div');
new_job.className = 'file_to_print form-inline';
new_job.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_job);
add_listener_for_id(new_index);
}
function del_job(event){
var job = event.target.parentNode;
job.parentNode.removeChild(job);
document.getElementById('id_form-TOTAL_FORMS').value --;
}
function add_listener_for_id(i){
document.getElementById('id_form-' + i.toString() + '-job-remove')
.addEventListener("click", function(event){
var job = event.target.parentNode;
job.parentNode.removeChild(job);
document.getElementById('id_form-TOTAL_FORMS').value --;
}
)
}
// Add events manager when DOM is fully loaded
document.addEventListener(
"DOMContentLoaded",
function() {
document.getElementById("add_one")
.addEventListener("click", add_job, true);
document.getElementById('id_form-0-job-remove')
.addEventListener("click", function(event){
var job = event.target.parentNode;
job.parentNode.removeChild(job);
document.getElementById('id_form-TOTAL_FORMS').value --;
}
)
}
);
</script>
{% endblock %}

View file

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% block title %}Printing interface{% endblock %}
{% block content %}
<h3>{% trans "Success" %}</h3>
{% endblock %}

3
printer/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
printer/urls.py Normal file
View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""printer.urls
The defined URLs for the printer app
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from __future__ import unicode_literals
from django.conf.urls import url
import re2o
from . import views
urlpatterns = [
url(r'^new_job/$', views.new_job, name="new-job"),
url(r'^success/$', views.success, name="success"),
]

72
printer/validators.py Normal file
View file

@ -0,0 +1,72 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.validators
Custom validators useful for printer application.
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.template.defaultfilters import filesizeformat
from django.utils.deconstruct import deconstructible
import mimetypes
@deconstructible
class FileValidator(object):
"""
Custom validator for files. It checks the size and mimetype.
Parameters:
* ```allowed_types``` is an iterable of allowed mimetypes. Example: ['application/pdf'] for a pdf file.
* ```max_size``` is the maximum size allowed in bytes. Example: 25*1024*1024 for 25 MB.
Usage example:
class UploadModel(models.Model):
file = fileField(..., validators=FileValidator(allowed_types = ['application/pdf'], max_size=25*1024*1024))
"""
def __init__(self, *args, **kwargs):
"""
Initialize the custom validator.
By default, all types and size are allowed.
"""
self.allowed_types = kwargs.pop('allowed_types', None)
self.max_size = kwargs.pop('max_size', None)
def __call__(self, value):
"""
Check the type and size.
"""
type_message = _("MIME type '%(type)s' is not valid. Please, use one of these types: %(allowed_types)s.")
type_code = 'invalidType'
oversized_message = _('The current file size is %(size)s. The maximum file size is %(max_size)s.')
oversized_code = 'oversized'
mimetype = mimetypes.guess_type(value.name)[0]
if self.allowed_types and not (mimetype in self.allowed_types):
type_params = {
'type': mimetype,
'allowed_types': ', '.join(self.allowed_types),
}
raise ValidationError(type_message, code=type_code, params=type_params)
filesize = len(value)
if self.max_size and filesize > self.max_size:
oversized_params = {
'size': '{}'.format(filesizeformat(filesize)),
'max_size': '{}'.format(filesizeformat(self.max_size)),
}
raise ValidationError(oversized_message, code=oversized_code, params=oversized_params)

55
printer/views.py Normal file
View file

@ -0,0 +1,55 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.views
The views for the printer app
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from __future__ import unicode_literals
from django.urls import reverse
from django.shortcuts import render, redirect
from django.forms import modelformset_factory, formset_factory
from django.contrib.auth.decorators import login_required
from re2o.views import form
from users.models import User
from . import settings
from .forms import (
JobWithOptionsForm,
)
@login_required
def new_job(request):
"""
View to create a new printing job
"""
job_formset = formset_factory(JobWithOptionsForm)(
request.POST or None, request.FILES or None,
)
if job_formset.is_valid():
for job in job_formset:
job = job.save(commit=False)
job.user=request.user
job.status='Printable'
job.save()
return redirect(reverse(
'printer:success',
))
return form(
{
'jobform': job_formset,
'action_name': "Print",
},
'printer/newjob.html',
request
)
def success(request):
return form(
{},
'printer/success.html',
request
)

View file

@ -0,0 +1,52 @@
{% extends "re2o/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load i18n %}
{% block title %}{% trans "Contact" %}{% endblock %}
{% block content %}
<h2>{% blocktrans %}Contacter l'association {{asso_name}}{% endblocktrans %}</h2>
</br>
{% for contact in contacts %}
<div class="panel panel-info">
<div class="panel-heading"><h4>{{ contact.get_name }}</h4></div>
<div class="panel-body">
<div class="row">
<div class="col-sm-9">{{ contact.commentary}}</div>
<div class="col-sm-3"><a href="mailto:{{ contact.address }}">{{ contact.address }}</a></div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View file

@ -121,6 +121,7 @@ MODEL_NAME = {
'OptionalTopologie': preferences.models.OptionalTopologie,
'GeneralOption': preferences.models.GeneralOption,
'preferences.Service': preferences.models.Service,
'preferences.MailContact': preferences.models.MailContact,
'AssoOption': preferences.models.AssoOption,
'MailMessageOption': preferences.models.MailMessageOption,
# topologie
@ -133,9 +134,9 @@ MODEL_NAME = {
'Room': topologie.models.Room,
'Building': topologie.models.Building,
'SwitchBay': topologie.models.SwitchBay,
'PortProfile': topologie.models.PortProfile,
# users
'User': users.models.User,
'Mail': users.models.Mail,
'MailAlias': users.models.MailAlias,
'Adherent': users.models.Adherent,
'Club': users.models.Club,

View file

@ -71,6 +71,7 @@ urlpatterns = [
r'^preferences/',
include('preferences.urls', namespace='preferences')
),
url(r'^printer/', include('printer.urls', namespace='printer')),
]
# Add debug_toolbar URLs if activated
if 'debug_toolbar' in settings.INSTALLED_APPS:

View file

@ -43,6 +43,7 @@ from django.views.decorators.cache import cache_page
import preferences
from preferences.models import (
Service,
MailContact,
GeneralOption,
AssoOption,
HomeOption
@ -86,6 +87,7 @@ HISTORY_BIND = {
'users': {
'user': users.models.User,
'ban': users.models.Ban,
'mailalias': users.models.MailAlias,
'whitelist': users.models.Whitelist,
'school': users.models.School,
'listright': users.models.ListRight,
@ -94,6 +96,7 @@ HISTORY_BIND = {
},
'preferences': {
'service': preferences.models.Service,
'mailcontact': preferences.models.MailContact,
},
'cotisations': {
'facture': cotisations.models.Facture,
@ -111,6 +114,7 @@ HISTORY_BIND = {
'accesspoint': topologie.models.AccessPoint,
'switchbay': topologie.models.SwitchBay,
'building': topologie.models.Building,
'portprofile': topologie.models.PortProfile,
},
'machines': {
'machine': machines.models.Machine,
@ -229,6 +233,21 @@ def about_page(request):
}
)
def contact_page(request):
"""The view for the contact page
Send all the objects MailContact
"""
address = MailContact.objects.all()
return render(
request,
"re2o/contact.html",
{
'contacts': address,
'asso_name': AssoOption.objects.first().name
}
)
def handler500(request):
"""The handler view for a 500 error"""

View file

@ -262,9 +262,9 @@ def search_single_word(word, filters, user,
) | Q(
related__switch__interface__domain__name__icontains=word
) | Q(
radius__icontains=word
custom_profile__name__icontains=word
) | Q(
vlan_force__name__icontains=word
custom_profile__profil_default__icontains=word
) | Q(
details__icontains=word
)

View file

@ -108,7 +108,6 @@ footer a {
overflow-y: visible;
}
/* For tables with long text in cells */
.table.long_text{
@ -124,3 +123,42 @@ td.long_text{
th.long_text{
width: 60%;
}
/* style for the user page */
.dashboard_container{
margin-top: 30px;
margin-bottom: 20px;
}
.panel-heading.dashboard{
text-align: center;
}
.panel-body.dashboard{
text-align: center;
height: 60px;
vertical-align:middle;
}
#grad_red {
background: red; /* For browsers that do not support gradients */
background: linear-gradient(#ff6363, #fefefe); /* Standard syntax (must be last) */
}
#grad_green {
background: green; /* For browsers that do not support gradients */
background: linear-gradient(#C8DD58,#4FB64A); /* Standard syntax (must be last) */
}
#grad_grey {
background: gray; /* For browsers that do not support gradients */
background: linear-gradient(#d4d4ff, #fefefe); /* Standard syntax (must be last) */
}
#grad_machines{
background: green;
background: linear-gradient(#c266e0,#fefefe)
}

View file

@ -112,6 +112,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</ul>
</li>
{% acl_end %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="glyphicon glyphicon-print"></i> Printer<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url "printer:new-job" %}"><i class="fa fa-print"></i> {% trans "Print" %}</a></li>
</ul>
</li>
{% can_view_app logs %}
<li><a href="{% url "logs:index" %}"><i class="fa fa-chart-area"></i> {% trans "Statistics" %}</a></li>
{% acl_end %}
@ -124,8 +130,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a>
</li>
{% acl_end %}
<li>
<a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> {% trans "About" %}</a>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="fas fa-info"></i> {% trans "Info" %}<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> {% trans "About" %}</a></li>
<li><a href="{% url 'contact' %}"><i class="fas fa-at"></i> {% trans "Contact" %}</a></li>
</ul>
</li>
{% if not request.user.is_authenticated %}
{% if var_sa %}

View file

@ -38,7 +38,8 @@ from .models import (
ConstructorSwitch,
AccessPoint,
SwitchBay,
Building
Building,
PortProfile,
)
@ -86,6 +87,9 @@ class BuildingAdmin(VersionAdmin):
"""Administration d'un batiment"""
pass
class PortProfileAdmin(VersionAdmin):
"""Administration of a port profile"""
pass
admin.site.register(Port, PortAdmin)
admin.site.register(AccessPoint, AccessPointAdmin)
@ -96,3 +100,4 @@ admin.site.register(ModelSwitch, ModelSwitchAdmin)
admin.site.register(ConstructorSwitch, ConstructorSwitchAdmin)
admin.site.register(Building, BuildingAdmin)
admin.site.register(SwitchBay, SwitchBayAdmin)
admin.site.register(PortProfile, PortProfileAdmin)

View file

@ -35,6 +35,7 @@ from __future__ import unicode_literals
from django import forms
from django.forms import ModelForm
from django.db.models import Prefetch
from django.utils.translation import ugettext_lazy as _
from machines.models import Interface
from machines.forms import (
@ -53,6 +54,7 @@ from .models import (
AccessPoint,
SwitchBay,
Building,
PortProfile,
)
@ -78,8 +80,8 @@ class EditPortForm(FormRevMixin, ModelForm):
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', 'custom_profile',
'state', 'details']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
@ -107,8 +109,8 @@ class AddPortForm(FormRevMixin, ModelForm):
'room',
'machine_interface',
'related',
'radius',
'vlan_force',
'custom_profile',
'state',
'details'
]
@ -262,3 +264,16 @@ class EditBuildingForm(FormRevMixin, ModelForm):
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditBuildingForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditPortProfileForm(FormRevMixin, ModelForm):
"""Form to edit a port profile"""
class Meta:
model = PortProfile
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditPortProfileForm, self).__init__(*args,
prefix=prefix,
**kwargs)

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 16:37
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('machines', '0082_auto_20180525_2209'),
('topologie', '0060_server'),
]
operations = [
migrations.CreateModel(
name='PortProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('profil_default', models.CharField(blank=True, choices=[('room', 'room'), ('nothing', 'nothing'), ('accespoint', 'accesspoint'), ('uplink', 'uplink'), ('asso_machine', 'asso_machine')], max_length=32, null=True, unique=True, verbose_name='profil default')),
('radius_type', models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], max_length=32, verbose_name='RADIUS type')),
('radius_mode', models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', max_length=32, verbose_name='RADIUS mode')),
('speed', models.CharField(choices=[('10-half', '10-half'), ('100-half', '100-half'), ('10-full', '10-full'), ('100-full', '100-full'), ('1000-full', '1000-full'), ('auto', 'auto'), ('auto-10', 'auto-10'), ('auto-100', 'auto-100')], default='auto', help_text='Mode de transmission et vitesse du port', max_length=32, verbose_name='Speed')),
('mac_limit', models.IntegerField(blank=True, help_text='Limit du nombre de mac sur le port', null=True, verbose_name='Mac limit')),
('flow_control', models.BooleanField(default=False, help_text='Gestion des débits', verbose_name='Flow control')),
('dhcp_snooping', models.BooleanField(default=False, help_text='Protection dhcp pirate', verbose_name='Dhcp snooping')),
('dhcpv6_snooping', models.BooleanField(default=False, help_text='Protection dhcpv6 pirate', verbose_name='Dhcpv6 snooping')),
('arp_protect', models.BooleanField(default=False, help_text="Verification assignation de l'IP par dhcp", verbose_name='Arp protect')),
('ra_guard', models.BooleanField(default=False, help_text='Protection contre ra pirate', verbose_name='Ra guard')),
('loop_protect', models.BooleanField(default=False, help_text='Protection contre les boucles', verbose_name='Loop Protect')),
('vlan_tagged', models.ManyToManyField(blank=True, related_name='vlan_tagged', to='machines.Vlan', verbose_name='VLAN(s) tagged')),
('vlan_untagged', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlan_untagged', to='machines.Vlan', verbose_name='VLAN untagged')),
],
options={
'verbose_name': 'Port profile',
'permissions': (('view_port_profile', 'Can view a port profile object'),),
'verbose_name_plural': 'Port profiles',
},
bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 23:23
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0061_portprofile'),
]
operations = [
migrations.AlterField(
model_name='portprofile',
name='radius_mode',
field=models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', help_text="En cas d'auth par mac, auth common ou strcit sur le port", max_length=32, verbose_name='RADIUS mode'),
),
migrations.AlterField(
model_name='portprofile',
name='radius_type',
field=models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], help_text="Choix du type d'authentification radius : non actif, mac ou 802.1X", max_length=32, verbose_name='RADIUS type'),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 07:49
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('topologie', '0062_auto_20180627_0123'),
]
operations = [
migrations.AddField(
model_name='port',
name='custom_profil',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='topologie.PortProfile'),
),
]

View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-12-31 19:53
from __future__ import unicode_literals
from django.db import migrations
def transfer_profil(apps, schema_editor):
db_alias = schema_editor.connection.alias
port = apps.get_model("topologie", "Port")
profil = apps.get_model("topologie", "PortProfile")
vlan = apps.get_model("machines", "Vlan")
port_list = port.objects.using(db_alias).all()
profil_nothing = profil.objects.using(db_alias).create(name='nothing', profil_default='nothing', radius_type='NO')
profil_uplink = profil.objects.using(db_alias).create(name='uplink', profil_default='uplink', radius_type='NO')
profil_machine = profil.objects.using(db_alias).create(name='asso_machine', profil_default='asso_machine', radius_type='NO')
profil_room = profil.objects.using(db_alias).create(name='room', profil_default='room', radius_type='NO')
profil_borne = profil.objects.using(db_alias).create(name='accesspoint', profil_default='accesspoint', radius_type='NO')
for vlan_instance in vlan.objects.using(db_alias).all():
if port.objects.using(db_alias).filter(vlan_force=vlan_instance):
custom_profil = profil.objects.using(db_alias).create(name='vlan-force-' + str(vlan_instance.vlan_id), radius_type='NO', vlan_untagged=vlan_instance)
port.objects.using(db_alias).filter(vlan_force=vlan_instance).update(custom_profil=custom_profil)
if port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').count() and port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count():
profil_room.radius_type = 'MAC-radius'
profil_room.radius_mode = 'STRICT'
common_profil = profil.objects.using(db_alias).create(name='mac-radius-common', radius_type='MAC-radius', radius_mode='COMMON')
no_rad_profil = profil.objects.using(db_alias).create(name='no-radius', radius_type='NO')
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').update(custom_profil=common_profil)
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=no_rad_profil)
elif port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').count() and port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count():
profil_room.radius_type = 'MAC-radius'
profil_room.radius_mode = 'COMMON'
strict_profil = profil.objects.using(db_alias).create(name='mac-radius-strict', radius_type='MAC-radius', radius_mode='STRICT')
no_rad_profil = profil.objects.using(db_alias).create(name='no-radius', radius_type='NO')
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').update(custom_profil=strict_profil)
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=no_rad_profil)
else:
strict_profil = profil.objects.using(db_alias).create(name='mac-radius-strict', radius_type='MAC-radius', radius_mode='STRICT')
common_profil = profil.objects.using(db_alias).create(name='mac-radius-common', radius_type='MAC-radius', radius_mode='COMMON')
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').update(custom_profil=strict_profil)
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=common_profil)
profil_room.save()
class Migration(migrations.Migration):
dependencies = [
('topologie', '0063_port_custom_profil'),
]
operations = [
migrations.RunPython(transfer_profil),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 15:03
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('topologie', '0064_createprofil'),
]
operations = [
migrations.RemoveField(
model_name='port',
name='radius',
),
migrations.RemoveField(
model_name='port',
name='vlan_force',
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 16:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0065_auto_20180630_1703'),
]
operations = [
migrations.AddField(
model_name='port',
name='state',
field=models.BooleanField(default=True, help_text='Etat du port Actif', verbose_name='Etat du port Actif'),
),
migrations.AlterField(
model_name='portprofile',
name='profil_default',
field=models.CharField(blank=True, choices=[('room', 'room'), ('accespoint', 'accesspoint'), ('uplink', 'uplink'), ('asso_machine', 'asso_machine'), ('nothing', 'nothing')], max_length=32, null=True, unique=True, verbose_name='profil default'),
),
]

View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 22:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0066_auto_20180630_1855'),
]
operations = [
migrations.RenameField(
model_name='port',
old_name='custom_profil',
new_name='custom_profile',
),
migrations.AlterField(
model_name='port',
name='state',
field=models.BooleanField(default=True, help_text='Port state Active', verbose_name='Port State Active'),
),
migrations.AlterField(
model_name='portprofile',
name='arp_protect',
field=models.BooleanField(default=False, help_text='Check if ip is dhcp assigned', verbose_name='Arp protect'),
),
migrations.AlterField(
model_name='portprofile',
name='dhcp_snooping',
field=models.BooleanField(default=False, help_text='Protect against rogue dhcp', verbose_name='Dhcp snooping'),
),
migrations.AlterField(
model_name='portprofile',
name='dhcpv6_snooping',
field=models.BooleanField(default=False, help_text='Protect against rogue dhcpv6', verbose_name='Dhcpv6 snooping'),
),
migrations.AlterField(
model_name='portprofile',
name='flow_control',
field=models.BooleanField(default=False, help_text='Flow control', verbose_name='Flow control'),
),
migrations.AlterField(
model_name='portprofile',
name='loop_protect',
field=models.BooleanField(default=False, help_text='Protect again loop', verbose_name='Loop Protect'),
),
migrations.AlterField(
model_name='portprofile',
name='mac_limit',
field=models.IntegerField(blank=True, help_text='Limit of mac-address on this port', null=True, verbose_name='Mac limit'),
),
migrations.AlterField(
model_name='portprofile',
name='ra_guard',
field=models.BooleanField(default=False, help_text='Protect against rogue ra', verbose_name='Ra guard'),
),
migrations.AlterField(
model_name='portprofile',
name='radius_mode',
field=models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', help_text='In case of mac-auth : mode common or strict on this port', max_length=32, verbose_name='RADIUS mode'),
),
migrations.AlterField(
model_name='portprofile',
name='radius_type',
field=models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], help_text='Type of radius auth : inactive, mac-address or 802.1X', max_length=32, verbose_name='RADIUS type'),
),
migrations.AlterField(
model_name='portprofile',
name='speed',
field=models.CharField(choices=[('10-half', '10-half'), ('100-half', '100-half'), ('10-full', '10-full'), ('100-full', '100-full'), ('1000-full', '1000-full'), ('auto', 'auto'), ('auto-10', 'auto-10'), ('auto-100', 'auto-100')], default='auto', help_text='Port speed limit', max_length=32, verbose_name='Speed'),
),
]

View file

@ -46,6 +46,7 @@ from django.dispatch import receiver
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db import transaction
from django.utils.translation import ugettext_lazy as _
from reversion import revisions as reversion
from machines.models import Machine, regen
@ -361,12 +362,6 @@ class Port(AclMixin, RevMixin, models.Model):
de forcer un port sur un vlan particulier. S'additionne à la politique
RADIUS"""
PRETTY_NAME = "Port de switch"
STATES = (
('NO', 'NO'),
('STRICT', 'STRICT'),
('BLOQ', 'BLOQ'),
('COMMON', 'COMMON'),
)
switch = models.ForeignKey(
'Switch',
@ -392,13 +387,17 @@ class Port(AclMixin, RevMixin, models.Model):
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,
custom_profile = models.ForeignKey(
'PortProfile',
on_delete=models.PROTECT,
blank=True,
null=True
)
state = models.BooleanField(
default=True,
help_text='Port state Active',
verbose_name=_("Port State Active")
)
details = models.CharField(max_length=255, blank=True)
class Meta:
@ -407,6 +406,34 @@ class Port(AclMixin, RevMixin, models.Model):
("view_port", "Peut voir un objet port"),
)
@cached_property
def get_port_profil(self):
"""Return the config profil for this port
:returns: the profile of self (port)"""
def profil_or_nothing(profil):
port_profil = PortProfile.objects.filter(profil_default=profil).first()
if port_profil:
return port_profil
else:
nothing = PortProfile.objects.filter(profil_default='nothing').first()
if not nothing:
nothing = PortProfile.objects.create(profil_default='nothing', name='nothing', radius_type='NO')
return nothing
if self.custom_profile:
return self.custom_profile
elif self.related:
return profil_or_nothing('uplink')
elif self.machine_interface:
if hasattr(self.machine_interface.machine, 'accesspoint'):
return profil_or_nothing('access_point')
else:
return profil_or_nothing('asso_machine')
elif self.room:
return profil_or_nothing('room')
else:
return profil_or_nothing('nothing')
@classmethod
def get_instance(cls, portid, *_args, **kwargs):
return (cls.objects
@ -484,6 +511,135 @@ class Room(AclMixin, RevMixin, models.Model):
return self.name
class PortProfile(AclMixin, RevMixin, models.Model):
"""Contains the information of the ports' configuration for a switch"""
TYPES = (
('NO', 'NO'),
('802.1X', '802.1X'),
('MAC-radius', 'MAC-radius'),
)
MODES = (
('STRICT', 'STRICT'),
('COMMON', 'COMMON'),
)
SPEED = (
('10-half', '10-half'),
('100-half', '100-half'),
('10-full', '10-full'),
('100-full', '100-full'),
('1000-full', '1000-full'),
('auto', 'auto'),
('auto-10', 'auto-10'),
('auto-100', 'auto-100'),
)
PROFIL_DEFAULT= (
('room', 'room'),
('accespoint', 'accesspoint'),
('uplink', 'uplink'),
('asso_machine', 'asso_machine'),
('nothing', 'nothing'),
)
name = models.CharField(max_length=255, verbose_name=_("Name"))
profil_default = models.CharField(
max_length=32,
choices=PROFIL_DEFAULT,
blank=True,
null=True,
unique=True,
verbose_name=_("profil default")
)
vlan_untagged = models.ForeignKey(
'machines.Vlan',
related_name='vlan_untagged',
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("VLAN untagged")
)
vlan_tagged = models.ManyToManyField(
'machines.Vlan',
related_name='vlan_tagged',
blank=True,
verbose_name=_("VLAN(s) tagged")
)
radius_type = models.CharField(
max_length=32,
choices=TYPES,
help_text="Type of radius auth : inactive, mac-address or 802.1X",
verbose_name=_("RADIUS type")
)
radius_mode = models.CharField(
max_length=32,
choices=MODES,
default='COMMON',
help_text="In case of mac-auth : mode common or strict on this port",
verbose_name=_("RADIUS mode")
)
speed = models.CharField(
max_length=32,
choices=SPEED,
default='auto',
help_text='Port speed limit',
verbose_name=_("Speed")
)
mac_limit = models.IntegerField(
null=True,
blank=True,
help_text='Limit of mac-address on this port',
verbose_name=_("Mac limit")
)
flow_control = models.BooleanField(
default=False,
help_text='Flow control',
verbose_name=_("Flow control")
)
dhcp_snooping = models.BooleanField(
default=False,
help_text='Protect against rogue dhcp',
verbose_name=_("Dhcp snooping")
)
dhcpv6_snooping = models.BooleanField(
default=False,
help_text='Protect against rogue dhcpv6',
verbose_name=_("Dhcpv6 snooping")
)
arp_protect = models.BooleanField(
default=False,
help_text='Check if ip is dhcp assigned',
verbose_name=_("Arp protect")
)
ra_guard = models.BooleanField(
default=False,
help_text='Protect against rogue ra',
verbose_name=_("Ra guard")
)
loop_protect = models.BooleanField(
default=False,
help_text='Protect again loop',
verbose_name=_("Loop Protect")
)
class Meta:
permissions = (
("view_port_profile", _("Can view a port profile object")),
)
verbose_name = _("Port profile")
verbose_name_plural = _("Port profiles")
security_parameters_fields = ['loop_protect', 'ra_guard', 'arp_protect', 'dhcpv6_snooping', 'dhcp_snooping', 'flow_control']
@cached_property
def security_parameters_enabled(self):
return [parameter for parameter in self.security_parameters_fields if getattr(self, parameter)]
@cached_property
def security_parameters_as_str(self):
return ','.join(self.security_parameters_enabled)
def __str__(self):
return self.name
@receiver(post_save, sender=AccessPoint)
def ap_post_save(**_kwargs):
"""Regeneration des noms des bornes vers le controleur"""

View file

@ -32,8 +32,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='radius' text='Radius' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='vlan' text='Vlan forcé' %}</th>
<th>Etat du port</th>
<th>Profil du port</th>
<th>Détails</th>
<th></th>
</tr>
@ -66,8 +66,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
{% endif %}
</td>
<td>{{ port.radius }}</td>
<td>{% if not port.vlan_force %}Aucun{% else %}{{ port.vlan_force }}{% endif %}</td>
<td>{% if port.state %} <i class="text-success">Actif</i>{% else %}<i class="text-danger">Désactivé</i>{% endif %}</td>
<td>{% if not port.custom_profile %}<u>Par défaut</u> : {% endif %}{{port.get_port_profil}}</td>
<td>{{ port.details }}</td>
<td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}">

View file

@ -0,0 +1,85 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2018 Gabriel Détraz
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load acl %}
{% load i18n %}
<div class="table-responsive">
{% if port_profile_list.paginator %}
{% include "pagination.html" with list=port_profile_list %}
{% endif %}
<thead>
<table class="table table-striped">
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Default for" %}</th>
<th>{% trans "VLANs" %}</th>
<th>{% trans "RADIUS settings" %}</th>
<th>{% trans "Speed" %}</th>
<th>{% trans "Mac address limit" %}</th>
<th>{% trans "Security" %}</th>
<th></th>
</tr>
</thead>
{% for port_profile in port_profile_list %}
<tr>
<td>{{port_profile.name}}</td>
<td>{{port_profile.profil_default}}</td>
<td>
{% if port_profile.vlan_untagged %}
<b>Untagged : </b>{{port_profile.vlan_untagged}}
<br>
{% endif %}
{% if port_profile.vlan_tagged.all %}
<b>Tagged : </b>{{port_profile.vlan_tagged.all|join:", "}}
{% endif %}
</td>
<td>
<b>Type : </b>{{port_profile.radius_type}}
{% if port_profile.radius_type == "MAC-radius" %}
<br>
<b>Mode : </b>{{port_profile.radius_mode}}</td>
{% endif %}
<td>{{port_profile.speed}}</td>
<td>{{port_profile.mac_limit}}</td>
<td>{{port_profile.security_parameters_enabled|join:"<br>"}}</td>
<td class="text-right">
{% include 'buttons/history.html' with href='topologie:history' name='portprofile' id=port_profile.pk %}
{% can_edit port_profile %}
{% include 'buttons/edit.html' with href='topologie:edit-port-profile' id=port_profile.pk %}
{% acl_end %}
{% can_delete port_profile %}
{% include 'buttons/suppr.html' with href='topologie:del-port-profile' id=port_profile.pk %}
{% acl_end %}
</td>
</tr>
{% endfor %}
</table>
{% if port_profile_list.paginator %}
{% include "pagination.html" with list=port_profile_list %}
{% endif %}
</div>

View file

@ -72,5 +72,4 @@ Topologie des Switchs
<br />
<br />
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "topologie/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2018 Gabriel Détraz
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load acl %}
{% load i18n %}
{% block title %}Switchs{% endblock %}
{% block content %}
<h2>{% trans "Port profiles" %}</h2>
{% can_create PortProfile %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:new-port-profile' %}"><i class="fa fa-plus"></i>{% trans " Add a port profile" %}</a>
<hr>
{% acl_end %}
{% include "topologie/aff_port_profile.html" with port_profile_list=port_profile_list %}
<br />
<br />
<br />
{% endblock %}

View file

@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-microchip"></i>
Switchs
</a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
<i class="fa fa-cogs"></i>
Config des ports switchs
</a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-ap" %}">
<i class="fa fa-wifi"></i>
Bornes WiFi

View file

@ -113,4 +113,16 @@ urlpatterns = [
url(r'^del_building/(?P<buildingid>[0-9]+)$',
views.del_building,
name='del-building'),
url(r'^index_port_profile/$',
views.index_port_profile,
name='index-port-profile'),
url(r'^new_port_profile/$',
views.new_port_profile,
name='new-port-profile'),
url(r'^edit_port_profile/(?P<portprofileid>[0-9]+)$',
views.edit_port_profile,
name='edit-port-profile'),
url(r'^del_port_profile/(?P<portprofileid>[0-9]+)$',
views.del_port_profile,
name='del-port-profile'),
]

View file

@ -47,6 +47,7 @@ from django.template.loader import get_template
from django.template import Context, Template, loader
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import ugettext as _
import tempfile
@ -80,6 +81,7 @@ from .models import (
SwitchBay,
Building,
Server,
PortProfile,
)
from .forms import (
EditPortForm,
@ -94,7 +96,8 @@ from .forms import (
AddAccessPointForm,
EditAccessPointForm,
EditSwitchBayForm,
EditBuildingForm
EditBuildingForm,
EditPortProfileForm,
)
from subprocess import (
@ -124,10 +127,12 @@ def index(request):
request.GET.get('order'),
SortTable.TOPOLOGIE_INDEX
)
pagination_number = GeneralOption.get_cached_value('pagination_number')
switch_list = re2o_paginator(request, switch_list, pagination_number)
if any(service_link.need_regen() for service_link in Service_link.objects.filter(service__service_type='graph_topo')):
if any(service_link.need_regen for service_link in Service_link.objects.filter(service__service_type='graph_topo')):
make_machine_graph()
for service_link in Service_link.objects.filter(service__service_type='graph_topo'):
service_link.done_regen()
@ -141,6 +146,19 @@ def index(request):
)
@login_required
@can_view_all(PortProfile)
def index_port_profile(request):
pagination_number = GeneralOption.get_cached_value('pagination_number')
port_profile_list = PortProfile.objects.all().select_related('vlan_untagged')
port_profile_list = re2o_paginator(request, port_profile_list, pagination_number)
return render(
request,
'topologie/index_portprofile.html',
{'port_profile_list': port_profile_list}
)
@login_required
@can_view_all(Port)
@can_view(Switch)
@ -955,6 +973,59 @@ def del_constructor_switch(request, constructor_switch, **_kwargs):
}, 'topologie/delete.html', request)
@login_required
@can_create(PortProfile)
def new_port_profile(request):
"""Create a new port profile"""
port_profile = EditPortProfileForm(request.POST or None)
if port_profile.is_valid():
port_profile.save()
messages.success(request, _("Port profile created"))
return redirect(reverse('topologie:index'))
return form(
{'topoform': port_profile, 'action_name': _("Create")},
'topologie/topo.html',
request
)
@login_required
@can_edit(PortProfile)
def edit_port_profile(request, port_profile, **_kwargs):
"""Edit a port profile"""
port_profile = EditPortProfileForm(request.POST or None, instance=port_profile)
if port_profile.is_valid():
if port_profile.changed_data:
port_profile.save()
messages.success(request, _("Port profile modified"))
return redirect(reverse('topologie:index'))
return form(
{'topoform': port_profile, 'action_name': _("Edit")},
'topologie/topo.html',
request
)
@login_required
@can_delete(PortProfile)
def del_port_profile(request, port_profile, **_kwargs):
"""Delete a port profile"""
if request.method == 'POST':
try:
port_profile.delete()
messages.success(request,
_("The port profile was successfully deleted"))
except ProtectedError:
messages.success(request,
_("Impossible to delete the port profile"))
return redirect(reverse('topologie:index'))
return form(
{'objet': port_profile, 'objet_name': _("Port profile")},
'topologie/delete.html',
request
)
def make_machine_graph():
"""
Create the graph of switchs, machines and access points.

View file

@ -34,7 +34,6 @@ from reversion.admin import VersionAdmin
from .models import (
User,
Mail,
MailAlias,
ServiceUser,
School,
@ -110,6 +109,11 @@ class BanAdmin(VersionAdmin):
pass
class MailAliasAdmin(VersionAdmin):
"""Gestion des alias mail"""
pass
class WhitelistAdmin(VersionAdmin):
"""Gestion des whitelist"""
pass
@ -127,7 +131,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
list_display = (
'pseudo',
'surname',
'email',
'external_mail',
'school',
'is_admin',
'shell'
@ -141,7 +145,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
'Personal info',
{
'fields':
('surname', 'email', 'school', 'shell', 'uid_number')
('surname', 'external_mail', 'school', 'shell', 'uid_number')
}
),
('Permissions', {'fields': ('is_admin', )}),
@ -156,7 +160,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
'fields': (
'pseudo',
'surname',
'email',
'external_mail',
'school',
'is_admin',
'password1',
@ -213,6 +217,7 @@ admin.site.register(School, SchoolAdmin)
admin.site.register(ListRight, ListRightAdmin)
admin.site.register(ListShell, ListShellAdmin)
admin.site.register(Ban, BanAdmin)
admin.site.register(MailAlias, MailAliasAdmin)
admin.site.register(Whitelist, WhitelistAdmin)
admin.site.register(Request, RequestAdmin)
# Now register the new UserAdmin...

View file

@ -140,7 +140,7 @@ class UserCreationForm(FormRevMixin, forms.ModelForm):
class Meta:
model = Adherent
fields = ('pseudo', 'surname', 'email')
fields = ('pseudo', 'surname')
def clean_password2(self):
"""Verifie que password1 et 2 sont identiques"""
@ -220,7 +220,7 @@ class UserChangeForm(FormRevMixin, forms.ModelForm):
class Meta:
model = Adherent
fields = ('pseudo', 'password', 'surname', 'email')
fields = ('pseudo', 'password', 'surname')
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
@ -306,14 +306,12 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['room'].label = 'Chambre'
self.fields['room'].empty_label = "Pas de chambre"
self.fields['school'].empty_label = "Séléctionner un établissement"
class Meta:
model = Adherent
fields = [
'name',
'surname',
'pseudo',
'email',
'school',
'comment',
'room',
@ -365,7 +363,6 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
fields = [
'surname',
'pseudo',
'email',
'school',
'comment',
'room',
@ -597,9 +594,23 @@ class MailAliasForm(FormRevMixin, ModelForm):
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(MailAliasForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['valeur'].label = 'nom de l\'adresse mail'
self.fields['extension'].label = 'extension de l\'adresse mail'
self.fields['valeur'].label = "Prefixe de l'alias mail. Ne peut contenir de @"
class Meta:
model = MailAlias
exclude = ['mail']
exclude = ['user']
class MailForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"""Creation, edition des paramètres mail"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(MailForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['external_mail'].label = 'Adresse mail externe'
if 'redirection' in self.fields:
self.fields['redirection'].label = 'Activation de la redirection vers l\'adress externe'
if 'internal_address' in self.fields:
self.fields['internal_address'].label = 'Adresse mail interne'
class Meta:
model = User
fields = ['external_mail', 'redirection', 'internal_address']

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 14:14
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('users', '0072_auto_20180426_2021'),
]
operations = [
migrations.CreateModel(
name='MailAlias',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('valeur', models.CharField(help_text="username de l'adresse mail", max_length=64, unique=True)),
],
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
),
migrations.RenameField(
model_name='user',
old_name='email',
new_name='external_mail',
),
migrations.AddField(
model_name='user',
name='internal_address',
field=models.BooleanField(default=False, help_text="Activer ou non l'utilisation de l'adresse mail interne"),
),
migrations.AddField(
model_name='user',
name='redirection',
field=models.BooleanField(default=False, help_text='Activer ou non la redirection du mail interne vers le mail externe'),
),
migrations.AddField(
model_name='mailalias',
name='user',
field=models.ForeignKey(blank=True, help_text='Utilisateur associé', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 15:17
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0073_auto_20180629_1614'),
]
def transfer_pseudo(apps, schema_editor):
db_alias = schema_editor.connection.alias
users = apps.get_model("users", "User")
mailalias = apps.get_model("users", "MailAlias")
users_list = users.objects.using(db_alias).all()
for user in users_list:
mailalias.objects.using(db_alias).create(valeur=user.pseudo, user=user)
def untransfer_pseudo(apps, schema_editor):
db_alias = schema_editor.connection.alias
mailalias = apps.get_model("users", "MailAlias")
mailalias.objects.using(db_alias).delete()
operations = [
migrations.AlterField(
model_name='mailalias',
name='user',
field=models.ForeignKey(help_text='Utilisateur associé', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='mailalias',
name='valeur',
field=models.CharField(help_text="Valeur de l'alias mail", max_length=128, unique=True),
),
migrations.RunPython(transfer_pseudo, untransfer_pseudo),
]

View file

@ -51,7 +51,7 @@ import datetime
from django.db import models
from django.db.models import Q
from django import forms
from django.forms import ValidationError
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.utils.functional import cached_property
@ -79,7 +79,7 @@ from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin
from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine, regen, Extension
from machines.models import Domain, Interface, Machine, regen
from preferences.models import GeneralOption, AssoOption, OptionalUser
from preferences.models import OptionalMachine, MailMessageOption
@ -97,7 +97,7 @@ def linux_user_validator(login):
""" Retourne une erreur de validation si le login ne respecte
pas les contraintes unix (maj, min, chiffres ou tiret)"""
if not linux_user_check(login):
raise forms.ValidationError(
raise ValidationError(
", ce pseudo ('%(label)s') contient des carractères interdits",
params={'label': login},
)
@ -134,7 +134,7 @@ class UserManager(BaseUserManager):
self,
pseudo,
surname,
email,
external_mail,
password=None,
su=False
):
@ -148,7 +148,7 @@ class UserManager(BaseUserManager):
pseudo=pseudo,
surname=surname,
name=surname,
email=self.normalize_email(email),
external_mail=external_mail,
)
user.set_password(password)
@ -157,19 +157,19 @@ class UserManager(BaseUserManager):
user.save(using=self._db)
return user
def create_user(self, pseudo, surname, email, password=None):
def create_user(self, pseudo, surname, external_mail, password=None):
"""
Creates and saves a User with the given pseudo, name, surname, email,
and password.
"""
return self._create_user(pseudo, surname, email, password, False)
return self._create_user(pseudo, surname, external_mail, password, False)
def create_superuser(self, pseudo, surname, email, password):
def create_superuser(self, pseudo, surname, external_mail, password):
"""
Creates and saves a superuser with the given pseudo, name, surname,
email, and password.
"""
return self._create_user(pseudo, surname, email, password, True)
return self._create_user(pseudo, surname, external_mail, password, True)
class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
@ -194,13 +194,15 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
help_text="Doit contenir uniquement des lettres, chiffres, ou tirets",
validators=[linux_user_validator]
)
email = models.EmailField()
"""
email= models.OneToOneField(
Mail,
on_delete=models.PROTECT
external_mail = models.EmailField()
redirection = models.BooleanField(
default=False,
help_text='Activer ou non la redirection du mail interne vers le mail externe'
)
internal_address = models.BooleanField(
default=False,
help_text='Activer ou non l\'utilisation de l\'adresse mail interne'
)
"""
school = models.ForeignKey(
'School',
on_delete=models.PROTECT,
@ -233,7 +235,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
)
USERNAME_FIELD = 'pseudo'
REQUIRED_FIELDS = ['surname', 'email']
REQUIRED_FIELDS = ['surname', 'external_mail']
objects = UserManager()
@ -527,7 +529,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
user_ldap.sn = self.pseudo
user_ldap.dialupAccess = str(self.has_access())
user_ldap.home_directory = '/home/' + self.pseudo
user_ldap.mail = self.email
user_ldap.mail = self.get_mail()
user_ldap.given_name = self.surname.lower() + '_'\
+ self.name.lower()[:3]
user_ldap.gid = LDAP['user_gid']
@ -680,10 +682,10 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
"""
Return the mail address choosen by the user
"""
if not self.mail.internal_activated:
return(self.mail.external)
if not OptionalUser.get_cached_value('mail_accounts') or not self.internal_address or self.redirection:
return str(self.external_mail)
else:
return(self.mail.mailalias_set.first())
return str(self.mailalias_set.get(valeur=self.pseudo))
def get_next_domain_name(self):
"""Look for an available name for a new interface for
@ -803,6 +805,32 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
"Droit requis pour changer le shell"
)
@staticmethod
def can_change_redirection(user_request, *_args, **_kwargs):
""" Check if a user can change redirection.
:param user_request: The user who request
:returns: a message and a boolean which is True if the user has
the right to change a redirection
"""
return (
OptionalUser.get_cached_value('mail_accounts'),
"La gestion des comptes mails doit être activée"
)
@staticmethod
def can_change_internal_address(user_request, *_args, **_kwargs):
""" Check if a user can change internal address .
:param user_request: The user who request
:returns: a message and a boolean which is True if the user has
the right to change internal address
"""
return (
OptionalUser.get_cached_value('mail_accounts'),
"La gestion des comptes mails doit être activée"
)
@staticmethod
def can_change_force(user_request, *_args, **_kwargs):
""" Check if a user can change a force
@ -897,12 +925,19 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
'shell': self.can_change_shell,
'force': self.can_change_force,
'selfpasswd': self.check_selfpasswd,
'redirection': self.can_change_redirection,
'internal_address' : self.can_change_internal_address,
}
def clean(self, *args, **kwargs):
"""Check if this pseudo is already used by any mailalias.
Better than raising an error in post-save and catching it"""
if MailAlias.objects.filter(valeur=self.pseudo).exclude(user=self):
raise ValidationError("Ce pseudo est déjà utilisé")
def __str__(self):
return self.pseudo
class Adherent(User):
""" A class representing a member (it's a user with special
informations) """
@ -1021,9 +1056,11 @@ class Club(User):
@receiver(post_save, sender=User)
def user_post_save(**kwargs):
""" Synchronisation post_save : envoie le mail de bienvenue si creation
Synchronise le pseudo, en créant un alias mail correspondant
Synchronise le ldap"""
is_created = kwargs['created']
user = kwargs['instance']
mail_alias, created = MailAlias.objects.get_or_create(valeur=user.pseudo, user=user)
if is_created:
user.notif_inscription()
user.ldap_sync(
@ -1594,62 +1631,53 @@ class LdapServiceUserGroup(ldapdb.models.Model):
return self.name
class Mail(RevMixin, AclMixin, models.Model):
"""
Mail account of a user
Compte mail d'un utilisateur
"""
external_mail = models.EmailField(help_text="Mail externe")
user = models.ForeignKey(
'User',
on_delete=models.CASCADE,
help_text="Object mail d'un User"
)
redirection = models.BooleanField(
default=False
)
internal_address = models.BooleanField(
default=False
)
def __str__(self):
return self.mail
class MailAlias(RevMixin, AclMixin, models.Model):
"""
Define a alias for a user Mail
Définit un aliase pour un Mail d'utilisateur
Définit un alias pour un Mail d'utilisateur
"""
mail = models.ForeignKey(
'Mail',
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
help_text="Objects Mail associé"
help_text="Utilisateur associé",
)
valeur = models.CharField(
max_length=64,
help_text="username de l'adresse mail"
unique=True,
max_length=128,
help_text="Valeur de l'alias mail"
)
extension = models.ForeignKey(
'Extension',
on_delete=models.CASCADE,
help_text="Extension mail interne"
)
class Meta:
unique_together = ('valeur', 'extension',)
def __str__(self):
return self.valeur + "@" + self.extension
return self.complete_mail
@cached_property
def complete_mail(self):
return self.valeur + OptionalUser.get_cached_value('mail_extension')
@staticmethod
def can_create(user_request, userid, *_args, **_kwargs):
"""Check if an user can create an mailalias object.
:param user_request: The user who wants to create a mailalias object.
:return: a message and a boolean which is True if the user can create
an user or if the `options.all_can_create` is set.
"""
if not user_request.has_perm('users.add_mailalias'):
if int(user_request.id) != int(userid):
return False, 'Vous n\'avez pas le droit d\'ajouter un alias à une autre personne'
elif user_request.mailalias_set.all().count() >= OptionalUser.get_cached_value('max_mail_alias'):
return False, "Vous avez atteint la limite de {} alias".format(OptionalUser.get_cached_value('max_mail_alias'))
else:
return True, None
return True, None
def can_view(self, user_request, *_args, **_kwargs):
"""
Check if the user can view the aliases
Check if the user can view the alias
"""
if user_request.has_perm('users.view_mailalias') or user.request == self.mail.user:
if user_request.has_perm('users.view_mailalias') or user.request == self.user:
return True, None
else:
return False, "Vous n'avais pas les droits suffisants et n'êtes pas propriétaire de ces alias"
@ -1662,8 +1690,8 @@ class MailAlias(RevMixin, AclMixin, models.Model):
if user_request.has_perm('users.delete_mailalias'):
return True, None
else:
if user_request == self.mail.user:
if self.id != 0:
if user_request == self.user:
if self.valeur != self.user.pseudo:
return True, None
else:
return False, "Vous ne pouvez pas supprimer l'alias lié à votre pseudo"
@ -1678,13 +1706,16 @@ class MailAlias(RevMixin, AclMixin, models.Model):
if user_request.has_perm('users.change_mailalias'):
return True, None
else:
if user_request == self.mail.user:
if self.id != 0:
if user_request == self.user:
if self.valeur != self.user.pseudo:
return True, None
else:
return False, "Vous ne pouvez pas modifier l'alias lié à votre pseudo"
else:
return False, "Vous n'avez pas les droits suffisants et n'êtes pas propriétairs de cet alias"
def clean(self, *args, **kwargs):
if "@" in self.valeur:
raise ValidationError("Cet alias ne peut contenir un @")
super(MailAlias, self).clean(*args, **kwargs)

View file

@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if alias_list.paginator %}
{% include "pagination.html" with list=alias_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
@ -37,12 +36,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ alias }}</td>
<td class="text-right">
{% can_delete alias %}
{% include 'buttons/suppr.html' with href='users:del-alias' id=alias.id %}
{% include 'buttons/suppr.html' with href='users:del-mailalias' id=alias.id %}
{% acl_end %}
{% can_edit alias %}
{% include 'buttons/edit.html' with href='users:edit-alias' id=alias.id %}
{% include 'buttons/edit.html' with href='users:edit-mailalias' id=alias.id %}
{% acl_end %}
{% include 'buttons/history.html' with href='users:history' name='alias' id=alias.id %}
{% include 'buttons/history.html' with href='users:history' name='mailalias' id=alias.id %}
</td>
</tr>
{% endfor %}

View file

@ -27,21 +27,99 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %}
{% block title %}Profil{% endblock %}
{% block content %}
<h2>{{ users.surname }} {{users.name}}</h2>
<p>Vous êtes {% if users.end_adhesion != None %}<span class="label label-success">
un {{ users.class_name | lower}}</span>{% else %}<span class="label label-danger">
non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
<span class="label label-success">active</span>{% else %}<span class="label label-danger">désactivée</span>{% endif %}.</p>
{% if user_solde %}
<p>Votre solde est de <span class="badge">{{ user.solde }}€</span>.
{% if allow_online_payment %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:recharge' %}">
<div align="center">
<h2>Bienvenue {{users.name}} {{ users.surname }}</h2>
</div>
<div class="dashboard_container">
<div class="row">
{% if solde_activated %}
<div class="col-sm-6 col-md-4">
{% else %}
<div class="col-sm-6 col-md-6">
{% endif %}
<div class="col-12">
{% if users.is_ban%}
<div class="panel panel-danger">
<div class="panel-heading dashboard">Votre compte est banni</div>
<div class="panel-body dashboard">
<i class="text-danger">Fin du ban : {{user.end_ban|date:"d M Y"}}</i>
</div>
</div>
{% elif not users.is_connected%}
<div class="panel panel-danger">
<div class="panel-heading dashboard">Pas d'accès à internet</div>
<div class="panel-body dashboard">
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}">
<i class="fas fa-sign-in-alt"></i>
Payer ma connexion
</a>
</div>
</div>
{% else %}
<div class="panel panel-success">
<div class="panel-heading dashboard">Connecté</div>
<div class="panel-body dashboard">
<i class="text-success">Fin de connexion: {{user.end_adhesion|date:"d M Y"}}</i>
</div>
</div>
{% endif %}
</div>
</div>
{% if solde_activated %}
<div class="col-sm-6 col-md-4">
<div class="col-12">
<div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
{{user.solde}} <i class="fas fa-euro-sign"></i>
</div>
<div class="panel-body dashboard">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}">
<i class="fa fa-euro-sign"></i>
Recharger
</a>
{% endif %}
</p>
{% endif %}
Modifier le solde
</a>
</div>
</div>
</div>
</div>
{% endif %}
{% if solde_activated %}
<div class="col-sm-6 col-md-4">
{% else %}
<div class="col-sm-6 col-md-6">
{% endif %}
<div class="col-12">
{% if nb_machines %}
<div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
<span class="badge">{{nb_machines}}</span>
Machines
<i class="fa fa-desktop"></i>
</div>
<div class="panel-body dashboard">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>
Ajouter une machine
</a>
</div>
</div>
{% else %}
<div class="panel panel-warning">
<div class="panel-heading dashboard">Aucune machine</div>
<div class="panel-body dashboard">
<a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>
Ajouter une machine
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="panel-group" id="accordion">
<div class="panel panel-default">
@ -50,7 +128,7 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
<i class="fa fa-user"></i> Informations détaillées
</h3>
</div>
<div class="panel-collapse collapse in" id="collapse1">
<div class="panel-collapse collapse" id="collapse1">
<div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="fa fa-edit"></i>
@ -349,6 +427,65 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse7">
<h3 class="panel-title pull-left">
<i class="fa fa-envelope"></i>
Paramètres mail
</h3>
</div>
<div id="collapse7" class="panel-collapse collapse">
<div class="panel-body">
{% can_edit users %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-mail' users.id %}">
<i class="fa fa-plus-square"></i>
Modifier les options mail
</a>
{% acl_end %}
{% if mail_accounts %}
<div class="table-responsive">
<table class="table">
<tr>
<th>Adresse mail externe</th>
<th>Compte mail {{ asso_name }}</th>
<th>Adresse mail de contact</th>
</tr>
<tr>
<td>{{ users.external_mail }}</td>
<td>{{ users.internal_address|yesno:"Activé,Désactivé" }}</td>
<td>{{ users.get_mail }}</td>
</table>
<div class="alert alert-info" role="alert">
Vous pouvez bénéficier d'une adresse mail {{ asso_name }}.
Vous pouvez également la rediriger vers une adresse externe en modifiant les options mail.
</div>
</div>
{% if users.internal_address %}
{% can_create MailAlias users.id %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-mailalias' users.id %}">
<i class="fa fa-plus-square"></i>
Ajouter un alias mail
</a>
{% acl_end %}
{% if alias_list %}
{% include "users/aff_mailalias.html" with alias_list=alias_list %}
{% endif %}
{% endif %}
{% else %}
<div class="table-responsive">
<table class="table">
<tr>
<th>Adresse mail</th>
</tr>
<tr>
<td>{{ users.external_mail }}</td>
</tr>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<br />
<br />

View file

@ -68,6 +68,7 @@ urlpatterns = [
url(r'^add_mailalias/(?P<userid>[0-9]+)$', views.add_mailalias, name='add-mailalias'),
url(r'^edit_mailalias/(?P<mailaliasid>[0-9]+)$', views.edit_mailalias, name='edit-mailalias'),
url(r'^del-mailalias/(?P<mailaliasid>[0-9]+)$', views.del_mailalias, name='del-mailalias'),
url(r'^edit_mail/(?P<userid>[0-9]+)$', views.edit_mail, name='edit-mail'),
url(r'^add_school/$', views.add_school, name='add-school'),
url(r'^edit_school/(?P<schoolid>[0-9]+)$',
views.edit_school,

View file

@ -80,10 +80,13 @@ from .models import (
Adherent,
Club,
ListShell,
MailAlias,
)
from .forms import (
BanForm,
WhitelistForm,
MailAliasForm,
MailForm,
DelSchoolForm,
DelListRightForm,
NewListRightForm,
@ -111,8 +114,8 @@ def new_user(request):
GTU_sum_up = GeneralOption.get_cached_value('GTU_sum_up')
GTU = GeneralOption.get_cached_value('GTU')
if user.is_valid():
user = user.save(commit=False)
user.save()
#user = user.save(commit=False)
user = user.save()
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)
@ -500,19 +503,18 @@ def del_whitelist(request, whitelist, **_kwargs):
@can_edit(User)
def add_mailalias(request, user, userid):
""" Créer un alias """
mailalias_instance = MailAlias(mail=user.mail)
whitelist = WhitelistForm(
mailalias_instance = MailAlias(user=user)
mailalias = MailAliasForm(
request.POST or None,
instance=whitelist_instance
instance=mailalias_instance
)
if whitelist.is_valid():
whitelist.save()
if mailalias.is_valid():
mailalias.save()
messages.success(request, "Alias créé")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(userid)}
))
return form(
{'userform': mailalias, 'action_name': 'Ajouter un alias mail'},
'users/user.html',
@ -527,11 +529,14 @@ def edit_mailalias(request, mailalias_instance, **_kwargs):
request.POST or None,
instance=mailalias_instance
)
if whitelist.is_valid():
if whitelist.changed_data:
whitelist.save()
if mailalias.is_valid():
if mailalias.changed_data:
mailalias.save()
messages.success(request, "Alias modifiée")
return redirect(reverse('users:index'))
return redirect(reverse(
'users:profil',
kwargs={'userid': str(mailalias_instance.user.id)}
))
return form(
{'userform': mailalias, 'action_name': 'Editer un alias mail'},
'users/user.html',
@ -547,7 +552,7 @@ def del_mailalias(request, mailalias, **_kwargs):
messages.success(request, "L'alias a été supprimé")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(mailalias.mail.user.id)}
kwargs={'userid': str(mailalias.user.id)}
))
return form(
{'objet': mailalias, 'objet_name': 'mailalias'},
@ -555,6 +560,29 @@ def del_mailalias(request, mailalias, **_kwargs):
request
)
@login_required
@can_edit(User)
def edit_mail(request, user_instance, **_kwargs):
""" Editer un compte mail"""
mail = MailForm(
request.POST or None,
instance=user_instance,
user=request.user
)
if mail.is_valid():
if mail.changed_data:
mail.save()
messages.success(request, "Option mail modifiée")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(user_instance.id)}
))
return form(
{'userform': mail, 'action_name': 'Editer les options mail'},
'users/user.html',
request
)
@login_required
@can_create(School)
def add_school(request):
@ -920,7 +948,7 @@ def profil(request, users, **_kwargs):
)
nb_machines = machines.count()
machines = re2o_paginator(request, machines, pagination_large_number)
factures = Facture.objects.filter(user=users)
factures = Facture.objects.filter(user=users).select_related('paiement').select_related('user')
factures = SortTable.sort(
factures,
request.GET.get('col'),
@ -955,6 +983,10 @@ def profil(request, users, **_kwargs):
'white_list': whitelists,
'user_solde': user_solde,
'allow_online_payment': allow_online_payment,
'solde_activated': OptionalUser.objects.first().user_solde,
'asso_name': AssoOption.objects.first().name,
'alias_list': users.mailalias_set.all(),
'mail_accounts': OptionalUser.objects.first().mail_accounts
}
)