From 7e0d5fdbce1835aaafa78633b3325193661da9e3 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 8 Nov 2018 02:47:25 +0100 Subject: [PATCH 01/86] =?UTF-8?q?Bug=20fix,=20le=20chiffrement/d=C3=A9chif?= =?UTF-8?q?frement=20AES=20marche=20en=20python2=20aussi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/aes_field.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/re2o/aes_field.py b/re2o/aes_field.py index 2720f5af..5f50ddd2 100644 --- a/re2o/aes_field.py +++ b/re2o/aes_field.py @@ -37,8 +37,8 @@ from django.db import models from django import forms from django.conf import settings -EOD = '`%EofD%`' # This should be something that will not occur in strings - +EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings +EOD = EOD_asbyte.decode('utf-8') def genstring(length=16, chars=string.printable): """ Generate a random string of length `length` and composed of @@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable): return ''.join([choice(chars) for i in range(length)]) -def encrypt(key, s): - """ AES Encrypt a secret `s` with the key `key` """ +def encrypt(key, secret): + """ AES Encrypt a secret with the key `key` """ obj = AES.new(key) - datalength = len(s) + len(EOD) + datalength = len(secret) + len(EOD) if datalength < 16: saltlength = 16 - datalength else: saltlength = 16 - datalength % 16 - ss = ''.join([s, EOD, genstring(saltlength)]) - return obj.encrypt(ss) + encrypted_secret = ''.join([secret, EOD, genstring(saltlength)]) + return obj.encrypt(encrypted_secret) -def decrypt(key, s): - """ AES Decrypt a secret `s` with the key `key` """ +def decrypt(key, secret): + """ AES Decrypt a secret with the key `key` """ obj = AES.new(key) - ss = obj.decrypt(s) - return ss.split(bytes(EOD, 'utf-8'))[0] + uncrypted_secret = obj.decrypt(secret) + return uncrypted_secret.split(EOD_asbyte)[0] class AESEncryptedFormField(forms.CharField): @@ -81,8 +81,7 @@ class AESEncryptedField(models.CharField): if value is None: return None try: - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') + return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8') except Exception as e: raise ValueError(value) @@ -90,18 +89,14 @@ class AESEncryptedField(models.CharField): if value is None: return value try: - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') + return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8') except Exception as e: raise ValueError(value) def get_prep_value(self, value): if value is None: return value - return binascii.b2a_base64(encrypt( - settings.AES_KEY, - value - )).decode('utf-8') + return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8') def formfield(self, **kwargs): defaults = {'form_class': AESEncryptedFormField} From aff96b6541f613c12ab503663a3f9afb2a375342 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Thu, 8 Nov 2018 19:56:38 +0000 Subject: [PATCH 02/86] =?UTF-8?q?Ajout=20du=20champ=20mail=20lors=20de=20l?= =?UTF-8?q?a=20cr=C3=A9ation=20d'un=20club?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/users/forms.py b/users/forms.py index e2f09dca..8b496019 100644 --- a/users/forms.py +++ b/users/forms.py @@ -429,6 +429,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): self.fields['surname'].label = _("Name") self.fields['school'].label = _("School") self.fields['comment'].label = _("Comment") + self.fields['email'].label = _("Email Address") if 'room' in self.fields: self.fields['room'].label = _("Room") self.fields['room'].empty_label = _("No room") @@ -444,6 +445,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): 'comment', 'room', 'telephone', + 'email', 'shell', 'mailing' ] From 0a4a0d5dd13d7db8b174c460abb8d1d85ae70e47 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sun, 4 Nov 2018 16:26:04 +0000 Subject: [PATCH 03/86] =?UTF-8?q?Anonymisation=20des=20donn=C3=A9e=20de=20?= =?UTF-8?q?l'utilisateur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/management/commands/anonymize.py | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 users/management/commands/anonymize.py diff --git a/users/management/commands/anonymize.py b/users/management/commands/anonymize.py new file mode 100644 index 00000000..a442dbe1 --- /dev/null +++ b/users/management/commands/anonymize.py @@ -0,0 +1,51 @@ +from django.core.management.base import BaseCommand +from users.models import User, School, Adherent +from django.db.models import F, Value +from django.db.models.functions import Concat + +class Command(BaseCommand): + help="Anonymize the data in the database in order to use them on critical servers (dev, personnal...). Every information will be overwritten using non-personnal informations. This script must follow any modification of the database." + + def handle(self, *args, **kwargs): + + total = User.objects.count() + self.stdout.write("Starting anonymizing the {} users data.".format(total)) + + u = User.objects.all() + a = Adherent.objects.all() + + self.stdout.write('Supression de l\'école...') + # Create a fake School to put everyone in it. + ecole = School(name="Ecole des Ninja") + ecole.save() + u.update(school=ecole) + self.stdout.write(self.style.SUCCESS('done ...')) + + self.stdout.write('Supression des chambres...') + a.update(room=None) + self.stdout.write(self.style.SUCCESS('done ...')) + + self.stdout.write('Supression des mails...') + u.update(email='example@example.org', + local_email_redirect = False, + local_email_enabled=False) + self.stdout.write(self.style.SUCCESS('done ...')) + + self.stdout.write('Supression des noms, prenoms, pseudo, telephone, commentaire...') + a.update(name=Concat(Value('name of '), 'id')) + self.stdout.write(self.style.SUCCESS('done name')) + + a.update(surname=Concat(Value('surname of '), 'id')) + self.stdout.write(self.style.SUCCESS('done surname')) + + a.update(pseudo=F('id')) + self.stdout.write(self.style.SUCCESS('done pseudo')) + + a.update(telephone=Concat(Value('phone of '), 'id')) + self.stdout.write(self.style.SUCCESS('done phone')) + + a.update(comment=Concat(Value('commentaire of '), 'id')) + self.stdout.write(self.style.SUCCESS('done ...')) + + + self.stdout.write("Data anonymized!") From 5bd3a929df00da8c607a48cac5c982947a46d190 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sun, 4 Nov 2018 17:09:24 +0000 Subject: [PATCH 04/86] Unification du mot de passe --- users/management/commands/anonymize.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/users/management/commands/anonymize.py b/users/management/commands/anonymize.py index a442dbe1..00cbb007 100644 --- a/users/management/commands/anonymize.py +++ b/users/management/commands/anonymize.py @@ -3,6 +3,10 @@ from users.models import User, School, Adherent from django.db.models import F, Value from django.db.models.functions import Concat +from re2o.login import hashNT, makeSecret + +import os, random, string + class Command(BaseCommand): help="Anonymize the data in the database in order to use them on critical servers (dev, personnal...). Every information will be overwritten using non-personnal informations. This script must follow any modification of the database." @@ -46,6 +50,21 @@ class Command(BaseCommand): a.update(comment=Concat(Value('commentaire of '), 'id')) self.stdout.write(self.style.SUCCESS('done ...')) + + self.stdout.write('Unification du mot de passe...') + # Define the password + chars = string.ascii_letters + string.digits + '!@#$%^&*()' + taille = 20 + random.seed = (os.urandom(1024)) + password = "" + for i in range(taille): + password+=random.choice(chars) + + self.stdout.write(self.style.HTTP_NOT_MODIFIED('The password will be: {}'.format(password))) + + a.update(pwd_ntlm = hashNT(password)) + a.update(password = makeSecret(password)) + self.stdout.write(self.style.SUCCESS('done...')) self.stdout.write("Data anonymized!") From dd4143dbe266d3431401bb9f2090fa1c7fc75ac3 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Mon, 5 Nov 2018 19:08:33 +0000 Subject: [PATCH 05/86] Anonymisation des clubs --- users/management/commands/anonymize.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/users/management/commands/anonymize.py b/users/management/commands/anonymize.py index 00cbb007..d2632576 100644 --- a/users/management/commands/anonymize.py +++ b/users/management/commands/anonymize.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from users.models import User, School, Adherent +from users.models import User, School, Adherent, Club from django.db.models import F, Value from django.db.models.functions import Concat @@ -12,11 +12,12 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): - total = User.objects.count() + total = Adherent.objects.count() self.stdout.write("Starting anonymizing the {} users data.".format(total)) u = User.objects.all() a = Adherent.objects.all() + c = Club.objects.all() self.stdout.write('Supression de l\'école...') # Create a fake School to put everyone in it. @@ -27,6 +28,7 @@ class Command(BaseCommand): self.stdout.write('Supression des chambres...') a.update(room=None) + c.update(room=None) self.stdout.write(self.style.SUCCESS('done ...')) self.stdout.write('Supression des mails...') @@ -42,7 +44,7 @@ class Command(BaseCommand): a.update(surname=Concat(Value('surname of '), 'id')) self.stdout.write(self.style.SUCCESS('done surname')) - a.update(pseudo=F('id')) + u.update(pseudo=F('id')) self.stdout.write(self.style.SUCCESS('done pseudo')) a.update(telephone=Concat(Value('phone of '), 'id')) @@ -62,9 +64,8 @@ class Command(BaseCommand): self.stdout.write(self.style.HTTP_NOT_MODIFIED('The password will be: {}'.format(password))) - a.update(pwd_ntlm = hashNT(password)) - a.update(password = makeSecret(password)) + u.update(pwd_ntlm = hashNT(password)) + u.update(password = makeSecret(password)) self.stdout.write(self.style.SUCCESS('done...')) - self.stdout.write("Data anonymized!") From 0f4c7fa7e92a41d84d93a6f782fc2623aaba5494 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 14:35:26 +0100 Subject: [PATCH 06/86] Variable globale pour le reminder --- api/serializers.py | 5 ++--- .../0055_generaloption_main_site_url.py | 20 +++++++++++++++++++ preferences/models.py | 1 + .../preferences/display_preferences.html | 8 +++++--- 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 preferences/migrations/0055_generaloption_main_site_url.py diff --git a/api/serializers.py b/api/serializers.py index f520cf98..32fdd0e8 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -407,9 +407,8 @@ class GeneralOptionSerializer(NamespacedHMSerializer): fields = ('general_message_fr', 'general_message_en', 'search_display_page', 'pagination_number', 'pagination_large_number', 'req_expire_hrs', - 'site_name', 'email_from', 'GTU_sum_up', - 'GTU') - + 'site_name', 'main_site_url', 'email_from', + 'GTU_sum_up', 'GTU') class HomeServiceSerializer(NamespacedHMSerializer): """Serialize `preferences.models.Service` objects. diff --git a/preferences/migrations/0055_generaloption_main_site_url.py b/preferences/migrations/0055_generaloption_main_site_url.py new file mode 100644 index 00000000..71ea9852 --- /dev/null +++ b/preferences/migrations/0055_generaloption_main_site_url.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-11-14 16:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0054_merge_20181025_1258'), + ] + + operations = [ + migrations.AddField( + model_name='generaloption', + name='main_site_url', + field=models.URLField(default='http://re2o.example.org', max_length=255), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index b21f42fe..43ff7580 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -431,6 +431,7 @@ class GeneralOption(AclMixin, PreferencesModel): req_expire_hrs = models.IntegerField(default=48) site_name = models.CharField(max_length=32, default="Re2o") email_from = models.EmailField(default="www-data@example.com") + main_site_url = models.URLField(max_length=255, default="http://re2o.example.org") GTU_sum_up = models.TextField( default="", blank=True, diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 1c29c595..96d3fe16 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -224,10 +224,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "General message displayed on the website" %} {{ generaloptions.general_message }} - {% trans "Summary of the General Terms of Use" %} - {{ generaloptions.GTU_sum_up }} - + {% trans "Main site url" %} + {{ generaloptions.main_site_url }} + + {% trans "Summary of the General Terms of Use" %} + {{ generaloptions.GTU_sum_up }} {% trans "General Terms of Use" %} {{ generaloptions.GTU }} From dd9090a14b403f8a07620bca4de866e7b60cd224 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 15:47:15 +0100 Subject: [PATCH 07/86] =?UTF-8?q?Docstrings=20adapt=C3=A9es=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/views.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/views.py b/api/views.py index 3d7f4bc2..a2a376e9 100644 --- a/api/views.py +++ b/api/views.py @@ -541,8 +541,8 @@ class ServiceRegenViewSet(viewsets.ModelViewSet): # Config des switches class SwitchPortView(generics.ListAPIView): - """Exposes the associations between hostname, mac address and IPv4 in - order to build the DHCP lease files. + """Output each port of a switch, to be serialized with + additionnal informations (profiles etc) """ queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room") @@ -551,16 +551,14 @@ class SwitchPortView(generics.ListAPIView): # Rappel fin adhésion class ReminderView(generics.ListAPIView): - """Exposes the associations between hostname, mac address and IPv4 in - order to build the DHCP lease files. + """Output for users to remind an end of their subscription. """ queryset = preferences.Reminder.objects.all() serializer_class = serializers.ReminderSerializer class RoleView(generics.ListAPIView): - """Exposes the associations between hostname, mac address and IPv4 in - order to build the DHCP lease files. + """Output of roles for each server """ queryset = machines.Role.objects.all().prefetch_related('servers') serializer_class = serializers.RoleSerializer From c93cbb1b59b14a625da5365d3d49e1e8fc513eb1 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 16:08:47 +0100 Subject: [PATCH 08/86] Serialize dname export --- api/serializers.py | 15 +++++++++++++-- machines/models.py | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 32fdd0e8..f2ff9954 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1000,6 +1000,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer): model = machines.Domain fields = ('alias', 'hostname') +class DNAMERecordSerializer(serializers.ModelSerializer): + """Serialize `machines.models.Domain` objects with the data needed to + generate a DNAME DNS record. + """ + alias = serializers.CharField(read_only=True) + zone = serializers.CharField(read_only=True) + + class Meta: + model = machines.DName + fields = ('alias', 'zone') + class DNSZonesSerializer(serializers.ModelSerializer): """Serialize the data about DNS Zones. @@ -1014,14 +1025,14 @@ class DNSZonesSerializer(serializers.ModelSerializer): a_records = ARecordSerializer(many=True, source='get_associated_a_records') aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records') cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records') + dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records') sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records') class Meta: model = machines.Extension fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6', 'mx_records', 'txt_records', 'srv_records', 'a_records', - 'aaaa_records', 'cname_records', 'sshfp_records') - + 'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records') #REMINDER diff --git a/machines/models.py b/machines/models.py index 48e50644..81b1738a 100644 --- a/machines/models.py +++ b/machines/models.py @@ -741,6 +741,9 @@ class Extension(RevMixin, AclMixin, models.Model): .filter(cname__interface_parent__in=all_active_assigned_interfaces()) .prefetch_related('cname')) + def get_associated_dname_records(self): + return (DName.objects.filter(alias=self)) + @staticmethod def can_use_all(user_request, *_args, **_kwargs): """Superdroit qui permet d'utiliser toutes les extensions sans From a0dc4a15a9eb4167866f1d88d1ff3f6ffe4ec339 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 16:23:09 +0100 Subject: [PATCH 09/86] Remove code mort --- machines/models.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/machines/models.py b/machines/models.py index 81b1738a..a6a1e1ae 100644 --- a/machines/models.py +++ b/machines/models.py @@ -1634,18 +1634,6 @@ class Role(RevMixin, AclMixin, models.Model): verbose_name = _("server role") verbose_name_plural = _("server roles") - @classmethod - def get_instance(cls, roleid, *_args, **_kwargs): - """Get the Role instance with roleid. - - Args: - roleid: The id - - Returns: - The role. - """ - return cls.objects.get(pk=roleid) - @classmethod def interface_for_roletype(cls, roletype): """Return interfaces for a roletype""" @@ -1660,14 +1648,6 @@ class Role(RevMixin, AclMixin, models.Model): machine__interface__role=cls.objects.filter(specific_role=roletype) ) - @classmethod - def get_instance(cls, roleid, *_args, **_kwargs): - """Get the Machine instance with machineid. - :param userid: The id - :return: The user - """ - return cls.objects.get(pk=roleid) - @classmethod def interface_for_roletype(cls, roletype): """Return interfaces for a roletype""" From bb952c0ba012dc41580905c7f6f1d2722001e2aa Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 16:25:15 +0100 Subject: [PATCH 10/86] Email field pour clubs --- users/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/users/forms.py b/users/forms.py index 8b496019..7695e6fc 100644 --- a/users/forms.py +++ b/users/forms.py @@ -444,6 +444,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): 'school', 'comment', 'room', + 'email', 'telephone', 'email', 'shell', From 6e08d3f4156d732c7f8b5abe96bd1cfb50aae40f Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 16:43:28 +0100 Subject: [PATCH 11/86] =?UTF-8?q?R=C3=A9cup=C3=A9ration=20correcte=20du=20?= =?UTF-8?q?sel=20dans=20le=20mot=20de=20passe=20encod=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/login.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/re2o/login.py b/re2o/login.py index 471c2e02..0b552239 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -114,9 +114,9 @@ class CryptPasswordHasher(hashers.BasePasswordHasher): Check password against encoded using CRYPT algorithm """ assert encoded.startswith(self.algorithm) - salt = hash_password_salt(challenge_password) - return constant_time_compare(crypt.crypt(password.encode(), salt), - challenge.encode()) + salt = hash_password_salt(encoded) + return constant_time_compare(crypt.crypt(password, salt), + encoded) def safe_summary(self, encoded): """ From caedb09d8fff74484d531721ef161b4233d873ff Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 17:14:51 +0100 Subject: [PATCH 12/86] Fonction de check de l'alias via smtp --- re2o/base.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ users/models.py | 8 ++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 re2o/base.py diff --git a/re2o/base.py b/re2o/base.py new file mode 100644 index 00000000..539cc30f --- /dev/null +++ b/re2o/base.py @@ -0,0 +1,48 @@ +# -*- 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. +# +# 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. + +# -*- coding: utf-8 -*- +""" +Regroupe les fonctions transversales utiles + +Et non corrélées/dépendantes des autres applications +""" + +import smtplib + +from django.utils.translation import ugettext_lazy as _ + +from re2o.settings import EMAIL_HOST + + +def smtp_check(local_part): + """Return True if the local_part is already taken + False if available""" + try: + srv = smtplib.SMTP(EMAIL_HOST) + srv.putcmd("vrfy", local_part) + reply_code = srv.getreply()[0] + srv.close() + if reply_code in [250, 252]: + return True, _("This domain is already taken") + except: + return True, _("Smtp unreachable") + return False, None diff --git a/users/models.py b/users/models.py index 63d0a875..2d271b3e 100755 --- a/users/models.py +++ b/users/models.py @@ -81,6 +81,7 @@ from re2o.settings import LDAP, GID_RANGES, UID_RANGES from re2o.login import hashNT from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin +from re2o.base import smtp_check from cotisations.models import Cotisation, Facture, Paiement, Vente from machines.models import Domain, Interface, Machine, regen @@ -1889,6 +1890,9 @@ class EMailAddress(RevMixin, AclMixin, models.Model): def clean(self, *args, **kwargs): self.local_part = self.local_part.lower() - if "@" in self.local_part: - raise ValidationError(_("The local part must not contain @.")) + if "@" in self.local_part or "+" in self.local_part: + raise ValidationError(_("The local part must not contain @ or +.")) + result, reason = smtp_check(self.local_part) + if result: + raise ValidationError(reason) super(EMailAddress, self).clean(*args, **kwargs) From 4317c39e0cd046ccf3f92c575f8987a05fbe6a6f Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 17:15:31 +0100 Subject: [PATCH 13/86] Archive users have no longer services access and ldap sync --- users/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/users/models.py b/users/models.py index 2d271b3e..f1577a24 100755 --- a/users/models.py +++ b/users/models.py @@ -577,7 +577,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, mac_refresh : synchronise les machines de l'user group_refresh : synchronise les group de l'user Si l'instance n'existe pas, on crée le ldapuser correspondant""" - if sys.version_info[0] >= 3: + if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\ + self.state != self.STATE_DISABLED: self.refresh_from_db() try: user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) From 6c6330dd4d0dd05e8c1651dc3409eb6acf25777f Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 18:58:45 +0100 Subject: [PATCH 14/86] =?UTF-8?q?S=C3=A9paration=20entre=20utils=20et=20ba?= =?UTF-8?q?se=20(dossier=20re2o)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/views.py | 5 +- logs/views.py | 7 +- machines/views.py | 2 + re2o/base.py | 219 +++++++++++++++++++++++++++++++++++++++++++ re2o/utils.py | 217 ------------------------------------------ search/forms.py | 2 +- search/views.py | 2 +- topologie/views.py | 5 +- users/forms.py | 3 +- users/views.py | 6 +- 10 files changed, 242 insertions(+), 226 deletions(-) diff --git a/cotisations/views.py b/cotisations/views.py index 4cd76f93..68118711 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -47,7 +47,10 @@ from users.models import User from re2o.settings import LOGO_PATH from re2o import settings from re2o.views import form -from re2o.utils import SortTable, re2o_paginator +from re2o.base import ( + SortTable, + re2o_paginator, +) from re2o.acl import ( can_create, can_edit, diff --git a/logs/views.py b/logs/views.py index 21e3c470..a54edd56 100644 --- a/logs/views.py +++ b/logs/views.py @@ -102,15 +102,18 @@ from re2o.utils import ( all_baned, all_has_access, all_adherent, + all_active_assigned_interfaces_count, + all_active_interfaces_count, +) +from re2o.base import ( re2o_paginator, + SortTable ) from re2o.acl import ( can_view_all, can_view_app, can_edit_history, ) -from re2o.utils import all_active_assigned_interfaces_count -from re2o.utils import all_active_interfaces_count, SortTable @login_required diff --git a/machines/views.py b/machines/views.py index 8d395749..59d4bd5a 100644 --- a/machines/views.py +++ b/machines/views.py @@ -55,6 +55,8 @@ from re2o.acl import ( from re2o.utils import ( all_active_assigned_interfaces, filter_active_interfaces, +) +from re2o.base import ( SortTable, re2o_paginator, ) diff --git a/re2o/base.py b/re2o/base.py index 539cc30f..023a16ff 100644 --- a/re2o/base.py +++ b/re2o/base.py @@ -29,10 +29,41 @@ Et non corrélées/dépendantes des autres applications import smtplib from django.utils.translation import ugettext_lazy as _ +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from re2o.settings import EMAIL_HOST +# Mapping of srtftime format for better understanding +# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior +datetime_mapping={ + '%a': '%a', + '%A': '%A', + '%w': '%w', + '%d': 'dd', + '%b': '%b', + '%B': '%B', + '%m': 'mm', + '%y': 'yy', + '%Y': 'yyyy', + '%H': 'HH', + '%I': 'HH(12h)', + '%p': 'AMPM', + '%M': 'MM', + '%S': 'SS', + '%f': 'µµ', + '%z': 'UTC(+/-HHMM)', + '%Z': 'UTC(TZ)', + '%j': '%j', + '%U': 'ww', + '%W': 'ww', + '%c': '%c', + '%x': '%x', + '%X': '%X', + '%%': '%%', +} + + def smtp_check(local_part): """Return True if the local_part is already taken False if available""" @@ -46,3 +77,191 @@ def smtp_check(local_part): except: return True, _("Smtp unreachable") return False, None + + +def convert_datetime_format(format): + i=0 + new_format = "" + while i < len(format): + if format[i] == '%': + char = format[i:i+2] + new_format += datetime_mapping.get(char, char) + i += 2 + else: + new_format += format[i] + i += 1 + return new_format + + +def get_input_formats_help_text(input_formats): + """Returns a help text about the possible input formats""" + if len(input_formats) > 1: + help_text_template="Format: {main} {more}" + else: + help_text_template="Format: {main}" + more_text_template="" + help_text = help_text_template.format( + main=convert_datetime_format(input_formats[0]), + more=more_text_template.format( + '\n'.join(map(convert_datetime_format, input_formats)) + ) + ) + return help_text + + +class SortTable: + """ Class gathering uselful stuff to sort the colums of a table, according + to the column and order requested. It's used with a dict of possible + values and associated model_fields """ + + # All the possible possible values + # The naming convention is based on the URL or the views function + # The syntax to describe the sort to apply is a dict where the keys are + # the url value and the values are a list of model field name to use to + # order the request. They are applied in the order they are given. + # A 'default' might be provided to specify what to do if the requested col + # doesn't match any keys. + + USERS_INDEX = { + 'user_name': ['name'], + 'user_surname': ['surname'], + 'user_pseudo': ['pseudo'], + 'user_room': ['room'], + 'default': ['state', 'pseudo'] + } + USERS_INDEX_BAN = { + 'ban_user': ['user__pseudo'], + 'ban_start': ['date_start'], + 'ban_end': ['date_end'], + 'default': ['-date_end'] + } + USERS_INDEX_WHITE = { + 'white_user': ['user__pseudo'], + 'white_start': ['date_start'], + 'white_end': ['date_end'], + 'default': ['-date_end'] + } + USERS_INDEX_SCHOOL = { + 'school_name': ['name'], + 'default': ['name'] + } + MACHINES_INDEX = { + 'machine_name': ['name'], + 'default': ['pk'] + } + COTISATIONS_INDEX = { + 'cotis_user': ['user__pseudo'], + 'cotis_paiement': ['paiement__moyen'], + 'cotis_date': ['date'], + 'cotis_id': ['id'], + 'default': ['-date'] + } + COTISATIONS_CUSTOM = { + 'invoice_date': ['date'], + 'invoice_id': ['id'], + 'invoice_recipient': ['recipient'], + 'invoice_address': ['address'], + 'invoice_payment': ['payment'], + 'default': ['-date'] + } + COTISATIONS_CONTROL = { + 'control_name': ['user__adherent__name'], + 'control_surname': ['user__surname'], + 'control_paiement': ['paiement'], + 'control_date': ['date'], + 'control_valid': ['valid'], + 'control_control': ['control'], + 'control_id': ['id'], + 'control_user-id': ['user__id'], + 'default': ['-date'] + } + TOPOLOGIE_INDEX = { + 'switch_dns': ['interface__domain__name'], + 'switch_ip': ['interface__ipv4__ipv4'], + 'switch_loc': ['switchbay__name'], + 'switch_ports': ['number'], + 'switch_stack': ['stack__name'], + 'default': ['switchbay', 'stack', 'stack_member_id'] + } + TOPOLOGIE_INDEX_PORT = { + 'port_port': ['port'], + 'port_room': ['room__name'], + 'port_interface': ['machine_interface__domain__name'], + 'port_related': ['related__switch__name'], + 'port_radius': ['radius'], + 'port_vlan': ['vlan_force__name'], + 'default': ['port'] + } + TOPOLOGIE_INDEX_ROOM = { + 'room_name': ['name'], + 'default': ['name'] + } + TOPOLOGIE_INDEX_BUILDING = { + 'building_name': ['name'], + 'default': ['name'] + } + TOPOLOGIE_INDEX_BORNE = { + 'ap_name': ['interface__domain__name'], + 'ap_ip': ['interface__ipv4__ipv4'], + 'ap_mac': ['interface__mac_address'], + 'default': ['interface__domain__name'] + } + TOPOLOGIE_INDEX_STACK = { + 'stack_name': ['name'], + 'stack_id': ['stack_id'], + 'default': ['stack_id'], + } + TOPOLOGIE_INDEX_MODEL_SWITCH = { + 'model-switch_name': ['reference'], + 'model-switch_contructor': ['constructor__name'], + 'default': ['reference'], + } + TOPOLOGIE_INDEX_SWITCH_BAY = { + 'switch-bay_name': ['name'], + 'switch-bay_building': ['building__name'], + 'default': ['name'], + } + TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = { + 'constructor-switch_name': ['name'], + 'default': ['name'], + } + LOGS_INDEX = { + 'sum_date': ['revision__date_created'], + 'default': ['-revision__date_created'], + } + LOGS_STATS_LOGS = { + 'logs_author': ['user__name'], + 'logs_date': ['date_created'], + 'default': ['-date_created'] + } + + @staticmethod + def sort(request, col, order, values): + """ Check if the given values are possible and add .order_by() and + a .reverse() as specified according to those values """ + fields = values.get(col, None) + if not fields: + fields = values.get('default', []) + request = request.order_by(*fields) + if values.get(col, None) and order == 'desc': + return request.reverse() + else: + return request + + +def re2o_paginator(request, query_set, pagination_number): + """Paginator script for list display in re2o. + :request: + :query_set: Query_set to paginate + :pagination_number: Number of entries to display""" + paginator = Paginator(query_set, pagination_number) + page = request.GET.get('page') + try: + results = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + results = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + results = paginator.page(paginator.num_pages) + return results diff --git a/re2o/utils.py b/re2o/utils.py index 6f7870f0..9836a98c 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -38,55 +38,11 @@ from __future__ import unicode_literals from django.utils import timezone from django.db.models import Q -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from cotisations.models import Cotisation, Facture, Vente from machines.models import Interface, Machine from users.models import Adherent, User, Ban, Whitelist -# Mapping of srtftime format for better understanding -# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior -datetime_mapping={ - '%a': '%a', - '%A': '%A', - '%w': '%w', - '%d': 'dd', - '%b': '%b', - '%B': '%B', - '%m': 'mm', - '%y': 'yy', - '%Y': 'yyyy', - '%H': 'HH', - '%I': 'HH(12h)', - '%p': 'AMPM', - '%M': 'MM', - '%S': 'SS', - '%f': 'µµ', - '%z': 'UTC(+/-HHMM)', - '%Z': 'UTC(TZ)', - '%j': '%j', - '%U': 'ww', - '%W': 'ww', - '%c': '%c', - '%x': '%x', - '%X': '%X', - '%%': '%%', -} - - -def convert_datetime_format(format): - i=0 - new_format = "" - while i < len(format): - if format[i] == '%': - char = format[i:i+2] - new_format += datetime_mapping.get(char, char) - i += 2 - else: - new_format += format[i] - i += 1 - return new_format - def all_adherent(search_time=None): """ Fonction renvoyant tous les users adherents. Optimisee pour n'est @@ -203,164 +159,6 @@ def all_active_assigned_interfaces_count(): return all_active_interfaces_count().filter(ipv4__isnull=False) -class SortTable: - """ Class gathering uselful stuff to sort the colums of a table, according - to the column and order requested. It's used with a dict of possible - values and associated model_fields """ - - # All the possible possible values - # The naming convention is based on the URL or the views function - # The syntax to describe the sort to apply is a dict where the keys are - # the url value and the values are a list of model field name to use to - # order the request. They are applied in the order they are given. - # A 'default' might be provided to specify what to do if the requested col - # doesn't match any keys. - - USERS_INDEX = { - 'user_name': ['name'], - 'user_surname': ['surname'], - 'user_pseudo': ['pseudo'], - 'user_room': ['room'], - 'default': ['state', 'pseudo'] - } - USERS_INDEX_BAN = { - 'ban_user': ['user__pseudo'], - 'ban_start': ['date_start'], - 'ban_end': ['date_end'], - 'default': ['-date_end'] - } - USERS_INDEX_WHITE = { - 'white_user': ['user__pseudo'], - 'white_start': ['date_start'], - 'white_end': ['date_end'], - 'default': ['-date_end'] - } - USERS_INDEX_SCHOOL = { - 'school_name': ['name'], - 'default': ['name'] - } - MACHINES_INDEX = { - 'machine_name': ['name'], - 'default': ['pk'] - } - COTISATIONS_INDEX = { - 'cotis_user': ['user__pseudo'], - 'cotis_paiement': ['paiement__moyen'], - 'cotis_date': ['date'], - 'cotis_id': ['id'], - 'default': ['-date'] - } - COTISATIONS_CUSTOM = { - 'invoice_date': ['date'], - 'invoice_id': ['id'], - 'invoice_recipient': ['recipient'], - 'invoice_address': ['address'], - 'invoice_payment': ['payment'], - 'default': ['-date'] - } - COTISATIONS_CONTROL = { - 'control_name': ['user__adherent__name'], - 'control_surname': ['user__surname'], - 'control_paiement': ['paiement'], - 'control_date': ['date'], - 'control_valid': ['valid'], - 'control_control': ['control'], - 'control_id': ['id'], - 'control_user-id': ['user__id'], - 'default': ['-date'] - } - TOPOLOGIE_INDEX = { - 'switch_dns': ['interface__domain__name'], - 'switch_ip': ['interface__ipv4__ipv4'], - 'switch_loc': ['switchbay__name'], - 'switch_ports': ['number'], - 'switch_stack': ['stack__name'], - 'default': ['switchbay', 'stack', 'stack_member_id'] - } - TOPOLOGIE_INDEX_PORT = { - 'port_port': ['port'], - 'port_room': ['room__name'], - 'port_interface': ['machine_interface__domain__name'], - 'port_related': ['related__switch__name'], - 'port_radius': ['radius'], - 'port_vlan': ['vlan_force__name'], - 'default': ['port'] - } - TOPOLOGIE_INDEX_ROOM = { - 'room_name': ['name'], - 'default': ['name'] - } - TOPOLOGIE_INDEX_BUILDING = { - 'building_name': ['name'], - 'default': ['name'] - } - TOPOLOGIE_INDEX_BORNE = { - 'ap_name': ['interface__domain__name'], - 'ap_ip': ['interface__ipv4__ipv4'], - 'ap_mac': ['interface__mac_address'], - 'default': ['interface__domain__name'] - } - TOPOLOGIE_INDEX_STACK = { - 'stack_name': ['name'], - 'stack_id': ['stack_id'], - 'default': ['stack_id'], - } - TOPOLOGIE_INDEX_MODEL_SWITCH = { - 'model-switch_name': ['reference'], - 'model-switch_contructor': ['constructor__name'], - 'default': ['reference'], - } - TOPOLOGIE_INDEX_SWITCH_BAY = { - 'switch-bay_name': ['name'], - 'switch-bay_building': ['building__name'], - 'default': ['name'], - } - TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = { - 'constructor-switch_name': ['name'], - 'default': ['name'], - } - LOGS_INDEX = { - 'sum_date': ['revision__date_created'], - 'default': ['-revision__date_created'], - } - LOGS_STATS_LOGS = { - 'logs_author': ['user__name'], - 'logs_date': ['date_created'], - 'default': ['-date_created'] - } - - @staticmethod - def sort(request, col, order, values): - """ Check if the given values are possible and add .order_by() and - a .reverse() as specified according to those values """ - fields = values.get(col, None) - if not fields: - fields = values.get('default', []) - request = request.order_by(*fields) - if values.get(col, None) and order == 'desc': - return request.reverse() - else: - return request - - -def re2o_paginator(request, query_set, pagination_number): - """Paginator script for list display in re2o. - :request: - :query_set: Query_set to paginate - :pagination_number: Number of entries to display""" - paginator = Paginator(query_set, pagination_number) - page = request.GET.get('page') - try: - results = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - results = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - results = paginator.page(paginator.num_pages) - return results - - def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ try: @@ -370,18 +168,3 @@ def remove_user_room(room): user.room = None user.save() - -def get_input_formats_help_text(input_formats): - """Returns a help text about the possible input formats""" - if len(input_formats) > 1: - help_text_template="Format: {main} {more}" - else: - help_text_template="Format: {main}" - more_text_template="" - help_text = help_text_template.format( - main=convert_datetime_format(input_formats[0]), - more=more_text_template.format( - '\n'.join(map(convert_datetime_format, input_formats)) - ) - ) - return help_text diff --git a/search/forms.py b/search/forms.py index 5c98415f..5fa5fca8 100644 --- a/search/forms.py +++ b/search/forms.py @@ -27,7 +27,7 @@ from __future__ import unicode_literals from django import forms from django.forms import Form from django.utils.translation import ugettext_lazy as _ -from re2o.utils import get_input_formats_help_text +from re2o.base import get_input_formats_help_text CHOICES_USER = ( ('0', _("Active")), diff --git a/search/views.py b/search/views.py index a92b0105..eb0027ec 100644 --- a/search/views.py +++ b/search/views.py @@ -46,7 +46,7 @@ from search.forms import ( CHOICES_AFF, initial_choices ) -from re2o.utils import SortTable +from re2o.base import SortTable from re2o.acl import can_view_all diff --git a/topologie/views.py b/topologie/views.py index 0bd0f6c2..a4db2dc6 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -48,7 +48,10 @@ from django.utils.translation import ugettext as _ import tempfile from users.views import form -from re2o.utils import re2o_paginator, SortTable +from re2o.base import ( + re2o_paginator, + SortTable, +) from re2o.acl import ( can_create, can_edit, diff --git a/users/forms.py b/users/forms.py index 7695e6fc..b96e3ad3 100644 --- a/users/forms.py +++ b/users/forms.py @@ -45,7 +45,8 @@ from django.utils.safestring import mark_safe from machines.models import Interface, Machine, Nas from topologie.models import Port from preferences.models import OptionalUser -from re2o.utils import remove_user_room, get_input_formats_help_text +from re2o.utils import remove_user_room +from re2o.base import get_input_formats_help_text from re2o.mixins import FormRevMixin from re2o.field_permissions import FieldPermissionFormMixin diff --git a/users/views.py b/users/views.py index a94a7927..060610f8 100644 --- a/users/views.py +++ b/users/views.py @@ -57,8 +57,10 @@ from preferences.models import OptionalUser, GeneralOption, AssoOption from re2o.views import form from re2o.utils import ( all_has_access, - SortTable, - re2o_paginator +) +from re2o.base import ( + re2o_paginator, + SortTable ) from re2o.acl import ( can_create, From 108f454c51b6a7e38656d553faac5bceb1aae4f2 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 15 Nov 2018 19:55:00 +0100 Subject: [PATCH 15/86] Boostrapform pour les alias et les ouvertures de ports --- machines/templates/machines/machine.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 1a8ce8b1..1e2e3700 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -95,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if interfaceform %}

