diff --git a/api/acl.py b/api/acl.py
index 8c39aed0..4d634beb 100644
--- a/api/acl.py
+++ b/api/acl.py
@@ -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.
diff --git a/api/authentication.py b/api/authentication.py
index 469c51f1..0198c4ce 100644
--- a/api/authentication.py
+++ b/api/authentication.py
@@ -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)
diff --git a/api/pagination.py b/api/pagination.py
index 20dcad6e..d34c17ab 100644
--- a/api/pagination.py
+++ b/api/pagination.py
@@ -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.
diff --git a/api/permissions.py b/api/permissions.py
index 53f06620..0b666ebd 100644
--- a/api/permissions.py
+++ b/api/permissions.py
@@ -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.
diff --git a/api/routers.py b/api/routers.py
index fcfb5077..2d245382 100644
--- a/api/routers.py
+++ b/api/routers.py
@@ -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.
diff --git a/api/serializers.py b/api/serializers.py
index d507efab..8f7c8035 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -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
diff --git a/api/settings.py b/api/settings.py
index f8171638..925d503a 100644
--- a/api/settings.py
+++ b/api/settings.py
@@ -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.
diff --git a/api/tests.py b/api/tests.py
index ef05cec2..0931ab8e 100644
--- a/api/tests.py
+++ b/api/tests.py
@@ -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.
diff --git a/api/urls.py b/api/urls.py
index bde01dc1..6003284b 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -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
diff --git a/api/views.py b/api/views.py
index e239ff71..60be3b46 100644
--- a/api/views.py
+++ b/api/views.py
@@ -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
diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py
index fcf55cfa..8450777b 100644
--- a/freeradius_utils/auth.py
+++ b/freeradius_utils/auth.py
@@ -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:
diff --git a/machines/forms.py b/machines/forms.py
index a4f20a91..eda62c7c 100644
--- a/machines/forms.py
+++ b/machines/forms.py
@@ -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):
diff --git a/machines/models.py b/machines/models.py
index b7e829d4..a0a84a8e 100644
--- a/machines/models.py
+++ b/machines/models.py
@@ -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 = (
diff --git a/preferences/admin.py b/preferences/admin.py
index 296dc57c..043370db 100644
--- a/preferences/admin.py
+++ b/preferences/admin.py
@@ -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)
diff --git a/preferences/forms.py b/preferences/forms.py
index afe111a2..6f727f3a 100644
--- a/preferences/forms.py
+++ b/preferences/forms.py
@@ -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()
diff --git a/preferences/migrations/0035_optionaluser_mail_extension.py b/preferences/migrations/0035_optionaluser_mail_extension.py
new file mode 100644
index 00000000..3d1b42b4
--- /dev/null
+++ b/preferences/migrations/0035_optionaluser_mail_extension.py
@@ -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),
+ ),
+ ]
diff --git a/preferences/migrations/0036_optionaluser_mail_accounts.py b/preferences/migrations/0036_optionaluser_mail_accounts.py
new file mode 100644
index 00000000..c3bf4fa8
--- /dev/null
+++ b/preferences/migrations/0036_optionaluser_mail_accounts.py
@@ -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'),
+ ),
+ ]
diff --git a/preferences/migrations/0037_optionaluser_max_mail_alias.py b/preferences/migrations/0037_optionaluser_max_mail_alias.py
new file mode 100644
index 00000000..8d6ca609
--- /dev/null
+++ b/preferences/migrations/0037_optionaluser_max_mail_alias.py
@@ -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"),
+ ),
+ ]
diff --git a/preferences/migrations/0038_mailcontact.py b/preferences/migrations/0038_mailcontact.py
new file mode 100644
index 00000000..6165b98a
--- /dev/null
+++ b/preferences/migrations/0038_mailcontact.py
@@ -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),
+ ),
+ ]
diff --git a/preferences/models.py b/preferences/models.py
index 560fa30f..cf4e5a33 100644
--- a/preferences/models.py
+++ b/preferences/models.py
@@ -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,13 +123,18 @@ 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)
def optionaluser_post_save(**kwargs):
@@ -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"""
diff --git a/preferences/templates/preferences/aff_mailcontact.html b/preferences/templates/preferences/aff_mailcontact.html
new file mode 100644
index 00000000..55d268f0
--- /dev/null
+++ b/preferences/templates/preferences/aff_mailcontact.html
@@ -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 %}
+
+
+
+ Adresse |
+ Commentaire |
+ |
+
+
+ {% for mailcontact in mailcontact_list %}
+
+ {{ mailcontact.address }} |
+ {{ mailcontact.commentary }} |
+
+ {% 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 %}
+ |
+
+ {% endfor %}
+
diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html
index 99e3e14f..7bd7df96 100644
--- a/preferences/templates/preferences/display_preferences.html
+++ b/preferences/templates/preferences/display_preferences.html
@@ -36,20 +36,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
+ Généralités
Téléphone obligatoirement requis |
{{ useroptions.is_tel_mandatory }} |
- Activation du solde pour les utilisateurs |
- {{ useroptions.user_solde }} |
+ Auto inscription |
+ {{ useroptions.self_adhesion }} |
Champ gpg fingerprint |
{{ useroptions.gpg_fingerprint }} |
- {% if useroptions.user_solde %}
- Solde négatif |
- {{ useroptions.solde_negatif }} |
- {% endif %}
+ Shell par défaut des utilisateurs |
+ {{ useroptions.shell_default }} |
Creations d'adhérents par tous |
@@ -57,21 +56,37 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Creations de clubs par tous |
{{ useroptions.all_can_create_club }} |
- {% if useroptions.user_solde %}
+
+ {% if useroptions.user_solde %}Gestion du solde{% else %}Gesion du solde{% endif%}
+
- Solde maximum |
- {{ useroptions.max_solde }} |
- Montant minimal de rechargement en ligne |
- {{ useroptions.min_online_payment }} |
-
- {% endif %}
-
- Auto inscription |
- {{ useroptions.self_adhesion }} |
- Shell par défaut des utilisateurs |
- {{ useroptions.shell_default }} |
+ Activation du solde pour les utilisateurs |
+ {{ useroptions.user_solde }} |
+
+ Solde négatif |
+ {{ useroptions.solde_negatif }} |
+
+
+ Solde maximum |
+ {{ useroptions.max_solde }} |
+ Montant minimal de rechargement en ligne |
+ {{ useroptions.min_online_payment }} |
+ {% if useroptions.mail_accounts %}Comptes mails{% else %}Comptes mails{% endif%}
+
+
+ Gestion des comptes mails |
+ {{ useroptions.mail_accounts }} |
+ Extension mail interne |
+ {{ useroptions.mail_extension }} |
+
+
+ Nombre d'alias maximum |
+ {{ useroption.max_mail_alias }} |
+ |
+
+
Préférences machines
@@ -215,9 +230,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Service%}
Ajouter un service
{% acl_end %}
- Supprimer un ou plusieurs service
+ Supprimer un ou plusieurs services
{% include "preferences/aff_service.html" with service_list=service_list %}
+ Liste des adresses mail de contact
+ {% can_create preferences.MailContact%}
+ Ajouter une adresse
+ {% acl_end %}
+ Supprimer une ou plusieurs adresses
+ {% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
+
+
Editer
diff --git a/preferences/urls.py b/preferences/urls.py
index bca7bb1e..b9e1663d 100644
--- a/preferences/urls.py
+++ b/preferences/urls.py
@@ -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[0-9]+)$',
+ views.edit_mailcontact,
+ name='edit-mailcontact'
+ ),
+ url(r'^del_mailcontact/$', views.del_mailcontact, name='del-mailcontact'),
url(
r'^history/(?P\w+)/(?P[0-9]+)$',
re2o.views.history,
diff --git a/preferences/views.py b/preferences/views.py
index b8ca39d2..1ad0b42d 100644
--- a/preferences/views.py
+++ b/preferences/views.py
@@ -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
+ )
diff --git a/printer/__init__.py b/printer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/printer/admin.py b/printer/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/printer/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/printer/apps.py b/printer/apps.py
new file mode 100644
index 00000000..62de6453
--- /dev/null
+++ b/printer/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class PrinterConfig(AppConfig):
+ name = 'printer'
diff --git a/printer/forms.py b/printer/forms.py
new file mode 100644
index 00000000..3d60fd38
--- /dev/null
+++ b/printer/forms.py
@@ -0,0 +1,37 @@
+# -*- mode: python; coding: utf-8 -*-
+
+"""printer.forms
+Form to add, edit, cancel printer jobs.
+Author : Maxime Bombar .
+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',
+ ]
+
diff --git a/printer/migrations/0001_initial.py b/printer/migrations/0001_initial.py
new file mode 100644
index 00000000..55f63a7f
--- /dev/null
+++ b/printer/migrations/0001_initial.py
@@ -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)),
+ ],
+ ),
+ ]
diff --git a/printer/migrations/0002_auto_20180628_2032.py b/printer/migrations/0002_auto_20180628_2032.py
new file mode 100644
index 00000000..a9439262
--- /dev/null
+++ b/printer/migrations/0002_auto_20180628_2032.py
@@ -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',
+ ),
+ ]
diff --git a/printer/migrations/__init__.py b/printer/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/printer/models.py b/printer/models.py
new file mode 100644
index 00000000..f16e620d
--- /dev/null
+++ b/printer/models.py
@@ -0,0 +1,116 @@
+# -*- mode: python; coding: utf-8 -*-
+
+"""printer.models
+Models of the printer application
+Author : Maxime Bombar .
+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_/
+ 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')
diff --git a/printer/settings.py b/printer/settings.py
new file mode 100644
index 00000000..c8879bd4
--- /dev/null
+++ b/printer/settings.py
@@ -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']
diff --git a/printer/templates/printer/echec.html b/printer/templates/printer/echec.html
new file mode 100644
index 00000000..d15907c3
--- /dev/null
+++ b/printer/templates/printer/echec.html
@@ -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 %}
+{% trans "Failure" %}
+{% endblock %}
diff --git a/printer/templates/printer/newjob.html b/printer/templates/printer/newjob.html
new file mode 100644
index 00000000..1167e18e
--- /dev/null
+++ b/printer/templates/printer/newjob.html
@@ -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 %}
+
+
+{% endblock %}
+
diff --git a/printer/templates/printer/success.html b/printer/templates/printer/success.html
new file mode 100644
index 00000000..c7649e0a
--- /dev/null
+++ b/printer/templates/printer/success.html
@@ -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 %}
+{% trans "Success" %}
+{% endblock %}
diff --git a/printer/tests.py b/printer/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/printer/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/printer/urls.py b/printer/urls.py
new file mode 100644
index 00000000..3629c741
--- /dev/null
+++ b/printer/urls.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+"""printer.urls
+The defined URLs for the printer app
+Author : Maxime Bombar .
+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"),
+]
diff --git a/printer/validators.py b/printer/validators.py
new file mode 100644
index 00000000..069383cb
--- /dev/null
+++ b/printer/validators.py
@@ -0,0 +1,72 @@
+# -*- mode: python; coding: utf-8 -*-
+
+
+"""printer.validators
+Custom validators useful for printer application.
+Author : Maxime Bombar .
+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)
diff --git a/printer/views.py b/printer/views.py
new file mode 100644
index 00000000..6767ab9c
--- /dev/null
+++ b/printer/views.py
@@ -0,0 +1,55 @@
+# -*- mode: python; coding: utf-8 -*-
+"""printer.views
+The views for the printer app
+Author : Maxime Bombar .
+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
+ )
diff --git a/re2o/templates/re2o/contact.html b/re2o/templates/re2o/contact.html
new file mode 100644
index 00000000..474ac7e8
--- /dev/null
+++ b/re2o/templates/re2o/contact.html
@@ -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 %}
+{% blocktrans %}Contacter l'association {{asso_name}}{% endblocktrans %}
+
+
+{% for contact in contacts %}
+
+
+
{{ contact.get_name }}
+
+
+
{{ contact.commentary}}
+
+
+
+
+
+{% endfor %}
+
+
+
+{% endblock %}
+
diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py
index f2eb1062..6a4fd670 100644
--- a/re2o/templatetags/acl.py
+++ b/re2o/templatetags/acl.py
@@ -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,
diff --git a/re2o/urls.py b/re2o/urls.py
index 3322e82b..b1a1037c 100644
--- a/re2o/urls.py
+++ b/re2o/urls.py
@@ -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:
diff --git a/re2o/views.py b/re2o/views.py
index 45b6ea73..5110cfd8 100644
--- a/re2o/views.py
+++ b/re2o/views.py
@@ -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"""
diff --git a/search/views.py b/search/views.py
index 871515fa..73333834 100644
--- a/search/views.py
+++ b/search/views.py
@@ -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
)
diff --git a/static/css/base.css b/static/css/base.css
index ab0bf945..39ca6372 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -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)
+}
+
+
diff --git a/templates/base.html b/templates/base.html
index d6b03798..85457292 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -112,6 +112,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
+
+ Printer
+
+
{% can_view_app logs %}
{% trans "Statistics" %}
{% acl_end %}
@@ -124,8 +130,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
-
- {% trans "About" %}
+
+ {% trans "Info" %}
+
{% if not request.user.is_authenticated %}
{% if var_sa %}
diff --git a/topologie/admin.py b/topologie/admin.py
index 62ffd6c4..d2a25461 100644
--- a/topologie/admin.py
+++ b/topologie/admin.py
@@ -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)
diff --git a/topologie/forms.py b/topologie/forms.py
index 18831217..ba37d395 100644
--- a/topologie/forms.py
+++ b/topologie/forms.py
@@ -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)
+
diff --git a/topologie/migrations/0061_portprofile.py b/topologie/migrations/0061_portprofile.py
new file mode 100644
index 00000000..7e130163
--- /dev/null
+++ b/topologie/migrations/0061_portprofile.py
@@ -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),
+ ),
+ ]
diff --git a/topologie/migrations/0062_auto_20180627_0123.py b/topologie/migrations/0062_auto_20180627_0123.py
new file mode 100644
index 00000000..b8135de8
--- /dev/null
+++ b/topologie/migrations/0062_auto_20180627_0123.py
@@ -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'),
+ ),
+ ]
diff --git a/topologie/migrations/0063_port_custom_profil.py b/topologie/migrations/0063_port_custom_profil.py
new file mode 100644
index 00000000..15feebce
--- /dev/null
+++ b/topologie/migrations/0063_port_custom_profil.py
@@ -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'),
+ ),
+ ]
diff --git a/topologie/migrations/0064_createprofil.py b/topologie/migrations/0064_createprofil.py
new file mode 100644
index 00000000..2f165386
--- /dev/null
+++ b/topologie/migrations/0064_createprofil.py
@@ -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),
+ ]
diff --git a/topologie/migrations/0065_auto_20180630_1703.py b/topologie/migrations/0065_auto_20180630_1703.py
new file mode 100644
index 00000000..9fed2d83
--- /dev/null
+++ b/topologie/migrations/0065_auto_20180630_1703.py
@@ -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',
+ ),
+ ]
diff --git a/topologie/migrations/0066_auto_20180630_1855.py b/topologie/migrations/0066_auto_20180630_1855.py
new file mode 100644
index 00000000..b197f568
--- /dev/null
+++ b/topologie/migrations/0066_auto_20180630_1855.py
@@ -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'),
+ ),
+ ]
diff --git a/topologie/migrations/0067_auto_20180701_0016.py b/topologie/migrations/0067_auto_20180701_0016.py
new file mode 100644
index 00000000..578ee7d6
--- /dev/null
+++ b/topologie/migrations/0067_auto_20180701_0016.py
@@ -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'),
+ ),
+ ]
diff --git a/topologie/models.py b/topologie/models.py
index 7c6865e2..2849a50a 100644
--- a/topologie/models.py
+++ b/topologie/models.py
@@ -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"""
diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html
index deeb0655..86216f15 100644
--- a/topologie/templates/topologie/aff_port.html
+++ b/topologie/templates/topologie/aff_port.html
@@ -32,8 +32,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %} |
{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %} |
{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %} |
- {% include "buttons/sort.html" with prefix='port' col='radius' text='Radius' %} |
- {% include "buttons/sort.html" with prefix='port' col='vlan' text='Vlan forcé' %} |
+ Etat du port |
+ Profil du port |
Détails |
|
@@ -66,8 +66,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
{% endif %}
- {{ port.radius }} |
- {% if not port.vlan_force %}Aucun{% else %}{{ port.vlan_force }}{% endif %} |
+ {% if port.state %} Actif{% else %}Désactivé{% endif %} |
+ {% if not port.custom_profile %}Par défaut : {% endif %}{{port.get_port_profil}} |
{{ port.details }} |
@@ -77,10 +77,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- {% acl_end %}
+ {% acl_end %}
{% can_delete port %}
-
-
+
+
{% acl_end %}
|
diff --git a/topologie/templates/topologie/aff_port_profile.html b/topologie/templates/topologie/aff_port_profile.html
new file mode 100644
index 00000000..12c8b365
--- /dev/null
+++ b/topologie/templates/topologie/aff_port_profile.html
@@ -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 %}
+
+
+
+{% if port_profile_list.paginator %}
+{% include "pagination.html" with list=port_profile_list %}
+{% endif %}
+
+
+
+
+
+ {% trans "Name" %} |
+ {% trans "Default for" %} |
+ {% trans "VLANs" %} |
+ {% trans "RADIUS settings" %} |
+ {% trans "Speed" %} |
+ {% trans "Mac address limit" %} |
+ {% trans "Security" %} |
+ |
+
+
+ {% for port_profile in port_profile_list %}
+
+ {{port_profile.name}} |
+ {{port_profile.profil_default}} |
+
+ {% if port_profile.vlan_untagged %}
+ Untagged : {{port_profile.vlan_untagged}}
+
+ {% endif %}
+ {% if port_profile.vlan_tagged.all %}
+ Tagged : {{port_profile.vlan_tagged.all|join:", "}}
+ {% endif %}
+ |
+
+ Type : {{port_profile.radius_type}}
+ {% if port_profile.radius_type == "MAC-radius" %}
+
+ Mode : {{port_profile.radius_mode}} |
+ {% endif %}
+ {{port_profile.speed}} |
+ {{port_profile.mac_limit}} |
+ {{port_profile.security_parameters_enabled|join:" "}} |
+
+ {% 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 %}
+ |
+
+ {% endfor %}
+
+
+{% if port_profile_list.paginator %}
+{% include "pagination.html" with list=port_profile_list %}
+{% endif %}
+
+
diff --git a/topologie/templates/topologie/index.html b/topologie/templates/topologie/index.html
index a7c4bb51..f596e6a5 100644
--- a/topologie/templates/topologie/index.html
+++ b/topologie/templates/topologie/index.html
@@ -72,5 +72,4 @@ Topologie des Switchs
-
{% endblock %}
diff --git a/topologie/templates/topologie/index_portprofile.html b/topologie/templates/topologie/index_portprofile.html
new file mode 100644
index 00000000..f95415c8
--- /dev/null
+++ b/topologie/templates/topologie/index_portprofile.html
@@ -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 %}
+
+{% trans "Port profiles" %}
+{% can_create PortProfile %}
+{% trans " Add a port profile" %}
+
+{% acl_end %}
+{% include "topologie/aff_port_profile.html" with port_profile_list=port_profile_list %}
+
+
+
+
+{% endblock %}
diff --git a/topologie/templates/topologie/sidebar.html b/topologie/templates/topologie/sidebar.html
index ce7b4114..04ee5202 100644
--- a/topologie/templates/topologie/sidebar.html
+++ b/topologie/templates/topologie/sidebar.html
@@ -33,7 +33,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Switchs
-
+
+
+ Config des ports switchs
+
+
Bornes WiFi
diff --git a/topologie/urls.py b/topologie/urls.py
index af3327b7..a827acf2 100644
--- a/topologie/urls.py
+++ b/topologie/urls.py
@@ -113,4 +113,16 @@ urlpatterns = [
url(r'^del_building/(?P[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[0-9]+)$',
+ views.edit_port_profile,
+ name='edit-port-profile'),
+ url(r'^del_port_profile/(?P[0-9]+)$',
+ views.del_port_profile,
+ name='del-port-profile'),
]
diff --git a/topologie/views.py b/topologie/views.py
index 3945abc5..e5453982 100644
--- a/topologie/views.py
+++ b/topologie/views.py
@@ -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.
diff --git a/users/admin.py b/users/admin.py
index 5f4c197b..ee71713c 100644
--- a/users/admin.py
+++ b/users/admin.py
@@ -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...
diff --git a/users/forms.py b/users/forms.py
index a14118cb..40e2d800 100644
--- a/users/forms.py
+++ b/users/forms.py
@@ -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']
diff --git a/users/migrations/0073_auto_20180629_1614.py b/users/migrations/0073_auto_20180629_1614.py
new file mode 100644
index 00000000..681cb722
--- /dev/null
+++ b/users/migrations/0073_auto_20180629_1614.py
@@ -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),
+ ),
+ ]
diff --git a/users/migrations/0074_auto_20180629_1717.py b/users/migrations/0074_auto_20180629_1717.py
new file mode 100644
index 00000000..7574ea09
--- /dev/null
+++ b/users/migrations/0074_auto_20180629_1717.py
@@ -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),
+ ]
diff --git a/users/models.py b/users/models.py
index 30aae2d1..4e1f4202 100644
--- a/users/models.py
+++ b/users/models.py
@@ -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)
diff --git a/users/templates/users/aff_alias.html b/users/templates/users/aff_mailalias.html
similarity index 91%
rename from users/templates/users/aff_alias.html
rename to users/templates/users/aff_mailalias.html
index 59a9b6f1..a441b9e9 100644
--- a/users/templates/users/aff_alias.html
+++ b/users/templates/users/aff_mailalias.html
@@ -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 %}
-
@@ -37,12 +36,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ alias }} |
{% 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 %}
|
{% endfor %}
diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html
index abba61a2..863897f2 100644
--- a/users/templates/users/profil.html
+++ b/users/templates/users/profil.html
@@ -27,21 +27,99 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %}
{% block title %}Profil{% endblock %}
{% block content %}
-{{ users.surname }} {{users.name}}
-Vous êtes {% if users.end_adhesion != None %}
-un {{ users.class_name | lower}}{% else %}
-non adhérent{% endif %} et votre connexion est {% if users.has_access %}
-active{% else %}désactivée{% endif %}.
-{% if user_solde %}
-Votre solde est de {{ user.solde }}€.
-{% if allow_online_payment %}
-
-
- Recharger
-
-{% endif %}
-
-{% endif %}
+
+
Bienvenue {{users.name}} {{ users.surname }}
+
+
+
+ {% if solde_activated %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% if users.is_ban%}
+
+
Votre compte est banni
+
+ Fin du ban : {{user.end_ban|date:"d M Y"}}
+
+
+ {% elif not users.is_connected%}
+
+
Pas d'accès à internet
+
+
+ {% else %}
+
+
Connecté
+
+ Fin de connexion: {{user.end_adhesion|date:"d M Y"}}
+
+
+ {% endif %}
+
+
+ {% if solde_activated %}
+
+ {% endif %}
+
+ {% if solde_activated %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% if nb_machines %}
+
+
+ {{nb_machines}}
+ Machines
+
+
+
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+
@@ -50,7 +128,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}
Informations détaillées
-
+
{% if machines_list %}
@@ -349,6 +427,65 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}
+
+
+
+
+ Paramètres mail
+
+
+
+
+ {% can_edit users %}
+
+
+ Modifier les options mail
+
+ {% acl_end %}
+ {% if mail_accounts %}
+
+
+
+ Adresse mail externe |
+ Compte mail {{ asso_name }} |
+ Adresse mail de contact |
+
+
+ {{ users.external_mail }} |
+ {{ users.internal_address|yesno:"Activé,Désactivé" }} |
+ {{ users.get_mail }} |
+
+
+ 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.
+
+
+ {% if users.internal_address %}
+ {% can_create MailAlias users.id %}
+
+
+ Ajouter un alias mail
+
+ {% acl_end %}
+ {% if alias_list %}
+ {% include "users/aff_mailalias.html" with alias_list=alias_list %}
+ {% endif %}
+ {% endif %}
+ {% else %}
+
+
+
+ Adresse mail |
+
+
+ {{ users.external_mail }} |
+
+
+
+ {% endif %}
+
+
+
diff --git a/users/urls.py b/users/urls.py
index f27a15c3..007eb940 100644
--- a/users/urls.py
+++ b/users/urls.py
@@ -68,6 +68,7 @@ urlpatterns = [
url(r'^add_mailalias/(?P
[0-9]+)$', views.add_mailalias, name='add-mailalias'),
url(r'^edit_mailalias/(?P[0-9]+)$', views.edit_mailalias, name='edit-mailalias'),
url(r'^del-mailalias/(?P[0-9]+)$', views.del_mailalias, name='del-mailalias'),
+ url(r'^edit_mail/(?P[0-9]+)$', views.edit_mail, name='edit-mail'),
url(r'^add_school/$', views.add_school, name='add-school'),
url(r'^edit_school/(?P[0-9]+)$',
views.edit_school,
diff --git a/users/views.py b/users/views.py
index 7473a3c8..731a56df 100644
--- a/users/views.py
+++ b/users/views.py
@@ -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
}
)