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 %} + + + + + + + + + {% for mailcontact in mailcontact_list %} + + + + + + {% endfor %} +
AdresseCommentaire
{{ 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 %} +
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
- - + + - {% if useroptions.user_solde %} - - - {% endif %} + + @@ -57,21 +56,37 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% if useroptions.user_solde %} +
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 }}Solde négatif{{ useroptions.solde_negatif }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
Creations d'adhérents par tousCreations de clubs par tous {{ useroptions.all_can_create_club }}
+
{% if useroptions.user_solde %}Gestion du solde{% else %}Gesion du solde{% endif%}
+ - - - - - - {% endif %} - - - - - + + + + + + + + + + +
Solde maximum{{ useroptions.max_solde }}Montant minimal de rechargement en ligne{{ useroptions.min_online_payment }}
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 %} +
+ {% csrf_token %} +

{% trans "Printing Menu" %}

+ {{ jobform.management_form }} + {% bootstrap_formset_errors jobform %} +
+ {% for job in jobform.forms %} +
+ {% bootstrap_form job label_class='sr-only' %} + +
+ {% endfor %} +
+ + {% bootstrap_button action_name button_type="submit" icon="star" %} +
+ +{% 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 }}

+
+ +
+
+ +{% 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 %} + {% 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" %} +
  • {% 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 %} + + + + + + + + + + + + + + + + {% for port_profile in port_profile_list %} + + + + + + {% endif %} + + + + + + {% endfor %} +
    {% trans "Name" %}{% trans "Default for" %}{% trans "VLANs" %}{% trans "RADIUS settings" %}{% trans "Speed" %}{% trans "Mac address limit" %}{% trans "Security" %}
    {{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}}
    {{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 %} +
    + +{% 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., {% 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 %} +
    +
    +
    +
    + {{user.solde}} +
    + +
    +
    +
    + {% endif %} + + {% if solde_activated %} +
    + {% else %} +
    + {% endif %} +
    + {% if nb_machines %} +
    +
    + {{nb_machines}} + Machines + +
    + +
    + {% else %} +
    +
    Aucune machine
    + +
    + {% 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 %} +
    +
    {{ 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 %}
    + + + + + + + + + +
    Adresse mail externeCompte mail {{ asso_name }}Adresse mail de contact
    {{ users.external_mail }}{{ users.internal_address|yesno:"Activé,Désactivé" }}{{ users.get_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 } )