{% trans "Interface" %}

{% if i_mbf_param %} - {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %} {% else %} - {% massive_bootstrap_form interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %} {% endif %} {% endif %} {% if domainform %} @@ -146,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if aliasform %}

{% trans "Alias" %}

- {% bootstrap_form aliasform %} + {% massive_bootstrap_form aliasform 'extension' %} {% endif %} {% if serviceform %}

{% trans "Service" %}

From 93e2424da758cde2c09b6821a0f29b0b8ae61393 Mon Sep 17 00:00:00 2001 From: detraz Date: Sat, 17 Nov 2018 19:51:37 +0100 Subject: [PATCH 16/86] Export users basic et critical --- api/serializers.py | 5 ++--- api/urls.py | 2 ++ api/views.py | 14 +++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index f2ff9954..e7b23f32 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -632,9 +632,8 @@ class AdherentSerializer(NamespacedHMSerializer): } -class HomeCreationSerializer(NamespacedHMSerializer): - """Serialize 'users.models.User' minimal infos to create home - """ +class BasicUserSerializer(NamespacedHMSerializer): + """Serialize 'users.models.User' minimal infos""" uid = serializers.IntegerField(source='uid_number') gid = serializers.IntegerField(source='gid_number') diff --git a/api/urls.py b/api/urls.py index e361d732..723ca78c 100644 --- a/api/urls.py +++ b/api/urls.py @@ -88,6 +88,8 @@ router.register(r'topologie/portprofile', views.PortProfileViewSet) # USERS router.register_viewset(r'users/user', views.UserViewSet, base_name='user') router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation') +router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser') +router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser') router.register_viewset(r'users/club', views.ClubViewSet) router.register_viewset(r'users/adherent', views.AdherentViewSet) router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) diff --git a/api/views.py b/api/views.py index a2a376e9..21f7b438 100644 --- a/api/views.py +++ b/api/views.py @@ -445,7 +445,19 @@ class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet): """Exposes infos of `users.models.Users` objects to create homes. """ queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE)) - serializer_class = serializers.HomeCreationSerializer + serializer_class = serializers.BasicUserSerializer + + +class NormalUserViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes infos of `users.models.Users`without specific rights objects.""" + queryset = users.User.objects.exclude(groups__listright__critical=True).distinct() + serializer_class = serializers.BasicUserSerializer + + +class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes infos of `users.models.Users`without specific rights objects.""" + queryset = users.User.objects.filter(groups__listright__critical=True).distinct() + serializer_class = serializers.BasicUserSerializer class ClubViewSet(viewsets.ReadOnlyModelViewSet): From 817e9659375f475f685ef86b2b8ccf1f1249ea38 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sat, 1 Dec 2018 11:59:17 +0000 Subject: [PATCH 17/86] =?UTF-8?q?Navigation=20dans=20les=20pr=C3=A9f=C3=A9?= =?UTF-8?q?rences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/preferences/sidebar.html | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/preferences/templates/preferences/sidebar.html b/preferences/templates/preferences/sidebar.html index 4f69298b..41a11df0 100644 --- a/preferences/templates/preferences/sidebar.html +++ b/preferences/templates/preferences/sidebar.html @@ -22,7 +22,51 @@ 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 %} {% block sidebar %} + {% if request.user.is_authenticated %} + + + {% trans "Générale" %} + + + + {% trans "Association" %} + + + + {% trans "Utilisateurs" %} + + + + {% trans "Machines" %} + + + + {% trans "Topologie" %} + + + + {% trans "Switchs" %} + + + + {% trans "Mail" %} + + + + {% trans "Rappels" %} + + + + {% trans "Services" %} + + + + {% trans "Adresses de contact" %} + + {% endif %} {% endblock %} From 58006000fee897e57063342efe8c88374466787b Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sat, 1 Dec 2018 14:42:38 +0000 Subject: [PATCH 18/86] =?UTF-8?q?Premier=20changement=20en=20accord=C3=A9o?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../preferences/display_preferences.html | 213 +++++++++++------- 1 file changed, 129 insertions(+), 84 deletions(-) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 96d3fe16..a5ee9f8d 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -31,50 +31,53 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} -

{% trans "User preferences" %}

- +
+ +
+ + +
+ {% trans "Edit" %} - -

-

-
{% trans "General preferences" %}
+ + - - - - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + +
{% trans "Creation of members by everyone" %}{{ useroptions.all_can_create_adherent|tick }}{% trans "Creation of clubs by everyone" %}{{ useroptions.all_can_create_club|tick }}{% trans "Website name" %}{{ generaloptions.site_name }}{% trans "Email address for automatic emailing" %}{{ generaloptions.email_from }}
{% trans "Self registration" %}{{ useroptions.self_adhesion|tick }}{% trans "Delete not yet active users after" %}{{ useroptions.delete_notyetactive }} days{% trans "Number of results displayed when searching" %}{{ generaloptions.search_display_page }}{% trans "Number of items per page (standard size)" %}{{ generaloptions.pagination_number }}
{% trans "Number of items per page (large size)" %}{{ generaloptions.pagination_large_number }}{% trans "Time before expiration of the reset password link (in hours)" %}{{ generaloptions.req_expire_hrs }}
{% trans "General message displayed on the website" %}{{ generaloptions.general_message }}{% trans "Main site url" %}{{ generaloptions.main_site_url }}
{% trans "Summary of the General Terms of Use" %}{{ generaloptions.GTU_sum_up }}{% trans "General Terms of Use" %}{{ generaloptions.GTU }}
- -
{% trans "Users general permissions" %}
- - - - - - - - - - - - - - - - - -
{% trans "Default shell for users" %}{{ useroptions.shell_default }}{% trans "Users can edit their shell" %}{{ useroptions.self_change_shell|tick }}
{% trans "Users can edit their room" %}{{ useroptions.self_change_room|tick }}{% trans "Telephone number required" %}{{ useroptions.is_tel_mandatory|tick }}
{% trans "GPG fingerprint field" %}{{ useroptions.gpg_fingerprint|tick }}
- -
{% trans "Email accounts preferences" %}
@@ -87,7 +90,66 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Local email accounts enabled" %}{{ useroptions.max_email_address }}
-

{% trans "Machines preferences" %}

+
+
+ +
+ + +
+ + + {% trans "Edit" %} + + + + + + + + + + + + + + +
{% trans "Creation of members by everyone" %}{{ useroptions.all_can_create_adherent|tick }}{% trans "Creation of clubs by everyone" %}{{ useroptions.all_can_create_club|tick }}
{% trans "Self registration" %}{{ useroptions.self_adhesion|tick }}{% trans "Delete not yet active users after" %}{{ useroptions.delete_notyetactive }} days
+ +

{% trans "Users general permissions" %}

+ + + + + + + + + + + + + + + + + +
{% trans "Default shell for users" %}{{ useroptions.shell_default }}{% trans "Users can edit their shell" %}{{ useroptions.self_change_shell|tick }}
{% trans "Users can edit their room" %}{{ useroptions.self_change_room|tick }}{% trans "Telephone number required" %}{{ useroptions.is_tel_mandatory|tick }}
{% trans "GPG fingerprint field" %}{{ useroptions.gpg_fingerprint|tick }}
+
+
+
+ + + + -

Configuration des switches

+
+ +
@@ -192,49 +272,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% if switchmanagementcred_list %} OK{% else %}Manquant{% endif %} {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %} + + -

{% trans "General preferences" %}

- - - {% trans "Edit" %} - -

-

-
Web management, activé si provision automatique
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Website name" %}{{ generaloptions.site_name }}{% trans "Email address for automatic emailing" %}{{ generaloptions.email_from }}
{% trans "Number of results displayed when searching" %}{{ generaloptions.search_display_page }}{% trans "Number of items per page (standard size)" %}{{ generaloptions.pagination_number }}
{% trans "Number of items per page (large size)" %}{{ generaloptions.pagination_large_number }}{% trans "Time before expiration of the reset password link (in hours)" %}{{ generaloptions.req_expire_hrs }}
{% trans "General message displayed on the website" %}{{ generaloptions.general_message }}{% trans "Main site url" %}{{ generaloptions.main_site_url }}
{% trans "Summary of the General Terms of Use" %}{{ generaloptions.GTU_sum_up }}{% trans "General Terms of Use" %}{{ generaloptions.GTU }} -
-

{% trans "Information about the organisation" %}

+

{% trans "Information about the organisation" %}

{% trans "Edit" %} @@ -269,7 +312,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ assooptions.description|safe }} -

{% trans "Custom email message" %}

+

{% trans "Custom email message" %}

{% trans "Edit" %} @@ -286,13 +329,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ mailmessageoptions.welcome_mail_en|safe }} -

Options pour le mail de fin d'adhésion

+

Options pour le mail de fin d'adhésion

{% can_create preferences.Reminder%}
Ajouter un rappel {% acl_end %} {% include "preferences/aff_reminder.html" with reminder_list=reminder_list %} -

{% trans "List of services and homepage preferences" %}

+

{% trans "List of services and homepage preferences" %}

{% can_create preferences.Service%} {% trans " Add a service" %} {% acl_end %} @@ -301,7 +344,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edit" %} -

{% trans "List of contact email addresses" %}

+

{% trans "List of contact email addresses" %}

{% can_create preferences.MailContact %} {% trans "Add an address" %} {% acl_end %} @@ -321,5 +364,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ homeoptions.facebook_url }} + +
{% endblock %} From 42bbdb67c7e5296e587bdf5c669b50b03a18223d Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sat, 1 Dec 2018 16:11:17 +0000 Subject: [PATCH 19/86] =?UTF-8?q?Accord=C3=A9on=20complet=20et=20suppressi?= =?UTF-8?q?on=20de=20la=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../preferences/display_preferences.html | 43 ++++++++++++++++--- .../templates/preferences/sidebar.html | 42 ------------------ 2 files changed, 38 insertions(+), 47 deletions(-) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index a5ee9f8d..05bbc1bd 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -276,8 +276,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- -

{% trans "Information about the organisation" %}

+ + + + +
+ +
{% can_create preferences.Reminder%} Ajouter un rappel {% acl_end %} @@ -344,7 +367,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edit" %} -

{% trans "List of contact email addresses" %}

+
+
+ +
+ +
{% can_create preferences.MailContact %} {% trans "Add an address" %} {% acl_end %} @@ -364,6 +396,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ homeoptions.facebook_url }} +
{% endblock %} diff --git a/preferences/templates/preferences/sidebar.html b/preferences/templates/preferences/sidebar.html index 41a11df0..98b597ea 100644 --- a/preferences/templates/preferences/sidebar.html +++ b/preferences/templates/preferences/sidebar.html @@ -27,46 +27,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block sidebar %} - {% if request.user.is_authenticated %} - - - {% trans "Générale" %} - - - - {% trans "Association" %} - - - - {% trans "Utilisateurs" %} - - - - {% trans "Machines" %} - - - - {% trans "Topologie" %} - - - - {% trans "Switchs" %} - - - - {% trans "Mail" %} - - - - {% trans "Rappels" %} - - - - {% trans "Services" %} - - - - {% trans "Adresses de contact" %} - - {% endif %} {% endblock %} From bb57aa3bd0b45cc9fb7e0c4b6120bc9e919f306b Mon Sep 17 00:00:00 2001 From: Grizzly Date: Mon, 3 Dec 2018 20:32:18 +0000 Subject: [PATCH 20/86] Icones et heading cliquables --- .../preferences/display_preferences.html | 85 ++++++++++++------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 05bbc1bd..ecdacf84 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -34,17 +34,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- + +
-
- - - {% trans "Edit" %} - + + + {% trans "Edit" %} + @@ -94,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+ @@ -144,9 +145,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+
@@ -178,9 +179,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+
@@ -219,9 +220,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+
@@ -277,9 +278,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+
@@ -321,9 +322,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+
@@ -347,9 +348,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
+
@@ -357,23 +358,29 @@ with this program; if not, write to the Free Software Foundation, Inc., Ajouter un rappel {% acl_end %} {% include "preferences/aff_reminder.html" with reminder_list=reminder_list %} +
+
-

{% trans "List of services and homepage preferences" %}

+ +
+ +
{% can_create preferences.Service%} {% trans " Add a service" %} {% acl_end %} {% include "preferences/aff_service.html" with service_list=service_list %} - - - {% trans "Edit" %} - +
-
+
@@ -382,8 +389,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} {% trans "Delete one or several addresses" %} {% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %} -

-

+
+
+ +
@@ -397,7 +416,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Twitter account URL" %}
+
-
{% endblock %} From dd2421411fb8965ea1c3fb04eb7d59c44a65ad6f Mon Sep 17 00:00:00 2001 From: Grizzly Date: Mon, 3 Dec 2018 21:31:50 +0000 Subject: [PATCH 21/86] Espacement des boutons --- .../preferences/display_preferences.html | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index ecdacf84..f52f721a 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -42,10 +42,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - + {% trans "Edit" %} +

@@ -102,10 +102,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
@@ -150,13 +154,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Machines preferences" %} +
{% trans "Creation of members by everyone" %}
@@ -185,12 +191,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Password per machine" %}
@@ -284,12 +291,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "General policy for VLAN setting" %}
@@ -327,13 +335,15 @@ with this program; if not, write to the Free Software Foundation, Inc., Message pour les mails -
+
{% trans "Name" %}
@@ -355,7 +365,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Reminder%} +

Ajouter un rappel +

{% acl_end %} {% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
@@ -370,7 +382,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Service%} +

{% trans " Add a service" %} +

{% acl_end %} {% include "preferences/aff_service.html" with service_list=service_list %} @@ -384,10 +398,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
+

{% can_create preferences.MailContact %} {% trans "Add an address" %} {% acl_end %} {% trans "Delete one or several addresses" %} +

{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
@@ -399,10 +415,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Welcome email (in French)" %}
From 3f63cb4b8f441ec3d3481c268f95d835d35a7cb9 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Wed, 5 Dec 2018 17:32:24 +0000 Subject: [PATCH 22/86] Espacement des boutons --- .../preferences/display_preferences.html | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index f52f721a..3fde911d 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -95,13 +95,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Twitter account URL" %}
@@ -190,9 +188,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Topology preferences" %} -
- +
+ @@ -290,9 +294,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Information about the organisation" %} -
+ -
+ -
+
{% can_create preferences.Reminder%} -

+ Ajouter un rappel

{% acl_end %} @@ -380,9 +382,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "List of services and homepage preferences" %}
-
+
{% can_create preferences.Service%} -

+ {% trans " Add a service" %}

{% acl_end %} @@ -397,8 +399,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "List of contact email addresses" %}
-
-

+
+ {% can_create preferences.MailContact %} {% trans "Add an address" %} {% acl_end %} @@ -414,9 +416,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Réseaux sociaux
-
+
-

{% trans "Edit" %} From c984f2acc2b45ef74e5f1c06e1013c2d4f11d243 Mon Sep 17 00:00:00 2001 From: edpibu Date: Sat, 8 Dec 2018 21:42:49 +0100 Subject: [PATCH 23/86] =?UTF-8?q?Fixed=20tables=20scrolling=20when=20openi?= =?UTF-8?q?ng=20menus=20=09modifi=C3=A9=C2=A0:=20=20=20=20=20=20=20=20=20s?= =?UTF-8?q?tatic/css/base.css?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/base.css b/static/css/base.css index 736935b3..dcb88db8 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -102,7 +102,7 @@ a > i.fa { } .table-responsive { - overflow-y: visible; + overflow: visible; } /* Make modal wider on wide screens */ From d17e71c53c98c3ee6eec236f9ef7419dbefbfc4b Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sat, 8 Dec 2018 21:18:42 +0000 Subject: [PATCH 24/86] machines prise en compte pour l'ajout automatique --- users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/models.py b/users/models.py index f1577a24..8703c978 100755 --- a/users/models.py +++ b/users/models.py @@ -695,7 +695,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, def autoregister_machine(self, mac_address, nas_type): """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue sur le compte de l'user""" - all_interfaces = self.user_interfaces(active=False) + all_interfaces = self.user_interfaces() if all_interfaces.count() > OptionalMachine.get_cached_value( 'max_lambdauser_interfaces' ): From ae5ef6a3c5643e79eeebb6e65b1558731d82ed94 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sat, 8 Dec 2018 22:20:31 +0000 Subject: [PATCH 25/86] =?UTF-8?q?Utilise=20l'acl=20pour=20la=20v=C3=A9rifi?= =?UTF-8?q?cation=20de=20la=20possibilit=C3=A9=20de=20cr=C3=A9ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/users/models.py b/users/models.py index 8703c978..7d7b43d4 100755 --- a/users/models.py +++ b/users/models.py @@ -696,9 +696,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue sur le compte de l'user""" all_interfaces = self.user_interfaces() - if all_interfaces.count() > OptionalMachine.get_cached_value( - 'max_lambdauser_interfaces' - ): + if Machine.can_create(self): return False, _("Maximum number of registered machines reached.") if not nas_type: return False, _("Re2o doesn't know wich machine type to assign.") From c5b3f509551003762f76a2db65308e909d226594 Mon Sep 17 00:00:00 2001 From: Grizzly Date: Sat, 8 Dec 2018 22:52:08 +0000 Subject: [PATCH 26/86] Suppression de la ligne inutile --- users/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/users/models.py b/users/models.py index 7d7b43d4..a198cfb5 100755 --- a/users/models.py +++ b/users/models.py @@ -695,7 +695,6 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, def autoregister_machine(self, mac_address, nas_type): """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue sur le compte de l'user""" - all_interfaces = self.user_interfaces() if Machine.can_create(self): return False, _("Maximum number of registered machines reached.") if not nas_type: From 7a26aadaa9c231a9b0b9381730c4e2c22c2ddc56 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 4 Dec 2018 14:29:47 +0100 Subject: [PATCH 27/86] fix migrations. --- preferences/migrations/0055_generaloption_main_site_url.py | 2 +- users/forms.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/preferences/migrations/0055_generaloption_main_site_url.py b/preferences/migrations/0055_generaloption_main_site_url.py index 71ea9852..655c0b07 100644 --- a/preferences/migrations/0055_generaloption_main_site_url.py +++ b/preferences/migrations/0055_generaloption_main_site_url.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('preferences', '0054_merge_20181025_1258'), + ('preferences', '0053_optionaluser_self_change_room'), ] operations = [ diff --git a/users/forms.py b/users/forms.py index b96e3ad3..70d8798b 100644 --- a/users/forms.py +++ b/users/forms.py @@ -384,8 +384,8 @@ class AdherentCreationForm(AdherentForm): # Checkbox for GTU gtu_check = forms.BooleanField(required=True) - gtu_check.label = mark_safe("{} {}{}".format( - _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _("."))) + #gtu_check.label = mark_safe("{} {}{}".format( + # _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _("."))) def __init__(self, *args, **kwargs): super(AdherentCreationForm, self).__init__(*args, **kwargs) From a773f5d717662754d167b6028f1abc2206d397ed Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 2 Sep 2018 16:53:21 +0200 Subject: [PATCH 28/86] Pre-subscription VLAN --- freeradius_utils/auth.py | 17 ++-- .../migrations/0052_auto_20181013_1629.py | 91 ++++++++++++++++++ preferences/models.py | 85 +++++++++++++++- .../preferences/aff_radiusoptions.html | 96 +++++++++++++++++++ .../preferences/display_preferences.html | 55 ++++++----- preferences/urls.py | 5 + preferences/views.py | 7 +- 7 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 preferences/migrations/0052_auto_20181013_1629.py create mode 100644 preferences/templates/preferences/aff_radiusoptions.html diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index ab10d457..9bd56015 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -63,6 +63,7 @@ from preferences.models import OptionalTopologie options, created = OptionalTopologie.objects.get_or_create() VLAN_NOK = options.vlan_decision_nok.vlan_id VLAN_OK = options.vlan_decision_ok.vlan_id +VLAN_NON_MEMBER = options.vlan_non_member.vlan_id RADIUS_POLICY = options.radius_general_policy #: Serveur radius de test (pas la prod) @@ -338,14 +339,15 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, - mode strict : - pas de chambre associée : VLAN_NOK - pas d'utilisateur dans la chambre : VLAN_NOK - - cotisation non à jour : VLAN_NOK + - cotisation non à jour : VLAN_NON_MEMBER - sinon passe à common (ci-dessous) - mode common : - interface connue (macaddress): - - utilisateur proprio non cotisant ou banni : VLAN_NOK + - utilisateur proprio non cotisant : VLAN_NON_MEMBER + - utilisateur proprio banni : VLAN_NOK - user à jour : VLAN_OK - interface inconnue : - - register mac désactivé : VLAN_NOK + - register mac désactivé : VLAN_NON_MEMBER - register mac activé -> redirection vers webauth """ # Get port from switch and port number @@ -414,8 +416,10 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, if not room_user: return (sw_name, room, u'Chambre non cotisante -> Web redirect', None, False) for user in room_user: - if not user.has_access(): + if user.is_ban() or user.state != User.STATE_ACTIVE: return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False) + elif not (user.is_connected() or user.is_whitelisted()): + return (sw_name, room, u'Utilisateur non cotisant', VLAN_NON_MEMBER) # else: user OK, on passe à la verif MAC # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd @@ -431,7 +435,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, # 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, True) + return (sw_name, "", u'Machine inconnue', VLAN_NON_MEMBER) # On rejette pour basculer sur du webauth else: return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False) @@ -445,8 +449,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, return (sw_name, room, u'Machine non active / adherent non cotisant', - VLAN_NOK, - True) + VLAN_NON_MEMBER) ## Si on choisi de placer les machines sur le vlan correspondant à leur type : if RADIUS_POLICY == 'MACHINE': DECISION_VLAN = interface.type.ip_type.vlan.vlan_id diff --git a/preferences/migrations/0052_auto_20181013_1629.py b/preferences/migrations/0052_auto_20181013_1629.py new file mode 100644 index 00000000..70498536 --- /dev/null +++ b/preferences/migrations/0052_auto_20181013_1629.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +def create_radius_policy(apps, schema_editor): + OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie') + RadiusOption = apps.get_model('preferences', 'RadiusOption') + RadiusPolicy = apps.get_model('preferences', 'RadiusPolicy') + + option,_ = OptionalTopologie.objects.get_or_create() + + radius_option = RadiusOption() + radius_option.radius_general_policy = option.radius_general_policy + radius_option.unknown_machine = RadiusPolicy.objects.create() + radius_option.unknown_port = RadiusPolicy.objects.create() + radius_option.unknown_room = RadiusPolicy.objects.create() + radius_option.non_member = RadiusPolicy.objects.create() + radius_option.banned = RadiusPolicy.objects.create() + radius_option.vlan_decision_ok = option.vlan_decision_ok + + radius_option.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0051_auto_20180919_2225'), + ] + + operations = [ + migrations.CreateModel( + name='RadiusOption', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('radius_general_policy', models.CharField(choices=[('MACHINE', "On the IP range's VLAN of the machine"), ('DEFINED', "Preset in 'VLAN for machines accepted by RADIUS'")], default='DEFINED', max_length=32)), + ], + options={ + 'verbose_name': 'radius policies', + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='RadiusPolicy', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('policy', models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32)), + ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.Vlan')), + ], + options={ + 'verbose_name': 'radius policy', + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='radiusoption', + name='non_member', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='non_member_option', to='preferences.RadiusPolicy', verbose_name='Policy non member users.'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_machine', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_option', to='preferences.RadiusPolicy', verbose_name='Policy for unknown machines'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_port', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_option', to='preferences.RadiusPolicy', verbose_name='Policy for unknown machines'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_room', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_option', to='preferences.RadiusPolicy', verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'), + ), + migrations.AddField( + model_name='radiusoption', + name='banned', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='banned_option', to='preferences.RadiusPolicy', verbose_name='Policy for banned users.'), + ), + migrations.AddField( + model_name='radiusoption', + name='vlan_decision_ok', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'), + ), + migrations.RunPython(create_radius_policy), + ] diff --git a/preferences/models.py b/preferences/models.py index 43ff7580..f3f5527e 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -218,6 +218,7 @@ class OptionalTopologie(AclMixin, PreferencesModel): blank=True, null=True ) + switchs_web_management = models.BooleanField( default=False, help_text="Web management, activé si provision automatique" @@ -309,7 +310,6 @@ class OptionalTopologie(AclMixin, PreferencesModel): """Return true if all settings are ok : switchs on automatic provision, ip_type""" return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds)) - class Meta: permissions = ( ("view_optionaltopologie", _("Can view the topology options")), @@ -588,3 +588,86 @@ class MailMessageOption(AclMixin, models.Model): ) verbose_name = _("email message options") + +class RadiusPolicy(AclMixin, models.Model): + class Meta: + verbose_name = _('radius policy') + + REJECT = 'REJECT' + SET_VLAN = 'SET_VLAN' + CHOICE_POLICY = ( + (REJECT, _('Reject the machine')), + (SET_VLAN, _('Place the machine on the VLAN')) + ) + + policy = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT + ) + + vlan = models.ForeignKey( + 'machines.Vlan', + on_delete=models.PROTECT, + blank=True, + null=True + ) + + +class RadiusOption(AclMixin, models.Model): + class Meta: + verbose_name = _("radius policies") + + MACHINE = 'MACHINE' + DEFINED = 'DEFINED' + CHOICE_RADIUS = ( + (MACHINE, _("On the IP range's VLAN of the machine")), + (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")), + ) + radius_general_policy = models.CharField( + max_length=32, + choices=CHOICE_RADIUS, + default='DEFINED' + ) + unknown_machine = models.ForeignKey( + RadiusPolicy, + on_delete=models.PROTECT, + verbose_name=_("Policy for unknown machines"), + related_name='unknown_machine_option', + ) + unknown_port = models.ForeignKey( + RadiusPolicy, + on_delete=models.PROTECT, + verbose_name=_("Policy for unknown machines"), + related_name='unknown_port_option', + ) + unknown_room = models.ForeignKey( + RadiusPolicy, + on_delete=models.PROTECT, + verbose_name=_( + "Policy for machine connecting from " + "unregistered room (relevant on ports with STRICT " + "radius mode)" + ), + related_name='unknown_room_option', + ) + non_member = models.ForeignKey( + RadiusPolicy, + on_delete=models.PROTECT, + verbose_name=_("Policy non member users."), + related_name='non_member_option', + ) + banned = models.ForeignKey( + RadiusPolicy, + on_delete=models.PROTECT, + verbose_name=_("Policy for banned users."), + related_name='banned_option' + ) + vlan_decision_ok = models.OneToOneField( + 'machines.Vlan', + on_delete=models.PROTECT, + related_name='vlan_ok_option', + blank=True, + null=True + ) + diff --git a/preferences/templates/preferences/aff_radiusoptions.html b/preferences/templates/preferences/aff_radiusoptions.html new file mode 100644 index 00000000..b434319d --- /dev/null +++ b/preferences/templates/preferences/aff_radiusoptions.html @@ -0,0 +1,96 @@ +{% 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 Hugo Levy-Falk + +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 i18n %} +{% load acl %} +{% load logs_extra %} + +
Web management, activé si provision automatique {{ topologieoptions.switchs_web_management }} Rest management, activé si provision auto
+ + + + + + + + + +
{% trans "General policy for VLAN setting" %}{{ radiusoptions.radius_general_policy }}{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}
{% trans "VLAN for machines accepted by RADIUS" %}{{ radiusoptions.vlan_decision_ok }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Situation" %}{% trans "Behavior" %}
{% trans "Unknown machine" %} + {% if radiusoptions.unknown_machine.policy == 'REJECT' %} + {% trans "Reject" %} + {% else %} + Vlan {{ radiusoptions.unknown_machine.vlan }} + {% endif %} +
{% trans "Unknown port" %} + {% if radiusoptions.unknown_port.policy == 'REJECT' %} + {% trans "Reject" %} + {% else %} + Vlan {{ radiusoptions.unknown_port.vlan }} + {% endif %} +
{% trans "Unknown room" %} + {% if radiusoptions.unknown_room.policy == 'REJECT' %} + {% trans "Reject" %} + {% else %} + Vlan {{ radiusoptions.unknown_room.vlan }} + {% endif %} +
{% trans "Non member" %} + {% if radiusoptions.non_member.policy == 'REJECT' %} + {% trans "Reject" %} + {% else %} + Vlan {{ radiusoptions.non_member.vlan }} + {% endif %} +
{% trans "Banned user" %} + {% if radiusoptions.unknown_port.policy == 'REJECT' %} + {% trans "Reject" %} + {% else %} + Vlan {{ radiusoptions.banned.vlan }} + {% endif %} +
+ diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 3fde911d..21d25b43 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -32,14 +32,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}
- +

@@ -159,7 +159,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edit" %} - +

@@ -208,10 +208,10 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - + + + +
{% trans "VLAN for machines rejected by RADIUS" %} {{ topologieoptions.vlan_decision_nok }}
Placement sur ce vlan par default en cas de rejet{{ topologieoptions.vlan_decision_nok }}
{% trans "VLAN for non members machines" %}{{ topologieoptions.vlan_non_member }}

Clef radius

@@ -219,7 +219,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Ajouter une clef radius {% acl_end %} {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %} - +
@@ -236,7 +236,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edit" %}

- + @@ -254,11 +254,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - + - + @@ -287,6 +287,18 @@ with this program; if not, write to the Free Software Foundation, Inc., +
+
+

{% trans "Radius preferences" %}

+
+
+ + + {% trans "Edit" %} + + {% include "preferences/aff_radiusoptions.html" %} +
+
@@ -295,7 +307,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Web management, activé si provision automatiqueSwitchs configurés automatiquement {{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %} OK{% else %}Manquant{% endif %}
Plage d'ip de management des switchs {{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %} OK{% else %}Manquant{% endif %}
Serveur des config des switchs {{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} OK{% else %}Manquant{% endif %}
- +
@@ -367,7 +378,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Reminder%} - + Ajouter un rappel

{% acl_end %} @@ -384,12 +395,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Service%} - + {% trans " Add a service" %}

{% acl_end %} {% include "preferences/aff_service.html" with service_list=service_list %} - +
@@ -400,7 +411,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- + {% can_create preferences.MailContact %} {% trans "Add an address" %} {% acl_end %} diff --git a/preferences/urls.py b/preferences/urls.py index 63ca6a39..30163868 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -66,6 +66,11 @@ urlpatterns = [ views.edit_options, name='edit-options' ), + url( + r'^edit_options/(?P
RadiusOption)$', + views.edit_options, + name='edit-options' + ), url(r'^add_service/$', views.add_service, name='add-service'), url( r'^edit_service/(?P[0-9]+)$', diff --git a/preferences/views.py b/preferences/views.py index ee81ae89..f62a5a0d 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -62,7 +62,8 @@ from .models import ( HomeOption, Reminder, RadiusKey, - SwitchManagementCred + SwitchManagementCred, + RadiusOption, ) from . import models from . import forms @@ -86,6 +87,7 @@ def display_options(request): reminder_list = Reminder.objects.all() radiuskey_list = RadiusKey.objects.all() switchmanagementcred_list = SwitchManagementCred.objects.all() + radiusoptions, _ = RadiusOption.objects.get_or_create() return form({ 'useroptions': useroptions, 'machineoptions': machineoptions, @@ -98,7 +100,8 @@ def display_options(request): 'mailcontact_list': mailcontact_list, 'reminder_list': reminder_list, 'radiuskey_list' : radiuskey_list, - 'switchmanagementcred_list': switchmanagementcred_list, + 'switchmanagementcred_list': switchmanagementcred_list, + 'radiusoptions' : radiusoptions, }, 'preferences/display_preferences.html', request) From f0736ebc39efca3f76dcba903ad1844bdd0f7569 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 2 Dec 2018 17:03:27 +0100 Subject: [PATCH 29/86] Allow policies edition --- preferences/forms.py | 8 + .../migrations/0052_auto_20181013_1629.py | 59 ++++---- preferences/models.py | 138 ++++++++++-------- .../preferences/edit_preferences.html | 6 + preferences/views.py | 4 +- 5 files changed, 125 insertions(+), 90 deletions(-) diff --git a/preferences/forms.py b/preferences/forms.py index 2f90927f..685400de 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -42,6 +42,7 @@ from .models import ( Reminder, RadiusKey, SwitchManagementCred, + RadiusOption, ) from topologie.models import Switch @@ -229,6 +230,13 @@ class EditHomeOptionForm(ModelForm): self.fields['twitter_account_name'].label = _("Twitter account name") +class EditRadiusOptionForm(ModelForm): + """Edition forms for Radius options""" + class Meta: + model = RadiusOption + fields = ['radius_general_policy', 'vlan_decision_ok'] + + class ServiceForm(ModelForm): """Edition, ajout de services sur la page d'accueil""" class Meta: diff --git a/preferences/migrations/0052_auto_20181013_1629.py b/preferences/migrations/0052_auto_20181013_1629.py index 70498536..0f1d0b74 100644 --- a/preferences/migrations/0052_auto_20181013_1629.py +++ b/preferences/migrations/0052_auto_20181013_1629.py @@ -10,17 +10,11 @@ import re2o.mixins def create_radius_policy(apps, schema_editor): OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie') RadiusOption = apps.get_model('preferences', 'RadiusOption') - RadiusPolicy = apps.get_model('preferences', 'RadiusPolicy') option,_ = OptionalTopologie.objects.get_or_create() radius_option = RadiusOption() radius_option.radius_general_policy = option.radius_general_policy - radius_option.unknown_machine = RadiusPolicy.objects.create() - radius_option.unknown_port = RadiusPolicy.objects.create() - radius_option.unknown_room = RadiusPolicy.objects.create() - radius_option.non_member = RadiusPolicy.objects.create() - radius_option.banned = RadiusPolicy.objects.create() radius_option.vlan_decision_ok = option.vlan_decision_ok radius_option.save() @@ -45,47 +39,56 @@ class Migration(migrations.Migration): }, bases=(re2o.mixins.AclMixin, models.Model), ), - migrations.CreateModel( - name='RadiusPolicy', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('policy', models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32)), - ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.Vlan')), - ], - options={ - 'verbose_name': 'radius policy', - }, - bases=(re2o.mixins.AclMixin, models.Model), + migrations.AddField( + model_name='radiusoption', + name='banned_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for banned if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banned_vlan', to='machines.Vlan', verbose_name='Banned Vlan'), ), migrations.AddField( model_name='radiusoption', - name='non_member', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='non_member_option', to='preferences.RadiusPolicy', verbose_name='Policy non member users.'), + name='non_member_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for non members if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='non_member_vlan', to='machines.Vlan', verbose_name='Non member Vlan'), ), migrations.AddField( model_name='radiusoption', - name='unknown_machine', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_option', to='preferences.RadiusPolicy', verbose_name='Policy for unknown machines'), + name='unknown_machine_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for unknown machines if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_vlan', to='machines.Vlan', verbose_name='Unknown machine Vlan'), ), migrations.AddField( model_name='radiusoption', - name='unknown_port', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_option', to='preferences.RadiusPolicy', verbose_name='Policy for unknown machines'), + name='unknown_port_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for unknown ports if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_vlan', to='machines.Vlan', verbose_name='Unknown port Vlan'), ), migrations.AddField( model_name='radiusoption', - name='unknown_room', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_option', to='preferences.RadiusPolicy', verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'), + name='unknown_room_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for unknown room if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_vlan', to='machines.Vlan', verbose_name='Unknown room Vlan'), ), migrations.AddField( model_name='radiusoption', name='banned', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='banned_option', to='preferences.RadiusPolicy', verbose_name='Policy for banned users.'), + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for banned users.'), ), migrations.AddField( model_name='radiusoption', - name='vlan_decision_ok', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'), + name='non_member', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy non member users.'), ), + migrations.AddField( + model_name='radiusoption', + name='unknown_machine', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_port', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_room', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'), + ), + migrations.RunPython(create_radius_policy), ] diff --git a/preferences/models.py b/preferences/models.py index f3f5527e..cbc42516 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -199,26 +199,6 @@ class OptionalTopologie(AclMixin, PreferencesModel): ('tftp', 'tftp'), ) - radius_general_policy = models.CharField( - max_length=32, - choices=CHOICE_RADIUS, - default='DEFINED' - ) - vlan_decision_ok = models.OneToOneField( - 'machines.Vlan', - on_delete=models.PROTECT, - related_name='decision_ok', - blank=True, - null=True - ) - vlan_decision_nok = models.OneToOneField( - 'machines.Vlan', - on_delete=models.PROTECT, - related_name='decision_nok', - blank=True, - null=True - ) - switchs_web_management = models.BooleanField( default=False, help_text="Web management, activé si provision automatique" @@ -589,31 +569,6 @@ class MailMessageOption(AclMixin, models.Model): verbose_name = _("email message options") -class RadiusPolicy(AclMixin, models.Model): - class Meta: - verbose_name = _('radius policy') - - REJECT = 'REJECT' - SET_VLAN = 'SET_VLAN' - CHOICE_POLICY = ( - (REJECT, _('Reject the machine')), - (SET_VLAN, _('Place the machine on the VLAN')) - ) - - policy = models.CharField( - max_length=32, - choices=CHOICE_POLICY, - default=REJECT - ) - - vlan = models.ForeignKey( - 'machines.Vlan', - on_delete=models.PROTECT, - blank=True, - null=True - ) - - class RadiusOption(AclMixin, models.Model): class Meta: verbose_name = _("radius policies") @@ -624,44 +579,105 @@ class RadiusOption(AclMixin, models.Model): (MACHINE, _("On the IP range's VLAN of the machine")), (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")), ) + REJECT = 'REJECT' + SET_VLAN = 'SET_VLAN' + CHOICE_POLICY = ( + (REJECT, _('Reject the machine')), + (SET_VLAN, _('Place the machine on the VLAN')) + ) radius_general_policy = models.CharField( max_length=32, choices=CHOICE_RADIUS, default='DEFINED' ) - unknown_machine = models.ForeignKey( - RadiusPolicy, - on_delete=models.PROTECT, + unknown_machine = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, verbose_name=_("Policy for unknown machines"), - related_name='unknown_machine_option', ) - unknown_port = models.ForeignKey( - RadiusPolicy, + unknown_machine_vlan = models.ForeignKey( + 'machines.Vlan', on_delete=models.PROTECT, + related_name='unknown_machine_vlan', + blank=True, + null=True, + verbose_name=_('Unknown machine Vlan'), + help_text=_( + 'Vlan for unknown machines if not rejected.' + ) + ) + unknown_port = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, verbose_name=_("Policy for unknown machines"), - related_name='unknown_port_option', ) - unknown_room = models.ForeignKey( - RadiusPolicy, + unknown_port_vlan = models.ForeignKey( + 'machines.Vlan', on_delete=models.PROTECT, + related_name='unknown_port_vlan', + blank=True, + null=True, + verbose_name=_('Unknown port Vlan'), + help_text=_( + 'Vlan for unknown ports if not rejected.' + ) + ) + unknown_room = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, verbose_name=_( "Policy for machine connecting from " "unregistered room (relevant on ports with STRICT " "radius mode)" ), - related_name='unknown_room_option', ) - non_member = models.ForeignKey( - RadiusPolicy, + unknown_room_vlan = models.ForeignKey( + 'machines.Vlan', + related_name='unknown_room_vlan', on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name=_('Unknown room Vlan'), + help_text=_( + 'Vlan for unknown room if not rejected.' + ) + ) + non_member = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, verbose_name=_("Policy non member users."), - related_name='non_member_option', ) - banned = models.ForeignKey( - RadiusPolicy, + non_member_vlan = models.ForeignKey( + 'machines.Vlan', + related_name='non_member_vlan', on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name=_('Non member Vlan'), + help_text=_( + 'Vlan for non members if not rejected.' + ) + ) + banned = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, verbose_name=_("Policy for banned users."), - related_name='banned_option' + ) + banned_vlan = models.ForeignKey( + 'machines.Vlan', + related_name='banned_vlan', + on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name=_('Banned Vlan'), + help_text=_( + 'Vlan for banned if not rejected.' + ) ) vlan_decision_ok = models.OneToOneField( 'machines.Vlan', diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index a1540f33..c3dd4652 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -37,6 +37,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %} + {% if formset %} + {{ formset.management_form }} + {% for f in formset %} + {% bootstrap_form f %} + {% endfor %} + {% endif %} {% trans "Edit" as tr_edit %} {% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
diff --git a/preferences/views.py b/preferences/views.py index f62a5a0d..586be60f 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -137,7 +137,9 @@ def edit_options(request, section): messages.success(request, _("The preferences were edited.")) return redirect(reverse('preferences:display-options')) return form( - {'options': options}, + { + 'options': options, + }, 'preferences/edit_preferences.html', request ) From 9506bd400244dfa91067ec2c66167dcba7b2d10d Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 4 Dec 2018 14:58:28 +0100 Subject: [PATCH 30/86] Fixing migrations is more a way of life than a way to have fun. --- ..._20181013_1629.py => 0056_radiusoption.py} | 19 +++++++++++++++++- .../migrations/0057_auto_20181204_0757.py | 20 +++++++++++++++++++ preferences/models.py | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) rename preferences/migrations/{0052_auto_20181013_1629.py => 0056_radiusoption.py} (87%) create mode 100644 preferences/migrations/0057_auto_20181204_0757.py diff --git a/preferences/migrations/0052_auto_20181013_1629.py b/preferences/migrations/0056_radiusoption.py similarity index 87% rename from preferences/migrations/0052_auto_20181013_1629.py rename to preferences/migrations/0056_radiusoption.py index 0f1d0b74..e329f598 100644 --- a/preferences/migrations/0052_auto_20181013_1629.py +++ b/preferences/migrations/0056_radiusoption.py @@ -24,7 +24,7 @@ class Migration(migrations.Migration): dependencies = [ ('machines', '0095_auto_20180919_2225'), - ('preferences', '0051_auto_20180919_2225'), + ('preferences', '0055_generaloption_main_site_url'), ] operations = [ @@ -89,6 +89,23 @@ class Migration(migrations.Migration): name='unknown_room', field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'), ), + migrations.AddField( + model_name='radiusoption', + name='vlan_decision_ok', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'), + ), migrations.RunPython(create_radius_policy), + migrations.RemoveField( + model_name='optionaltopologie', + name='radius_general_policy', + ), + migrations.RemoveField( + model_name='optionaltopologie', + name='vlan_decision_nok', + ), + migrations.RemoveField( + model_name='optionaltopologie', + name='vlan_decision_ok', + ), ] diff --git a/preferences/migrations/0057_auto_20181204_0757.py b/preferences/migrations/0057_auto_20181204_0757.py new file mode 100644 index 00000000..ba4e1a6f --- /dev/null +++ b/preferences/migrations/0057_auto_20181204_0757.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-04 13:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0056_radiusoption'), + ] + + operations = [ + migrations.AlterField( + model_name='radiusoption', + name='unknown_port', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown port'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index cbc42516..4e6ebd6b 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -611,7 +611,7 @@ class RadiusOption(AclMixin, models.Model): max_length=32, choices=CHOICE_POLICY, default=REJECT, - verbose_name=_("Policy for unknown machines"), + verbose_name=_("Policy for unknown port"), ) unknown_port_vlan = models.ForeignKey( 'machines.Vlan', From b269a5f6ef5cb7e63c2c2c8b892c06839bb0b2b5 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 4 Dec 2018 14:59:23 +0100 Subject: [PATCH 31/86] Allow policies editions through form --- preferences/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preferences/forms.py b/preferences/forms.py index 685400de..1126b399 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -234,7 +234,7 @@ class EditRadiusOptionForm(ModelForm): """Edition forms for Radius options""" class Meta: model = RadiusOption - fields = ['radius_general_policy', 'vlan_decision_ok'] + fields = '__all__' class ServiceForm(ModelForm): From 6df47b08ec366b55aab10e55780956725a94b821 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 4 Dec 2018 18:46:57 +0100 Subject: [PATCH 32/86] Adapt preferences' display to new model layout --- .../preferences/aff_radiusoptions.html | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/preferences/templates/preferences/aff_radiusoptions.html b/preferences/templates/preferences/aff_radiusoptions.html index b434319d..60ee15f5 100644 --- a/preferences/templates/preferences/aff_radiusoptions.html +++ b/preferences/templates/preferences/aff_radiusoptions.html @@ -45,50 +45,50 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Unknown machine" %} - {% if radiusoptions.unknown_machine.policy == 'REJECT' %} + {% if radiusoptions.unknown_machine == 'REJECT' %} {% trans "Reject" %} {% else %} - Vlan {{ radiusoptions.unknown_machine.vlan }} + Vlan {{ radiusoptions.unknown_machine_vlan }} {% endif %} {% trans "Unknown port" %} - {% if radiusoptions.unknown_port.policy == 'REJECT' %} + {% if radiusoptions.unknown_port == 'REJECT' %} {% trans "Reject" %} {% else %} - Vlan {{ radiusoptions.unknown_port.vlan }} + Vlan {{ radiusoptions.unknown_port_vlan }} {% endif %} {% trans "Unknown room" %} - {% if radiusoptions.unknown_room.policy == 'REJECT' %} + {% if radiusoptions.unknown_room == 'REJECT' %} {% trans "Reject" %} {% else %} - Vlan {{ radiusoptions.unknown_room.vlan }} + Vlan {{ radiusoptions.unknown_room_vlan }} {% endif %} {% trans "Non member" %} - {% if radiusoptions.non_member.policy == 'REJECT' %} + {% if radiusoptions.non_member == 'REJECT' %} {% trans "Reject" %} {% else %} - Vlan {{ radiusoptions.non_member.vlan }} + Vlan {{ radiusoptions.non_member_vlan }} {% endif %} {% trans "Banned user" %} - {% if radiusoptions.unknown_port.policy == 'REJECT' %} + {% if radiusoptions.unknown_port == 'REJECT' %} {% trans "Reject" %} {% else %} - Vlan {{ radiusoptions.banned.vlan }} + Vlan {{ radiusoptions.banned_vlan }} {% endif %} From 2702d7c11635162c4773e75a7de0e44058b12ab7 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 4 Dec 2018 20:00:18 +0100 Subject: [PATCH 33/86] Radius opers according to policies. --- freeradius_utils/auth.py | 205 +++++++++++++++++++++++++++------------ 1 file changed, 145 insertions(+), 60 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 9bd56015..922fd153 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -57,14 +57,12 @@ application = get_wsgi_application() from machines.models import Interface, IpList, Nas, Domain from topologie.models import Port, Switch from users.models import User -from preferences.models import OptionalTopologie +from preferences.models import RadiusOption -options, created = OptionalTopologie.objects.get_or_create() -VLAN_NOK = options.vlan_decision_nok.vlan_id -VLAN_OK = options.vlan_decision_ok.vlan_id -VLAN_NON_MEMBER = options.vlan_non_member.vlan_id -RADIUS_POLICY = options.radius_general_policy +OPTIONS, created = RadiusOption.objects.get_or_create() +VLAN_OK = OPTIONS.vlan_decision_ok.vlan_id +RADIUS_POLICY = OPTIONS.radius_general_policy #: Serveur radius de test (pas la prod) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) @@ -288,6 +286,7 @@ def find_nas_from_request(nas_id): .select_related('machine__switch__stack')) return nas.first() + def check_user_machine_and_register(nas_type, username, mac_address): """Verifie le username et la mac renseignee. L'enregistre si elle est inconnue. @@ -328,27 +327,44 @@ def check_user_machine_and_register(nas_type, username, mac_address): def decide_vlan_switch(nas_machine, nas_type, port_number, - mac_address): + mac_address): """Fonction de placement vlan pour un switch en radius filaire auth par mac. Plusieurs modes : - - nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK - - pas de radius sur le port : VLAN_OK - - bloq : VLAN_NOK - - force : placement sur le vlan indiqué dans la bdd - - mode strict : - - pas de chambre associée : VLAN_NOK - - pas d'utilisateur dans la chambre : VLAN_NOK - - cotisation non à jour : VLAN_NON_MEMBER - - sinon passe à common (ci-dessous) - - mode common : - - interface connue (macaddress): - - utilisateur proprio non cotisant : VLAN_NON_MEMBER - - utilisateur proprio banni : VLAN_NOK - - user à jour : VLAN_OK - - interface inconnue : - - register mac désactivé : VLAN_NON_MEMBER - - register mac activé -> redirection vers webauth + - tous les modes: + - nas inconnu: VLAN_OK + - port inconnu: Politique définie dans RadiusOption + - pas de radius sur le port: VLAN_OK + - force: placement sur le vlan indiqué dans la bdd + - mode strict: + - pas de chambre associée: Politique définie + dans RadiusOption + - pas d'utilisateur dans la chambre : Rejet + (redirection web si disponible) + - utilisateur de la chambre banni ou désactivé : Rejet + (redirection web si disponible) + - utilisateur de la chambre non cotisant et non whiteslist: + Politique définie dans RadiusOption + + - sinon passe à common (ci-dessous) + - mode common : + - interface connue (macaddress): + - utilisateur proprio non cotisant / machine désactivée: + Politique définie dans RadiusOption + - utilisateur proprio banni : + Politique définie dans RadiusOption + - user à jour : VLAN_OK (réassignation de l'ipv4 au besoin) + - interface inconnue : + - register mac désactivé : Politique définie + dans RadiusOption + - register mac activé: redirection vers webauth + Returns: + tuple avec : + - Nom du switch (str) + - chambre (str) + - raison de la décision (str) + - vlan_id (int) + - decision (bool) """ # Get port from switch and port number extra_log = "" @@ -369,7 +385,13 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, # 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, True) + return ( + sw_name, + "Chambre inconnue", + u'Port inconnu', + OPTIONS.unknown_port_vlan.vlan_id, + OPTIONS.unknown_port != OPTIONS.REJECT + ) # On récupère le profil du port port_profile = port.get_port_profile @@ -382,9 +404,9 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, else: DECISION_VLAN = VLAN_OK - # Si le port est désactivé, on rejette sur le vlan de déconnexion + # Si le port est désactivé, on rejette la connexion if not port.state: - return (sw_name, port.room, u'Port desactivé', VLAN_NOK, True) + return (sw_name, port.room, u'Port desactivé', None, False) # Si radius est désactivé, on laisse passer if port_profile.radius_type == 'NO': @@ -394,35 +416,68 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, DECISION_VLAN, True) - # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment + # 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_profile.radius_type == '802.1X': + if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'): room = port.room or "Chambre/local inconnu" - return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN, True) + return ( + sw_name, + room, + u'Acceptation authentification 802.1X', + DECISION_VLAN, + True + ) # 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 + # 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_profile.radius_mode == 'STRICT': room = port.room if not room: - return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK, True) + return ( + sw_name, + "Inconnue", + u'Chambre inconnue', + OPTIONS.unknown_room_vlan.vlan_id, + OPTIONS.unknown_room != OPTIONS.REJECT + ) room_user = User.objects.filter( Q(club__room=port.room) | Q(adherent__room=port.room) ) if not room_user: - return (sw_name, room, u'Chambre non cotisante -> Web redirect', None, False) + return ( + sw_name, + room, + u'Chambre non cotisante -> Web redirect', + None, + False + ) for user in room_user: if user.is_ban() or user.state != User.STATE_ACTIVE: - return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False) + return ( + sw_name, + room, + u'Utilisateur banni ou désactivé -> Web redirect', + None, + False + ) elif not (user.is_connected() or user.is_whitelisted()): - return (sw_name, room, u'Utilisateur non cotisant', VLAN_NON_MEMBER) + return ( + sw_name, + room, + u'Utilisateur non cotisant', + OPTIONS.non_member_vlan.vlan_id, + OPTIONS.non_member != OPTIONS.REJECT + ) # else: user OK, on passe à la verif MAC - # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd + # Si on fait de l'auth par mac, on cherche l'interface + # via sa mac dans la bdd if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT': # Authentification par mac interface = (Interface.objects @@ -432,37 +487,67 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, .first()) if not interface: room = port.room - # 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_NON_MEMBER) - # On rejette pour basculer sur du webauth + # On essaye de register la mac, si l'autocapture a été activée, + # on rejette pour faire une redirection web si possible. + if nas_type.autocapture_mac: + return ( + sw_name, + room, + u'Machine Inconnue -> Web redirect', + None, + False + ) + # Sinon on bascule sur la politique définie dans les options + # radius. else: - return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False) + return ( + sw_name, + "", + u'Machine inconnue', + OPTIONS.unknown_machine_vlan.vlan_id, + OPTIONS.unknown_machine != OPTIONS.REJECT + ) - # L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject + # 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 interface.machine.user.is_banned(): + return ( + sw_name, + room, + u'Adherent banni', + OPTIONS.banned_vlan.vlan_id, + OPTIONS.banned != OPTIONS.REJECT + ) if not interface.is_active: - return (sw_name, - room, - u'Machine non active / adherent non cotisant', - VLAN_NON_MEMBER) - ## Si on choisi de placer les machines sur le vlan correspondant à leur type : + return ( + sw_name, + room, + u'Machine non active / adherent non cotisant', + OPTIONS.non_member_vlan.vlan_id, + OPTIONS.non_member != OPTIONS.REJECT + ) + # Si on choisi de placer les machines sur le vlan + # correspondant à leur type : if RADIUS_POLICY == 'MACHINE': DECISION_VLAN = interface.type.ip_type.vlan.vlan_id if not interface.ipv4: interface.assign_ipv4() - return (sw_name, - room, - u"Ok, Reassignation de l'ipv4" + extra_log, - DECISION_VLAN, - True) + return ( + sw_name, + room, + u"Ok, Reassignation de l'ipv4" + extra_log, + DECISION_VLAN, + True + ) else: - return (sw_name, - room, - u'Machine OK' + extra_log, - DECISION_VLAN, - True) + return ( + sw_name, + room, + u'Machine OK' + extra_log, + DECISION_VLAN, + True + ) From b2be55a909b0b2e96cc63c5db08298c23d7b0744 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Wed, 5 Dec 2018 12:19:29 +0100 Subject: [PATCH 34/86] =?UTF-8?q?Typo=20sur=20la=20v=C3=A9rification=20de?= =?UTF-8?q?=20bannissement.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freeradius_utils/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 922fd153..66465c0e 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -514,7 +514,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, # Enfin on laisse passer sur le vlan pertinent else: room = port.room - if interface.machine.user.is_banned(): + if interface.machine.user.is_ban(): return ( sw_name, room, From 0e2f1258d429c1e33f6327413d00e9803de853b0 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Wed, 5 Dec 2018 12:28:07 +0100 Subject: [PATCH 35/86] Add PreferencesModel as ancestor of RadiusOption --- preferences/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preferences/models.py b/preferences/models.py index 4e6ebd6b..4644aa1c 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -569,7 +569,7 @@ class MailMessageOption(AclMixin, models.Model): verbose_name = _("email message options") -class RadiusOption(AclMixin, models.Model): +class RadiusOption(AclMixin, PreferencesModel): class Meta: verbose_name = _("radius policies") From 7e9de612fa48fd879a5a8322276fb6f9ed4f2c6f Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Wed, 5 Dec 2018 23:14:53 +0100 Subject: [PATCH 36/86] Prettier display of vlan OK --- preferences/templates/preferences/aff_radiusoptions.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preferences/templates/preferences/aff_radiusoptions.html b/preferences/templates/preferences/aff_radiusoptions.html index 60ee15f5..17e2a869 100644 --- a/preferences/templates/preferences/aff_radiusoptions.html +++ b/preferences/templates/preferences/aff_radiusoptions.html @@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "VLAN for machines accepted by RADIUS" %} - {{ radiusoptions.vlan_decision_ok }} + Vlan {{ radiusoptions.vlan_decision_ok }}
From 789a648bd0cf1ac87a8fec71b951ee0406ef3b6f Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 8 Dec 2018 21:47:21 +0100 Subject: [PATCH 37/86] cached values for radius policies --- freeradius_utils/auth.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 66465c0e..3fa7e23f 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -60,10 +60,6 @@ from users.models import User from preferences.models import RadiusOption -OPTIONS, created = RadiusOption.objects.get_or_create() -VLAN_OK = OPTIONS.vlan_decision_ok.vlan_id -RADIUS_POLICY = OPTIONS.radius_general_policy - #: Serveur radius de test (pas la prod) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) @@ -370,7 +366,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, extra_log = "" # Si le NAS est inconnu, on place sur le vlan defaut if not nas_machine: - return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK, True) + return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True) sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine))) @@ -389,8 +385,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, "Chambre inconnue", u'Port inconnu', - OPTIONS.unknown_port_vlan.vlan_id, - OPTIONS.unknown_port != OPTIONS.REJECT + RadiusOption('unknown_port_vlan').vlan_id, + RadiusOption('unknown_port')!= RadiusOption.REJECT ) # On récupère le profil du port @@ -402,7 +398,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) extra_log = u"Force sur vlan " + str(DECISION_VLAN) else: - DECISION_VLAN = VLAN_OK + DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok') # Si le port est désactivé, on rejette la connexion if not port.state: @@ -442,8 +438,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, "Inconnue", u'Chambre inconnue', - OPTIONS.unknown_room_vlan.vlan_id, - OPTIONS.unknown_room != OPTIONS.REJECT + RadiusOption('unknown_room_vlan').vlan_id, + RadiusOption('unknown_room')!= RadiusOption.REJECT ) room_user = User.objects.filter( @@ -471,8 +467,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, room, u'Utilisateur non cotisant', - OPTIONS.non_member_vlan.vlan_id, - OPTIONS.non_member != OPTIONS.REJECT + RadiusOption('non_member_vlan').vlan_id, + RadiusOption('non_member')!= RadiusOption.REJECT ) # else: user OK, on passe à la verif MAC @@ -504,8 +500,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, "", u'Machine inconnue', - OPTIONS.unknown_machine_vlan.vlan_id, - OPTIONS.unknown_machine != OPTIONS.REJECT + RadiusOption('unknown_machine_vlan').vlan_id, + RadiusOption('unknown_machine')!= RadiusOption.REJECT ) # L'interface a été trouvée, on vérifie qu'elle est active, @@ -519,20 +515,20 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, room, u'Adherent banni', - OPTIONS.banned_vlan.vlan_id, - OPTIONS.banned != OPTIONS.REJECT + RadiusOption('banned_vlan').vlan_id, + RadiusOption('banned')!= RadiusOption.REJECT ) if not interface.is_active: return ( sw_name, room, u'Machine non active / adherent non cotisant', - OPTIONS.non_member_vlan.vlan_id, - OPTIONS.non_member != OPTIONS.REJECT + RadiusOption('non_member_vlan').vlan_id, + RadiusOption('non_member')!= RadiusOption.REJECT ) # Si on choisi de placer les machines sur le vlan # correspondant à leur type : - if RADIUS_POLICY == 'MACHINE': + if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE': DECISION_VLAN = interface.type.ip_type.vlan.vlan_id if not interface.ipv4: interface.assign_ipv4() From a15d50bebe1b2e4710af81d45925bd53d2bd5a3a Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 8 Dec 2018 23:07:49 +0100 Subject: [PATCH 38/86] RadiusOption Display --- .../preferences/display_preferences.html | 614 +++++++++--------- 1 file changed, 307 insertions(+), 307 deletions(-) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 21d25b43..0e34d6e9 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -33,203 +33,203 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}
-
- -
+
- - {% trans "Edit" %} + + {% trans "Edit" %} -

+

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Website name" %}{{ generaloptions.site_name }}{% trans "Email address for automatic emailing" %}{{ generaloptions.email_from }}
{% trans "Number of results displayed when searching" %}{{ generaloptions.search_display_page }}{% trans "Number of items per page (standard size)" %}{{ generaloptions.pagination_number }}
{% trans "Number of items per page (large size)" %}{{ generaloptions.pagination_large_number }}{% trans "Time before expiration of the reset password link (in hours)" %}{{ generaloptions.req_expire_hrs }}
{% trans "General message displayed on the website" %}{{ generaloptions.general_message }}{% trans "Main site url" %}{{ generaloptions.main_site_url }}
{% trans "Summary of the General Terms of Use" %}{{ generaloptions.GTU_sum_up }}{% trans "General Terms of Use" %}{{ generaloptions.GTU }} -
- - - - - - - - - - - -
{% trans "Local email accounts enabled" %}{{ useroptions.local_email_accounts_enabled|tick }}{% trans "Local email domain" %}{{ useroptions.local_email_domain }}
{% trans "Maximum number of email aliases allowed" %}{{ useroptions.max_email_address }}
-
-
- -
-
-

- {% trans "User preferences" %} -

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Website name" %}{{ generaloptions.site_name }}{% trans "Email address for automatic emailing" %}{{ generaloptions.email_from }}
{% trans "Number of results displayed when searching" %}{{ generaloptions.search_display_page }}{% trans "Number of items per page (standard size)" %}{{ generaloptions.pagination_number }}
{% trans "Number of items per page (large size)" %}{{ generaloptions.pagination_large_number }}{% trans "Time before expiration of the reset password link (in hours)" %}{{ generaloptions.req_expire_hrs }}
{% trans "General message displayed on the website" %}{{ generaloptions.general_message }}{% trans "Main site url" %}{{ generaloptions.main_site_url }}
{% trans "Summary of the General Terms of Use" %}{{ generaloptions.GTU_sum_up }}{% trans "General Terms of Use" %}{{ generaloptions.GTU }} +
+ + + + + + + + + + + +
{% trans "Local email accounts enabled" %}{{ useroptions.local_email_accounts_enabled|tick }}{% trans "Local email domain" %}{{ useroptions.local_email_domain }}
{% trans "Maximum number of email aliases allowed" %}{{ useroptions.max_email_address }}
+
-
-

- - - {% trans "Edit" %} - -

+
+ +
- - - - - - - - - - - - - +

+ + + {% trans "Edit" %} + +

+ +
{% trans "Creation of members by everyone" %}{{ useroptions.all_can_create_adherent|tick }}{% trans "Creation of clubs by everyone" %}{{ useroptions.all_can_create_club|tick }}
{% trans "Self registration" %}{{ useroptions.self_adhesion|tick }}{% trans "Delete not yet active users after" %}{{ useroptions.delete_notyetactive }} days
+ + + + + + + + + + + +
{% trans "Creation of members by everyone" %}{{ useroptions.all_can_create_adherent|tick }}{% trans "Creation of clubs by everyone" %}{{ useroptions.all_can_create_club|tick }}
{% trans "Self registration" %}{{ useroptions.self_adhesion|tick }}{% trans "Delete not yet active users after" %}{{ useroptions.delete_notyetactive }} days

{% trans "Users general permissions" %}

- - - - - - - - - - - - - - - - -
{% trans "Default shell for users" %}{{ useroptions.shell_default }}{% trans "Users can edit their shell" %}{{ useroptions.self_change_shell|tick }}
{% trans "Users can edit their room" %}{{ useroptions.self_change_room|tick }}{% trans "Telephone number required" %}{{ useroptions.is_tel_mandatory|tick }}
{% trans "GPG fingerprint field" %}{{ useroptions.gpg_fingerprint|tick }}
-
+ + {% trans "Default shell for users" %} + {{ useroptions.shell_default }} + {% trans "Users can edit their shell" %} + {{ useroptions.self_change_shell|tick }} + + + {% trans "Users can edit their room" %} + {{ useroptions.self_change_room|tick }} + {% trans "Telephone number required" %} + {{ useroptions.is_tel_mandatory|tick }} + + + {% trans "GPG fingerprint field" %} + {{ useroptions.gpg_fingerprint|tick }} + +
+
-
- +
+ -
+
- - - {% trans "Edit" %} - -

- - - - - - - - - - - - - - - - - -
{% trans "Password per machine" %}{{ machineoptions.password_machine|tick }}{% trans "Maximum number of interfaces allowed for a standard user" %}{{ machineoptions.max_lambdauser_interfaces }}
{% trans "Maximum number of DNS aliases allowed for a standard user" %}{{ machineoptions.max_lambdauser_aliases }}{% trans "IPv6 support" %}{{ machineoptions.ipv6_mode }}
{% trans "Creation of machines" %}{{ machineoptions.create_machine|tick }}
-
-
+ + + {% trans "Edit" %} + +

+ + + + + + + + + + + + + + + + + +
{% trans "Password per machine" %}{{ machineoptions.password_machine|tick }}{% trans "Maximum number of interfaces allowed for a standard user" %}{{ machineoptions.max_lambdauser_interfaces }}
{% trans "Maximum number of DNS aliases allowed for a standard user" %}{{ machineoptions.max_lambdauser_aliases }}{% trans "IPv6 support" %}{{ machineoptions.ipv6_mode }}
{% trans "Creation of machines" %}{{ machineoptions.create_machine|tick }}
+
+
-
- -
+
+ +
{% trans "Edit" %}

- - - - - - - - - - - - - - - - - -
{% trans "General policy for VLAN setting" %}{{ topologieoptions.radius_general_policy }}{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}
{% trans "VLAN for machines accepted by RADIUS" %}{{ topologieoptions.vlan_decision_ok }}{% trans "VLAN for machines rejected by RADIUS" %}{{ topologieoptions.vlan_decision_nok }}
{% trans "VLAN for non members machines" %}{{ topologieoptions.vlan_non_member }}
+ + + + + + + + + + + + + + + + + +
{% trans "General policy for VLAN setting" %}{{ topologieoptions.radius_general_policy }}{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}
{% trans "VLAN for machines accepted by RADIUS" %}{{ topologieoptions.vlan_decision_ok }}{% trans "VLAN for machines rejected by RADIUS" %}{{ topologieoptions.vlan_decision_nok }}
{% trans "VLAN for non members machines" %}{{ topologieoptions.vlan_non_member }}
-

Clef radius

- {% can_create RadiusKey%} - Ajouter une clef radius - {% acl_end %} - {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %} +

Clef radius

+ {% can_create RadiusKey%} + Ajouter une clef radius + {% acl_end %} + {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %} -
-
+
+
-
+
- +
@@ -238,72 +238,72 @@ with this program; if not, write to the Free Software Foundation, Inc.,

- - - - - - -
Web management, activé si provision automatique{{ topologieoptions.switchs_web_management }}Rest management, activé si provision auto{{ topologieoptions.switchs_rest_management }}
+ + Web management, activé si provision automatique + {{ topologieoptions.switchs_web_management }} + Rest management, activé si provision auto + {{ topologieoptions.switchs_rest_management }} + + -
{% if topologieoptions.provision_switchs_enabled %}Provision de la config des switchs{% else %}Provision de la config des switchs{% endif%}
- - - - - - - - - - - - - - - - - - - - - - - - - -
Switchs configurés automatiquement{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %} OK{% else %}Manquant{% endif %}
Plage d'ip de management des switchs{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %} OK{% else %}Manquant{% endif %}
Serveur des config des switchs{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} OK{% else %}Manquant{% endif %}
Mode de provision des switchs{{ topologieoptions.switchs_provision }}
Mode TFTP OK
Mode SFTP{% if topologieoptions.switchs_management_sftp_creds %} OK{% else %}Creds manquants{% endif %}
+
{% if topologieoptions.provision_switchs_enabled %}Provision de la config des switchs{% else %}Provision de la config des switchs{% endif%}
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Switchs configurés automatiquement{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %} OK{% else %}Manquant{% endif %}
Plage d'ip de management des switchs{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %} OK{% else %}Manquant{% endif %}
Serveur des config des switchs{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} OK{% else %}Manquant{% endif %}
Mode de provision des switchs{{ topologieoptions.switchs_provision }}
Mode TFTP OK
Mode SFTP{% if topologieoptions.switchs_management_sftp_creds %} OK{% else %}Creds manquants{% endif %}
-
Creds de management des switchs
- {% can_create SwitchManagementCred%} -
Ajouter un id/mdp de management switch - {% acl_end %} -

-

- {% if switchmanagementcred_list %} OK{% else %}Manquant{% endif %} - {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %} -
-
+
Creds de management des switchs
+ {% can_create SwitchManagementCred%} + Ajouter un id/mdp de management switch + {% acl_end %} +

+

+ {% if switchmanagementcred_list %} OK{% else %}Manquant{% endif %} + {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %} +
+
-
-
-

{% trans "Radius preferences" %}

-
-
- - - {% trans "Edit" %} - - {% include "preferences/aff_radiusoptions.html" %} -
-
+
+ +
+ + + {% trans "Edit" %} + + {% include "preferences/aff_radiusoptions.html" %} +
+
-
+
@@ -312,43 +312,43 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edit" %}

- - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Name" %}{{ assooptions.name }}{% trans "SIRET number" %}{{ assooptions.siret }}
{% trans "Address" %}{{ assooptions.adresse1 }}
- {{ assooptions.adresse2 }} -
{% trans "Contact email address" %}{{ assooptions.contact }}
{% trans "Telephone number" %}{{ assooptions.telephone }}{% trans "Usual name" %}{{ assooptions.pseudo }}
{% trans "User object of the organisation" %}{{ assooptions.utilisateur_asso }}{% trans "Description of the organisation" %}{{ assooptions.description|safe }}
-
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Name" %}{{ assooptions.name }}{% trans "SIRET number" %}{{ assooptions.siret }}
{% trans "Address" %}{{ assooptions.adresse1 }}
+ {{ assooptions.adresse2 }} +
{% trans "Contact email address" %}{{ assooptions.contact }}
{% trans "Telephone number" %}{{ assooptions.telephone }}{% trans "Usual name" %}{{ assooptions.pseudo }}
{% trans "User object of the organisation" %}{{ assooptions.utilisateur_asso }}{% trans "Description of the organisation" %}{{ assooptions.description|safe }}
+
-
+
@@ -357,26 +357,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,

- - - - - - - - - -
{% trans "Welcome email (in French)" %}{{ mailmessageoptions.welcome_mail_fr|safe }}
{% trans "Welcome email (in English)" %}{{ mailmessageoptions.welcome_mail_en|safe }}
-
-
+ + + + + + + + + +
{% trans "Welcome email (in French)" %}{{ mailmessageoptions.welcome_mail_fr|safe }}
{% trans "Welcome email (in English)" %}{{ mailmessageoptions.welcome_mail_en|safe }}
+
+ -
+
- +
{% can_create preferences.Reminder%} Ajouter un rappel @@ -384,13 +384,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} {% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
-
+
-
+
@@ -401,16 +401,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} {% include "preferences/aff_service.html" with service_list=service_list %} -
-
+
+ -
+
- +
{% can_create preferences.MailContact %} {% trans "Add an address" %} @@ -419,12 +419,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
-
+
-
+
@@ -434,20 +434,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edit" %}

- - - - - - - - - - - -
{% trans "Twitter account URL" %}{{ homeoptions.twitter_url }}{% trans "Twitter account name" %}{{ homeoptions.twitter_account_name }}
{% trans "Facebook account URL" %}{{ homeoptions.facebook_url }}
-
+ + + + + + + + + + + +
{% trans "Twitter account URL" %}{{ homeoptions.twitter_url }}{% trans "Twitter account name" %}{{ homeoptions.twitter_account_name }}
{% trans "Facebook account URL" %}{{ homeoptions.facebook_url }}
+
{% endblock %} From 29fb5dc8482ea54d762ba778a01d3ef01e0f25ad Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Mon, 10 Dec 2018 13:14:56 +0100 Subject: [PATCH 39/86] Remove generated locales from git tree Now compiled locale files (.mo) are generated on the server side. This cleans up the git tree and make translation contributions much easier to merge. Please note that you will need to generate those files after each pull, so remember to execute install_re2o.sh. --- .gitignore | 2 +- apt_requirements.txt | 1 + cotisations/locale/fr/LC_MESSAGES/django.mo | Bin 16926 -> 0 bytes install_re2o.sh | 4 ++++ logs/locale/fr/LC_MESSAGES/django.mo | Bin 5018 -> 0 bytes machines/locale/fr/LC_MESSAGES/django.mo | Bin 32489 -> 0 bytes preferences/locale/fr/LC_MESSAGES/django.mo | Bin 11769 -> 0 bytes re2o/locale/fr/LC_MESSAGES/django.mo | Bin 5643 -> 0 bytes search/locale/fr/LC_MESSAGES/django.mo | Bin 2426 -> 0 bytes templates/locale/fr/LC_MESSAGES/django.mo | Bin 5886 -> 0 bytes topologie/locale/fr/LC_MESSAGES/django.mo | Bin 14114 -> 0 bytes users/locale/fr/LC_MESSAGES/django.mo | Bin 27089 -> 0 bytes 12 files changed, 6 insertions(+), 1 deletion(-) delete mode 100644 cotisations/locale/fr/LC_MESSAGES/django.mo delete mode 100644 logs/locale/fr/LC_MESSAGES/django.mo delete mode 100644 machines/locale/fr/LC_MESSAGES/django.mo delete mode 100644 preferences/locale/fr/LC_MESSAGES/django.mo delete mode 100644 re2o/locale/fr/LC_MESSAGES/django.mo delete mode 100644 search/locale/fr/LC_MESSAGES/django.mo delete mode 100644 templates/locale/fr/LC_MESSAGES/django.mo delete mode 100644 topologie/locale/fr/LC_MESSAGES/django.mo delete mode 100644 users/locale/fr/LC_MESSAGES/django.mo diff --git a/.gitignore b/.gitignore index 49df31ac..c978acac 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ __pycache__/ *.swp # Translations -#*.mo TODO +*.mo *.pot # Django stuff diff --git a/apt_requirements.txt b/apt_requirements.txt index 5a3fc103..0cf83a7c 100644 --- a/apt_requirements.txt +++ b/apt_requirements.txt @@ -17,3 +17,4 @@ libjs-bootstrap fonts-font-awesome graphviz git +gettext diff --git a/cotisations/locale/fr/LC_MESSAGES/django.mo b/cotisations/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 3a1671bbfcbedf829e2923ff601b75f00d441560..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16926 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 diff --git a/install_re2o.sh b/install_re2o.sh index 6168ec08..b6d8b2aa 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -316,6 +316,10 @@ update_django() { echo "Collecting web frontend statics ..." python3 manage.py collectstatic --noinput echo "Collecting web frontend statics: Done" + + echo "Generating locales ..." + python3 manage.py compilemessages + echo "Generating locales: Done" } diff --git a/logs/locale/fr/LC_MESSAGES/django.mo b/logs/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 030b0cac0059428ce6aeedd0cde9e2a0d4404c1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5018 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 diff --git a/machines/locale/fr/LC_MESSAGES/django.mo b/machines/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index c9696d928833fc70692673eb34d0416314ae069c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32489 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 kTGTPqmT<1pQh1xPsz=(2+jh^mNzERJNz9 z>Bo3&fkZ%v95@6a4k$uGa3Ibp95^7sg#ihWa>*g52-rJtL4qs4uexVEw%3bcwC(?N z_g7WltN!(${qw{3{m|nx&GkvHufN0do(122Cx7_-;a#3L4*n4oo4-}}kKX5bPjbHi zJ`4un7}x_p2)+ou2mC>WuYh9zv+DhyK>m4e^XL8G9Z>B50V?oc;Jd*`?)SX+f{%mp zeiHl$I0IthT>-xZegXV0_y#C`Y%*BztDxB30>#e+l=Z$1%DgXwza!QkRnNcjzA_KD zz#F`O37i1`0zL-b&!is$p8!7reh#dG*TEU^#R`8>J-<`oLl2a8=fPR*Zh=pc>mP%k z;r>mWYJmR+p8%&m;CZLP74T882TERF20se^6lAFPD^T+CdrlgT=i~(9|J>B?7soN1bzqn9{AuxT;MC9*uMsf-7mopgTJZX zzX^(ecfjA$Gk>q{UnhAI&#%ERgKvQn?<|9*&o)6>|2FtI_|&BUpW%``OApCMdg#+!wB7JCJ^WVIT5hGsBtMkX z z0{3o9-i;a`V^^B2>#ohU+FV~bVwSpBm|bYvBFCW%S!S>KU75Umw65o@cOeLLmZ|QJ zN>bZ0k@mz#9p_-2`@s-Z-qFgYZ9g`dpBo!9rO%XGaXF`$r>0rtHqBIIvfR5&B;IA6 z=s47IV07kPiQByCcuO{l?6!JtVk+M@d9KqfKL~6Q=Yz?XQbu&Vt5#M`wf8*p*>q=s z%C=bDHeb*|PB_oouJ)E&YR4AJk5cW2JF4UNwDMzBO+2k4+g2v7tD6`Fyy*I|-_|lF zRaw!@g484~tlTYnAk^v7tm_9I6YGq5Ll%mv!8+|3l2{+O5ZZV$KkR_o(YYHZPHy{g zu2=`>B>9rr<(M?YmCfN)Pi88sR+&}-0aA14xZRFbqSMUAel!TR?ycB%+r$#Cx4K`~ zYx;KXU9+*iO)@F4;!1bw-kMHZHtp(AHFvzVRQHT6GEbUA83|SSVb@UfJT1$fTN^R8 z$qjDEAhqDDPO4ky$2)nRWb-pKsotd|`EH$lnHgVP=T@z9zcAx3mdR5sy{)>s+r)XP zrj@)R+W~ycu)p9Oz z81hRWVr5m6&H!B)q3chnM=P~moZC?>wr$f}3~8l4VHZJOq*`Ued}GXc968&;SJ_(*kcLdM4Nwlu$SGotf;i^ZES0uXJu<0&vu)!YYD=X zB#LixaHL(=-rf!BA#^+X>`PU`7&0*>1iZDan`BGK{U!u6LP0WJYTBKHghQ87m7;!6 z>xyZ7HNhTrWP5M>P)C|_o!DN7Y^eXuTB2c#$zgDBUwQ_DIq}@inr2ORMq^z|t(-8m z<*-&2VP4JEpB`J+iB0p`YSuPk?PAf+Y8!T5jjergqjpKinQGPK@WZ+>d#=_vSDQPl zW}lvKoITT+Ycw#dtwXa?)dO~CYiDQG>@)L?&!1^D8gpYSuup9R`H)3E5zng?-xfMn zOXW$i5V>0zKn{-QvLYe>LA{kOGBR!#9B;J^?RRl)a6eBgZY`~ktuC)#*^g2d~9C`IfbK+ zl|qsr6>=6k*(37hpl$j5u5##ZhBQ9<3L&II+5)c>Z?QDQR~iVZZPj1IgJ z5)pya-xIFx|3r1!hUMNKs3aePKdh9njV47JRRD7GFnNT82B+O&Ay2MBa4+^$WB1 z^sWl~doK`b5ldx~Tu&>5e9KMC=-rx_)zu9#PHZ9O?fza$z&)GeVPNxtIAn?gNtnh& zj+IJ53D3za@^Bz+>O)83N#{!9%NZJ2W~NT1JBpDKiF24g5k4TJh9w&6DD3>=_Hc5U z;gCZ?+TUYz>AKDmKhTw!igkpVtHKUS8eZM6z0`vaL(b+h1Nc#KuNsS0+v8R29WOti5k{W(RC4N-K7*5hy?tM53{bFx~Mj-f# z3m7@YLhMwwQKs1`lAF`)9;4h^Hf9NAm|yVQD{&QO2DrbQxb2;)me8BzKjXa9Zr{ls zkz+CE85xE{Gf8Y+UAA$cvPqiSZB02m#AZi0LW&br{GyB70~I>pooX8={OAi=kKUKP zD@^3%W8xg2MiWvy$U-E42Tz$voOa7W0YYUF?Su9SR6N6 zB(;oiuY73Ekvujc`XzG8?MZr(#d)Xs!*aPzOPDtiNL%K*4*--z-h-k9Z bnR0I4+pnIG)`?wm*6!?+z3sxtT#U%)2#EBH8g>QTm?1b0+HbL^E1wIYl z1}oqONPg#F3VsgKzQ4ee;NKw09mC{R#Ns6QF~&1cN_xM=&lzwBq;vcTlKy)j?Y|G6 z0)Gde0{;Zb-an52x%CrIly=X7Z({vCNb$c3o(F~F7jAyXjqib!p9kO@UUaiKRe!pskVkK6^_B5I88dd4Xup>bAFV!4^1kk_@Hp?>>QlWjxeUvG(M!* zY$@w^;poGT%kJw|n<8zHG`(CVdt5C{b1=zU5n~4$Nf;T=+DXx_1F$VXCo-%WZ-6GD}X$?NW&} zzQ`mO-0unvA*rhj_OvN~hdKLb?=WmpryJ6@-hAM#K~AMfgHKkcDoc{W=(S8o1>TLk zW4x8L!7GcmR=im)g@vGZ6KRKZtv>D5roH+UuTMAXQvP#}7!eylWV0ESGO^@Wr zEm-#E5Tu9UAu3r`Vo3K9*Amr-Oj1b>`Y6t#7_pbCF;T`8=24cILZa0sEj3Y+sP)ng zLc=ISJfPb!oXF|uSI3uccWR;@^juI-$cidv=?2lU? zmh-svqO8X)B9?I*ha0<|#&M2J$lT>cihP`GZWRjM$&NXx@cqFZ!w27Ut0?3hRGX6d zHgwK9S4LUX`=f<`M*`1<%kW5wo8sx%%ULk!w{y#8kMNwW7B0%q+(UvwDc_MS%{5+< zK_5>Et{@(hFdXzXE~?>iY7aaog>;_a1`NF^pBBjrh4iKrB2d>ASq6Q1g6m2T%ixZd Wcv|*FYIynfM-TrjMVN-sVgCaK0i(G9 diff --git a/templates/locale/fr/LC_MESSAGES/django.mo b/templates/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 6f33dd5aafcee3fcf2ffc7ef650b954c1eab0dea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5886 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 diff --git a/topologie/locale/fr/LC_MESSAGES/django.mo b/topologie/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 61d2bf80dd0cb2001db830715e237a3de4ce039f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14114 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 Date: Mon, 10 Dec 2018 21:39:39 +0100 Subject: [PATCH 40/86] hotfix : do not fail if vlan policy is not set. --- freeradius_utils/auth.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 3fa7e23f..e605aea5 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -385,8 +385,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, "Chambre inconnue", u'Port inconnu', - RadiusOption('unknown_port_vlan').vlan_id, - RadiusOption('unknown_port')!= RadiusOption.REJECT + getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT ) # On récupère le profil du port @@ -438,8 +438,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, "Inconnue", u'Chambre inconnue', - RadiusOption('unknown_room_vlan').vlan_id, - RadiusOption('unknown_room')!= RadiusOption.REJECT + getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT ) room_user = User.objects.filter( @@ -467,8 +467,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, room, u'Utilisateur non cotisant', - RadiusOption('non_member_vlan').vlan_id, - RadiusOption('non_member')!= RadiusOption.REJECT + getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT ) # else: user OK, on passe à la verif MAC @@ -500,8 +500,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, "", u'Machine inconnue', - RadiusOption('unknown_machine_vlan').vlan_id, - RadiusOption('unknown_machine')!= RadiusOption.REJECT + getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT ) # L'interface a été trouvée, on vérifie qu'elle est active, @@ -515,16 +515,16 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, sw_name, room, u'Adherent banni', - RadiusOption('banned_vlan').vlan_id, - RadiusOption('banned')!= RadiusOption.REJECT + getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT ) if not interface.is_active: return ( sw_name, room, u'Machine non active / adherent non cotisant', - RadiusOption('non_member_vlan').vlan_id, - RadiusOption('non_member')!= RadiusOption.REJECT + getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT ) # Si on choisi de placer les machines sur le vlan # correspondant à leur type : From fc18a37fbf0166cb50970d0327c36b696e9ba184 Mon Sep 17 00:00:00 2001 From: detraz Date: Sat, 22 Dec 2018 00:35:39 +0100 Subject: [PATCH 41/86] =?UTF-8?q?Add=20start=20date=20recherche=20adh?= =?UTF-8?q?=C3=A9rents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/re2o/utils.py b/re2o/utils.py index 9836a98c..7ec4bd36 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -59,7 +59,7 @@ def all_adherent(search_time=None): vente__in=Vente.objects.filter( facture__in=Facture.objects.all().exclude(valid=False) ) - ).filter(date_end__gt=search_time) + ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time)) ) ) ).distinct() @@ -71,7 +71,7 @@ def all_baned(search_time=None): search_time = timezone.now() return User.objects.filter( ban__in=Ban.objects.filter( - date_end__gt=search_time + Q(date_start__lt=search_time) & Q(date_end__gt=search_time) ) ).distinct() @@ -82,7 +82,7 @@ def all_whitelisted(search_time=None): search_time = timezone.now() return User.objects.filter( whitelist__in=Whitelist.objects.filter( - date_end__gt=search_time + Q(date_start__lt=search_time) & Q(date_end__gt=search_time) ) ).distinct() @@ -94,8 +94,8 @@ def all_has_access(search_time=None): search_time = timezone.now() return User.objects.filter( Q(state=User.STATE_ACTIVE) & - ~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) & - (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) | + ~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) & + (Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) | Q(facture__in=Facture.objects.filter( vente__in=Vente.objects.filter( cotisation__in=Cotisation.objects.filter( @@ -104,7 +104,7 @@ def all_has_access(search_time=None): facture__in=Facture.objects.all() .exclude(valid=False) ) - ).filter(date_end__gt=search_time) + ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time)) ) ))) ).distinct() From bf4f46ef19d36e566140b388a3e7e0b03f78b6e3 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sat, 22 Dec 2018 13:10:31 +0100 Subject: [PATCH 42/86] Fix initial password for serviceuser --- users/forms.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/users/forms.py b/users/forms.py index 70d8798b..b5b8b602 100644 --- a/users/forms.py +++ b/users/forms.py @@ -492,13 +492,14 @@ class PasswordForm(FormRevMixin, ModelForm): class ServiceUserForm(FormRevMixin, ModelForm): - """ Modification d'un service user""" + """Service user creation + force initial password set""" password = forms.CharField( label=_("New password"), max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, - required=False + required=True ) class Meta: @@ -510,7 +511,7 @@ class ServiceUserForm(FormRevMixin, ModelForm): super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) def save(self, commit=True): - """Changement du mot de passe""" + """Password change""" user = super(ServiceUserForm, self).save(commit=False) if self.cleaned_data['password']: user.set_password(self.cleaned_data.get("password")) @@ -520,6 +521,14 @@ class ServiceUserForm(FormRevMixin, ModelForm): class EditServiceUserForm(ServiceUserForm): """Formulaire d'edition de base d'un service user. Ne permet d'editer que son group d'acl et son commentaire""" + password = forms.CharField( + label=_("New password"), + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput, + required=False + ) + class Meta(ServiceUserForm.Meta): fields = ['access_group', 'comment'] From 9e67fc5574fd24780b380bee0bdeb6675c0195e2 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 23 Dec 2018 18:48:25 +0100 Subject: [PATCH 43/86] =?UTF-8?q?Ipv6=20:=20Renvoie=20une=20liste=20vide?= =?UTF-8?q?=20plutot=20que=20None=20(=C3=A9valu=C3=A9=20=C3=A0=20null=20qu?= =?UTF-8?q?and=20meme)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- machines/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machines/models.py b/machines/models.py index a6a1e1ae..1ad783f1 100644 --- a/machines/models.py +++ b/machines/models.py @@ -1092,7 +1092,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): .get_cached_value('ipv6_mode') == 'DHCPV6'): return self.ipv6list.filter(slaac_ip=False) else: - return None + return [] def mac_bare(self): """ Formatage de la mac type mac_bare""" From f0c13edf4b19afa3960070bf6937fe07a80f579c Mon Sep 17 00:00:00 2001 From: Benjamin Graillot Date: Mon, 24 Dec 2018 15:01:19 +0100 Subject: [PATCH 44/86] [machines] Added a dnssec field to Extension --- machines/forms.py | 1 + machines/migrations/0097_extension_dnssec.py | 20 +++++++++++++++++++ machines/models.py | 4 ++++ .../templates/machines/aff_extension.html | 1 + 4 files changed, 26 insertions(+) create mode 100644 machines/migrations/0097_extension_dnssec.py diff --git a/machines/forms.py b/machines/forms.py index abc96811..94b9293a 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -273,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm): self.fields['origin'].label = _("A record origin") self.fields['origin_v6'].label = _("AAAA record origin") self.fields['soa'].label = _("SOA record to use") + self.fields['dnssec'].label = _("Sign with DNSSEC") class DelExtensionForm(FormRevMixin, Form): diff --git a/machines/migrations/0097_extension_dnssec.py b/machines/migrations/0097_extension_dnssec.py new file mode 100644 index 00000000..48e41f77 --- /dev/null +++ b/machines/migrations/0097_extension_dnssec.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-24 14:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0096_auto_20181013_1417'), + ] + + operations = [ + migrations.AddField( + model_name='extension', + name='dnssec', + field=models.BooleanField(default=False, help_text='Should the zone be signed with DNSSEC'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 1ad783f1..888e4ac0 100644 --- a/machines/models.py +++ b/machines/models.py @@ -696,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model): 'SOA', on_delete=models.CASCADE ) + dnssec = models.BooleanField( + default=False, + help_text=_("Should the zone be signed with DNSSEC") + ) class Meta: permissions = ( diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index b8493f5f..2eb893e9 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -56,6 +56,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} {% history_button extension %} + {{ extension.dnssec|tick }} {% endfor %} From 762091f2a43154c6da1d856b96deec36b1b6bac5 Mon Sep 17 00:00:00 2001 From: Benjamin Graillot Date: Mon, 24 Dec 2018 15:13:48 +0100 Subject: [PATCH 45/86] [machines] Fix extension template --- machines/templates/machines/aff_extension.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index 2eb893e9..fae4e25b 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if ipv6_enabled %} {% trans "AAAA record origin" %} {% endif %} + {% trans "DNSSEC" %} @@ -50,13 +51,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if ipv6_enabled %} {{ extension.origin_v6 }} {% endif %} + {{ extension.dnssec|tick }} {% can_edit extension %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% acl_end %} {% history_button extension %} - {{ extension.dnssec|tick }} {% endfor %} From e2845c469057efa3e04cca12e52e0385945e4668 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 27 Dec 2018 20:46:33 +0100 Subject: [PATCH 46/86] No more pseudo with uppercase --- users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/models.py b/users/models.py index a198cfb5..9f17615d 100755 --- a/users/models.py +++ b/users/models.py @@ -94,7 +94,7 @@ from preferences.models import OptionalMachine, MailMessageOption def linux_user_check(login): """ Validation du pseudo pour respecter les contraintes unix""" - UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9-]*[$]?$") + UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$") return UNIX_LOGIN_PATTERN.match(login) From e0fc3d384676bc30c16a0fac75b09e880b6d886a Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Fri, 28 Dec 2018 20:32:39 +0100 Subject: [PATCH 47/86] Menage ordre des fonctions du forms de users --- users/forms.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/users/forms.py b/users/forms.py index b5b8b602..ca01c52d 100644 --- a/users/forms.py +++ b/users/forms.py @@ -324,14 +324,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): self.fields['room'].empty_label = _("No room") self.fields['school'].empty_label = _("Select a school") - def clean_email(self): - if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'): - return self.cleaned_data.get('email').lower() - else: - raise forms.ValidationError( - _("You can't use a {} address.").format( - OptionalUser.objects.first().local_email_domain)) - class Meta: model = Adherent fields = [ @@ -345,6 +337,19 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): 'room', ] + force = forms.BooleanField( + label=_("Force the move?"), + initial=False, + required=False + ) + + def clean_email(self): + if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'): + return self.cleaned_data.get('email').lower() + else: + raise forms.ValidationError( + _("You can't use a {} address.").format( + OptionalUser.objects.first().local_email_domain)) def clean_telephone(self): """Verifie que le tel est présent si 'option est validée @@ -356,12 +361,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): ) return telephone - force = forms.BooleanField( - label=_("Force the move?"), - initial=False, - required=False - ) - def clean_force(self): """On supprime l'ancien user de la chambre si et seulement si la case est cochée""" From bfd79d44eb20276c4efa9d05e524e4d55279f939 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Fri, 28 Dec 2018 20:58:43 +0100 Subject: [PATCH 48/86] Fix #192 : Gpgfp validation et formatage --- users/forms.py | 7 +----- users/migrations/0079_auto_20181228_2039.py | 20 ++++++++++++++++ users/models.py | 26 ++++++++++++++++----- 3 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 users/migrations/0079_auto_20181228_2039.py diff --git a/users/forms.py b/users/forms.py index ca01c52d..81c31cae 100644 --- a/users/forms.py +++ b/users/forms.py @@ -368,6 +368,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): remove_user_room(self.cleaned_data.get('room')) return + class AdherentCreationForm(AdherentForm): """Formulaire de création d'un user. AdherentForm auquel on ajoute une checkbox afin d'éviter les @@ -398,12 +399,6 @@ class AdherentEditForm(AdherentForm): if 'shell' in self.fields: self.fields['shell'].empty_label = _("Default shell") - def clean_gpg_fingerprint(self): - """Format the GPG fingerprint""" - gpg_fingerprint = self.cleaned_data.get('gpg_fingerprint', None) - if gpg_fingerprint: - return gpg_fingerprint.replace(' ', '').upper() - class Meta: model = Adherent fields = [ diff --git a/users/migrations/0079_auto_20181228_2039.py b/users/migrations/0079_auto_20181228_2039.py new file mode 100644 index 00000000..79ab56d9 --- /dev/null +++ b/users/migrations/0079_auto_20181228_2039.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-28 19:39 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0078_auto_20181011_1405'), + ] + + operations = [ + migrations.AlterField( + model_name='adherent', + name='gpg_fingerprint', + field=models.CharField(blank=True, max_length=49, null=True), + ), + ] diff --git a/users/models.py b/users/models.py index 9f17615d..b8f79942 100755 --- a/users/models.py +++ b/users/models.py @@ -1053,20 +1053,27 @@ class Adherent(User): null=True ) gpg_fingerprint = models.CharField( - max_length=40, + max_length=49, blank=True, null=True, - validators=[RegexValidator( - '^[0-9A-F]{40}$', - message=_("A GPG fingerprint must contain 40 hexadecimal" - " characters.") - )] ) class Meta(User.Meta): verbose_name = _("member") verbose_name_plural = _("members") + def format_gpgfp(self): + """Format gpg finger print as AAAA BBBB... from a string AAAABBBB....""" + self.gpg_fingerprint = ' '.join([self.gpg_fingerprint[i:i + 4] for i in range(0, len(self.gpg_fingerprint), 4)]) + + def validate_gpgfp(self): + """Validate from raw entry if is it a valid gpg fp""" + if self.gpg_fingerprint: + gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper() + if not re.compile("^[0-9A-F]{40}$").match(gpg_fingerprint): + raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters")) + self.gpg_fingerprint = gpg_fingerprint + @classmethod def get_instance(cls, adherentid, *_args, **_kwargs): """Try to find an instance of `Adherent` with the given id. @@ -1097,6 +1104,13 @@ class Adherent(User): _("You don't have the right to create a user.") ) + def clean(self, *args, **kwargs): + """Format the GPG fingerprint""" + super(Adherent, self).clean(*args, **kwargs) + if self.gpg_fingerprint: + self.validate_gpgfp() + self.format_gpgfp() + class Club(User): """ A class representing a club (it is considered as a user From e076c1da33f340c90081d9447da535108af7bfa0 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Fri, 28 Dec 2018 23:23:05 +0100 Subject: [PATCH 49/86] Re match direct --- users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/models.py b/users/models.py index b8f79942..a0985675 100755 --- a/users/models.py +++ b/users/models.py @@ -1070,7 +1070,7 @@ class Adherent(User): """Validate from raw entry if is it a valid gpg fp""" if self.gpg_fingerprint: gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper() - if not re.compile("^[0-9A-F]{40}$").match(gpg_fingerprint): + if not re.match("^[0-9A-F]{40}$", gpg_fingerprint): raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters")) self.gpg_fingerprint = gpg_fingerprint From 70a5996f5d47b1091906e7e8397cf1ba94573c0c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Fri, 28 Dec 2018 23:27:56 +0100 Subject: [PATCH 50/86] Fix available articles --- cotisations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cotisations/models.py b/cotisations/models.py index 6dd63b6f..f8e53b50 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -624,7 +624,7 @@ class Article(RevMixin, AclMixin, models.Model): objects_pool = cls.objects.filter( Q(type_user='All') | Q(type_user='Adherent') ) - if not target_user.is_adherent(): + if target_user is not None and not target_user.is_adherent(): objects_pool = objects_pool.filter( Q(type_cotisation='All') | Q(type_cotisation='Adhesion') ) From 97593920e5a6d9adbfce7f23e5ea0a5e6d76b203 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sat, 29 Dec 2018 20:35:01 +0100 Subject: [PATCH 51/86] =?UTF-8?q?Fix=20#150=20:=20l'utilisateur=20asso=20e?= =?UTF-8?q?t=20ses=20machines=20ont=20toujours=20acc=C3=A8s=20=C3=A0=20int?= =?UTF-8?q?ernet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/utils.py | 17 ++++++++++++----- users/models.py | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/re2o/utils.py b/re2o/utils.py index 7ec4bd36..20218a81 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -42,7 +42,7 @@ from django.db.models import Q from cotisations.models import Cotisation, Facture, Vente from machines.models import Interface, Machine from users.models import Adherent, User, Ban, Whitelist - +from preferences.models import AssoOption def all_adherent(search_time=None): """ Fonction renvoyant tous les users adherents. Optimisee pour n'est @@ -88,11 +88,14 @@ def all_whitelisted(search_time=None): def all_has_access(search_time=None): - """ Renvoie tous les users beneficiant d'une connexion - : user adherent ou whiteliste et non banni """ + """ Return all connected users : active users and whitelisted + + asso_user defined in AssoOption pannel + ---- + Renvoie tous les users beneficiant d'une connexion + : user adherent et whiteliste non banni plus l'utilisateur asso""" if search_time is None: search_time = timezone.now() - return User.objects.filter( + filter_user = ( Q(state=User.STATE_ACTIVE) & ~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) & (Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) | @@ -107,7 +110,11 @@ def all_has_access(search_time=None): ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time)) ) ))) - ).distinct() + ) + asso_user = AssoOption.get_cached_value('utilisateur_asso') + if asso_user: + filter_user |= Q(id=asso_user.id) + return User.objects.filter(filter_user).distinct() def filter_active_interfaces(interface_set): diff --git a/users/models.py b/users/models.py index a0985675..fe96df1e 100755 --- a/users/models.py +++ b/users/models.py @@ -475,7 +475,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, """ Renvoie si un utilisateur a accès à internet """ return (self.state == User.STATE_ACTIVE and not self.is_ban() and - (self.is_connected() or self.is_whitelisted())) + (self.is_connected() or self.is_whitelisted())) \ + or self == AssoOption.get_cached_value('utilisateur_asso') def end_access(self): """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)""" From 573e6c2ad244a8e41f90f525e3b94fac5dc26563 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Dec 2018 12:06:20 +0100 Subject: [PATCH 52/86] Use a detailed list rather than a table on profile --- static/css/base.css | 18 +- users/templates/users/profil.html | 288 +++++++++++++++++------------- 2 files changed, 170 insertions(+), 136 deletions(-) diff --git a/static/css/base.css b/static/css/base.css index dcb88db8..05409553 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -79,19 +79,6 @@ a > i.fa { vertical-align: middle; } -/* Pull sidebars to the bottom */ -@media (min-width: 767px) { - .row { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } - .row > [class*='col-'] { - flex-direction: column; - } -} - /* On small screens, set height to 'auto' for sidenav and grid */ @media screen and (max-width: 767px) { .sidenav { @@ -145,3 +132,8 @@ th.long_text{ .dashboard{ text-align: center; } + +/* Detailed information on profile page */ +dl.profile-info > div { + padding: 8px; +} diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index cdc9a9d1..370089ff 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -117,147 +117,189 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
-

- {% trans " Detailed information" %} +
+

+ {% trans " Detailed information" %}

-
- - - {% trans "Edit" %} - - - - {% trans "Change the password" %} - - {% can_change User state %} - - - {% trans "Change the state" %} - - {% acl_end %} - {% can_change User groups %} - - - {% trans "Edit the groups" %} - - {% acl_end %} - {% history_button users text=True %} +
+ + + {% trans "Edit" %} + + + + {% trans "Change the password" %} + + {% can_change User state %} + + + {% trans "Change the state" %} + + {% acl_end %} + {% can_change User groups %} + + + {% trans "Edit the groups" %} + + {% acl_end %} + {% history_button users text=True %}
-
- - +
+
{% if users.is_class_club %} -
- {% if users.club.mailing %} - - {% else %} - - {% endif %} - {% else %} - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if users.end_adhesion != None %} - +
{% trans "Mailing" %}
+ {% if users.club.mailing %} +
{{ users.pseudo }}(-admin)
+ {% else %} +
{% trans "Mailing disabled" %}
+ {% endif %} {% else %} - +
{% trans "Firt name" %}
+
{{ users.name }}
{% endif %} - - {% if users.end_whitelist != None %} - - {% else %} - - {% endif %} - - - {% if users.end_ban != None %} - - {% else %} - - {% endif %} - - {% if users.state == 0 %} - - {% elif users.state == 1 %} - - {% elif users.state == 2 %} - - {% elif users.state == 3 %} - + +
+
{% trans "Surname" %}
+
{{ users.surname }}
+
+ +
+
{% trans "Username" %}
+
{{ users.pseudo }}
+
+ +
+
{% trans "Email address" %}
+
{{ users.email }}
+
+ +
+
{% trans "Room" %}
+
+ {{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} / + {{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %} +
+
+ +
+
{% trans "Telephone number" %}
+
{{ users.telephone }}
+
+ +
+
{% trans "School" %}
+
{{ users.school }}
+
+ +
+
{% trans "Comment" %}
+
{{ users.comment }}
+
+ +
+
{% trans "Registration date" %}
+
{{ users.registered }}
+
+ +
+
{% trans "Last login" %}
+
{{ users.last_login }}
+
+ +
+
{% trans "End of membership" %}
+ {% if users.end_adhesion != None %} +
{{ users.end_adhesion }}
+ {% else %} +
{% trans "Not a member" %}
{% endif %} -
- - + + +
+
{% trans "Whitelist" %}
+ {% if users.end_whitelist != None %} +
{{ users.end_whitelist }}
+ {% else %} +
{% trans "None" %}
+ {% endif %} +
+ +
+
{% trans "Ban" %}
+ {% if users.end_ban != None %} +
{{ users.end_ban }}
+ {% else %} +
{% trans "Not banned" %}
+ {% endif %} +
+ +
+
{% trans "State" %}
+ {% if users.state == 0 %} +
{% trans "Active" %}
+ {% elif users.state == 1 %} +
{% trans "Disabled" %}
+ {% elif users.state == 2 %} +
{% trans "Archived" %}
+ {% elif users.state == 3 %} +
{% trans "Not yet Member" %}
+ {% endif %} +
+ +
+
{% trans "Internet access" %}
{% if users.has_access == True %} -
+
+ {% blocktrans with end_access=users.end_access %}Active + (until {{ end_access }}){% endblocktrans %}
{% else %} - +
{% trans "Disabled" %}
{% endif %} - + + +
+
{% trans "Groups of rights" %}
{% if users.groups.all %} -
+
{{ users.groups.all|join:", " }}
{% else %} - +
{% trans "None" %}
{% endif %} - - - - - {% if users.adherent.gpg_fingerprint %} - - - {% endif %} - - - {% if users.shell %} - - - {% endif %} - -
{% trans "Mailing" %}{{ users.pseudo }}(-admin){% trans "Mailing disabled" %}{% trans "Firt name" %}{{ users.name }}{% trans "Surname" %}{{ users.surname }}
{% trans "Username" %}{{ users.pseudo }}{% trans "Email address" %}{{users.email}}
{% trans "Room" %}{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} / {{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}{% trans "Telephone number" %}{{ users.telephone }}
{% trans "School" %}{{ users.school }}{% trans "Comment" %}{{ users.comment }}
{% trans "Registration date" %}{{ users.registered }}{% trans "Last login" %}{{ users.last_login }}
{% trans "End of membership" %}{{ users.end_adhesion }}{% trans "Not a member" %}{% trans "Whitelist" %}{{ users.end_whitelist }}{% trans "None" %}
{% trans "Ban" %}{{ users.end_ban }}{% trans "Not banned" %}{% trans "State" %}{% trans "Active" %}{% trans "Disabled" %}{% trans "Archived" %}{% trans "Not yet Member" %}
{% trans "Internet access" %}{% blocktrans with end_access=users.end_access %}Active (until {{ end_access }}){% endblocktrans %}{% trans "Disabled" %}{% trans "Groups of rights" %}{{ users.groups.all|join:", "}}{% trans "None" %}
{% trans "Balance" %}{{ users.solde }} € - {% if user_solde %} - - - {% trans "Refill" %} - + + +
+
{% trans "Balance" %}
+
+ {{ users.solde }} € + {% if user_solde %} + + + {% trans "Refill" %} + + {% endif %} +
+
+ +
+ {% if users.adherent.gpg_fingerprint %} +
{% trans "GPG fingerprint" %}
+
{{ users.adherent.gpg_fingerprint }}
{% endif %} -
{% trans "GPG fingerprint" %}{{ users.adherent.gpg_fingerprint }}
{% trans "Shell" %}{{ users.shell }}
-
+
+ +
+ {% if users.shell %} +
{% trans "Shell" %}
+
{{ users.shell }}
+ {% endif %} +
+

From 5ae1a53172a79d812bbbaa1ad7ef97c5a9a2d7dd Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Dec 2018 12:35:41 +0100 Subject: [PATCH 53/86] Add lines to split profile information --- static/css/base.css | 6 ++++++ users/templates/users/profil.html | 16 ++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/static/css/base.css b/static/css/base.css index 05409553..a13c596f 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -134,6 +134,12 @@ th.long_text{ } /* Detailed information on profile page */ +dl.profile-info { + margin-top: -16px; + margin-bottom: 0; +} + dl.profile-info > div { padding: 8px; + border-top: 1px solid #ddd; } diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 370089ff..741ecac0 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -286,19 +286,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
- {% if users.adherent.gpg_fingerprint %} + {% if users.adherent.gpg_fingerprint %} +
{% trans "GPG fingerprint" %}
{{ users.adherent.gpg_fingerprint }}
- {% endif %} -
+
+ {% endif %} -
- {% if users.shell %} + {% if users.shell %} +
{% trans "Shell" %}
{{ users.shell }}
- {% endif %} -
+
+ {% endif %}
From 509390590bf7f3e9571d68b39cafdf932dd47baf Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Dec 2018 10:42:05 +0100 Subject: [PATCH 54/86] Split migration 0056 We can not change structure and migrate data at the same time on some db engines. So this commit splits the operation. --- ...radiusoption.py => 0056_1_radiusoption.py} | 27 --------------- preferences/migrations/0056_2_radiusoption.py | 33 +++++++++++++++++++ preferences/migrations/0056_3_radiusoption.py | 31 +++++++++++++++++ .../migrations/0057_auto_20181204_0757.py | 2 +- 4 files changed, 65 insertions(+), 28 deletions(-) rename preferences/migrations/{0056_radiusoption.py => 0056_1_radiusoption.py} (84%) create mode 100644 preferences/migrations/0056_2_radiusoption.py create mode 100644 preferences/migrations/0056_3_radiusoption.py diff --git a/preferences/migrations/0056_radiusoption.py b/preferences/migrations/0056_1_radiusoption.py similarity index 84% rename from preferences/migrations/0056_radiusoption.py rename to preferences/migrations/0056_1_radiusoption.py index e329f598..8a7cb45c 100644 --- a/preferences/migrations/0056_radiusoption.py +++ b/preferences/migrations/0056_1_radiusoption.py @@ -7,19 +7,6 @@ import django.db.models.deletion import re2o.mixins -def create_radius_policy(apps, schema_editor): - OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie') - RadiusOption = apps.get_model('preferences', 'RadiusOption') - - option,_ = OptionalTopologie.objects.get_or_create() - - radius_option = RadiusOption() - radius_option.radius_general_policy = option.radius_general_policy - radius_option.vlan_decision_ok = option.vlan_decision_ok - - radius_option.save() - - class Migration(migrations.Migration): dependencies = [ @@ -94,18 +81,4 @@ class Migration(migrations.Migration): name='vlan_decision_ok', field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'), ), - - migrations.RunPython(create_radius_policy), - migrations.RemoveField( - model_name='optionaltopologie', - name='radius_general_policy', - ), - migrations.RemoveField( - model_name='optionaltopologie', - name='vlan_decision_nok', - ), - migrations.RemoveField( - model_name='optionaltopologie', - name='vlan_decision_ok', - ), ] diff --git a/preferences/migrations/0056_2_radiusoption.py b/preferences/migrations/0056_2_radiusoption.py new file mode 100644 index 00000000..69d81055 --- /dev/null +++ b/preferences/migrations/0056_2_radiusoption.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +def create_radius_policy(apps, schema_editor): + OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie') + RadiusOption = apps.get_model('preferences', 'RadiusOption') + + option,_ = OptionalTopologie.objects.get_or_create() + + radius_option = RadiusOption() + radius_option.radius_general_policy = option.radius_general_policy + radius_option.vlan_decision_ok = option.vlan_decision_ok + + radius_option.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0055_generaloption_main_site_url'), + ('preferences', '0056_1_radiusoption'), + ] + + operations = [ + migrations.RunPython(create_radius_policy), + ] diff --git a/preferences/migrations/0056_3_radiusoption.py b/preferences/migrations/0056_3_radiusoption.py new file mode 100644 index 00000000..f3e5f98c --- /dev/null +++ b/preferences/migrations/0056_3_radiusoption.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 14:29 +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', '0095_auto_20180919_2225'), + ('preferences', '0055_generaloption_main_site_url'), + ('preferences', '0056_2_radiusoption'), + ] + + operations = [ + migrations.RemoveField( + model_name='optionaltopologie', + name='radius_general_policy', + ), + migrations.RemoveField( + model_name='optionaltopologie', + name='vlan_decision_nok', + ), + migrations.RemoveField( + model_name='optionaltopologie', + name='vlan_decision_ok', + ), + ] diff --git a/preferences/migrations/0057_auto_20181204_0757.py b/preferences/migrations/0057_auto_20181204_0757.py index ba4e1a6f..8d93fff9 100644 --- a/preferences/migrations/0057_auto_20181204_0757.py +++ b/preferences/migrations/0057_auto_20181204_0757.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('preferences', '0056_radiusoption'), + ('preferences', '0056_3_radiusoption'), ] operations = [ From 7318ee47583bb1cf7d0e68e209f2fcee72e4d267 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 14:56:25 +0100 Subject: [PATCH 55/86] Rename migration + revert function radiusoption --- preferences/migrations/0056_2_radiusoption.py | 5 ++++- .../{0057_auto_20181204_0757.py => 0056_4_radiusoption.py} | 0 2 files changed, 4 insertions(+), 1 deletion(-) rename preferences/migrations/{0057_auto_20181204_0757.py => 0056_4_radiusoption.py} (100%) diff --git a/preferences/migrations/0056_2_radiusoption.py b/preferences/migrations/0056_2_radiusoption.py index 69d81055..1a8ecccd 100644 --- a/preferences/migrations/0056_2_radiusoption.py +++ b/preferences/migrations/0056_2_radiusoption.py @@ -19,6 +19,9 @@ def create_radius_policy(apps, schema_editor): radius_option.save() +def revert_radius(apps, schema_editor): + pass + class Migration(migrations.Migration): @@ -29,5 +32,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(create_radius_policy), + migrations.RunPython(create_radius_policy, revert_radius), ] diff --git a/preferences/migrations/0057_auto_20181204_0757.py b/preferences/migrations/0056_4_radiusoption.py similarity index 100% rename from preferences/migrations/0057_auto_20181204_0757.py rename to preferences/migrations/0056_4_radiusoption.py From 58ede006968d0390e2cc49becc83cdb581d434ca Mon Sep 17 00:00:00 2001 From: detraz Date: Mon, 31 Dec 2018 00:13:01 +0100 Subject: [PATCH 56/86] Hotfix autocreate --- users/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/users/models.py b/users/models.py index fe96df1e..e4c25956 100755 --- a/users/models.py +++ b/users/models.py @@ -696,7 +696,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, def autoregister_machine(self, mac_address, nas_type): """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue sur le compte de l'user""" - if Machine.can_create(self): + allowed, _message = Machine.can_create(self, self.id) + if not allowed: return False, _("Maximum number of registered machines reached.") if not nas_type: return False, _("Re2o doesn't know wich machine type to assign.") From e7a7e81a2c9916a624b3aff11935677e9b5aae80 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 14:01:22 +0100 Subject: [PATCH 57/86] Add discount for custom invoices. --- cotisations/forms.py | 45 +++- .../templates/cotisations/facture.html | 196 ++++++++++-------- cotisations/tex.py | 15 ++ cotisations/views.py | 14 +- 4 files changed, 172 insertions(+), 98 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 01e52756..73cb4971 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -46,7 +46,7 @@ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin -from .models import Article, Paiement, Facture, Banque, CustomInvoice +from .models import Article, Paiement, Facture, Banque, CustomInvoice, Vente from .payment_methods import balance @@ -104,7 +104,44 @@ class SelectArticleForm(FormRevMixin, Form): user = kwargs.pop('user') target_user = kwargs.pop('target_user', None) super(SelectArticleForm, self).__init__(*args, **kwargs) - self.fields['article'].queryset = Article.find_allowed_articles(user, target_user) + self.fields['article'].queryset = Article.find_allowed_articles( + user, target_user) + + +class DiscountForm(Form): + """ + Form used in oder to create a discount on an invoice. + """ + is_relative = forms.BooleanField( + label=_("Discount is on percentage"), + required=False, + ) + discount = forms.DecimalField( + label=_("Discount"), + max_value=100, + min_value=0, + max_digits=5, + decimal_places=2, + required=False, + ) + + def apply_to_invoice(self, invoice): + invoice_price = invoice.prix_total() + discount = self.cleaned_data['discount'] + is_relative = self.cleaned_data['is_relative'] + if is_relative: + amount = discount/100 * invoice_price + else: + amount = discount + if amount > 0: + name = _("{}% discount") if is_relative else _("{}€ discount") + name = name.format(discount) + Vente.objects.create( + facture=invoice, + name=name, + prix=-amount, + number=1 + ) class CustomInvoiceForm(FormRevMixin, ModelForm): @@ -248,7 +285,8 @@ class RechargeForm(FormRevMixin, Form): super(RechargeForm, self).__init__(*args, **kwargs) self.fields['payment'].empty_label = \ _("Select a payment method") - self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True) + self.fields['payment'].queryset = Paiement.find_allowed_payments( + user_source).exclude(is_balance=True) def clean(self): """ @@ -266,4 +304,3 @@ class RechargeForm(FormRevMixin, Form): } ) return self.cleaned_data - diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 1f87f579..ff9ed837 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -44,6 +44,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}

{% endif %} +{% bootstrap_form_errors factureform %} +{% bootstrap_form_errors discount_form %}
{% csrf_token %} @@ -68,8 +70,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %} +

{% trans "Discount" %}

+ {% if discount_form %} + {% bootstrap_form discount_form %} + {% endif %}

- {% blocktrans %}Total price: 0,00 €{% endblocktrans %} + {% blocktrans %}Total price: 0,00 €{% endblocktrans %}

{% endif %} {% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %} @@ -78,105 +84,117 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if articlesformset or payment_method%} {% endif %} diff --git a/cotisations/tex.py b/cotisations/tex.py index 3f404f22..d6c0ae5f 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -36,6 +36,7 @@ from django.template import Context from django.http import HttpResponse from django.conf import settings from django.utils.text import slugify +import logging TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') @@ -93,6 +94,20 @@ def create_pdf(template, ctx={}): return pdf +def escape_chars(string): + """Escape the '%' and the '€' signs to avoid messing with LaTeX""" + if not isinstance(string, str): + return string + mapping = ( + ('€', r'\euro'), + ('%', r'\%'), + ) + r = str(string) + for k, v in mapping: + r = r.replace(k, v) + return r + + def render_tex(_request, template, ctx={}): """Creates a PDF from a LaTex templates using pdflatex. diff --git a/cotisations/views.py b/cotisations/views.py index 68118711..7d4185cc 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -80,9 +80,10 @@ from .forms import ( DelBanqueForm, SelectArticleForm, RechargeForm, - CustomInvoiceForm + CustomInvoiceForm, + DiscountForm ) -from .tex import render_invoice +from .tex import render_invoice, escape_chars from .payment_methods.forms import payment_method_factory from .utils import find_payment_method @@ -198,8 +199,9 @@ def new_custom_invoice(request): request.POST or None, form_kwargs={'user': request.user} ) + discount_form = DiscountForm(request.POST or None) - if invoice_form.is_valid() and articles_formset.is_valid(): + if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid(): new_invoice_instance = invoice_form.save() for art_item in articles_formset: if art_item.cleaned_data: @@ -213,6 +215,7 @@ def new_custom_invoice(request): duration=article.duration, number=quantity ) + discount_form.apply_to_invoice(new_invoice_instance) messages.success( request, _("The custom invoice was created.") @@ -223,7 +226,8 @@ def new_custom_invoice(request): 'factureform': invoice_form, 'action_name': _("Confirm"), 'articlesformset': articles_formset, - 'articlelist': articles + 'articlelist': articles, + 'discount_form': discount_form }, 'cotisations/facture.html', request) @@ -382,7 +386,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs): purchases_info = [] for purchase in purchases_objects: purchases_info.append({ - 'name': purchase.name, + 'name': escape_chars(purchase.name), 'price': purchase.prix, 'quantity': purchase.number, 'total_price': purchase.prix_total From fd57a9b9250543763be85d34d0b640ed999ad158 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 15:15:33 +0100 Subject: [PATCH 58/86] Display payment method on invoices --- .../templates/cotisations/factures.tex | 24 ++++++++++++------- cotisations/views.py | 6 +++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 3f2ebedc..226682e7 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -43,7 +43,7 @@ \begin{document} - + %---------------------------------------------------------------------------------------- % HEADING SECTION %---------------------------------------------------------------------------------------- @@ -70,7 +70,7 @@ {\bf Siret :} {{siret|safe}} \vspace{2cm} - + \begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r} {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ @@ -84,20 +84,20 @@ %---------------------------------------------------------------------------------------- % TABLE OF EXPENSES %---------------------------------------------------------------------------------------- - + \begin{tabularx}{\textwidth}{|X|r|r|r|} \hline \textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\ \doublehline - + {% for a in article %} {{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\ \hline {% endfor %} - + \end{tabularx} - + \vspace{1cm} \hfill @@ -109,14 +109,22 @@ \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ \hline \end{tabular} - + + \vspace{1cm} + \begin{tabularx}{\textwidth}{c X} + \hline + \textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\ + \hline + \end{tabularx} + + \vfill %---------------------------------------------------------------------------------------- % FOOTNOTE %---------------------------------------------------------------------------------------- - + \hrule \smallskip \footnotesize{TVA non applicable, art. 293 B du CGI} diff --git a/cotisations/views.py b/cotisations/views.py index 7d4185cc..902db508 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -270,7 +270,8 @@ def facture_pdf(request, facture, **_kwargs): 'siret': AssoOption.get_cached_value('siret'), 'email': AssoOption.get_cached_value('contact'), 'phone': AssoOption.get_cached_value('telephone'), - 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': facture.paiement.moyen, }) @@ -405,7 +406,8 @@ def custom_invoice_pdf(request, invoice, **_kwargs): 'siret': AssoOption.get_cached_value('siret'), 'email': AssoOption.get_cached_value('contact'), 'phone': AssoOption.get_cached_value('telephone'), - 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': invoice.payment, }) From f612e4192f8ae5d4a5cf039d0453979e3553508c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 15:25:52 +0100 Subject: [PATCH 59/86] Add remark to custom invoices --- .../migrations/0036_custominvoice_remark.py | 20 +++++++++++++++++++ cotisations/models.py | 5 +++++ .../templates/cotisations/factures.tex | 6 +++++- cotisations/views.py | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 cotisations/migrations/0036_custominvoice_remark.py diff --git a/cotisations/migrations/0036_custominvoice_remark.py b/cotisations/migrations/0036_custominvoice_remark.py new file mode 100644 index 00000000..7719b31d --- /dev/null +++ b/cotisations/migrations/0036_custominvoice_remark.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-29 14:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0035_notepayment'), + ] + + operations = [ + migrations.AddField( + model_name='custominvoice', + name='remark', + field=models.TextField(blank=True, null=True, verbose_name='Remark'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index f8e53b50..979b444a 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -286,6 +286,11 @@ class CustomInvoice(BaseInvoice): paid = models.BooleanField( verbose_name=_("Paid") ) + remark = models.TextField( + verbose_name=_("Remark"), + blank=True, + null=True + ) # TODO : change Vente to Purchase diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 226682e7..11e490d7 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -111,10 +111,14 @@ \end{tabular} \vspace{1cm} - \begin{tabularx}{\textwidth}{c X} + \begin{tabularx}{\textwidth}{r X} \hline \textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\ \hline + {% if remark %} + \textbf{Remarque} & {{remark|safe}} \\ + \hline + {% endif %} \end{tabularx} diff --git a/cotisations/views.py b/cotisations/views.py index 902db508..ec746bb7 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -408,6 +408,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs): 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), 'payment_method': invoice.payment, + 'remark': invoice.remark, }) From b85384b226a67f040c78aa34850aebf16d37a0e3 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 23:55:18 +0100 Subject: [PATCH 60/86] Do not fail on empty discount --- cotisations/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 73cb4971..56e90c58 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -133,7 +133,7 @@ class DiscountForm(Form): amount = discount/100 * invoice_price else: amount = discount - if amount > 0: + if amount: name = _("{}% discount") if is_relative else _("{}€ discount") name = name.format(discount) Vente.objects.create( From 37dbfd2fbf3d41590d12d08b9a50d00fbb7fcd27 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 31 Dec 2018 23:58:37 +0100 Subject: [PATCH 61/86] Add Cost Estimates --- cotisations/admin.py | 8 +- cotisations/forms.py | 14 +- cotisations/migrations/0037_costestimate.py | 28 +++ .../migrations/0038_auto_20181231_1657.py | 31 +++ cotisations/models.py | 54 ++++- .../cotisations/aff_cost_estimate.html | 101 +++++++++ .../templates/cotisations/edit_facture.html | 4 + .../templates/cotisations/factures.tex | 10 + .../cotisations/index_cost_estimate.html | 36 ++++ .../templates/cotisations/sidebar.html | 5 + cotisations/tex.py | 3 +- cotisations/urls.py | 30 +++ cotisations/views.py | 196 +++++++++++++++++- 13 files changed, 511 insertions(+), 9 deletions(-) create mode 100644 cotisations/migrations/0037_costestimate.py create mode 100644 cotisations/migrations/0038_auto_20181231_1657.py create mode 100644 cotisations/templates/cotisations/aff_cost_estimate.html create mode 100644 cotisations/templates/cotisations/index_cost_estimate.html diff --git a/cotisations/admin.py b/cotisations/admin.py index afe4621c..4b47ccc8 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,7 +30,7 @@ from django.contrib import admin from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente -from .models import CustomInvoice +from .models import CustomInvoice, CostEstimate class FactureAdmin(VersionAdmin): @@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin): pass +class CostEstimateAdmin(VersionAdmin): + """Admin class for cost estimates.""" + pass + + class CustomInvoiceAdmin(VersionAdmin): """Admin class for custom invoices.""" pass @@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin) admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin) +admin.site.register(CostEstimate, CostEstimateAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 56e90c58..57bd7355 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin -from .models import Article, Paiement, Facture, Banque, CustomInvoice, Vente +from .models import ( + Article, Paiement, Facture, Banque, + CustomInvoice, Vente, CostEstimate +) from .payment_methods import balance @@ -153,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm): fields = '__all__' +class CostEstimateForm(FormRevMixin, ModelForm): + """ + Form used to create a cost estimate. + """ + class Meta: + model = CostEstimate + exclude = ['paid', 'final_invoice'] + + class ArticleForm(FormRevMixin, ModelForm): """ Form used to create an article. diff --git a/cotisations/migrations/0037_costestimate.py b/cotisations/migrations/0037_costestimate.py new file mode 100644 index 00000000..3d97f3f3 --- /dev/null +++ b/cotisations/migrations/0037_costestimate.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-29 21:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0036_custominvoice_remark'), + ] + + operations = [ + migrations.CreateModel( + name='CostEstimate', + fields=[ + ('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')), + ('validity', models.DurationField(verbose_name='Period of validity')), + ('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')), + ], + options={ + 'permissions': (('view_costestimate', 'Can view a cost estimate object'),), + }, + bases=('cotisations.custominvoice',), + ), + ] diff --git a/cotisations/migrations/0038_auto_20181231_1657.py b/cotisations/migrations/0038_auto_20181231_1657.py new file mode 100644 index 00000000..a9415bf0 --- /dev/null +++ b/cotisations/migrations/0038_auto_20181231_1657.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-31 22:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0037_costestimate'), + ] + + operations = [ + migrations.AlterField( + model_name='costestimate', + name='final_invoice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'), + ), + migrations.AlterField( + model_name='costestimate', + name='validity', + field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'), + ), + migrations.AlterField( + model_name='custominvoice', + name='paid', + field=models.BooleanField(default=False, verbose_name='Paid'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 979b444a..623db068 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -284,7 +284,8 @@ class CustomInvoice(BaseInvoice): verbose_name=_("Address") ) paid = models.BooleanField( - verbose_name=_("Paid") + verbose_name=_("Paid"), + default=False ) remark = models.TextField( verbose_name=_("Remark"), @@ -293,6 +294,57 @@ class CustomInvoice(BaseInvoice): ) +class CostEstimate(CustomInvoice): + class Meta: + permissions = ( + ('view_costestimate', _("Can view a cost estimate object")), + ) + validity = models.DurationField( + verbose_name=_("Period of validity"), + help_text="DD HH:MM:SS" + ) + final_invoice = models.ForeignKey( + CustomInvoice, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="origin_cost_estimate", + primary_key=False + ) + + def create_invoice(self): + """Create a CustomInvoice from the CostEstimate.""" + if self.final_invoice is not None: + return self.final_invoice + invoice = CustomInvoice() + invoice.recipient = self.recipient + invoice.payment = self.payment + invoice.address = self.address + invoice.paid = False + invoice.remark = self.remark + invoice.date = timezone.now() + invoice.save() + self.final_invoice = invoice + self.save() + for sale in self.vente_set.all(): + Vente.objects.create( + facture=invoice, + name=sale.name, + prix=sale.prix, + number=sale.number, + ) + return invoice + + def can_delete(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.delete_costestimate'): + return False, _("You don't have the right " + "to delete a cost estimate.") + if self.final_invoice is not None: + return False, _("The cost estimate has an " + "invoice and cannot be deleted.") + return True, None + + # TODO : change Vente to Purchase class Vente(RevMixin, AclMixin, models.Model): """ diff --git a/cotisations/templates/cotisations/aff_cost_estimate.html b/cotisations/templates/cotisations/aff_cost_estimate.html new file mode 100644 index 00000000..d4a3f60d --- /dev/null +++ b/cotisations/templates/cotisations/aff_cost_estimate.html @@ -0,0 +1,101 @@ +{% 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 Hugo Levy-Falk + +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 i18n %} +{% load acl %} +{% load logs_extra %} +{% load design %} + +
+ {% if cost_estimate_list.paginator %} + {% include 'pagination.html' with list=cost_estimate_list%} + {% endif %} + + + + + + + + + + + + + + + + + {% for estimate in cost_estimate_list %} + + + + + + + + + + + + {% endfor %} +
+ {% trans "Recipient" as tr_recip %} + {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %} + {% trans "Designation" %}{% trans "Total price" %} + {% trans "Payment method" as tr_payment_method %} + {% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %} + + {% trans "Date" as tr_date %} + {% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %} + + {% trans "Validity" as tr_validity %} + {% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %} + + {% trans "Cost estimate ID" as tr_estimate_id %} + {% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %} + + {% trans "Invoice created" as tr_invoice_created%} + {% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %} +
{{ estimate.recipient }}{{ estimate.name }}{{ estimate.prix_total }}{{ estimate.payment }}{{ estimate.date }}{{ estimate.validity }}{{ estimate.id }} + {% if estimate.final_invoice %} + + {% else %} + ' + {% endif %} + + {% can_edit estimate %} + {% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %} + {% acl_end %} + {% history_button estimate %} + {% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %} + + + + + {% trans "PDF" %} + +
+ + {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} +
diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index a00084f6..c7a6975c 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} + {% if title %} +

{{title}}

+ {% else %}

{% trans "Edit the invoice" %}

+ {% endif %} {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }}

{% trans "Articles" %}

diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 11e490d7..2cfd4f46 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -75,8 +75,12 @@ {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ {% if fid is not None %} + {% if is_estimate %} + {\bf Devis n\textsuperscript{o} :} {{ fid }} & \\ + {% else %} {\bf Facture n\textsuperscript{o} :} {{ fid }} & \\ {% endif %} + {% endif %} \end{tabular*} \\ @@ -104,9 +108,11 @@ \begin{tabular}{|l|r|} \hline \textbf{Total} & {{total|floatformat:2}} \euro \\ + {% if not is_estimate %} \textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\ \doublehline \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ + {% endif %} \hline \end{tabular} @@ -119,6 +125,10 @@ \textbf{Remarque} & {{remark|safe}} \\ \hline {% endif %} + {% if end_validity %} + \textbf{Validité} & Jusqu'au {{end_validity}} \\ + \hline + {% endif %} \end{tabularx} diff --git a/cotisations/templates/cotisations/index_cost_estimate.html b/cotisations/templates/cotisations/index_cost_estimate.html new file mode 100644 index 00000000..a0b3a661 --- /dev/null +++ b/cotisations/templates/cotisations/index_cost_estimate.html @@ -0,0 +1,36 @@ +{% extends "cotisations/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 acl %} +{% load i18n %} + +{% block title %}{% trans "Cost estimates" %}{% endblock %} + +{% block content %} +

{% trans "Cost estimates list" %}

+{% can_create CostEstimate %} +{% include "buttons/add.html" with href='cotisations:new-cost-estimate'%} +{% acl_end %} +{% include 'cotisations/aff_cost_estimate.html' %} +{% endblock %} diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 4f077fad..c3240a9a 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Custom invoices" %} {% acl_end %} + {% can_view_all CostEstimate %} + + {% trans "Cost estimate" %} + + {% acl_end %} {% can_view_all Article %} {% trans "Available articles" %} diff --git a/cotisations/tex.py b/cotisations/tex.py index d6c0ae5f..4d3715af 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -49,8 +49,9 @@ def render_invoice(_request, ctx={}): Render an invoice using some available information such as the current date, the user, the articles, the prices, ... """ + is_estimate = ctx.get('is_estimate', False) filename = '_'.join([ - 'invoice', + 'cost_estimate' if is_estimate else 'invoice', slugify(ctx.get('asso_name', "")), slugify(ctx.get('recipient_name', "")), str(ctx.get('DATE', datetime.now()).year), diff --git a/cotisations/urls.py b/cotisations/urls.py index edc448fe..45032fe2 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -51,11 +51,41 @@ urlpatterns = [ views.facture_pdf, name='facture-pdf' ), + url( + r'^new_cost_estimate/$', + views.new_cost_estimate, + name='new-cost-estimate' + ), + url( + r'^index_cost_estimate/$', + views.index_cost_estimate, + name='index-cost-estimate' + ), + url( + r'^cost_estimate_pdf/(?P[0-9]+)$', + views.cost_estimate_pdf, + name='cost-estimate-pdf', + ), url( r'^index_custom_invoice/$', views.index_custom_invoice, name='index-custom-invoice' ), + url( + r'^edit_cost_estimate/(?P[0-9]+)$', + views.edit_cost_estimate, + name='edit-cost-estimate' + ), + url( + r'^cost_estimate_to_invoice/(?P[0-9]+)$', + views.cost_estimate_to_invoice, + name='cost-estimate-to-invoice' + ), + url( + r'^del_cost_estimate/(?P[0-9]+)$', + views.del_cost_estimate, + name='del-cost-estimate' + ), url( r'^new_custom_invoice/$', views.new_custom_invoice, diff --git a/cotisations/views.py b/cotisations/views.py index ec746bb7..d4805dc2 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -68,7 +68,8 @@ from .models import ( Paiement, Banque, CustomInvoice, - BaseInvoice + BaseInvoice, + CostEstimate ) from .forms import ( FactureForm, @@ -81,7 +82,8 @@ from .forms import ( SelectArticleForm, RechargeForm, CustomInvoiceForm, - DiscountForm + DiscountForm, + CostEstimateForm, ) from .tex import render_invoice, escape_chars from .payment_methods.forms import payment_method_factory @@ -179,7 +181,58 @@ def new_facture(request, user, userid): ) -# TODO : change facture to invoice +@login_required +@can_create(CostEstimate) +def new_cost_estimate(request): + """ + View used to generate a custom invoice. It's mainly used to + get invoices that are not taken into account, for the administrative + point of view. + """ + # The template needs the list of articles (for the JS part) + articles = Article.objects.filter( + Q(type_user='All') | Q(type_user=request.user.class_name) + ) + # Building the invocie form and the article formset + cost_estimate_form = CostEstimateForm(request.POST or None) + + articles_formset = formset_factory(SelectArticleForm)( + request.POST or None, + form_kwargs={'user': request.user} + ) + discount_form = DiscountForm(request.POST or None) + + if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid(): + cost_estimate_instance = cost_estimate_form.save() + for art_item in articles_formset: + if art_item.cleaned_data: + article = art_item.cleaned_data['article'] + quantity = art_item.cleaned_data['quantity'] + Vente.objects.create( + facture=cost_estimate_instance, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + discount_form.apply_to_invoice(cost_estimate_instance) + messages.success( + request, + _("The cost estimate was created.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + + return form({ + 'factureform': cost_estimate_form, + 'action_name': _("Confirm"), + 'articlesformset': articles_formset, + 'articlelist': articles, + 'discount_form': discount_form, + 'title': _("Cost estimate"), + }, 'cotisations/facture.html', request) + + @login_required @can_create(CustomInvoice) def new_custom_invoice(request): @@ -336,6 +389,55 @@ def del_facture(request, facture, **_kwargs): }, 'cotisations/delete.html', request) +@login_required +@can_edit(CostEstimate) +def edit_cost_estimate(request, invoice, **kwargs): + # Building the invocie form and the article formset + invoice_form = CostEstimateForm( + request.POST or None, + instance=invoice + ) + purchases_objects = Vente.objects.filter(facture=invoice) + purchase_form_set = modelformset_factory( + Vente, + fields=('name', 'number'), + extra=0, + max_num=len(purchases_objects) + ) + purchase_form = purchase_form_set( + request.POST or None, + queryset=purchases_objects + ) + if invoice_form.is_valid() and purchase_form.is_valid(): + if invoice_form.changed_data: + invoice_form.save() + purchase_form.save() + messages.success( + request, + _("The cost estimate was edited.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + + return form({ + 'factureform': invoice_form, + 'venteform': purchase_form, + 'title': "Edit the cost estimate" + }, 'cotisations/edit_facture.html', request) + + +@login_required +@can_edit(CostEstimate) +@can_create(CustomInvoice) +def cost_estimate_to_invoice(request, cost_estimate, **_kwargs): + """Create a custom invoice from a cos estimate""" + cost_estimate.create_invoice() + messages.success( + request, + _("An invoice was successfully created from your cost estimate.") + ) + return redirect(reverse('cotisations:index-custom-invoice')) + + @login_required @can_edit(CustomInvoice) def edit_custom_invoice(request, invoice, **kwargs): @@ -371,6 +473,68 @@ def edit_custom_invoice(request, invoice, **kwargs): }, 'cotisations/edit_facture.html', request) +@login_required +@can_view(CostEstimate) +def cost_estimate_pdf(request, invoice, **_kwargs): + """ + View used to generate a PDF file from an existing cost estimate in database + Creates a line for each Purchase (thus article sold) and generate the + invoice with the total price, the payment method, the address and the + legal information for the user. + """ + # TODO : change vente to purchase + purchases_objects = Vente.objects.all().filter(facture=invoice) + # Get the article list and build an list out of it + # contiaining (article_name, article_price, quantity, total_price) + purchases_info = [] + for purchase in purchases_objects: + purchases_info.append({ + 'name': escape_chars(purchase.name), + 'price': purchase.prix, + 'quantity': purchase.number, + 'total_price': purchase.prix_total + }) + return render_invoice(request, { + 'paid': invoice.paid, + 'fid': invoice.id, + 'DATE': invoice.date, + 'recipient_name': invoice.recipient, + 'address': invoice.address, + 'article': purchases_info, + 'total': invoice.prix_total(), + 'asso_name': AssoOption.get_cached_value('name'), + 'line1': AssoOption.get_cached_value('adresse1'), + 'line2': AssoOption.get_cached_value('adresse2'), + 'siret': AssoOption.get_cached_value('siret'), + 'email': AssoOption.get_cached_value('contact'), + 'phone': AssoOption.get_cached_value('telephone'), + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': invoice.payment, + 'remark': invoice.remark, + 'end_validity': invoice.date + invoice.validity, + 'is_estimate': True, + }) + + +@login_required +@can_delete(CostEstimate) +def del_cost_estimate(request, estimate, **_kwargs): + """ + View used to delete an existing invocie. + """ + if request.method == "POST": + estimate.delete() + messages.success( + request, + _("The cost estimate was deleted.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + return form({ + 'objet': estimate, + 'objet_name': _("Cost Estimate") + }, 'cotisations/delete.html', request) + + @login_required @can_view(CustomInvoice) def custom_invoice_pdf(request, invoice, **_kwargs): @@ -412,7 +576,6 @@ def custom_invoice_pdf(request, invoice, **_kwargs): }) -# TODO : change facture to invoice @login_required @can_delete(CustomInvoice) def del_custom_invoice(request, invoice, **_kwargs): @@ -763,12 +926,35 @@ def index_banque(request): }) +@login_required +@can_view_all(CustomInvoice) +def index_cost_estimate(request): + """View used to display every custom invoice.""" + pagination_number = GeneralOption.get_cached_value('pagination_number') + cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set') + cost_estimate_list = SortTable.sort( + cost_estimate_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_CUSTOM + ) + cost_estimate_list = re2o_paginator( + request, + cost_estimate_list, + pagination_number, + ) + return render(request, 'cotisations/index_cost_estimate.html', { + 'cost_estimate_list': cost_estimate_list + }) + + @login_required @can_view_all(CustomInvoice) def index_custom_invoice(request): """View used to display every custom invoice.""" pagination_number = GeneralOption.get_cached_value('pagination_number') - custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set') + cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')] + custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids) custom_invoice_list = SortTable.sort( custom_invoice_list, request.GET.get('col'), From 4de9c1efd264c454275af5f98661b502485b1175 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Dec 2018 15:27:07 +0100 Subject: [PATCH 62/86] Make URL hash control Bootstrap collapse --- static/js/collapse-from-url.js | 33 +++++++++++++++++++++++++++++++++ templates/base.html | 1 + 2 files changed, 34 insertions(+) create mode 100644 static/js/collapse-from-url.js diff --git a/static/js/collapse-from-url.js b/static/js/collapse-from-url.js new file mode 100644 index 00000000..6c85762b --- /dev/null +++ b/static/js/collapse-from-url.js @@ -0,0 +1,33 @@ +// 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 Alexandre Iooss +// +// 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. + +// This script makes URL hash controls Bootstrap collapse +// e.g. if there is #information in the URL +// then the collapse with id "information" will be open. + +$(document).ready(function () { + if(location.hash != null && location.hash !== ""){ + // Open the collapse corresponding to URL hash + $(location.hash + '.collapse').collapse('show'); + } else { + // Open default collapse + $('.collapse-default.collapse').collapse('show'); + } +}); diff --git a/templates/base.html b/templates/base.html index 76ba975a..867be422 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,6 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% bootstrap_javascript %} + {# Load CSS #} {% bootstrap_css %} From 45cda20c71249837d83b60ba7559ee901f35f770 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Dec 2018 15:36:15 +0100 Subject: [PATCH 63/86] Rename user profile collapses --- users/templates/users/profil.html | 46 +++++++++++++++++-------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 741ecac0..b4fa7875 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -23,7 +23,6 @@ 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 logs_extra %} {% load design %} @@ -78,8 +77,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if solde_activated %}
-
- {{ users.solde }} +
+ {{ users.solde }}
@@ -92,8 +92,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if nb_machines %}
-
- {% trans " Machines" %} {{ nb_machines }} +
+ {% trans " Machines" %} {{ nb_machines }}
{% else %}
-
{% trans "No machine" %}
+
+ {% trans "No machine" %} +
{% trans " Add a machine" %} @@ -118,12 +122,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
+ data-target="#information">

{% trans " Detailed information" %}

-
+
{% if users.is_class_club %}
-
+

{% trans " Manage the club" %}

-
+
{% endif %}
-
+

{% trans "Machines" %} {{nb_machines}}

-
+
-
+

{% trans "Subscriptions" %}

-
+
-
+

{% trans "Bans" %}

-
+
-
+

{% trans "Whitelists" %}

-
+
-
+

{% trans " Email settings" %}

-
+
{% can_edit users %} From 3ed137cf3183b15b3ed9e4c630d0191c4b1c7998 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Dec 2018 16:28:03 +0100 Subject: [PATCH 64/86] Paginator styling and go to id feature --- machines/templates/machines/aff_machines.html | 4 +- templates/pagination.html | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index 60e1a57a..d5a83ed3 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if machines_list.paginator %} - {% include "pagination.html" with list=machines_list %} + {% include "pagination.html" with list=machines_list go_to_id="machines" %} {% endif %} @@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if machines_list.paginator %} - {% include "pagination.html" with list=machines_list %} + {% include "pagination.html" with list=machines_list go_to_id="machines" %} {% endif %} diff --git a/templates/pagination.html b/templates/pagination.html index cf488c5d..5ecced6d 100644 --- a/templates/pagination.html +++ b/templates/pagination.html @@ -23,23 +23,52 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load url_insert_param %} +{% load i18n %} {% if list.paginator.num_pages > 1 %} - {% endif %} - From e1729235780fd9a4523a2534ec39a1a038082370 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 00:18:35 +0100 Subject: [PATCH 65/86] Fix #103 --- topologie/models.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/topologie/models.py b/topologie/models.py index cd191d7e..25a7bda6 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -281,31 +281,18 @@ class Switch(AclMixin, Machine): def create_ports(self, begin, end): """ Crée les ports de begin à end si les valeurs données sont cohérentes. """ - - s_begin = s_end = 0 - nb_ports = self.ports.count() - if nb_ports > 0: - ports = self.ports.order_by('port').values('port') - s_begin = ports.first().get('port') - s_end = ports.last().get('port') - if end < begin: raise ValidationError(_("The end port is less than the start" " port.")) - if end - begin > self.number: + ports_to_create = list(range(begin, end + 1)) + existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True) + non_existing_ports = list(set(ports_to_create) - set(existing_ports)) + + if len(non_existing_ports) + existing_ports.count() > self.number: raise ValidationError(_("This switch can't have that many ports.")) - begin_range = range(begin, s_begin) - end_range = range(s_end+1, end+1) - for i in itertools.chain(begin_range, end_range): - port = Port() - port.switch = self - port.port = i - try: - with transaction.atomic(), reversion.create_revision(): - port.save() - reversion.set_comment(_("Creation")) - except IntegrityError: - ValidationError(_("Creation of an existing port.")) + with transaction.atomic(), reversion.create_revision(): + reversion.set_comment(_("Creation")) + Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports]) def main_interface(self): """ Returns the 'main' interface of the switch From 92e6ae45ad22413f370008f5fb5a83b81a77e9ee Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 00:34:17 +0100 Subject: [PATCH 66/86] Refactor aussi la fonction du views pour la route --- topologie/views.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/topologie/views.py b/topologie/views.py index a4db2dc6..d852177d 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -533,19 +533,14 @@ def create_ports(request, switchid): except Switch.DoesNotExist: messages.error(request, _("Nonexistent switch")) return redirect(reverse('topologie:index')) - - s_begin = s_end = 0 - nb_ports = switch.ports.count() - if nb_ports > 0: - ports = switch.ports.order_by('port').values('port') - s_begin = ports.first().get('port') - s_end = ports.last().get('port') - + + first_port = getattr(switch.ports.order_by('port').first(), 'port', 1) + s_begin = first_port + s_end = switch.number + first_port - 1 port_form = CreatePortsForm( request.POST or None, initial={'begin': s_begin, 'end': s_end} ) - if port_form.is_valid(): begin = port_form.cleaned_data['begin'] end = port_form.cleaned_data['end'] From 7ddb627b6b0f780334712146e34dc8cdd73efcea Mon Sep 17 00:00:00 2001 From: klafyvel Date: Sun, 30 Dec 2018 14:27:41 +0100 Subject: [PATCH 67/86] Do not create an useless list since `set` can be created from any iterable. --- topologie/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topologie/models.py b/topologie/models.py index 25a7bda6..269516ff 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -284,7 +284,7 @@ class Switch(AclMixin, Machine): if end < begin: raise ValidationError(_("The end port is less than the start" " port.")) - ports_to_create = list(range(begin, end + 1)) + ports_to_create = range(begin, end + 1) existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True) non_existing_ports = list(set(ports_to_create) - set(existing_ports)) From c238a9a2fa2dac6febc8bd5a5b01ac3bd25732cd Mon Sep 17 00:00:00 2001 From: klafyvel Date: Sun, 30 Dec 2018 14:31:36 +0100 Subject: [PATCH 68/86] Remove useless variable in create_ports and give explicit name to `s_end`. --- topologie/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/topologie/views.py b/topologie/views.py index d852177d..89d48b0b 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -535,11 +535,10 @@ def create_ports(request, switchid): return redirect(reverse('topologie:index')) first_port = getattr(switch.ports.order_by('port').first(), 'port', 1) - s_begin = first_port - s_end = switch.number + first_port - 1 + last_port = switch.number + first_port - 1 port_form = CreatePortsForm( request.POST or None, - initial={'begin': s_begin, 'end': s_end} + initial={'begin': first_port, 'end': last_port} ) if port_form.is_valid(): begin = port_form.cleaned_data['begin'] From 71dc9c9bd1ec3468bc49f712723267b09ef65e7d Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 1 Jan 2019 19:32:40 +0100 Subject: [PATCH 69/86] Remove code about radius in OptionalTopology edit form. --- preferences/forms.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/preferences/forms.py b/preferences/forms.py index 1126b399..fd052edd 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -115,11 +115,6 @@ class EditOptionalTopologieForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['radius_general_policy'].label = _("RADIUS general policy") - self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted" - " by RADIUS") - self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected" - " by RADIUS") self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name') From 21f54486b20e8413153e53d00fa5cab3132307c9 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 1 Jan 2019 20:17:27 +0100 Subject: [PATCH 70/86] Fix switch creation --- topologie/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topologie/models.py b/topologie/models.py index 269516ff..e08a1b21 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -304,7 +304,7 @@ class Switch(AclMixin, Machine): @cached_property def get_name(self): - return self.name or self.main_interface().domain.name + return self.name or getattr(self.main_interface(), 'domain', 'Unknown') @cached_property def get_radius_key(self): From c76a5d237679e707b224b7867919a35e7347349d Mon Sep 17 00:00:00 2001 From: Maxime Bombar Date: Tue, 1 Jan 2019 16:11:02 +0100 Subject: [PATCH 71/86] Prevents from crashing where there is no defined prefix_v6 --- machines/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/machines/models.py b/machines/models.py index 888e4ac0..1cf840d7 100644 --- a/machines/models.py +++ b/machines/models.py @@ -1380,7 +1380,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): .filter(interface=self.interface, slaac_ip=True) .exclude(id=self.id)): raise ValidationError(_("A SLAAC IP address is already registered.")) - prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') + try: + prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') + except AttributeError: # Prevents from crashing when there is no defined prefix_v6 + prefix_v6 = None if prefix_v6: if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]): From 4ac403ec29fbfc1ec8bec76deeed7b8380948d68 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Wed, 2 Jan 2019 15:18:40 +0100 Subject: [PATCH 72/86] Hotfix boostrapform --- cotisations/templates/cotisations/facture.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index ff9ed837..8a1a6d7a 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -44,8 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}

{% endif %} +{% if factureform %} {% bootstrap_form_errors factureform %} +{% endif %} +{% if discount_form %} {% bootstrap_form_errors discount_form %} +{% endif %} {% csrf_token %} From 02229983a024fdce8020ffd60649a1f303841031 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 11:27:54 +0100 Subject: [PATCH 73/86] =?UTF-8?q?Exporte=20la=20liste=20des=20r=C3=A9cursi?= =?UTF-8?q?fs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preferences/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/preferences/models.py b/preferences/models.py index 4644aa1c..e8476354 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -278,12 +278,13 @@ class OptionalTopologie(AclMixin, PreferencesModel): log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type) radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type) dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server") + dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursif-server").filter(type__ip_type=self.switchs_ip_type) subnet = None subnet6 = None if self.switchs_ip_type: subnet = self.switchs_ip_type.ip_set_full_info subnet6 = self.switchs_ip_type.ip6_set_full_info - return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6} + return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6} @cached_property def provision_switchs_enabled(self): From 2c9ff4ea8e7c78e226cead8b2fb179d0b5a91ece Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 17:04:21 +0100 Subject: [PATCH 74/86] Module switch, model et fonctions basiques de modification --- topologie/forms.py | 21 ++++++ .../migrations/0067_auto_20181230_1556.py | 63 +++++++++++++++++ topologie/models.py | 64 +++++++++++++++++ .../templates/topologie/aff_modules.html | 67 ++++++++++++++++++ .../templates/topologie/index_module.html | 43 ++++++++++++ topologie/templates/topologie/sidebar.html | 4 ++ topologie/templates/topologie/topo.html | 2 +- topologie/urls.py | 8 ++- topologie/views.py | 70 +++++++++++++++++++ 9 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 topologie/migrations/0067_auto_20181230_1556.py create mode 100644 topologie/templates/topologie/aff_modules.html create mode 100644 topologie/templates/topologie/index_module.html diff --git a/topologie/forms.py b/topologie/forms.py index fa089507..dc4a5e9b 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -55,6 +55,7 @@ from .models import ( SwitchBay, Building, PortProfile, + ModuleSwitch ) @@ -269,3 +270,23 @@ class EditPortProfileForm(FormRevMixin, ModelForm): prefix=prefix, **kwargs) +class EditModuleForm(FormRevMixin, ModelForm): + """Add and edit module instance""" + class Meta: + model = ModuleSwitch + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs) + self.fields['switchs'].queryset = (Switch.objects.filter(model__is_modular=True)) + + def save(self, commit=True): + # TODO : None of the parents of ServiceForm use the commit + # parameter in .save() + instance = super(EditModuleForm, self).save(commit=False) + if commit: + instance.save() + instance.process_link(self.cleaned_data.get('switchs')) + return instance + diff --git a/topologie/migrations/0067_auto_20181230_1556.py b/topologie/migrations/0067_auto_20181230_1556.py new file mode 100644 index 00000000..cd0d368c --- /dev/null +++ b/topologie/migrations/0067_auto_20181230_1556.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-30 14:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0066_modelswitch_commercial_name'), + ] + + operations = [ + migrations.CreateModel( + name='ModuleOnSwitch', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slot', models.CharField(help_text='Slot on switch', max_length=15, verbose_name='Slot')), + ], + options={ + 'verbose_name': 'link between switchs and modules', + 'permissions': (('view_moduleonswitch', 'Can view a moduleonswitch object'),), + }, + bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model), + ), + migrations.CreateModel( + name='ModuleSwitch', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reference', models.CharField(help_text='Reference of a module', max_length=255, verbose_name='Module reference')), + ('comment', models.CharField(blank=True, help_text='Comment', max_length=255, null=True, verbose_name='Comment')), + ('switchs', models.ManyToManyField(through='topologie.ModuleOnSwitch', to='topologie.Switch')), + ], + options={ + 'verbose_name': 'Module of a switch', + 'permissions': (('view_moduleswitch', 'Can view a module object'),), + }, + bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model), + ), + migrations.AddField( + model_name='modelswitch', + name='is_itself_module', + field=models.BooleanField(default=False, help_text='Does the switch, itself, considered as a module'), + ), + migrations.AddField( + model_name='modelswitch', + name='is_modular', + field=models.BooleanField(default=False, help_text='Is this switch model modular'), + ), + migrations.AddField( + model_name='moduleonswitch', + name='module', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.ModuleSwitch'), + ), + migrations.AddField( + model_name='moduleonswitch', + name='switch', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.Switch'), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index e08a1b21..a4f5f3bc 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -251,6 +251,7 @@ class Switch(AclMixin, Machine): default=False, help_text='Provision automatique de ce switch', ) + class Meta: unique_together = ('stack', 'stack_member_id') @@ -389,6 +390,14 @@ class ModelSwitch(AclMixin, RevMixin, models.Model): null=True, blank=True ) + is_modular = models.BooleanField( + default=False, + help_text=_("Is this switch model modular"), + ) + is_itself_module = models.BooleanField( + default=False, + help_text=_("Does the switch, itself, considered as a module"), + ) class Meta: permissions = ( @@ -404,6 +413,61 @@ class ModelSwitch(AclMixin, RevMixin, models.Model): return str(self.constructor) + ' ' + self.reference +class ModuleSwitch(AclMixin, RevMixin, models.Model): + """A module of a switch""" + reference = models.CharField( + max_length=255, + help_text=_("Reference of a module"), + verbose_name=_("Module reference") + ) + comment = models.CharField( + max_length=255, + null=True, + blank=True, + help_text=_("Comment"), + verbose_name=_("Comment") + ) + switchs = models.ManyToManyField('Switch', through='ModuleOnSwitch') + + class Meta: + permissions = ( + ("view_moduleswitch", _("Can view a module object")), + ) + verbose_name = _("Module of a switch") + + def process_link(self, switchs): + """Django can't create itself foreignkey with explicit through""" + ModuleOnSwitch.objects.bulk_create( + [ModuleOnSwitch( + module=self, switch=sw + ) for sw in switchs.exclude( + pk__in=Switch.objects.filter(moduleswitch=self) + )] + ) + ModuleOnSwitch.objects.filter(module=self).exclude(switch__in=switchs).delete() + return + + def __str__(self): + return str(self.reference) + + +class ModuleOnSwitch(AclMixin, RevMixin, models.Model): + """Link beetween module and switch""" + module = models.ForeignKey('ModuleSwitch', on_delete=models.CASCADE) + switch = models.ForeignKey('Switch', on_delete=models.CASCADE) + slot = models.CharField( + max_length=15, + help_text=_("Slot on switch"), + verbose_name=_("Slot") + ) + + class Meta: + permissions = ( + ("view_moduleonswitch", _("Can view a moduleonswitch object")), + ) + verbose_name = _("link between switchs and modules") + + class ConstructorSwitch(AclMixin, RevMixin, models.Model): """Un constructeur de switch""" diff --git a/topologie/templates/topologie/aff_modules.html b/topologie/templates/topologie/aff_modules.html new file mode 100644 index 00000000..8233260c --- /dev/null +++ b/topologie/templates/topologie/aff_modules.html @@ -0,0 +1,67 @@ +{% 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 %} +{% load logs_extra %} +{% load i18n %} + +{% if module_list.paginator %} +{% include "pagination.html" with list=module_list %} +{% endif %} + +
+ + + + + + + + + {% for module in module_list %} + + + + + + + {% endfor %} +
{% trans "Reference" %}{% trans "Comment" %}{% trans "Switchs" %}
{{ module.reference }}{{ module.comment }}{{ module.switchs.all }} + {% can_edit module %} + + + + {% acl_end %} + {% history_button module %} + {% can_delete module %} + + + + {% acl_end %} +
+ +{% if module_list.paginator %} +{% include "pagination.html" with list=module_list %} +{% endif %} + diff --git a/topologie/templates/topologie/index_module.html b/topologie/templates/topologie/index_module.html new file mode 100644 index 00000000..5c4c5c7c --- /dev/null +++ b/topologie/templates/topologie/index_module.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 © 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 acl %} +{% load i18n %} + +{% block title %}{% trans "Topology" %}{% endblock %} + +{% block content %} +

{% trans "Modules of switchs" %}

+{% can_create ModuleSwitch %} +{% trans " Add a module" %} +
+{% acl_end %} + {% include "topologie/aff_modules.html" with module_list=module_list %} +
+
+
+{% endblock %} + diff --git a/topologie/templates/topologie/sidebar.html b/topologie/templates/topologie/sidebar.html index a35721f9..80317a16 100644 --- a/topologie/templates/topologie/sidebar.html +++ b/topologie/templates/topologie/sidebar.html @@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Switches" %} + + + + {% trans "Switches modules" %} diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index bf9f760b..d1ec9bb3 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% csrf_token %} - {% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged' %} + {% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switchs' %} {% bootstrap_button action_name icon='ok' button_class='btn-success' %}
diff --git a/topologie/urls.py b/topologie/urls.py index 77d68d50..7af7df9a 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -123,4 +123,10 @@ urlpatterns = [ url(r'^edit_vlanoptions/(?P[0-9]+)$', views.edit_vlanoptions, name='edit-vlanoptions'), - ] + url(r'^add_module/$', views.add_module, name='add-module'), + url(r'^edit_module/(?P[0-9]+)$', + views.edit_module, + name='edit-module'), + url(r'^del_module/(?P[0-9]+)$', views.del_module, name='del-module'), + url(r'^index_module/$', views.index_module, name='index-module'), +] diff --git a/topologie/views.py b/topologie/views.py index 89d48b0b..f5c72627 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -86,6 +86,7 @@ from .models import ( Building, Server, PortProfile, + ModuleSwitch, ) from .forms import ( EditPortForm, @@ -102,6 +103,7 @@ from .forms import ( EditSwitchBayForm, EditBuildingForm, EditPortProfileForm, + EditModuleForm ) from subprocess import ( @@ -316,6 +318,20 @@ def index_model_switch(request): ) +@login_required +@can_view_all(ModuleSwitch) +def index_module(request): + """Display all modules of switchs""" + module_list = ModuleSwitch.objects.all() + pagination_number = GeneralOption.get_cached_value('pagination_number') + module_list = re2o_paginator(request, module_list, pagination_number) + return render( + request, + 'topologie/index_module.html', + {'module_list': module_list} + ) + + @login_required @can_edit(Vlan) def edit_vlanoptions(request, vlan_instance, **_kwargs): @@ -1048,6 +1064,60 @@ def del_port_profile(request, port_profile, **_kwargs): ) +@can_create(ModuleSwitch) +def add_module(request): + """ View used to add a Module object """ + module = EditModuleForm(request.POST or None) + if module.is_valid(): + module.save() + messages.success(request, _("The module was created.")) + return redirect(reverse('topologie:index-module')) + return form( + {'topoform': module, 'action_name': _("Create a module")}, + 'topologie/topo.html', + request + ) + + +@login_required +@can_edit(ModuleSwitch) +def edit_module(request, module_instance, **_kwargs): + """ View used to edit a Module object """ + module = EditModuleForm(request.POST or None, instance=module_instance) + if module.is_valid(): + if module.changed_data: + module.save() + messages.success(request, _("The module was edited.")) + return redirect(reverse('topologie:index-module')) + return form( + {'topoform': module, 'action_name': _("Edit")}, + 'topologie/topo.html', + request + ) + + +@login_required +@can_delete(ModuleSwitch) +def del_module(request, module, **_kwargs): + """Compleete delete a module""" + if request.method == "POST": + try: + module.delete() + messages.success(request, _("The module was deleted.")) + except ProtectedError: + messages.error( + request, + (_("The module %s is used by another object, impossible to" + " deleted it.") % module) + ) + return redirect(reverse('topologie:index-module')) + return form( + {'objet': module, 'objet_name': _("Module")}, + 'topologie/delete.html', + request + ) + + def make_machine_graph(): """ Create the graph of switchs, machines and access points. From 4dbbb00cf7fc4867ce1ef0c2258600d54b150cf0 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 17:16:30 +0100 Subject: [PATCH 75/86] Export of modules of a switch --- topologie/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/topologie/models.py b/topologie/models.py index a4f5f3bc..53e9ffe6 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -368,6 +368,17 @@ class Switch(AclMixin, Machine): """Return dict ip6:subnet for all ipv6 of the switch""" return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all()) + @cached_property + def list_modules(self): + """Return modules of that switch, list of dict (rank, reference)""" + modules = [] + if self.model.is_modular: + if self.model.is_itself_module: + modules.append((1, self.model.reference)) + for module_of_self in self.moduleonswitch_set.all(): + modules.append((module_of_self.slot, module_of_self.module.reference)) + return modules + def __str__(self): return str(self.get_name) From 683cf229e96fe4c9539a201790a32d8b126072cc Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 18:20:06 +0100 Subject: [PATCH 76/86] Edition fine des modules pour chaque switchs avec le slot --- topologie/forms.py | 21 +++---- ...230_1556.py => 0067_auto_20181230_1819.py} | 7 ++- topologie/models.py | 16 ++--- .../templates/topologie/aff_modules.html | 20 ++++++- topologie/templates/topologie/topo.html | 2 +- topologie/urls.py | 9 ++- topologie/views.py | 59 ++++++++++++++++++- 7 files changed, 105 insertions(+), 29 deletions(-) rename topologie/migrations/{0067_auto_20181230_1556.py => 0067_auto_20181230_1819.py} (93%) diff --git a/topologie/forms.py b/topologie/forms.py index dc4a5e9b..ed6fa5b9 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -55,7 +55,8 @@ from .models import ( SwitchBay, Building, PortProfile, - ModuleSwitch + ModuleSwitch, + ModuleOnSwitch, ) @@ -279,14 +280,14 @@ class EditModuleForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['switchs'].queryset = (Switch.objects.filter(model__is_modular=True)) - def save(self, commit=True): - # TODO : None of the parents of ServiceForm use the commit - # parameter in .save() - instance = super(EditModuleForm, self).save(commit=False) - if commit: - instance.save() - instance.process_link(self.cleaned_data.get('switchs')) - return instance +class EditSwitchModuleForm(FormRevMixin, ModelForm): + """Add/edit a switch to a module""" + class Meta: + model = ModuleOnSwitch + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditSwitchModuleForm, self).__init__(*args, prefix=prefix, **kwargs) diff --git a/topologie/migrations/0067_auto_20181230_1556.py b/topologie/migrations/0067_auto_20181230_1819.py similarity index 93% rename from topologie/migrations/0067_auto_20181230_1556.py rename to topologie/migrations/0067_auto_20181230_1819.py index cd0d368c..57f268ea 100644 --- a/topologie/migrations/0067_auto_20181230_1556.py +++ b/topologie/migrations/0067_auto_20181230_1819.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2018-12-30 14:56 +# Generated by Django 1.10.7 on 2018-12-30 17:19 from __future__ import unicode_literals from django.db import migrations, models @@ -32,7 +32,6 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('reference', models.CharField(help_text='Reference of a module', max_length=255, verbose_name='Module reference')), ('comment', models.CharField(blank=True, help_text='Comment', max_length=255, null=True, verbose_name='Comment')), - ('switchs', models.ManyToManyField(through='topologie.ModuleOnSwitch', to='topologie.Switch')), ], options={ 'verbose_name': 'Module of a switch', @@ -60,4 +59,8 @@ class Migration(migrations.Migration): name='switch', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.Switch'), ), + migrations.AlterUniqueTogether( + name='moduleonswitch', + unique_together=set([('slot', 'switch')]), + ), ] diff --git a/topologie/models.py b/topologie/models.py index 53e9ffe6..b2c75f24 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -438,7 +438,6 @@ class ModuleSwitch(AclMixin, RevMixin, models.Model): help_text=_("Comment"), verbose_name=_("Comment") ) - switchs = models.ManyToManyField('Switch', through='ModuleOnSwitch') class Meta: permissions = ( @@ -446,17 +445,6 @@ class ModuleSwitch(AclMixin, RevMixin, models.Model): ) verbose_name = _("Module of a switch") - def process_link(self, switchs): - """Django can't create itself foreignkey with explicit through""" - ModuleOnSwitch.objects.bulk_create( - [ModuleOnSwitch( - module=self, switch=sw - ) for sw in switchs.exclude( - pk__in=Switch.objects.filter(moduleswitch=self) - )] - ) - ModuleOnSwitch.objects.filter(module=self).exclude(switch__in=switchs).delete() - return def __str__(self): return str(self.reference) @@ -477,6 +465,10 @@ class ModuleOnSwitch(AclMixin, RevMixin, models.Model): ("view_moduleonswitch", _("Can view a moduleonswitch object")), ) verbose_name = _("link between switchs and modules") + unique_together = ['slot', 'switch'] + + def __str__(self): + return 'On slot ' + str(self.slot) + ' of ' + str(self.switch) class ConstructorSwitch(AclMixin, RevMixin, models.Model): diff --git a/topologie/templates/topologie/aff_modules.html b/topologie/templates/topologie/aff_modules.html index 8233260c..d73fffeb 100644 --- a/topologie/templates/topologie/aff_modules.html +++ b/topologie/templates/topologie/aff_modules.html @@ -43,9 +43,27 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ module.reference }} {{ module.comment }} - {{ module.switchs.all }} + + {% for module_switch in module.moduleonswitch_set.all %} + Slot {{ module_switch.slot }} of {{ module_switch.switch }} + {% can_edit module_switch %} +
+ + + {% acl_end %} + {% can_delete module_switch %} + + + + {% acl_end %} +
+ {% endfor %} + {% can_edit module %} + + + diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index d1ec9bb3..a7824020 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %}
{% csrf_token %} - {% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switchs' %} + {% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %} {% bootstrap_button action_name icon='ok' button_class='btn-success' %}

diff --git a/topologie/urls.py b/topologie/urls.py index 7af7df9a..70eae8e4 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -124,9 +124,14 @@ urlpatterns = [ views.edit_vlanoptions, name='edit-vlanoptions'), url(r'^add_module/$', views.add_module, name='add-module'), - url(r'^edit_module/(?P[0-9]+)$', + url(r'^edit_module/(?P[0-9]+)$', views.edit_module, name='edit-module'), - url(r'^del_module/(?P[0-9]+)$', views.del_module, name='del-module'), + url(r'^del_module/(?P[0-9]+)$', views.del_module, name='del-module'), url(r'^index_module/$', views.index_module, name='index-module'), + url(r'^add_module_on/$', views.add_module_on, name='add-module-on'), + url(r'^edit_module_on/(?P[0-9]+)$', + views.edit_module_on, + name='edit-module-on'), + url(r'^del_module_on/(?P[0-9]+)$', views.del_module_on, name='del-module-on'), ] diff --git a/topologie/views.py b/topologie/views.py index f5c72627..5174f05d 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -87,6 +87,7 @@ from .models import ( Server, PortProfile, ModuleSwitch, + ModuleOnSwitch, ) from .forms import ( EditPortForm, @@ -103,7 +104,8 @@ from .forms import ( EditSwitchBayForm, EditBuildingForm, EditPortProfileForm, - EditModuleForm + EditModuleForm, + EditSwitchModuleForm, ) from subprocess import ( @@ -1064,6 +1066,7 @@ def del_port_profile(request, port_profile, **_kwargs): ) +@login_required @can_create(ModuleSwitch) def add_module(request): """ View used to add a Module object """ @@ -1117,6 +1120,60 @@ def del_module(request, module, **_kwargs): request ) +@login_required +@can_create(ModuleOnSwitch) +def add_module_on(request): + """Add a module to a switch""" + module_switch = EditSwitchModuleForm(request.POST or None) + if module_switch.is_valid(): + module_switch.save() + messages.success(request, _("The module added to that switch")) + return redirect(reverse('topologie:index-module')) + return form( + {'topoform': module_switch, 'action_name': _("Create")}, + 'topologie/topo.html', + request + ) + + +@login_required +@can_edit(ModuleOnSwitch) +def edit_module_on(request, module_instance, **_kwargs): + """ View used to edit a Module object """ + module = EditSwitchModuleForm(request.POST or None, instance=module_instance) + if module.is_valid(): + if module.changed_data: + module.save() + messages.success(request, _("The module was edited.")) + return redirect(reverse('topologie:index-module')) + return form( + {'topoform': module, 'action_name': _("Edit")}, + 'topologie/topo.html', + request + ) + + +@login_required +@can_delete(ModuleOnSwitch) +def del_module_on(request, module, **_kwargs): + """Compleete delete a module""" + if request.method == "POST": + try: + module.delete() + messages.success(request, _("The module was deleted.")) + except ProtectedError: + messages.error( + request, + (_("The module %s is used by another object, impossible to" + " deleted it.") % module) + ) + return redirect(reverse('topologie:index-module')) + return form( + {'objet': module, 'objet_name': _("Module")}, + 'topologie/delete.html', + request + ) + def make_machine_graph(): """ From 82802de47706ed66299168d4e42d77f8bae02456 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 18:45:18 +0100 Subject: [PATCH 77/86] More clear front of all switchs modular --- .../templates/topologie/aff_modules.html | 25 +++++++++++++++++++ .../templates/topologie/index_module.html | 2 +- topologie/views.py | 4 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/topologie/templates/topologie/aff_modules.html b/topologie/templates/topologie/aff_modules.html index d73fffeb..0c7a3207 100644 --- a/topologie/templates/topologie/aff_modules.html +++ b/topologie/templates/topologie/aff_modules.html @@ -83,3 +83,28 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "pagination.html" with list=module_list %} {% endif %} +

{% trans "All modular switchs" %}

+ + + + + + + + {% for switch in modular_switchs %} + {% if switch.list_modules %} + + + + {% for module in switch.list_modules %} + + + + + + {% endfor %} +{% endif %} +{% endfor %} +
{% trans "Switch" %}{% trans "Reference" %}{% trans "Slot" %}
+ {{ switch }} +
{{ module.1 }}{{ module.0 }}
diff --git a/topologie/templates/topologie/index_module.html b/topologie/templates/topologie/index_module.html index 5c4c5c7c..d9cc2925 100644 --- a/topologie/templates/topologie/index_module.html +++ b/topologie/templates/topologie/index_module.html @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans " Add a module" %}
{% acl_end %} - {% include "topologie/aff_modules.html" with module_list=module_list %} + {% include "topologie/aff_modules.html" with module_list=module_list modular_switchs=modular_switchs %}


diff --git a/topologie/views.py b/topologie/views.py index 5174f05d..55f0a060 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -325,12 +325,14 @@ def index_model_switch(request): def index_module(request): """Display all modules of switchs""" module_list = ModuleSwitch.objects.all() + modular_switchs = Switch.objects.filter(model__is_modular=True) pagination_number = GeneralOption.get_cached_value('pagination_number') module_list = re2o_paginator(request, module_list, pagination_number) return render( request, 'topologie/index_module.html', - {'module_list': module_list} + {'module_list': module_list, + 'modular_switchs': modular_switchs} ) From 64301bda9f5e369e3f38a6e74b662f3e6f2e7478 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 30 Dec 2018 19:22:43 +0100 Subject: [PATCH 78/86] Export list_modules + return [] if no models for modules --- api/serializers.py | 3 ++- topologie/models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index e7b23f32..af92e0d0 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -811,7 +811,8 @@ class SwitchPortSerializer(serializers.ModelSerializer): model = topologie.Switch fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6', 'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled', - 'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value') + 'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value', + 'list_modules') # LOCAL EMAILS diff --git a/topologie/models.py b/topologie/models.py index b2c75f24..fd054c43 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -372,7 +372,7 @@ class Switch(AclMixin, Machine): def list_modules(self): """Return modules of that switch, list of dict (rank, reference)""" modules = [] - if self.model.is_modular: + if getattr(self.model, 'is_modular', None): if self.model.is_itself_module: modules.append((1, self.model.reference)) for module_of_self in self.moduleonswitch_set.all(): From 80e20d0f91324c0c0f21fb23a83994b12c73c0b5 Mon Sep 17 00:00:00 2001 From: klafyvel Date: Tue, 1 Jan 2019 20:31:36 +0100 Subject: [PATCH 79/86] English is a beautiful language --- preferences/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preferences/models.py b/preferences/models.py index e8476354..104c261d 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -278,7 +278,7 @@ class OptionalTopologie(AclMixin, PreferencesModel): log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type) radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type) dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server") - dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursif-server").filter(type__ip_type=self.switchs_ip_type) + dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type) subnet = None subnet6 = None if self.switchs_ip_type: From 80b8779c5579273ecc3b576fff18b821c19b8d26 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 Jan 2019 00:54:13 +0100 Subject: [PATCH 80/86] dns-recursif-server -> dns-recursive-server --- machines/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machines/models.py b/machines/models.py index 1cf840d7..a4685676 100644 --- a/machines/models.py +++ b/machines/models.py @@ -1612,7 +1612,7 @@ class Role(RevMixin, AclMixin, models.Model): ROLE = ( ('dhcp-server', _("DHCP server")), ('switch-conf-server', _("Switches configuration server")), - ('dns-recursif-server', _("Recursive DNS server")), + ('dns-recursive-server', _("Recursive DNS server")), ('ntp-server', _("NTP server")), ('radius-server', _("RADIUS server")), ('log-server', _("Log server")), From 2e3be7ad56f7c430e5a8ee98d1e0b4beb8466445 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 Jan 2019 00:54:54 +0100 Subject: [PATCH 81/86] migrations for recursive dns --- .../migrations/0098_auto_20190102_1745.py | 20 ++++++++++++++ .../migrations/0099_role_recursive_dns.py | 26 +++++++++++++++++++ .../migrations/0100_auto_20190102_1753.py | 20 ++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 machines/migrations/0098_auto_20190102_1745.py create mode 100644 machines/migrations/0099_role_recursive_dns.py create mode 100644 machines/migrations/0100_auto_20190102_1753.py diff --git a/machines/migrations/0098_auto_20190102_1745.py b/machines/migrations/0098_auto_20190102_1745.py new file mode 100644 index 00000000..e886e8a1 --- /dev/null +++ b/machines/migrations/0098_auto_20190102_1745.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0097_extension_dnssec'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='specific_role', + field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True), + ), + ] diff --git a/machines/migrations/0099_role_recursive_dns.py b/machines/migrations/0099_role_recursive_dns.py new file mode 100644 index 00000000..c1ce3965 --- /dev/null +++ b/machines/migrations/0099_role_recursive_dns.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def migrate(apps, schema_editor): + Role = apps.get_model('machines', 'Role') + + for role in Role.objects.filter(specific_role='dns-recursif-server'): + role.specific_role = 'dns-recursive-server' + role.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0098_auto_20190102_1745'), + ] + + operations = [ + migrations.RunPython(migrate), + ] + + diff --git a/machines/migrations/0100_auto_20190102_1753.py b/machines/migrations/0100_auto_20190102_1753.py new file mode 100644 index 00000000..35f7b78d --- /dev/null +++ b/machines/migrations/0100_auto_20190102_1753.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0099_role_recursive_dns'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='specific_role', + field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True), + ), + ] From af714a7f331982a311a3aca76926c1a928e3edcb Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 Jan 2019 01:02:02 +0100 Subject: [PATCH 82/86] typo --- .../migrations/0068_auto_20190102_1758.py | 20 +++++++++++++++++++ topologie/models.py | 14 ++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 topologie/migrations/0068_auto_20190102_1758.py diff --git a/topologie/migrations/0068_auto_20190102_1758.py b/topologie/migrations/0068_auto_20190102_1758.py new file mode 100644 index 00000000..b03e7ae5 --- /dev/null +++ b/topologie/migrations/0068_auto_20190102_1758.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0067_auto_20181230_1819'), + ] + + operations = [ + migrations.AlterField( + model_name='modelswitch', + name='is_itself_module', + field=models.BooleanField(default=False, help_text='Is the switch, itself, considered as a module'), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index fd054c43..e05fa50e 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -251,7 +251,7 @@ class Switch(AclMixin, Machine): default=False, help_text='Provision automatique de ce switch', ) - + class Meta: unique_together = ('stack', 'stack_member_id') @@ -404,11 +404,11 @@ class ModelSwitch(AclMixin, RevMixin, models.Model): is_modular = models.BooleanField( default=False, help_text=_("Is this switch model modular"), - ) + ) is_itself_module = models.BooleanField( default=False, - help_text=_("Does the switch, itself, considered as a module"), - ) + help_text=_("Is the switch, itself, considered as a module"), + ) class Meta: permissions = ( @@ -429,14 +429,14 @@ class ModuleSwitch(AclMixin, RevMixin, models.Model): reference = models.CharField( max_length=255, help_text=_("Reference of a module"), - verbose_name=_("Module reference") + verbose_name=_("Module reference") ) comment = models.CharField( max_length=255, null=True, blank=True, help_text=_("Comment"), - verbose_name=_("Comment") + verbose_name=_("Comment") ) class Meta: @@ -457,7 +457,7 @@ class ModuleOnSwitch(AclMixin, RevMixin, models.Model): slot = models.CharField( max_length=15, help_text=_("Slot on switch"), - verbose_name=_("Slot") + verbose_name=_("Slot") ) class Meta: From edf89dac982e4bca3a41134f768399c1c05b6902 Mon Sep 17 00:00:00 2001 From: detraz Date: Fri, 4 Jan 2019 12:31:10 +0100 Subject: [PATCH 83/86] Fix crash of optionaltopologie serializer --- api/serializers.py | 16 ++++++++++++++-- api/urls.py | 1 + api/views.py | 11 +++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index af92e0d0..8c22ed21 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -391,13 +391,25 @@ class OptionalTopologieSerializer(NamespacedHMSerializer): class Meta: model = preferences.OptionalTopologie - fields = ('radius_general_policy', 'vlan_decision_ok', - 'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management', + fields = ('switchs_ip_type', 'switchs_web_management', 'switchs_web_management_ssl', 'switchs_rest_management', 'switchs_management_utils', 'switchs_management_interface_ip', 'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds') +class RadiusOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.RadiusOption` objects + """ + + class Meta: + model = preferences.RadiusOption + fields = ('radius_general_policy', 'unknown_machine', + 'unknown_machine_vlan', 'unknown_port', + 'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan', + 'non_member', 'non_member_vlan', 'banned', 'banned_vlan', + 'vlan_decision_ok') + + class GeneralOptionSerializer(NamespacedHMSerializer): """Serialize `preferences.models.GeneralOption` objects. """ diff --git a/api/urls.py b/api/urls.py index 723ca78c..4a34c1de 100644 --- a/api/urls.py +++ b/api/urls.py @@ -67,6 +67,7 @@ router.register_viewset(r'machines/role', views.RoleViewSet) router.register_view(r'preferences/optionaluser', views.OptionalUserView), router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView), +router.register_view(r'preferences/radiusoption', views.RadiusOptionView), router.register_view(r'preferences/generaloption', views.GeneralOptionView), router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'), router.register_view(r'preferences/assooption', views.AssoOptionView), diff --git a/api/views.py b/api/views.py index 21f7b438..8f7b9c1f 100644 --- a/api/views.py +++ b/api/views.py @@ -292,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView): return preferences.OptionalTopologie.objects.first() +class RadiusOptionView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.OptionalTopologie` settings. + """ + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.RadiusOption.can_view_all]} + serializer_class = serializers.RadiusOptionSerializer + + def get_object(self): + return preferences.RadiusOption.objects.first() + + class GeneralOptionView(generics.RetrieveAPIView): """Exposes details of `preferences.models.GeneralOption` settings. """ From 20e1ee3bb47dc123cdbd031e0fdf4872364a1a49 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sat, 5 Jan 2019 16:19:38 +0100 Subject: [PATCH 84/86] A granted user can set active at creation --- users/forms.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/users/forms.py b/users/forms.py index 81c31cae..effcc60c 100644 --- a/users/forms.py +++ b/users/forms.py @@ -387,6 +387,20 @@ class AdherentCreationForm(AdherentForm): #gtu_check.label = mark_safe("{} {}{}".format( # _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _("."))) + class Meta: + model = Adherent + fields = [ + 'name', + 'surname', + 'pseudo', + 'email', + 'school', + 'comment', + 'telephone', + 'room', + 'state', + ] + def __init__(self, *args, **kwargs): super(AdherentCreationForm, self).__init__(*args, **kwargs) From 0b1c35900fbec6d815a7f9e4a37993a290344619 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sat, 5 Jan 2019 18:32:54 +0100 Subject: [PATCH 85/86] =?UTF-8?q?Ajoute=20un=20reglage=20pour=20set=20tous?= =?UTF-8?q?=20les=20comptes=20actifs=20=C3=A0=20l'initialisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0057_optionaluser_all_users_active.py | 20 +++++++++++++++++++ preferences/models.py | 5 +++++ .../preferences/display_preferences.html | 4 ++++ users/forms.py | 1 + users/models.py | 2 +- 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 preferences/migrations/0057_optionaluser_all_users_active.py diff --git a/preferences/migrations/0057_optionaluser_all_users_active.py b/preferences/migrations/0057_optionaluser_all_users_active.py new file mode 100644 index 00000000..3f0cc8c1 --- /dev/null +++ b/preferences/migrations/0057_optionaluser_all_users_active.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-05 17:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0056_4_radiusoption'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='all_users_active', + field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 104c261d..228807a6 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -116,6 +116,11 @@ class OptionalUser(AclMixin, PreferencesModel): default=False, help_text=_("A new user can create their account on Re2o") ) + all_users_active = models.BooleanField( + default=False, + help_text=_("If True, all new created and connected users are active.\ + If False, only when a valid registration has been paid") + ) class Meta: permissions = ( diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 0e34d6e9..e4321f5b 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -122,6 +122,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Delete not yet active users after" %} {{ useroptions.delete_notyetactive }} days + + {% trans "All users are active by default" %} + {{ useroptions.all_users_active|tick }} +

{% trans "Users general permissions" %}

diff --git a/users/forms.py b/users/forms.py index effcc60c..d4110dcd 100644 --- a/users/forms.py +++ b/users/forms.py @@ -117,6 +117,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): """Changement du mot de passe""" user = super(PassForm, self).save(commit=False) user.set_password(self.cleaned_data.get("passwd1")) + user.set_active() user.save() diff --git a/users/models.py b/users/models.py index e4c25956..c5287fb6 100755 --- a/users/models.py +++ b/users/models.py @@ -337,7 +337,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, def set_active(self): """Enable this user if he subscribed successfully one time before""" if self.state == self.STATE_NOT_YET_ACTIVE: - if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists(): + if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists() or OptionalUser.get_cached_value('all_users_active'): self.state = self.STATE_ACTIVE self.save() From 84889388c9bec59ec1dfd7007d2d86dddfe03cd3 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sat, 5 Jan 2019 20:11:36 +0100 Subject: [PATCH 86/86] Avoid crash when 'email' field is not here --- users/models.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/users/models.py b/users/models.py index c5287fb6..a2798207 100755 --- a/users/models.py +++ b/users/models.py @@ -1026,17 +1026,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ): raise ValidationError("This pseudo is already in use.") if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE): - raise ValidationError( - {'email': ( - _("There is neither a local email address nor an external" + raise ValidationError(_("There is neither a local email address nor an external" " email address for this user.") - ), } ) if self.local_email_redirect and not self.email: - raise ValidationError( - {'local_email_redirect': ( - _("You can't redirect your local emails if no external email" - " address has been set.")), } + raise ValidationError(_("You can't redirect your local emails if no external email" + " address has been set.") ) def __str__(self):