From 74d41402ced46ce08f3ff829ff3515fd6db5c476 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Mon, 2 Oct 2017 18:10:24 +0200 Subject: [PATCH 001/138] OUverture et non config --- machines/templates/machines/sidebar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machines/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html index e635d69a..6ca3a07f 100644 --- a/machines/templates/machines/sidebar.html +++ b/machines/templates/machines/sidebar.html @@ -58,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if is_cableur %} - Configuration de ports + Ouverture de ports {%endif%} {% endblock %} From 0bb5095d1751024d2cbb95826a0eccf7724064d5 Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Tue, 3 Oct 2017 18:27:06 +0200 Subject: [PATCH 002/138] =?UTF-8?q?Politique=20d'ouverture=20des=20ports?= =?UTF-8?q?=20par=20d=C3=A9faut=20associ=C3=A9e=20=C3=A0=20un=20range=20d'?= =?UTF-8?q?IP.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- machines/forms.py | 4 ++-- .../migrations/0060_iptype_ouverture_ports.py | 21 +++++++++++++++++++ machines/models.py | 1 + machines/serializers.py | 5 +++-- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 machines/migrations/0060_iptype_ouverture_ports.py diff --git a/machines/forms.py b/machines/forms.py index 18631651..7922654a 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -145,7 +145,7 @@ class DelMachineTypeForm(Form): class IpTypeForm(ModelForm): class Meta: model = IpType - fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan'] + fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports'] def __init__(self, *args, **kwargs): @@ -154,7 +154,7 @@ class IpTypeForm(ModelForm): class EditIpTypeForm(IpTypeForm): class Meta(IpTypeForm.Meta): - fields = ['extension','type','need_infra', 'prefix_v6', 'vlan'] + fields = ['extension','type','need_infra', 'prefix_v6', 'vlan', 'ouverture_ports'] class DelIpTypeForm(Form): iptypes = forms.ModelMultipleChoiceField(queryset=IpType.objects.all(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple) diff --git a/machines/migrations/0060_iptype_ouverture_ports.py b/machines/migrations/0060_iptype_ouverture_ports.py new file mode 100644 index 00000000..e35f398f --- /dev/null +++ b/machines/migrations/0060_iptype_ouverture_ports.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-03 16:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0059_iptype_prefix_v6'), + ] + + operations = [ + migrations.AddField( + model_name='iptype', + name='ouverture_ports', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='machines.OuverturePortList'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 8e7b1c3c..5d08038e 100644 --- a/machines/models.py +++ b/machines/models.py @@ -72,6 +72,7 @@ class IpType(models.Model): domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4') prefix_v6 = models.GenericIPAddressField(protocol='IPv6', null=True, blank=True) vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True) + ouverture_ports = models.ForeignKey('OuverturePortList', blank=True, null=True) @cached_property def ip_range(self): diff --git a/machines/serializers.py b/machines/serializers.py index 51daa4b5..6561bba9 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -80,10 +80,10 @@ class ExtensionNameField(serializers.RelatedField): class TypeSerializer(serializers.ModelSerializer): extension = ExtensionNameField(read_only=True) - + class Meta: model = IpType - fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop') + fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', 'ouverture_ports') class ExtensionSerializer(serializers.ModelSerializer): origin = serializers.SerializerMethodField('get_origin_ip') @@ -184,3 +184,4 @@ class ServiceServersSerializer(serializers.ModelSerializer): def get_regen_status(self, obj): return obj.need_regen() + From 8e10bc64a8121173c0fa1e3744c54fedd1504304 Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Tue, 3 Oct 2017 19:07:53 +0200 Subject: [PATCH 003/138] =?UTF-8?q?Serialisation=20des=20ouvertures=20asso?= =?UTF-8?q?ci=C3=A9es=20=C3=A0=20une=20range=20d'IP.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- machines/serializers.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/machines/serializers.py b/machines/serializers.py index 6561bba9..6adc22cc 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -23,7 +23,7 @@ #Augustin Lemesle from rest_framework import serializers -from machines.models import Interface, IpType, Extension, IpList, MachineType, Domain, Text, Mx, Service_link, Ns +from machines.models import Interface, IpType, Extension, IpList, MachineType, Domain, Text, Mx, Service_link, Ns, OuverturePortList, OuverturePort class IpTypeField(serializers.RelatedField): def to_representation(self, value): @@ -80,10 +80,31 @@ class ExtensionNameField(serializers.RelatedField): class TypeSerializer(serializers.ModelSerializer): extension = ExtensionNameField(read_only=True) - + ouverture_ports_tcp_in = serializers.SerializerMethodField('get_port_policy_input_tcp') + ouverture_ports_tcp_out = serializers.SerializerMethodField('get_port_policy_output_tcp') + ouverture_ports_udp_in = serializers.SerializerMethodField('get_port_policy_input_udp') + ouverture_ports_udp_out = serializers.SerializerMethodField('get_port_policy_output_udp') + class Meta: model = IpType - fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', 'ouverture_ports') + fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', 'ouverture_ports_udp_in', 'ouverture_ports_udp_out', ) + + def get_port_policy(self, obj, protocole, io): + if not obj.ouverture_ports: + return [] + return [str(port) for port in obj.ouverture_ports.ouvertureport_set.filter(protocole=protocole).filter(io=io)] + + def get_port_policy_input_tcp(self, obj): + return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.IN) + + def get_port_policy_output_tcp(self, obj): + return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.OUT) + + def get_port_policy_input_udp(self, obj): + return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.IN) + + def get_port_policy_output_udp(self, obj): + return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.OUT) class ExtensionSerializer(serializers.ModelSerializer): origin = serializers.SerializerMethodField('get_origin_ip') From 3abbc707938de0b8a6c994e446d5adba4337958e Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Wed, 4 Oct 2017 22:03:26 +0200 Subject: [PATCH 004/138] =?UTF-8?q?Affichage=20REST=20des=20r=C3=A8gles=20?= =?UTF-8?q?particuli=C3=A8res=20des=20machines.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- machines/serializers.py | 25 +++++++++++++++++++++++++ machines/urls.py | 1 + machines/views.py | 26 +++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/machines/serializers.py b/machines/serializers.py index 6adc22cc..34716cd8 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -206,3 +206,28 @@ class ServiceServersSerializer(serializers.ModelSerializer): def get_regen_status(self, obj): return obj.need_regen() +class OuverturePortsSerializer(serializers.Serializer): + ipv4 = serializers.SerializerMethodField() + ipv6 = serializers.SerializerMethodField() + + def get_ipv4(): + return {i.ipv4.ipv4: + { + "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], + "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + } + for i in Interface.objects.all() if i.ipv4 + } + + def get_ipv6(): + return {i.ipv6: + { + "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], + "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + } + for i in Interface.objects.all() if i.ipv6 + } diff --git a/machines/urls.py b/machines/urls.py index 62576a4e..e0ff1e77 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -92,6 +92,7 @@ urlpatterns = [ url(r'^rest/text/$', views.text, name='text'), url(r'^rest/zones/$', views.zones, name='zones'), url(r'^rest/service_servers/$', views.service_servers, name='service-servers'), + url(r'^rest/ouverture_ports/$', views.ouverture_ports, name='ouverture-ports'), url(r'index_portlist/$', views.index_portlist, name='index-portlist'), url(r'^edit_portlist/(?P[0-9]+)$', views.edit_portlist, name='edit-portlist'), url(r'^del_portlist/(?P[0-9]+)$', views.del_portlist, name='del-portlist'), diff --git a/machines/views.py b/machines/views.py index 4be86c03..b286db75 100644 --- a/machines/views.py +++ b/machines/views.py @@ -41,7 +41,7 @@ from django.contrib.auth import authenticate, login from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer -from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer +from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer, OuverturePortsSerializer from reversion import revisions as reversion from reversion.models import Version @@ -1108,6 +1108,30 @@ def service_servers(request): @csrf_exempt @login_required @permission_required('serveur') +def ouverture_ports(request): + r = {'ipv4':{}, 'ipv6':{}} + for i in Interface.objects.all(): + if not i.may_have_port_open(): + continue + if i.ipv4: + r['ipv4'][i.ipv4.ipv4] = {"tcp_in":[],"tcp_out":[],"udp_in":[],"udp_out":[]} + if i.ipv6: + r['ipv6'][i.ipv6] = {"tcp_in":[],"tcp_out":[],"udp_in":[],"udp_out":[]} + for j in i.port_lists.all(): + if i.ipv4: + r['ipv4'][i.ipv4.ipv4]["tcp_in"].extend(j.tcp_ports_in()) + r['ipv4'][i.ipv4.ipv4]["tcp_out"].extend(j.tcp_ports_out()) + r['ipv4'][i.ipv4.ipv4]["udp_in"].extend(j.udp_ports_in()) + r['ipv4'][i.ipv4.ipv4]["udp_out"].extend(j.udp_ports_out()) + if i.ipv6: + r['ipv6'][i.ipv6]["tcp_in"].extend(j.tcp_ports_in()) + r['ipv6'][i.ipv6]["tcp_out"].extend(j.tcp_ports_out()) + r['ipv6'][i.ipv6]["udp_in"].extend(j.udp_ports_in()) + r['ipv6'][i.ipv6]["udp_out"].extend(j.udp_ports_out()) + return JSONResponse(r) +@csrf_exempt +@login_required +@permission_required('serveur') def regen_achieved(request): obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server']))) if obj: From 1b7617dd757611f363a15ce37602fc79cc3e1d9a Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Thu, 5 Oct 2017 00:09:23 +0200 Subject: [PATCH 005/138] =?UTF-8?q?Fix=20de=20la=20d=C3=A9tection=20d'ipv6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- machines/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/machines/views.py b/machines/views.py index 580ad4d8..c98936dc 100644 --- a/machines/views.py +++ b/machines/views.py @@ -1114,7 +1114,7 @@ def ouverture_ports(request): continue if i.ipv4: r['ipv4'][i.ipv4.ipv4] = {"tcp_in":[],"tcp_out":[],"udp_in":[],"udp_out":[]} - if i.ipv6: + if i.ipv6_object: r['ipv6'][i.ipv6] = {"tcp_in":[],"tcp_out":[],"udp_in":[],"udp_out":[]} for j in i.port_lists.all(): if i.ipv4: @@ -1122,7 +1122,7 @@ def ouverture_ports(request): r['ipv4'][i.ipv4.ipv4]["tcp_out"].extend(j.tcp_ports_out()) r['ipv4'][i.ipv4.ipv4]["udp_in"].extend(j.udp_ports_in()) r['ipv4'][i.ipv4.ipv4]["udp_out"].extend(j.udp_ports_out()) - if i.ipv6: + if i.ipv6_object: r['ipv6'][i.ipv6]["tcp_in"].extend(j.tcp_ports_in()) r['ipv6'][i.ipv6]["tcp_out"].extend(j.tcp_ports_out()) r['ipv6'][i.ipv6]["udp_in"].extend(j.udp_ports_in()) From d65fbbf8d1189f59efd5fd867ec4d3027c8a7383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sun, 8 Oct 2017 20:22:04 +0000 Subject: [PATCH 006/138] Evite les doublons dans les id et les names des forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Force la variable prefix a être setup pour chacun des modelForm avec le nom du model mais n'override pas si une autre value est donnée. L'id et le name des champs HTML généré sont donc prefixé par le nom du model et on peut mettre plusieurs modelForms basé sur des models différent dans la même page HTML sans souci de duplication --- cotisations/forms.py | 12 +++-- machines/forms.py | 52 +++++++++++++++---- .../templatetags/bootstrap_form_typeahead.py | 38 +++++++------- machines/views.py | 2 +- preferences/forms.py | 23 +++++--- topologie/forms.py | 26 +++++++++- users/forms.py | 45 +++++++++++++--- 7 files changed, 149 insertions(+), 49 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 90c9e826..9b34b332 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -30,7 +30,8 @@ from .models import Article, Paiement, Facture, Banque, Vente class NewFactureForm(ModelForm): def __init__(self, *args, **kwargs): - super(NewFactureForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'facture') + super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['cheque'].required = False self.fields['banque'].required = False self.fields['cheque'].label = 'Numero de chèque' @@ -102,7 +103,8 @@ class ArticleForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(ArticleForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'article') + super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = "Désignation de l'article" class DelArticleForm(Form): @@ -114,7 +116,8 @@ class PaiementForm(ModelForm): fields = ['moyen', 'type_paiement'] def __init__(self, *args, **kwargs): - super(PaiementForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'paiement') + super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['moyen'].label = 'Moyen de paiement à ajouter' self.fields['type_paiement'].label = 'Type de paiement à ajouter' @@ -127,7 +130,8 @@ class BanqueForm(ModelForm): fields = ['name'] def __init__(self, *args, **kwargs): - super(BanqueForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'banque') + super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Banque à ajouter' class DelBanqueForm(Form): diff --git a/machines/forms.py b/machines/forms.py index 63539888..25ffad0c 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -40,7 +40,8 @@ class EditMachineForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditMachineForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'machine') + super(EditMachineForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Nom de la machine' class NewMachineForm(EditMachineForm): @@ -57,7 +58,8 @@ class EditInterfaceForm(ModelForm): fields = ['machine', 'type', 'ipv4', 'mac_address', 'details'] def __init__(self, *args, **kwargs): - super(EditInterfaceForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'interface') + super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['mac_address'].label = 'Adresse mac' self.fields['type'].label = 'Type de machine' self.fields['type'].empty_label = "Séléctionner un type de machine" @@ -110,9 +112,10 @@ class AliasForm(ModelForm): fields = ['name','extension'] def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'domain') if 'infra' in kwargs: infra = kwargs.pop('infra') - super(AliasForm, self).__init__(*args, **kwargs) + super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs) class DomainForm(AliasForm): class Meta(AliasForm.Meta): @@ -125,7 +128,8 @@ class DomainForm(AliasForm): initial = kwargs.get('initial', {}) initial['name'] = user.get_next_domain_name() kwargs['initial'] = initial - super(DomainForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'domain') + super(DomainForm, self).__init__(*args, prefix=prefix, **kwargs) class DelAliasForm(Form): alias = forms.ModelMultipleChoiceField(queryset=Domain.objects.all(), label="Alias actuels", widget=forms.CheckboxSelectMultiple) @@ -141,7 +145,8 @@ class MachineTypeForm(ModelForm): fields = ['type','ip_type'] def __init__(self, *args, **kwargs): - super(MachineTypeForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'machinetype') + super(MachineTypeForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['type'].label = 'Type de machine à ajouter' self.fields['ip_type'].label = "Type d'ip relié" @@ -153,9 +158,9 @@ class IpTypeForm(ModelForm): model = IpType fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan'] - def __init__(self, *args, **kwargs): - super(IpTypeForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'iptype') + super(IpTypeForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['type'].label = 'Type ip à ajouter' class EditIpTypeForm(IpTypeForm): @@ -171,7 +176,8 @@ class ExtensionForm(ModelForm): fields = ['name', 'need_infra', 'origin'] def __init__(self, *args, **kwargs): - super(ExtensionForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'extension') + super(ExtensionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Extension à ajouter' self.fields['origin'].label = 'Enregistrement A origin' @@ -184,7 +190,8 @@ class MxForm(ModelForm): fields = ['zone', 'priority', 'name'] def __init__(self, *args, **kwargs): - super(MxForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'mx') + super(MxForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None) class DelMxForm(Form): @@ -196,7 +203,8 @@ class NsForm(ModelForm): fields = ['zone', 'ns'] def __init__(self, *args, **kwargs): - super(NsForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'ns') + super(NsForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None) class DelNsForm(Form): @@ -207,6 +215,10 @@ class TextForm(ModelForm): model = Text fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'text') + super(TextForm, self).__init__(*args, prefix=prefix, **kwargs) + class DelTextForm(Form): text = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Text actuels", widget=forms.CheckboxSelectMultiple) @@ -215,6 +227,10 @@ class NasForm(ModelForm): model = Nas fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'nas') + super(NasForm, self).__init__(*args, prefix=prefix, **kwargs) + class DelNasForm(Form): nas = forms.ModelMultipleChoiceField(queryset=Nas.objects.all(), label="Enregistrements Nas actuels", widget=forms.CheckboxSelectMultiple) @@ -223,6 +239,10 @@ class ServiceForm(ModelForm): model = Service fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'service') + super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) + def save(self, commit=True): instance = super(ServiceForm, self).save(commit=False) if commit: @@ -238,6 +258,10 @@ class VlanForm(ModelForm): model = Vlan fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'vlan') + super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) + class DelVlanForm(Form): vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple) @@ -246,8 +270,16 @@ class EditOuverturePortConfigForm(ModelForm): model = Interface fields = ['port_lists'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'interface') + super(EditOuverturePortConfigForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditOuverturePortListForm(ModelForm): class Meta: model = OuverturePortList fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'ouvertureportlist') + super(EditOuverturePortListForm, self).__init__(*args, prefix=prefix, **kwargs) + diff --git a/machines/templatetags/bootstrap_form_typeahead.py b/machines/templatetags/bootstrap_form_typeahead.py index 05dd3147..7ccab390 100644 --- a/machines/templatetags/bootstrap_form_typeahead.py +++ b/machines/templatetags/bootstrap_form_typeahead.py @@ -195,20 +195,20 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): return mark_safe( form ) -def input_id( f_name ) : +def input_id( f_bound ) : """ The id of the HTML input element """ - return 'id_'+f_name + return f_bound.auto_id -def hidden_id( f_name ): +def hidden_id( f_bound ): """ The id of the HTML hidden input element """ - return 'typeahead_hidden_'+f_name + return input_id( f_bound ) +'_hidden' def hidden_tag( f_bound, f_name ): """ The HTML hidden input element """ return render_tag( 'input', attrs={ - 'id': hidden_id(f_name), + 'id': hidden_id( f_bound ), 'name': f_name, 'type': 'hidden', 'value': f_bound.value() or "" @@ -249,10 +249,10 @@ def typeahead_js( f_name, f_value, f_bound, f_name = f_name, choices = choices, engine = engine, - input_id = input_id( f_name ), + input_id = input_id( f_bound ), datasets = default_datasets( f_name, match_func ), - updater = typeahead_updater( f_name ), - change = typeahead_change( f_name ), + updater = typeahead_updater( f_bound ), + change = typeahead_change( f_bound ), updates = ''.join( [ ( '$( "#{u_id}" ).change( function() {{' 'setup_{f_name}();' @@ -260,7 +260,7 @@ def typeahead_js( f_name, f_value, f_bound, '}} );' ).format( u_id = u_id, - reset_input = reset_input( f_name ), + reset_input = reset_input( f_bound ), f_name = f_name ) for u_id in update_on ] ), @@ -276,24 +276,24 @@ def init_input( f_name, f_bound ) : '$( "#{input_id}" ).typeahead("val", {init_val});' '$( "#{hidden_id}" ).val( {init_key} );' ).format( - input_id = input_id( f_name ), + input_id = input_id( f_bound ), init_val = '""' if init_key == '""' else 'engine_{f_name}.get( {init_key} )[0].value'.format( f_name = f_name, init_key = init_key ), init_key = init_key, - hidden_id = hidden_id( f_name ) + hidden_id = hidden_id( f_bound ) ) -def reset_input( f_name ) : +def reset_input( f_bound ) : """ The JS script to reset the fields values """ return ( '$( "#{input_id}" ).typeahead("val", "");' '$( "#{hidden_id}" ).val( "" );' ).format( - input_id = input_id( f_name ), - hidden_id = hidden_id( f_name ) + input_id = input_id( f_bound ), + hidden_id = hidden_id( f_bound ) ) def default_choices( f_value ) : @@ -355,7 +355,7 @@ def default_match_func ( f_name ) : f_name = f_name ) -def typeahead_updater( f_name ): +def typeahead_updater( f_bound ): """ The JS script creating the function triggered when an item is selected through typeahead """ return ( @@ -365,10 +365,10 @@ def typeahead_updater( f_name ): 'return item;' '}}' ).format( - hidden_id = hidden_id( f_name ) + hidden_id = hidden_id( f_bound ) ) -def typeahead_change( f_name ): +def typeahead_change( f_bound ): """ The JS script creating the function triggered when an item is changed (i.e. looses focus and value has changed since the moment it gained focus """ @@ -380,7 +380,7 @@ def typeahead_change( f_name ): '}}' '}}' ).format( - input_id = input_id( f_name ), - hidden_id = hidden_id( f_name ) + input_id = input_id( f_bound ), + hidden_id = hidden_id( f_bound ) ) diff --git a/machines/views.py b/machines/views.py index ac37d8c6..2e5fab2b 100644 --- a/machines/views.py +++ b/machines/views.py @@ -82,7 +82,7 @@ def f_type_id( is_type_tt ): """ The id that will be used in HTML to store the value of the field type. Depends on the fact that type is generate using typeahead or not """ - return hidden_id('type') if is_type_tt else input_id('type') + return 'id_interface-type_hidden' if is_type_tt else 'id_interface-type' def generate_ipv4_choices( form ) : """ Generate the parameter choices for the bootstrap_form_typeahead tag diff --git a/preferences/forms.py b/preferences/forms.py index 20c7ca95..2cb9334f 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -33,7 +33,8 @@ class EditOptionalUserForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditOptionalUserForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'optionaluser') + super(EditOptionalUserForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['is_tel_mandatory'].label = 'Exiger un numéro de téléphone' self.fields['user_solde'].label = 'Activation du solde pour les utilisateurs' @@ -43,7 +44,8 @@ class EditOptionalMachineForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditOptionalMachineForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'optionalmachine') + super(EditOptionalMachineForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['password_machine'].label = "Possibilité d'attribuer un mot de passe par interface" self.fields['max_lambdauser_interfaces'].label = "Maximum d'interfaces autorisées pour un user normal" self.fields['max_lambdauser_aliases'].label = "Maximum d'alias dns autorisés pour un user normal" @@ -54,7 +56,8 @@ class EditOptionalTopologieForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditOptionalTopologieForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'optionaltopologie') + super(EditOptionalTopologieForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['vlan_decision_ok'].label = "Vlan où placer les machines après acceptation RADIUS" self.fields['vlan_decision_nok'].label = "Vlan où placer les machines après rejet RADIUS" @@ -64,7 +67,8 @@ class EditGeneralOptionForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditGeneralOptionForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'generaloption') + super(EditGeneralOptionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['search_display_page'].label = 'Resultats affichés dans une recherche' self.fields['pagination_number'].label = 'Items par page, taille normale (ex users)' self.fields['pagination_large_number'].label = 'Items par page, taille élevée (machines)' @@ -78,7 +82,8 @@ class EditAssoOptionForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditAssoOptionForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'assooption') + super(EditAssoOptionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Nom de l\'asso' self.fields['siret'].label = 'SIRET' self.fields['adresse1'].label = 'Adresse (ligne 1)' @@ -94,7 +99,8 @@ class EditMailMessageOptionForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - super(EditMailMessageOptionForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'mailmessageoption') + super(EditMailMessageOptionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['welcome_mail_fr'].label = 'Message dans le mail de bienvenue en français' self.fields['welcome_mail_en'].label = 'Message dans le mail de bienvenue en anglais' @@ -103,5 +109,10 @@ class ServiceForm(ModelForm): model = Service fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'service') + super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) + class DelServiceForm(Form): services = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), label="Enregistrements service actuels", widget=forms.CheckboxSelectMultiple) + diff --git a/topologie/forms.py b/topologie/forms.py index 87a3917d..5396f843 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -31,12 +31,17 @@ class PortForm(ModelForm): model = Port fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'port') + super(PortForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditPortForm(ModelForm): class Meta(PortForm.Meta): fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details'] def __init__(self, *args, **kwargs): - super(EditPortForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'port') + super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['machine_interface'].queryset = Interface.objects.all().select_related('domain__extension') self.fields['related'].queryset = Port.objects.all().select_related('switch__switch_interface__domain__extension').order_by('switch', 'port') @@ -44,18 +49,27 @@ class AddPortForm(ModelForm): class Meta(PortForm.Meta): fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'port') + super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs) + class StackForm(ModelForm): class Meta: model = Stack fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'stack') + super(StackForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditSwitchForm(ModelForm): class Meta: model = Switch fields = '__all__' def __init__(self, *args, **kwargs): - super(EditSwitchForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'switch') + super(EditSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['location'].label = 'Localisation' self.fields['number'].label = 'Nombre de ports' @@ -63,8 +77,16 @@ class NewSwitchForm(ModelForm): class Meta(EditSwitchForm.Meta): fields = ['location', 'number', 'details', 'stack', 'stack_member_id'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'switch') + super(NewSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditRoomForm(ModelForm): class Meta: model = Room fields = '__all__' + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'room') + super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs) + diff --git a/users/forms.py b/users/forms.py index 0099176f..faf3fbaf 100644 --- a/users/forms.py +++ b/users/forms.py @@ -54,6 +54,10 @@ class UserCreationForm(forms.ModelForm): password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255) is_admin = forms.BooleanField(label='is admin') + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'user') + super(UserCreationForm, self).__init__(*args, prefix=prefix, **kwargs) + class Meta: model = User fields = ('pseudo', 'name', 'surname', 'email') @@ -80,6 +84,10 @@ class ServiceUserCreationForm(forms.ModelForm): password1 = forms.CharField(label='Password', widget=forms.PasswordInput, min_length=8, max_length=255) password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, min_length=8, max_length=255) + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'serviceuser') + super(ServiceUserCreationForm, self).__init__(*args, prefix=prefix, **kwargs) + class Meta: model = ServiceUser fields = ('pseudo',) @@ -112,7 +120,8 @@ class UserChangeForm(forms.ModelForm): fields = ('pseudo', 'password', 'name', 'surname', 'email') def __init__(self, *args, **kwargs): - super(UserChangeForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'user') + super(UserChangeForm, self).__init__(*args, prefix=prefix, **kwargs) print("User is admin : %s" % kwargs['instance'].is_admin) self.initial['is_admin'] = kwargs['instance'].is_admin @@ -137,6 +146,10 @@ class ServiceUserChangeForm(forms.ModelForm): """ password = ReadOnlyPasswordHashField() + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'serviceuser') + super(ServiceUserChangeForm, self).__init__(*args, prefix=prefix, **kwargs) + class Meta: model = ServiceUser fields = ('pseudo',) @@ -163,7 +176,8 @@ class MassArchiveForm(forms.Form): class BaseInfoForm(ModelForm): def __init__(self, *args, **kwargs): - super(BaseInfoForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'user') + super(BaseInfoForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Prénom' self.fields['surname'].label = 'Nom' self.fields['school'].label = 'Établissement' @@ -226,6 +240,10 @@ class PasswordForm(ModelForm): model = User fields = ['password', 'pwd_ntlm'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'user') + super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs) + class ServiceUserForm(ModelForm): """ Modification d'un service user""" password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False) @@ -234,6 +252,10 @@ class ServiceUserForm(ModelForm): model = ServiceUser fields = ('pseudo','access_group') + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'serviceuser') + super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditServiceUserForm(ServiceUserForm): class Meta(ServiceUserForm.Meta): fields = ['access_group','comment'] @@ -244,6 +266,10 @@ class StateForm(ModelForm): model = User fields = ['state'] + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', 'user') + super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) + class SchoolForm(ModelForm): class Meta: @@ -251,7 +277,8 @@ class SchoolForm(ModelForm): fields = ['name'] def __init__(self, *args, **kwargs): - super(SchoolForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'school') + super(SchoolForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Établissement' class ListRightForm(ModelForm): @@ -260,7 +287,8 @@ class ListRightForm(ModelForm): fields = ['listright', 'details'] def __init__(self, *args, **kwargs): - super(ListRightForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'listright') + super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['listright'].label = 'Nom du droit/groupe' class NewListRightForm(ListRightForm): @@ -279,7 +307,8 @@ class DelSchoolForm(Form): class RightForm(ModelForm): def __init__(self, *args, **kwargs): - super(RightForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'right') + super(RightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['right'].label = 'Droit' self.fields['right'].empty_label = "Choisir un nouveau droit" @@ -297,7 +326,8 @@ class DelRightForm(Form): class BanForm(ModelForm): def __init__(self, *args, **kwargs): - super(BanForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'ban') + super(BanForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' class Meta: @@ -313,7 +343,8 @@ class BanForm(ModelForm): class WhitelistForm(ModelForm): def __init__(self, *args, **kwargs): - super(WhitelistForm, self).__init__(*args, **kwargs) + prefix = kwargs.pop('prefix', 'whitelist') + super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' class Meta: From 7057eafa49746022d8eb519fc65c3fc2d6570422 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 9 Oct 2017 01:29:42 +0200 Subject: [PATCH 007/138] Optimisation sur les select related --- machines/views.py | 12 ++++++------ topologie/views.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/machines/views.py b/machines/views.py index ac37d8c6..b3f4eeb7 100644 --- a/machines/views.py +++ b/machines/views.py @@ -857,13 +857,13 @@ def index(request): @login_required @permission_required('cableur') def index_iptype(request): - iptype_list = IpType.objects.select_related('extension').order_by('type') + iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type') return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list}) @login_required @permission_required('cableur') def index_vlan(request): - vlan_list = Vlan.objects.order_by('vlan_id') + vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id') return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list}) @login_required @@ -875,7 +875,7 @@ def index_machinetype(request): @login_required @permission_required('cableur') def index_nas(request): - nas_list = Nas.objects.select_related('machine_type').order_by('name') + nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name') return render(request, 'machines/index_nas.html', {'nas_list':nas_list}) @login_required @@ -903,8 +903,8 @@ def index_alias(request, interfaceid): @login_required @permission_required('cableur') def index_service(request): - service_list = Service.objects.all() - servers_list = Service_link.objects.all() + service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all() + servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all() return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list}) @login_required @@ -1012,7 +1012,7 @@ def history(request, object, id): @login_required @permission_required('cableur') def index_portlist(request): - port_list = OuverturePortList.objects.all().order_by('name') + port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set').prefetch_related('interface_set').order_by('name') return render(request, "machines/index_portlist.html", {'port_list':port_list}) @login_required diff --git a/topologie/views.py b/topologie/views.py index eba00e0c..42cd09e7 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -45,7 +45,7 @@ from preferences.models import AssoOption, GeneralOption @permission_required('cableur') def index(request): """ Vue d'affichage de tous les swicthes""" - switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain') + switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack') return render(request, 'topologie/index.html', {'switch_list': switch_list}) @login_required @@ -128,7 +128,7 @@ def index_room(request): @login_required @permission_required('infra') def index_stack(request): - stack_list = Stack.objects.order_by('name') + stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension') return render(request, 'topologie/index_stack.html', {'stack_list': stack_list}) From 172afb2e1e6db1f9a86c3397a24732ea63968ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sun, 8 Oct 2017 23:34:49 +0000 Subject: [PATCH 008/138] =?UTF-8?q?Remplace=20le=20nom=20donn=C3=A9=20?= =?UTF-8?q?=C3=A0=20la=20main=20par=20le=20nom=20de=20la=20classe=20automa?= =?UTF-8?q?tiquement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/forms.py | 8 ++++---- machines/forms.py | 30 +++++++++++++++--------------- machines/views.py | 2 +- preferences/forms.py | 14 +++++++------- topologie/forms.py | 14 +++++++------- users/forms.py | 26 +++++++++++++------------- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 9b34b332..97bc82ab 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -30,7 +30,7 @@ from .models import Article, Paiement, Facture, Banque, Vente class NewFactureForm(ModelForm): def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'facture') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['cheque'].required = False self.fields['banque'].required = False @@ -103,7 +103,7 @@ class ArticleForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'article') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = "Désignation de l'article" @@ -116,7 +116,7 @@ class PaiementForm(ModelForm): fields = ['moyen', 'type_paiement'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'paiement') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['moyen'].label = 'Moyen de paiement à ajouter' self.fields['type_paiement'].label = 'Type de paiement à ajouter' @@ -130,7 +130,7 @@ class BanqueForm(ModelForm): fields = ['name'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'banque') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Banque à ajouter' diff --git a/machines/forms.py b/machines/forms.py index 25ffad0c..7ff85695 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -40,7 +40,7 @@ class EditMachineForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'machine') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditMachineForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Nom de la machine' @@ -58,7 +58,7 @@ class EditInterfaceForm(ModelForm): fields = ['machine', 'type', 'ipv4', 'mac_address', 'details'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'interface') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['mac_address'].label = 'Adresse mac' self.fields['type'].label = 'Type de machine' @@ -112,7 +112,7 @@ class AliasForm(ModelForm): fields = ['name','extension'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'domain') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) if 'infra' in kwargs: infra = kwargs.pop('infra') super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -128,7 +128,7 @@ class DomainForm(AliasForm): initial = kwargs.get('initial', {}) initial['name'] = user.get_next_domain_name() kwargs['initial'] = initial - prefix = kwargs.pop('prefix', 'domain') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(DomainForm, self).__init__(*args, prefix=prefix, **kwargs) class DelAliasForm(Form): @@ -145,7 +145,7 @@ class MachineTypeForm(ModelForm): fields = ['type','ip_type'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'machinetype') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(MachineTypeForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['type'].label = 'Type de machine à ajouter' self.fields['ip_type'].label = "Type d'ip relié" @@ -159,7 +159,7 @@ class IpTypeForm(ModelForm): fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'iptype') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(IpTypeForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['type'].label = 'Type ip à ajouter' @@ -176,7 +176,7 @@ class ExtensionForm(ModelForm): fields = ['name', 'need_infra', 'origin'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'extension') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ExtensionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Extension à ajouter' self.fields['origin'].label = 'Enregistrement A origin' @@ -190,7 +190,7 @@ class MxForm(ModelForm): fields = ['zone', 'priority', 'name'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'mx') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(MxForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].queryset = Domain.objects.exclude(interface_parent=None) @@ -203,7 +203,7 @@ class NsForm(ModelForm): fields = ['zone', 'ns'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'ns') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NsForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['ns'].queryset = Domain.objects.exclude(interface_parent=None) @@ -216,7 +216,7 @@ class TextForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'text') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(TextForm, self).__init__(*args, prefix=prefix, **kwargs) class DelTextForm(Form): @@ -228,7 +228,7 @@ class NasForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'nas') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NasForm, self).__init__(*args, prefix=prefix, **kwargs) class DelNasForm(Form): @@ -240,7 +240,7 @@ class ServiceForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'service') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) def save(self, commit=True): @@ -259,7 +259,7 @@ class VlanForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'vlan') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) class DelVlanForm(Form): @@ -271,7 +271,7 @@ class EditOuverturePortConfigForm(ModelForm): fields = ['port_lists'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'interface') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditOuverturePortConfigForm, self).__init__(*args, prefix=prefix, **kwargs) class EditOuverturePortListForm(ModelForm): @@ -280,6 +280,6 @@ class EditOuverturePortListForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'ouvertureportlist') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditOuverturePortListForm, self).__init__(*args, prefix=prefix, **kwargs) diff --git a/machines/views.py b/machines/views.py index 2e5fab2b..3f6d8b37 100644 --- a/machines/views.py +++ b/machines/views.py @@ -82,7 +82,7 @@ def f_type_id( is_type_tt ): """ The id that will be used in HTML to store the value of the field type. Depends on the fact that type is generate using typeahead or not """ - return 'id_interface-type_hidden' if is_type_tt else 'id_interface-type' + return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type' def generate_ipv4_choices( form ) : """ Generate the parameter choices for the bootstrap_form_typeahead tag diff --git a/preferences/forms.py b/preferences/forms.py index 2cb9334f..887d768d 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -33,7 +33,7 @@ class EditOptionalUserForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'optionaluser') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditOptionalUserForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['is_tel_mandatory'].label = 'Exiger un numéro de téléphone' self.fields['user_solde'].label = 'Activation du solde pour les utilisateurs' @@ -44,7 +44,7 @@ class EditOptionalMachineForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'optionalmachine') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditOptionalMachineForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['password_machine'].label = "Possibilité d'attribuer un mot de passe par interface" self.fields['max_lambdauser_interfaces'].label = "Maximum d'interfaces autorisées pour un user normal" @@ -56,7 +56,7 @@ class EditOptionalTopologieForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'optionaltopologie') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditOptionalTopologieForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['vlan_decision_ok'].label = "Vlan où placer les machines après acceptation RADIUS" self.fields['vlan_decision_nok'].label = "Vlan où placer les machines après rejet RADIUS" @@ -67,7 +67,7 @@ class EditGeneralOptionForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'generaloption') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditGeneralOptionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['search_display_page'].label = 'Resultats affichés dans une recherche' self.fields['pagination_number'].label = 'Items par page, taille normale (ex users)' @@ -82,7 +82,7 @@ class EditAssoOptionForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'assooption') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditAssoOptionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Nom de l\'asso' self.fields['siret'].label = 'SIRET' @@ -99,7 +99,7 @@ class EditMailMessageOptionForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'mailmessageoption') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditMailMessageOptionForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['welcome_mail_fr'].label = 'Message dans le mail de bienvenue en français' self.fields['welcome_mail_en'].label = 'Message dans le mail de bienvenue en anglais' @@ -110,7 +110,7 @@ class ServiceForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'service') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) class DelServiceForm(Form): diff --git a/topologie/forms.py b/topologie/forms.py index 5396f843..8c82afba 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -32,7 +32,7 @@ class PortForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'port') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PortForm, self).__init__(*args, prefix=prefix, **kwargs) class EditPortForm(ModelForm): @@ -40,7 +40,7 @@ class EditPortForm(ModelForm): fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'port') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['machine_interface'].queryset = Interface.objects.all().select_related('domain__extension') self.fields['related'].queryset = Port.objects.all().select_related('switch__switch_interface__domain__extension').order_by('switch', 'port') @@ -50,7 +50,7 @@ class AddPortForm(ModelForm): fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'port') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs) class StackForm(ModelForm): @@ -59,7 +59,7 @@ class StackForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'stack') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(StackForm, self).__init__(*args, prefix=prefix, **kwargs) class EditSwitchForm(ModelForm): @@ -68,7 +68,7 @@ class EditSwitchForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'switch') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['location'].label = 'Localisation' self.fields['number'].label = 'Nombre de ports' @@ -78,7 +78,7 @@ class NewSwitchForm(ModelForm): fields = ['location', 'number', 'details', 'stack', 'stack_member_id'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'switch') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) class EditRoomForm(ModelForm): @@ -87,6 +87,6 @@ class EditRoomForm(ModelForm): fields = '__all__' def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'room') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs) diff --git a/users/forms.py b/users/forms.py index faf3fbaf..a8b1a219 100644 --- a/users/forms.py +++ b/users/forms.py @@ -55,7 +55,7 @@ class UserCreationForm(forms.ModelForm): is_admin = forms.BooleanField(label='is admin') def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'user') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(UserCreationForm, self).__init__(*args, prefix=prefix, **kwargs) class Meta: @@ -85,7 +85,7 @@ class ServiceUserCreationForm(forms.ModelForm): password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, min_length=8, max_length=255) def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'serviceuser') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceUserCreationForm, self).__init__(*args, prefix=prefix, **kwargs) class Meta: @@ -120,7 +120,7 @@ class UserChangeForm(forms.ModelForm): fields = ('pseudo', 'password', 'name', 'surname', 'email') def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'user') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(UserChangeForm, self).__init__(*args, prefix=prefix, **kwargs) print("User is admin : %s" % kwargs['instance'].is_admin) self.initial['is_admin'] = kwargs['instance'].is_admin @@ -147,7 +147,7 @@ class ServiceUserChangeForm(forms.ModelForm): password = ReadOnlyPasswordHashField() def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'serviceuser') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceUserChangeForm, self).__init__(*args, prefix=prefix, **kwargs) class Meta: @@ -176,7 +176,7 @@ class MassArchiveForm(forms.Form): class BaseInfoForm(ModelForm): def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'user') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(BaseInfoForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Prénom' self.fields['surname'].label = 'Nom' @@ -241,7 +241,7 @@ class PasswordForm(ModelForm): fields = ['password', 'pwd_ntlm'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'user') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs) class ServiceUserForm(ModelForm): @@ -253,7 +253,7 @@ class ServiceUserForm(ModelForm): fields = ('pseudo','access_group') def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'serviceuser') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) class EditServiceUserForm(ServiceUserForm): @@ -267,7 +267,7 @@ class StateForm(ModelForm): fields = ['state'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'user') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -277,7 +277,7 @@ class SchoolForm(ModelForm): fields = ['name'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'school') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(SchoolForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Établissement' @@ -287,7 +287,7 @@ class ListRightForm(ModelForm): fields = ['listright', 'details'] def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'listright') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['listright'].label = 'Nom du droit/groupe' @@ -307,7 +307,7 @@ class DelSchoolForm(Form): class RightForm(ModelForm): def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'right') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(RightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['right'].label = 'Droit' self.fields['right'].empty_label = "Choisir un nouveau droit" @@ -326,7 +326,7 @@ class DelRightForm(Form): class BanForm(ModelForm): def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'ban') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(BanForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' @@ -343,7 +343,7 @@ class BanForm(ModelForm): class WhitelistForm(ModelForm): def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', 'whitelist') + prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' From ad4e30f6e59b042020550d7995ad3dedca921310 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Mon, 9 Oct 2017 23:59:25 +0200 Subject: [PATCH 009/138] Politique par default d'ouverture --- machines/templates/machines/aff_iptype.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index aafc4c1d..454b169d 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Fin Préfixe v6 Sur vlan + Ouverture ports par défault @@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ type.domaine_ip_stop }} {{ type.prefix_v6 }} {{ type.vlan }} + {{ type.ouverture_ports }} {% if is_infra %} {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} From ee1dab9642467566821068aa2fe6b87da0b2650f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Tue, 10 Oct 2017 18:28:21 +0000 Subject: [PATCH 010/138] Ajout des licenses des modules JS --- static/js/handlebars/LICENSE | 19 ++ static/js/{ => handlebars}/handlebars.js | 0 static/js/konami/LICENSE.md | 21 ++ static/js/konami/konami.js | 139 +++++++++++ static/js/sapphire.js | 297 +++++++++++++++++++++++ static/js/typeahead/LICENSE | 19 ++ static/js/{ => typeahead}/typeahead.js | 0 templates/base.html | 6 +- 8 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 static/js/handlebars/LICENSE rename static/js/{ => handlebars}/handlebars.js (100%) create mode 100644 static/js/konami/LICENSE.md create mode 100644 static/js/konami/konami.js create mode 100644 static/js/sapphire.js create mode 100644 static/js/typeahead/LICENSE rename static/js/{ => typeahead}/typeahead.js (100%) diff --git a/static/js/handlebars/LICENSE b/static/js/handlebars/LICENSE new file mode 100644 index 00000000..b802d14e --- /dev/null +++ b/static/js/handlebars/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2011-2017 by Yehuda Katz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/static/js/handlebars.js b/static/js/handlebars/handlebars.js similarity index 100% rename from static/js/handlebars.js rename to static/js/handlebars/handlebars.js diff --git a/static/js/konami/LICENSE.md b/static/js/konami/LICENSE.md new file mode 100644 index 00000000..ee46fc71 --- /dev/null +++ b/static/js/konami/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Snaptortoise + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/static/js/konami/konami.js b/static/js/konami/konami.js new file mode 100644 index 00000000..0fcaab2c --- /dev/null +++ b/static/js/konami/konami.js @@ -0,0 +1,139 @@ +/* + * Konami-JS ~ + * :: Now with support for touch events and multiple instances for + * :: those situations that call for multiple easter eggs! + * Code: https://github.com/snaptortoise/konami-js + * Examples: http://www.snaptortoise.com/konami-js + * Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com) + * Version: 1.5.1 (9/4/2017) + * Licensed under the MIT License (http://opensource.org/licenses/MIT) + * Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android + */ + +var Konami = function (callback) { + var konami = { + addEvent: function (obj, type, fn, ref_obj) { + if (obj.addEventListener) + obj.addEventListener(type, fn, false); + else if (obj.attachEvent) { + // IE + obj["e" + type + fn] = fn; + obj[type + fn] = function () { + obj["e" + type + fn](window.event, ref_obj); + } + obj.attachEvent("on" + type, obj[type + fn]); + } + }, + removeEvent: function (obj, eventName, eventCallback) { + if (obj.removeEventListener) { + obj.removeEventListener(eventName, eventCallback); + } else if (obj.attachEvent) { + obj.detachEvent(eventName); + } + }, + input: "", + pattern: "38384040373937396665", + keydownHandler: function (e, ref_obj) { + if (ref_obj) { + konami = ref_obj; + } // IE + konami.input += e ? e.keyCode : event.keyCode; + if (konami.input.length > konami.pattern.length) { + konami.input = konami.input.substr((konami.input.length - konami.pattern.length)); + } + if (konami.input === konami.pattern) { + konami.code(this._currentlink); + konami.input = ''; + e.preventDefault(); + return false; + } + }, + load: function (link) { + this.addEvent(document, "keydown", this.keydownHandler, this); + this.iphone.load(link); + }, + unload: function () { + this.removeEvent(document, 'keydown', this.keydownHandler); + this.iphone.unload(); + }, + code: function (link) { + window.location = link + }, + iphone: { + start_x: 0, + start_y: 0, + stop_x: 0, + stop_y: 0, + tap: false, + capture: false, + orig_keys: "", + keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"], + input: [], + code: function (link) { + konami.code(link); + }, + touchmoveHandler: function (e) { + if (e.touches.length === 1 && konami.iphone.capture === true) { + var touch = e.touches[0]; + konami.iphone.stop_x = touch.pageX; + konami.iphone.stop_y = touch.pageY; + konami.iphone.tap = false; + konami.iphone.capture = false; + konami.iphone.check_direction(); + } + }, + toucheendHandler: function () { + if (konami.iphone.tap === true) { + konami.iphone.check_direction(this._currentLink); + } + }, + touchstartHandler: function (e) { + konami.iphone.start_x = e.changedTouches[0].pageX; + konami.iphone.start_y = e.changedTouches[0].pageY; + konami.iphone.tap = true; + konami.iphone.capture = true; + }, + load: function (link) { + this.orig_keys = this.keys; + konami.addEvent(document, "touchmove", this.touchmoveHandler); + konami.addEvent(document, "touchend", this.toucheendHandler, false); + konami.addEvent(document, "touchstart", this.touchstartHandler); + }, + unload: function () { + konami.removeEvent(document, 'touchmove', this.touchmoveHandler); + konami.removeEvent(document, 'touchend', this.toucheendHandler); + konami.removeEvent(document, 'touchstart', this.touchstartHandler); + }, + check_direction: function () { + x_magnitude = Math.abs(this.start_x - this.stop_x); + y_magnitude = Math.abs(this.start_y - this.stop_y); + x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT"; + y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP"; + result = (x_magnitude > y_magnitude) ? x : y; + result = (this.tap === true) ? "TAP" : result; + return result; + } + } + } + + typeof callback === "string" && konami.load(callback); + if (typeof callback === "function") { + konami.code = callback; + konami.load(); + } + + return konami; +}; + + +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = Konami; +} else { + if (typeof define === 'function' && define.amd) { + define([], function() { + return Konami; + }); + } else { + window.Konami = Konami; + } +} diff --git a/static/js/sapphire.js b/static/js/sapphire.js new file mode 100644 index 00000000..d5e0f897 --- /dev/null +++ b/static/js/sapphire.js @@ -0,0 +1,297 @@ +// 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 Maël Kervella +// +// 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. + +// General options +//===================================== +// Times the canvas is refreshed a second +var FPS = 30; +// Determine the length of the trail (0=instant disappear, maximum=window.innerHeight=no disappear) +var TRAIL_TIME = 5; +// The color of the characters +var RAIN_COLOR = "#00F"; +// The characters displayed +var CHARACTERS = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split(""); +// The font size used to display the characters +var FONT_SIZE = 10; +// The maximum number of characters displayed by column +var MAX_CHAR = 7; + +var Sapphire = function () { + var sapphire = { + triggerHandle: undefined, + activated: false, + runOnce: false, + + getClass: function(elt, main, name) { elt.obj = main.getElementsByClassName(name); }, + getTag: function(elt, main, name) { elt.obj = main.getElementsByTagName(name); }, + + getProp: function(elt) { + for (var i=0 ; i sapphire.canvas.height && Math.random() > 0.975) + drops[i][j] = 0; + drops[i][j]++; + } + } + } + + function drawEverything() { + attenuateBackground(); + drawMatrixRainDrop(); + } + + sapphire.triggerHandle = setInterval(drawEverything, 1000/FPS); + }, + stop: function() { + sapphire.canvas.parentNode.removeChild(sapphire.canvas); + clearInterval(sapphire.triggerHandle); + }, + + alterElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].alter(main); } }, + revertElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].revert(main); } }, + + activate: function() { + if (!sapphire.runOnce) { + sapphire.runOnce = true; + sapphire.init(); + } + if (!sapphire.activated) { + sapphire.activated = true; + sapphire.alterElts(); + sapphire.run() + } + else { + sapphire.activated = false; + sapphire.stop(); + sapphire.revertElts(); + } + } + } + + return sapphire; +} + +var s = Sapphire(); +Konami(s.activate); + diff --git a/static/js/typeahead/LICENSE b/static/js/typeahead/LICENSE new file mode 100644 index 00000000..83817bac --- /dev/null +++ b/static/js/typeahead/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-2014 Twitter, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/static/js/typeahead.js b/static/js/typeahead/typeahead.js similarity index 100% rename from static/js/typeahead.js rename to static/js/typeahead/typeahead.js diff --git a/templates/base.html b/templates/base.html index bd1a4bb1..42505b23 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,8 +35,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% bootstrap_javascript %} - - + + + + {{ site_name }} : {% block title %}Accueil{% endblock %} From 58eea72c13167493289ba5d470f9d3c83b3307a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Tue, 10 Oct 2017 21:00:41 +0000 Subject: [PATCH 011/138] Gitignore des logos de la page d'acceuil J'en ai marre d'avoir le logo du RezoMetz pour le lien du wiki --- .gitignore | 1 + static/logo/etherpad.png | Bin 4416 -> 0 bytes static/logo/federez.png | Bin 25231 -> 0 bytes static/logo/gitlab.png | Bin 16989 -> 0 bytes static/logo/kanboard.png | Bin 16495 -> 0 bytes static/logo/wiki.png | Bin 16484 -> 0 bytes static/logo/zerobin.png | Bin 4391 -> 0 bytes static_files/.static | 0 8 files changed, 1 insertion(+) delete mode 100644 static/logo/etherpad.png delete mode 100644 static/logo/federez.png delete mode 100644 static/logo/gitlab.png delete mode 100644 static/logo/kanboard.png delete mode 100644 static/logo/wiki.png delete mode 100644 static/logo/zerobin.png delete mode 100644 static_files/.static diff --git a/.gitignore b/.gitignore index 31d6b3f8..438dfbcd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ settings_local.py re2o.png __pycache__/* static_files/* +static/logo/* diff --git a/static/logo/etherpad.png b/static/logo/etherpad.png deleted file mode 100644 index 4dde5bf3fd7bfd347931f79bbf570a0b5aaabbd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4416 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 diff --git a/static/logo/federez.png b/static/logo/federez.png deleted file mode 100644 index 439de178bf70d6b0d73f1f2c703df6c07610c2b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25231 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 HDWM4fWX0V& diff --git a/static/logo/kanboard.png b/static/logo/kanboard.png deleted file mode 100644 index 5c13c937e47c7f6a50ca295f47d554496696fe73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16495 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 diff --git a/static/logo/wiki.png b/static/logo/wiki.png deleted file mode 100644 index c4437bb070355637cec2664c974f1473402cc552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16484 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 b6(Fy|Ijy_9kLvWtg)!U=R;lL@#l-Y~#Dm6W diff --git a/static_files/.static b/static_files/.static deleted file mode 100644 index e69de29b..00000000 From a03e7400161a0a0daddd49866b6c6671134f4985 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 11 Oct 2017 01:02:50 +0200 Subject: [PATCH 012/138] Vire __pseudo, inutile --- users/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/users/views.py b/users/views.py index 66a5f7ad..aa72517b 100644 --- a/users/views.py +++ b/users/views.py @@ -651,10 +651,10 @@ def profil(request, userid): if not request.user.has_perms(('cableur',)) and users != request.user: messages.error(request, "Vous ne pouvez pas afficher un autre user que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) - machines = Machine.objects.filter(user__pseudo=users).select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type__extension').prefetch_related('interface_set__type').prefetch_related('interface_set__domain__related_domain__extension') - factures = Facture.objects.filter(user__pseudo=users) - bans = Ban.objects.filter(user__pseudo=users) - whitelists = Whitelist.objects.filter(user__pseudo=users) + machines = Machine.objects.filter(user=users).select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type__extension').prefetch_related('interface_set__type').prefetch_related('interface_set__domain__related_domain__extension') + factures = Facture.objects.filter(user=users) + bans = Ban.objects.filter(user=users) + whitelists = Whitelist.objects.filter(user=users) list_droits = Right.objects.filter(user=users) options, created = OptionalUser.objects.get_or_create() user_solde = options.user_solde From 74ca8c8a9ac55cedd1d70668d3472a943bbb829d Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 11 Oct 2017 22:22:31 +0200 Subject: [PATCH 013/138] Rationalise les import + corrige les auteurs --- freeradius_utils/auth.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 5ea4e48c..e3e272c9 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -3,6 +3,7 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # +# Copyirght © 2017 Daniel Stan # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle @@ -30,20 +31,18 @@ moment de l'authentification, en WiFi, filaire, ou par les NAS eux-mêmes. Inspirés d'autres exemples trouvés ici : https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ + +Inspiré du travail de Daniel Stan au Crans """ import logging import netaddr import radiusd # Module magique freeradius (radiusd.py is dummy) -import os import binascii import hashlib - import os, sys -import os, sys - proj_path = "/var/www/re2o/" # This is so Django knows where to find stuff. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") From 5eaaa41bc49bad66a50fb3f297982e0a071d87d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Wed, 11 Oct 2017 21:33:21 +0000 Subject: [PATCH 014/138] Renome enregistrement Text en enregistrement Txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parce que ça s'appelle comme ça Faudra aussi bouger dans la bdd mais là c'est plus délicat (migrations) --- machines/forms.py | 8 ++-- .../machines/{aff_text.html => aff_txt.html} | 16 +++---- .../templates/machines/index_extension.html | 8 ++-- machines/urls.py | 8 ++-- machines/views.py | 44 +++++++++---------- 5 files changed, 42 insertions(+), 42 deletions(-) rename machines/templates/machines/{aff_text.html => aff_txt.html} (84%) diff --git a/machines/forms.py b/machines/forms.py index 7ff85695..6112a182 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -210,17 +210,17 @@ class NsForm(ModelForm): class DelNsForm(Form): ns = forms.ModelMultipleChoiceField(queryset=Ns.objects.all(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple) -class TextForm(ModelForm): +class TxtForm(ModelForm): class Meta: model = Text fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(TextForm, self).__init__(*args, prefix=prefix, **kwargs) + super(TxtForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelTextForm(Form): - text = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Text actuels", widget=forms.CheckboxSelectMultiple) +class DelTxtForm(Form): + txt = forms.ModelMultipleChoiceField(queryset=Text.objects.all(), label="Enregistrements Txt actuels", widget=forms.CheckboxSelectMultiple) class NasForm(ModelForm): class Meta: diff --git a/machines/templates/machines/aff_text.html b/machines/templates/machines/aff_txt.html similarity index 84% rename from machines/templates/machines/aff_text.html rename to machines/templates/machines/aff_txt.html index f3ada132..fd7c5ee6 100644 --- a/machines/templates/machines/aff_text.html +++ b/machines/templates/machines/aff_txt.html @@ -25,21 +25,21 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - + + + - {% for text in text_list %} + {% for txt in txt_list %} - - + + {% endfor %} diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html index 20587d85..88eb5411 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -47,12 +47,12 @@ with this program; if not, write to the Free Software Foundation, Inc., Supprimer un enregistrement NS {% endif %} {% include "machines/aff_ns.html" with ns_list=ns_list %} -

Liste des enregistrements Text

+

Liste des enregistrements TXT

{% if is_infra %} - Ajouter un enregistrement TXT - Supprimer un enregistrement TXT + Ajouter un enregistrement TXT + Supprimer un enregistrement TXT {% endif %} - {% include "machines/aff_text.html" with text_list=text_list %} + {% include "machines/aff_txt.html" with txt_list=txt_list %}


diff --git a/machines/urls.py b/machines/urls.py index 886c7c0a..1ce5db7d 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -47,9 +47,9 @@ urlpatterns = [ url(r'^add_mx/$', views.add_mx, name='add-mx'), url(r'^edit_mx/(?P[0-9]+)$', views.edit_mx, name='edit-mx'), url(r'^del_mx/$', views.del_mx, name='del-mx'), - url(r'^add_text/$', views.add_text, name='add-text'), - url(r'^edit_text/(?P[0-9]+)$', views.edit_text, name='edit-text'), - url(r'^del_text/$', views.del_text, name='del-text'), + url(r'^add_txt/$', views.add_txt, name='add-txt'), + url(r'^edit_txt/(?P[0-9]+)$', views.edit_txt, name='edit-txt'), + url(r'^del_txt/$', views.del_txt, name='del-txt'), url(r'^add_ns/$', views.add_ns, name='add-ns'), url(r'^edit_ns/(?P[0-9]+)$', views.edit_ns, name='edit-ns'), url(r'^del_ns/$', views.del_ns, name='del-ns'), @@ -76,7 +76,7 @@ urlpatterns = [ url(r'^history/(?Pextension)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pmx)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pns)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Ptext)/(?P[0-9]+)$', views.history, name='history'), + url(r'^history/(?Ptxt)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Piptype)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Palias)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pvlan)/(?P[0-9]+)$', views.history, name='history'), diff --git a/machines/views.py b/machines/views.py index a0bc92cf..06843a76 100644 --- a/machines/views.py +++ b/machines/views.py @@ -49,7 +49,7 @@ from reversion.models import Version import re from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm -from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm +from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TxtForm, DelTxtForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort from users.models import User @@ -573,11 +573,11 @@ def del_ns(request): @login_required @permission_required('infra') -def add_text(request): - text = TextForm(request.POST or None) - if text.is_valid(): +def add_txt(request): + txt = TxtForm(request.POST or None) + if txt.is_valid(): with transaction.atomic(), reversion.create_revision(): - text.save() + txt.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Cet enregistrement text a été ajouté") @@ -586,36 +586,36 @@ def add_text(request): @login_required @permission_required('infra') -def edit_text(request, textid): +def edit_txt(request, txtid): try: - text_instance = Text.objects.get(pk=textid) + txt_instance = Text.objects.get(pk=txtid) except Text.DoesNotExist: messages.error(request, u"Entrée inexistante" ) return redirect("/machines/index_extension/") - text = TextForm(request.POST or None, instance=text_instance) - if text.is_valid(): + txt = TxtForm(request.POST or None, instance=txt_instance) + if txt.is_valid(): with transaction.atomic(), reversion.create_revision(): - text.save() + txt.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in text.changed_data)) - messages.success(request, "Text modifié") + reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data)) + messages.success(request, "Txt modifié") return redirect("/machines/index_extension/") return form({'machineform': text}, 'machines/machine.html', request) @login_required @permission_required('infra') -def del_text(request): - text = DelTextForm(request.POST or None) - if text.is_valid(): - text_dels = text.cleaned_data['text'] - for text_del in text_dels: +def del_txt(request): + txt = DelTxtForm(request.POST or None) + if txt.is_valid(): + txt_dels = txt.cleaned_data['txt'] + for txt_del in txt_dels: try: with transaction.atomic(), reversion.create_revision(): - text_del.delete() + txt_del.delete() reversion.set_user(request.user) - messages.success(request, "Le text a été supprimé") + messages.success(request, "Le txt a été supprimé") except ProtectedError: - messages.error(request, "Erreur le Text suivant %s ne peut être supprimé" % text_del) + messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del) return redirect("/machines/index_extension") return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request) @@ -960,11 +960,11 @@ def history(request, object, id): except Mx.DoesNotExist: messages.error(request, "Mx inexistant") return redirect("/machines/") - elif object == 'text' and request.user.has_perms(('cableur',)): + elif object == 'txt' and request.user.has_perms(('cableur',)): try: object_instance = Text.objects.get(pk=id) except Text.DoesNotExist: - messages.error(request, "Text inexistant") + messages.error(request, "Txt inexistant") return redirect("/machines/") elif object == 'ns' and request.user.has_perms(('cableur',)): try: From 946694b87962ce757cd0ee1ef190d5258bd59eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Wed, 11 Oct 2017 21:36:14 +0000 Subject: [PATCH 015/138] =?UTF-8?q?D=C3=A9place=20le=20BFT=20tag=20dans=20?= =?UTF-8?q?l'app=20Re2o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plus générique que dans une app spécifique donc plus adapté pour utiliser le tag dans d'autre apps que machine --- machines/views.py | 2 +- {machines => re2o}/templatetags/__init__.py | 0 {machines => re2o}/templatetags/bootstrap_form_typeahead.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename {machines => re2o}/templatetags/__init__.py (100%) rename {machines => re2o}/templatetags/bootstrap_form_typeahead.py (100%) diff --git a/machines/views.py b/machines/views.py index 06843a76..06b151f0 100644 --- a/machines/views.py +++ b/machines/views.py @@ -55,7 +55,7 @@ from .models import IpType, Machine, Interface, IpList, MachineType, Extension, from users.models import User from users.models import all_has_access from preferences.models import GeneralOption, OptionalMachine -from .templatetags.bootstrap_form_typeahead import hidden_id, input_id +from re2o.templatetags.bootstrap_form_typeahead import hidden_id, input_id def all_active_interfaces(): """Renvoie l'ensemble des machines autorisées à sortir sur internet """ diff --git a/machines/templatetags/__init__.py b/re2o/templatetags/__init__.py similarity index 100% rename from machines/templatetags/__init__.py rename to re2o/templatetags/__init__.py diff --git a/machines/templatetags/bootstrap_form_typeahead.py b/re2o/templatetags/bootstrap_form_typeahead.py similarity index 100% rename from machines/templatetags/bootstrap_form_typeahead.py rename to re2o/templatetags/bootstrap_form_typeahead.py From c8ae698bbf29b4601abe76ae8d7507245543ebc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Thu, 12 Oct 2017 09:53:11 +0000 Subject: [PATCH 016/138] Prise en compte du prefix dans le bft tag --- re2o/templatetags/bootstrap_form_typeahead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/re2o/templatetags/bootstrap_form_typeahead.py b/re2o/templatetags/bootstrap_form_typeahead.py index 7ccab390..4c665361 100644 --- a/re2o/templatetags/bootstrap_form_typeahead.py +++ b/re2o/templatetags/bootstrap_form_typeahead.py @@ -209,7 +209,7 @@ def hidden_tag( f_bound, f_name ): 'input', attrs={ 'id': hidden_id( f_bound ), - 'name': f_name, + 'name': f_bound.html_name, 'type': 'hidden', 'value': f_bound.value() or "" } From 4aa67bf4d9c776511783f8dc00e737a3fcb3a408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Thu, 12 Oct 2017 09:57:16 +0000 Subject: [PATCH 017/138] Utilisation massive du bft tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le BFT tag est utilisé sur tous les champs qui ont pas mal de résultats (user, machines, ip, chambre, ...). Normalement tous les select à choix uniques concerné ont été modifiés. Je pense en avoir oublié aucun. --- .../templates/cotisations/edit_facture.html | 3 +- machines/templates/machines/machine.html | 78 +++++++++++++++++-- machines/views.py | 60 +++++++------- .../preferences/edit_preferences.html | 3 +- static/js/sapphire.js | 61 ++++++++++----- topologie/templates/topologie/switch.html | 11 ++- topologie/views.py | 9 ++- users/templates/users/user.html | 3 +- 8 files changed, 160 insertions(+), 68 deletions(-) diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index 2d6663f7..11e454f5 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load staticfiles%} +{% load bootstrap_form_typeahead %} {% block title %}Création et modification de factures{% endblock %} @@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Editer la facture

- {% bootstrap_form factureform %} + {% bootstrap_form_typeahead factureform 'user' %} {{ venteform.management_form }}

Articles de la facture

Zone concernéeEnregistrementZone concernéeEnregistrement
{{ text.zone }}{{ text.dns_entry }}{{ txt.zone }}{{ txt.dns_entry }} {% if is_infra %} - {% include 'buttons/edit.html' with href='machines:edit-text' id=text.id %} + {% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %} {% endif %} - {% include 'buttons/history.html' with href='machines:history' name='text' id=text.id %} + {% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %}
diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index d34dccb9..4f68b6ee 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -39,6 +39,36 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if domainform %} {% bootstrap_form_errors domainform %} {% endif %} +{% if iptypeform %} +{% bootstrap_form_errors iptypeform %} +{% endif %} +{% if machinetypeform %} +{% bootstrap_form_errors machinetypeform %} +{% endif %} +{% if extensionform %} +{% bootstrap_form_errors extensionform %} +{% endif %} +{% if mxform %} +{% bootstrap_form_errors mxform %} +{% endif %} +{% if nsform %} +{% bootstrap_form_errors nsform %} +{% endif %} +{% if txtform %} +{% bootstrap_form_errors txtform %} +{% endif %} +{% if aliasform %} +{% bootstrap_form_errors aliasform %} +{% endif %} +{% if serviceform %} +{% bootstrap_form_errors serviceform %} +{% endif %} +{% if vlanform %} +{% bootstrap_form_errors vlanform %} +{% endif %} +{% if nasform %} +{% bootstrap_form_errors nasform %} +{% endif %} {% csrf_token %} @@ -49,23 +79,55 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if interfaceform %}

Interface

{% if i_bft_param %} - {% if 'machine' in interfaceform.fields %} {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} - {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4' bft_param=i_bft_param %} - {% endif %} {% else %} - {% if 'machine' in interfaceform.fields %} {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} - {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4' %} - {% endif %} {% endif %} {% endif %} {% if domainform %}

Domaine

{% bootstrap_form domainform %} {% endif %} + {% if iptypeform %} +

Type d'IP

+ {% bootstrap_form iptypeform %} + {% endif %} + {% if machinetypeform %} +

Type de machine

+ {% bootstrap_form machinetypeform %} + {% endif %} + {% if extensionform %} +

Extension

+ {% bootstrap_form_typeahead extensionform 'origin' %} + {% endif %} + {% if mxform %} +

Enregistrement MX

+ {% bootstrap_form_typeahead mxform 'name' %} + {% endif %} + {% if nsform %} +

Enregistrement NS

+ {% bootstrap_form_typeahead nsform 'ns' %} + {% endif %} + {% if txtform %} +

Enregistrement TXT

+ {% bootstrap_form txtform %} + {% endif %} + {% if aliasform %} +

Alias

+ {% bootstrap_form aliasform %} + {% endif %} + {% if serviceform %} +

Service

+ {% bootstrap_form serviceform %} + {% endif %} + {% if vlanform %} +

Vlan

+ {% bootstrap_form vlanform %} + {% endif %} + {% if nasform %} +

NAS

+ {% bootstrap_form nasform %} + {% endif %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/machines/views.py b/machines/views.py index 06b151f0..0e00dc67 100644 --- a/machines/views.py +++ b/machines/views.py @@ -340,7 +340,7 @@ def add_iptype(request): reversion.set_comment("Création") messages.success(request, "Ce type d'ip a été ajouté") return redirect("/machines/index_iptype") - return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'iptypeform': iptype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -359,7 +359,7 @@ def edit_iptype(request, iptypeid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data)) messages.success(request, "Type d'ip modifié") return redirect("/machines/index_iptype/") - return form({'machineform': iptype}, 'machines/machine.html', request) + return form({'iptypeform': iptype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -377,7 +377,7 @@ def del_iptype(request): except ProtectedError: messages.error(request, "Le type d'ip %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % iptype_del) return redirect("/machines/index_iptype") - return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'iptypeform': iptype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -390,7 +390,7 @@ def add_machinetype(request): reversion.set_comment("Création") messages.success(request, "Ce type de machine a été ajouté") return redirect("/machines/index_machinetype") - return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -408,7 +408,7 @@ def edit_machinetype(request, machinetypeid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data)) messages.success(request, "Type de machine modifié") return redirect("/machines/index_machinetype/") - return form({'machineform': machinetype}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -425,7 +425,7 @@ def del_machinetype(request): except ProtectedError: messages.error(request, "Le type de machine %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % machinetype_del) return redirect("/machines/index_machinetype") - return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -438,7 +438,7 @@ def add_extension(request): reversion.set_comment("Création") messages.success(request, "Cette extension a été ajoutée") return redirect("/machines/index_extension") - return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request) + return form({'extensionform': extension}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -456,7 +456,7 @@ def edit_extension(request, extensionid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data)) messages.success(request, "Extension modifiée") return redirect("/machines/index_extension/") - return form({'machineform': extension}, 'machines/machine.html', request) + return form({'extensionform': extension}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -473,7 +473,7 @@ def del_extension(request): except ProtectedError: messages.error(request, "L'extension %s est affectée à au moins un type de machine, vous ne pouvez pas la supprimer" % extension_del) return redirect("/machines/index_extension") - return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request) + return form({'extensionform': extension}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -486,7 +486,7 @@ def add_mx(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement mx a été ajouté") return redirect("/machines/index_extension") - return form({'machineform': mx, 'interfaceform': None}, 'machines/machine.html', request) + return form({'mxform': mx}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -504,7 +504,7 @@ def edit_mx(request, mxid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data)) messages.success(request, "Mx modifié") return redirect("/machines/index_extension/") - return form({'machineform': mx}, 'machines/machine.html', request) + return form({'mxform': mx}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -521,7 +521,7 @@ def del_mx(request): except ProtectedError: messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del) return redirect("/machines/index_extension") - return form({'machineform': mx, 'interfaceform': None}, 'machines/machine.html', request) + return form({'mxform': mx}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -534,7 +534,7 @@ def add_ns(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement ns a été ajouté") return redirect("/machines/index_extension") - return form({'machineform': ns, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nsform': ns}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -552,7 +552,7 @@ def edit_ns(request, nsid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data)) messages.success(request, "Ns modifié") return redirect("/machines/index_extension/") - return form({'machineform': ns}, 'machines/machine.html', request) + return form({'nsform': ns}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -569,7 +569,7 @@ def del_ns(request): except ProtectedError: messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del) return redirect("/machines/index_extension") - return form({'machineform': ns, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nsform': ns}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -582,7 +582,7 @@ def add_txt(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement text a été ajouté") return redirect("/machines/index_extension") - return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request) + return form({'txtform': txt}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -600,7 +600,7 @@ def edit_txt(request, txtid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data)) messages.success(request, "Txt modifié") return redirect("/machines/index_extension/") - return form({'machineform': text}, 'machines/machine.html', request) + return form({'txtform': txt}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -617,7 +617,7 @@ def del_txt(request): except ProtectedError: messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del) return redirect("/machines/index_extension") - return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request) + return form({'txtform': txt}, 'machines/machine.html', request) @login_required def add_alias(request, interfaceid): @@ -645,7 +645,7 @@ def add_alias(request, interfaceid): reversion.set_comment("Création") messages.success(request, "Cet alias a été ajouté") return redirect("/machines/index_alias/" + str(interfaceid)) - return form({'machineform': alias, 'interfaceform': None}, 'machines/machine.html', request) + return form({'aliasform': alias}, 'machines/machine.html', request) @login_required def edit_alias(request, aliasid): @@ -665,7 +665,7 @@ def edit_alias(request, aliasid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data)) messages.success(request, "Alias modifié") return redirect("/machines/index_alias/" + str(alias_instance.cname.interface_parent.id)) - return form({'machineform': alias}, 'machines/machine.html', request) + return form({'aliasform': alias}, 'machines/machine.html', request) @login_required def del_alias(request, interfaceid): @@ -689,7 +689,7 @@ def del_alias(request, interfaceid): except ProtectedError: messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del) return redirect("/machines/index_alias/" + str(interfaceid)) - return form({'machineform': alias, 'interfaceform': None}, 'machines/machine.html', request) + return form({'aliasform': alias}, 'machines/machine.html', request) @login_required @@ -703,7 +703,7 @@ def add_service(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement service a été ajouté") return redirect("/machines/index_service") - return form({'machineform': service}, 'machines/machine.html', request) + return form({'serviceform': service}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -721,7 +721,7 @@ def edit_service(request, serviceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data)) messages.success(request, "Service modifié") return redirect("/machines/index_service/") - return form({'machineform': service}, 'machines/machine.html', request) + return form({'serviceform': service}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -738,7 +738,7 @@ def del_service(request): except ProtectedError: messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del) return redirect("/machines/index_service") - return form({'machineform': service}, 'machines/machine.html', request) + return form({'serviceform': service}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -751,7 +751,7 @@ def add_vlan(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement vlan a été ajouté") return redirect("/machines/index_vlan") - return form({'machineform': vlan, 'interfaceform': None}, 'machines/machine.html', request) + return form({'vlanform': vlan}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -769,7 +769,7 @@ def edit_vlan(request, vlanid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data)) messages.success(request, "Vlan modifié") return redirect("/machines/index_vlan/") - return form({'machineform': vlan}, 'machines/machine.html', request) + return form({'vlanform': vlan}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -786,7 +786,7 @@ def del_vlan(request): except ProtectedError: messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del) return redirect("/machines/index_vlan") - return form({'machineform': vlan, 'interfaceform': None}, 'machines/machine.html', request) + return form({'vlanform': vlan}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -799,7 +799,7 @@ def add_nas(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement nas a été ajouté") return redirect("/machines/index_nas") - return form({'machineform': nas, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nasform': nas}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -817,7 +817,7 @@ def edit_nas(request, nasid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data)) messages.success(request, "Nas modifié") return redirect("/machines/index_nas/") - return form({'machineform': nas}, 'machines/machine.html', request) + return form({'nasform': nas}, 'machines/machine.html', request) @login_required @permission_required('infra') @@ -834,7 +834,7 @@ def del_nas(request): except ProtectedError: messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del) return redirect("/machines/index_nas") - return form({'machineform': nas, 'interfaceform': None}, 'machines/machine.html', request) + return form({'nasform': nas}, 'machines/machine.html', request) @login_required @permission_required('cableur') diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 25fa4c02..610889dd 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load bootstrap_form_typeahead %} {% block title %}Création et modification des préférences{% endblock %} @@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} -{% bootstrap_form options %} +{% bootstrap_form_typeahead options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/static/js/sapphire.js b/static/js/sapphire.js index d5e0f897..db65fdd1 100644 --- a/static/js/sapphire.js +++ b/static/js/sapphire.js @@ -207,6 +207,9 @@ var Sapphire = function () { } }, + columns: undefined, + alpha: undefined, + drops: undefined, canvas: undefined, init: function() { @@ -214,44 +217,56 @@ var Sapphire = function () { for (var e in sapphire.elts) { sapphire.elts[e].get(main); } }, + resize: function() { + var ctx = sapphire.canvas.getContext("2d"); + var img = ctx.getImageData( 0, 0, sapphire.canvas.width, sapphire.canvas.height ); + sapphire.canvas.width = window.innerWidth; + sapphire.canvas.height = window.innerHeight; + ctx.fillStyle = "rgba(0, 0, 0, 1)"; + ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height); + ctx.putImageData( img, 0, 0 ); + sapphire.columns = sapphire.canvas.width/FONT_SIZE; + sapphire.alpha = Math.max( 0, Math.min( 1, TRAIL_TIME / ( sapphire.canvas.height/FONT_SIZE ) ) ); + var newDrops = []; + for(var x = 0; x < sapphire.columns; x++) { + if ( sapphire.drops && sapphire.drops[x] ) { newDrops[x] = sapphire.drops[x] } + else { + newDrops[x] = []; + var nb = Math.floor(Math.random()*MAX_CHAR); + for (var y = 0; y < nb; y++) + newDrops[x][y] = 0; + } + } + sapphire.drops = newDrops; + }, + run: function() { sapphire.canvas = document.createElement("canvas"); document.body.appendChild(sapphire.canvas); - sapphire.canvas.width = window.innerWidth; - sapphire.canvas.height = window.innerHeight; sapphire.canvas.style.position = "fixed"; sapphire.canvas.style.zIndex = -1; sapphire.canvas.style.left = 0; sapphire.canvas.style.top = 0; var ctx = sapphire.canvas.getContext("2d"); - - var columns = sapphire.canvas.width/FONT_SIZE; - var alpha = Math.max( 0, Math.min( 1, TRAIL_TIME / ( sapphire.canvas.height/FONT_SIZE ) ) ); - var drops = []; - for(var x = 0; x < columns; x++) - { - drops[x] = []; - var nb = Math.floor(Math.random()*MAX_CHAR); - for (var y = 0; y < nb; y++) - drops[x][y] = 1; - } + ctx.fillStyle = "rgba(0, 0, 0, 1)"; + ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height); function attenuateBackground() { - ctx.fillStyle = "rgba(0, 0, 0, "+alpha+")"; + ctx.fillStyle = "rgba(0, 0, 0, "+sapphire.alpha+")"; ctx.fillRect(0, 0, sapphire.canvas.width, sapphire.canvas.height); } function drawMatrixRainDrop() { ctx.fillStyle = RAIN_COLOR; ctx.font = FONT_SIZE + "px arial"; - for(var i = 0; i < drops.length; i++) { - for (var j = 0; j < drops[i].length; j++) { + for(var i = 0; i < sapphire.drops.length; i++) { + for (var j = 0; j < sapphire.drops[i].length; j++) { var text = CHARACTERS[Math.floor(Math.random()*CHARACTERS.length)]; - ctx.fillText(text, i*FONT_SIZE, drops[i][j]*FONT_SIZE); - if(drops[i][j]*FONT_SIZE > sapphire.canvas.height && Math.random() > 0.975) - drops[i][j] = 0; - drops[i][j]++; + ctx.fillText(text, i*FONT_SIZE, sapphire.drops[i][j]*FONT_SIZE); + if(sapphire.drops[i][j]*FONT_SIZE > sapphire.canvas.height && Math.random() > 0.975) + sapphire.drops[i][j] = 0; + sapphire.drops[i][j]++; } } } @@ -261,11 +276,15 @@ var Sapphire = function () { drawMatrixRainDrop(); } + sapphire.resize(); + window.addEventListener('resize', sapphire.resize); sapphire.triggerHandle = setInterval(drawEverything, 1000/FPS); }, + stop: function() { - sapphire.canvas.parentNode.removeChild(sapphire.canvas); + window.removeEventListener('resize', sapphire.resize); clearInterval(sapphire.triggerHandle); + sapphire.canvas.parentNode.removeChild(sapphire.canvas); }, alterElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].alter(main); } }, diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index 51cec7f6..cb84e846 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load bootstrap_form_typeahead %} {% block title %}Création et modification d'un switch{% endblock %} @@ -46,13 +47,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} {% if topoform %} - {% bootstrap_form topoform %} + {% bootstrap_form_typeahead topoform 'switch_interface' %} {% endif %} {% if machineform %} - {% bootstrap_form machineform %} + {% bootstrap_form_typeahead machineform 'user' %} {% endif %} {% if interfaceform %} - {% bootstrap_form interfaceform %} + {% if i_bft_param %} + {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% else %} + {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% endif %} {% endif %} {% if domainform %} {% bootstrap_form domainform %} diff --git a/topologie/views.py b/topologie/views.py index 42cd09e7..f3dedff8 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -38,6 +38,7 @@ from users.views import form from users.models import User from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from machines.views import generate_ipv4_bft_param from preferences.models import AssoOption, GeneralOption @@ -307,9 +308,10 @@ def new_switch(request): new_switch.save() reversion.set_user(request.user) reversion.set_comment("Création") - messages.success(request, "Le switch a été crée") + messages.success(request, "Le switch a été créé") return redirect("/topologie/") - return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'topologie/switch.html', request) + i_bft_param = generate_ipv4_bft_param( interface, False ) + return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) @login_required @permission_required('infra') @@ -348,7 +350,8 @@ def edit_switch(request, switch_id): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in switch_form.changed_data)) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'topologie/switch.html', request) + i_bft_param = generate_ipv4_bft_param( interface_form, False ) + return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) @login_required @permission_required('infra') diff --git a/users/templates/users/user.html b/users/templates/users/user.html index bbdc7fc5..62d05146 100644 --- a/users/templates/users/user.html +++ b/users/templates/users/user.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load bootstrap_form_typeahead %} {% block title %}Création et modification d'utilisateur{% endblock %} @@ -32,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} - {% bootstrap_form userform %} + {% bootstrap_form_typeahead userform 'room' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
From 2320472a3efc95e0868a78ef56280a5a2cd190fa Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Thu, 12 Oct 2017 20:03:12 +0200 Subject: [PATCH 018/138] Graph re2o sources dia (archi) --- docs_utils/re2o-archi.dia | Bin 0 -> 3922 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs_utils/re2o-archi.dia diff --git a/docs_utils/re2o-archi.dia b/docs_utils/re2o-archi.dia new file mode 100644 index 0000000000000000000000000000000000000000..1137480ea6238a12368de3b4a47c1e06d1323a36 GIT binary patch literal 3922 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 literal 0 HcmV?d00001 From 8f0474932073970d06fbaccf2302f1309815c2c8 Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Thu, 12 Oct 2017 21:02:19 +0200 Subject: [PATCH 019/138] Optimisation de l'export REST --- machines/views.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/machines/views.py b/machines/views.py index c103a080..d83b8d92 100644 --- a/machines/views.py +++ b/machines/views.py @@ -1205,24 +1205,28 @@ def service_servers(request): @permission_required('serveur') def ouverture_ports(request): r = {'ipv4':{}, 'ipv6':{}} - for i in Interface.objects.all(): - if not i.may_have_port_open(): - continue - if i.ipv4: - r['ipv4'][i.ipv4.ipv4] = {"tcp_in":[],"tcp_out":[],"udp_in":[],"udp_out":[]} - if i.ipv6_object: - r['ipv6'][i.ipv6] = {"tcp_in":[],"tcp_out":[],"udp_in":[],"udp_out":[]} - for j in i.port_lists.all(): - if i.ipv4: - r['ipv4'][i.ipv4.ipv4]["tcp_in"].extend(j.tcp_ports_in()) - r['ipv4'][i.ipv4.ipv4]["tcp_out"].extend(j.tcp_ports_out()) - r['ipv4'][i.ipv4.ipv4]["udp_in"].extend(j.udp_ports_in()) - r['ipv4'][i.ipv4.ipv4]["udp_out"].extend(j.udp_ports_out()) + for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set'): + pl = { + "tcp_in":set(map(str,o.tcp_ports_in())), + "tcp_out":set(map(str,o.tcp_ports_out())), + "udp_in":set(map(str,o.udp_ports_in())), + "udp_out":set(map(str,o.udp_ports_out())), + } + for i in o.interface_set.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)).select_related('domain').select_related('machine').select_related('type').select_related('ipv4').select_related('domain__extension').select_related('ipv4__ip_type').distinct(): + if i.may_have_port_open(): + d = r['ipv4'].get(i.ipv4.ipv4, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv4'][i.ipv4.ipv4] = d if i.ipv6_object: - r['ipv6'][i.ipv6]["tcp_in"].extend(j.tcp_ports_in()) - r['ipv6'][i.ipv6]["tcp_out"].extend(j.tcp_ports_out()) - r['ipv6'][i.ipv6]["udp_in"].extend(j.udp_ports_in()) - r['ipv6'][i.ipv6]["udp_out"].extend(j.udp_ports_out()) + d = r['ipv6'].get(i.ipv6, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv6'][i.ipv6] = d return JSONResponse(r) @csrf_exempt @login_required From e7aa29aa72e7e46835f21470785678f52189a265 Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Thu, 12 Oct 2017 23:25:05 +0200 Subject: [PATCH 020/138] Prefetch des interfaces. --- machines/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machines/views.py b/machines/views.py index d83b8d92..5c9e31e6 100644 --- a/machines/views.py +++ b/machines/views.py @@ -1205,7 +1205,7 @@ def service_servers(request): @permission_required('serveur') def ouverture_ports(request): r = {'ipv4':{}, 'ipv6':{}} - for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set'): + for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set'): pl = { "tcp_in":set(map(str,o.tcp_ports_in())), "tcp_out":set(map(str,o.tcp_ports_out())), From 2f6a2eb083d172d341c04c25deeef248817f6e9d Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Thu, 12 Oct 2017 23:54:50 +0200 Subject: [PATCH 021/138] map c'est mieux que faire un for, lalala --- machines/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/machines/serializers.py b/machines/serializers.py index 5ecf439a..b18bbfb0 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -91,9 +91,9 @@ class TypeSerializer(serializers.ModelSerializer): fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', 'ouverture_ports_udp_in', 'ouverture_ports_udp_out', ) def get_port_policy(self, obj, protocole, io): - if not obj.ouverture_ports: + if obj.ouverture_ports is None: return [] - return [str(port) for port in obj.ouverture_ports.ouvertureport_set.filter(protocole=protocole).filter(io=io)] + return map(str, obj.ouverture_ports.ouvertureport_set.filter(protocole=protocole).filter(io=io)) def get_port_policy_input_tcp(self, obj): return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.IN) From 993aeb31c81a4ac860bbfbcf004e511cadcd3caf Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 04:07:56 +0200 Subject: [PATCH 022/138] Pep8 + docstrings --- cotisations/models.py | 88 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index fca6bfa5..6388b38c 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -36,43 +36,73 @@ from django.utils import timezone from machines.models import regen + class Facture(models.Model): + """ Définition du modèle des factures. Une facture regroupe une ou + plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement + et si il y a lieu un numero pour les chèques. Possède les valeurs + valides et controle (trésorerie)""" PRETTY_NAME = "Factures émises" user = models.ForeignKey('users.User', on_delete=models.PROTECT) paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) - banque = models.ForeignKey('Banque', on_delete=models.PROTECT, blank=True, null=True) + banque = models.ForeignKey( + 'Banque', + on_delete=models.PROTECT, + blank=True, + null=True) cheque = models.CharField(max_length=255, blank=True) date = models.DateTimeField(auto_now_add=True) valid = models.BooleanField(default=True) control = models.BooleanField(default=False) def prix(self): - prix = Vente.objects.filter(facture=self).aggregate(models.Sum('prix'))['prix__sum'] + prix = Vente.objects.filter( + facture=self + ).aggregate(models.Sum('prix'))['prix__sum'] return prix def prix_total(self): - return Vente.objects.filter(facture=self).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] + """Prix total : somme des produits prix_unitaire et quantité des + ventes de l'objet""" + return Vente.objects.filter( + facture=self + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] def name(self): - name = ' - '.join(Vente.objects.filter(facture=self).values_list('name', flat=True)) + """String, somme des name des ventes de self""" + name = ' - '.join(Vente.objects.filter( + facture=self + ).values_list('name', flat=True)) return name def __str__(self): return str(self.user) + ' ' + str(self.date) + @receiver(post_save, sender=Facture) def facture_post_save(sender, **kwargs): + """Post save d'une facture, synchronise l'user ldap""" facture = kwargs['instance'] user = facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + @receiver(post_delete, sender=Facture) def facture_post_delete(sender, **kwargs): user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + class Vente(models.Model): + """Objet vente, contient une quantité, une facture parente, un nom, + un prix. Peut-être relié à un objet cotisation, via le boolean + iscotisation""" PRETTY_NAME = "Ventes effectuées" facture = models.ForeignKey('Facture', on_delete=models.CASCADE) @@ -80,44 +110,64 @@ class Vente(models.Model): name = models.CharField(max_length=255) prix = models.DecimalField(max_digits=5, decimal_places=2) iscotisation = models.BooleanField() - duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True) + duration = models.IntegerField( + help_text="Durée exprimée en mois entiers", + blank=True, + null=True) def prix_total(self): + """Renvoie le prix_total de self (nombre*prix)""" return self.prix*self.number def update_cotisation(self): if hasattr(self, 'cotisation'): cotisation = self.cotisation - cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number) + cotisation.date_end = cotisation.date_start + relativedelta( + months=self.duration*self.number) return def create_cotis(self, date_start=False): - """ Update et crée l'objet cotisation associé à une facture, prend en argument l'user, la facture pour la quantitéi, et l'article pour la durée""" + """Update et crée l'objet cotisation associé à une facture, prend + en argument l'user, la facture pour la quantitéi, et l'article pour + la durée""" if not hasattr(self, 'cotisation'): - cotisation=Cotisation(vente=self) + cotisation = Cotisation(vente=self) if date_start: - end_adhesion = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self.facture.user).exclude(valid=False))).filter(date_start__lt=date_start).aggregate(Max('date_end'))['date_end__max'] + end_adhesion = Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self.facture.user + ).exclude(valid=False)) + ).filter( + date_start__lt=date_start + ).aggregate(Max('date_end'))['date_end__max'] else: end_adhesion = self.facture.user.end_adhesion() date_start = date_start or timezone.now() end_adhesion = end_adhesion or date_start date_max = max(end_adhesion, date_start) cotisation.date_start = date_max - cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number) + cotisation.date_end = cotisation.date_start + relativedelta( + months=self.duration*self.number + ) return def save(self, *args, **kwargs): # On verifie que si iscotisation, duration est présent if self.iscotisation and not self.duration: - raise ValidationError("Cotisation et durée doivent être présents ensembles") + raise ValidationError("Cotisation et durée doivent être présents\ + ensembles") self.update_cotisation() super(Vente, self).save(*args, **kwargs) def __str__(self): return str(self.name) + ' ' + str(self.facture) + @receiver(post_save, sender=Vente) def vente_post_save(sender, **kwargs): + """Post save d'une vente, déclencge la création de l'objet cotisation + si il y a lieu(si iscotisation) """ vente = kwargs['instance'] if hasattr(vente, 'cotisation'): vente.cotisation.vente = vente @@ -128,6 +178,7 @@ def vente_post_save(sender, **kwargs): user = vente.facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + @receiver(post_delete, sender=Vente) def vente_post_delete(sender, **kwargs): vente = kwargs['instance'] @@ -135,7 +186,10 @@ def vente_post_delete(sender, **kwargs): user = vente.facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + class Article(models.Model): + """Liste des articles en vente : prix, nom, et attribut iscotisation + et duree si c'est une cotisation""" PRETTY_NAME = "Articles en vente" name = models.CharField(max_length=255, unique=True) @@ -154,7 +208,9 @@ class Article(models.Model): def __str__(self): return self.name + class Banque(models.Model): + """Liste des banques""" PRETTY_NAME = "Banques enregistrées" name = models.CharField(max_length=255) @@ -162,7 +218,9 @@ class Banque(models.Model): def __str__(self): return self.name + class Paiement(models.Model): + """Moyens de paiement""" PRETTY_NAME = "Moyens de paiement" PAYMENT_TYPES = ( (0, 'Autre'), @@ -179,11 +237,15 @@ class Paiement(models.Model): self.moyen = self.moyen.title() def save(self, *args, **kwargs): + """Un seul type de paiement peut-etre cheque...""" if Paiement.objects.filter(type_paiement=1).count() > 1: - raise ValidationError("On ne peut avoir plusieurs mode de paiement chèque") + raise ValidationError("On ne peut avoir plusieurs mode de paiement\ + chèque") super(Paiement, self).save(*args, **kwargs) + class Cotisation(models.Model): + """Objet cotisation, debut et fin, relié en onetoone à une vente""" PRETTY_NAME = "Cotisations" vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True) @@ -193,6 +255,7 @@ class Cotisation(models.Model): def __str__(self): return str(self.vente) + @receiver(post_save, sender=Cotisation) def cotisation_post_save(sender, **kwargs): regen('dns') @@ -200,6 +263,7 @@ def cotisation_post_save(sender, **kwargs): regen('mac_ip_list') regen('mailing') + @receiver(post_delete, sender=Cotisation) def vente_post_delete(sender, **kwargs): cotisation = kwargs['instance'] From 500884458c135be209b73c57ce389c98a278db4d Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 05:08:30 +0200 Subject: [PATCH 023/138] PEP8 et doc strings sur views de cotisations --- cotisations/views.py | 323 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 259 insertions(+), 64 deletions(-) diff --git a/cotisations/views.py b/cotisations/views.py index 0a2a7648..517eafd1 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -40,7 +40,10 @@ from reversion import revisions as reversion from reversion.models import Version from .models import Facture, Article, Vente, Cotisation, Paiement, Banque -from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, CreditSoldeForm, SelectArticleForm +from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm +from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm +from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf +from .forms import SelectArticleForm, CreditSoldeForm from users.models import User from .tex import render_tex from re2o.settings import LOGO_PATH @@ -50,18 +53,27 @@ from preferences.models import OptionalUser, AssoOption, GeneralOption from dateutil.relativedelta import relativedelta from django.utils import timezone + def form(ctx, template, request): c = ctx c.update(csrf(request)) return render(request, template, c) + @login_required @permission_required('cableur') def new_facture(request, userid): + """Creation d'une facture pour un user. Renvoie la liste des articles + et crée des factures dans un formset. Utilise un peu de js coté template + pour ajouter des articles. + Parse les article et boucle dans le formset puis save les ventes, + enfin sauve la facture parente. + TODO : simplifier cette fonction, déplacer l'intelligence coté models + Facture et Vente.""" try: user = User.objects.get(pk=userid) except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) + messages.error(request, u"Utilisateur inexistant") return redirect("/cotisations/") facture = Facture(user=user) # Le template a besoin de connaitre les articles pour le js @@ -77,15 +89,20 @@ def new_facture(request, userid): options, created = OptionalUser.objects.get_or_create() user_solde = options.user_solde solde_negatif = options.solde_negatif - # Si on paye par solde, que l'option est activée, on vérifie que le négatif n'est pas atteint + # Si on paye par solde, que l'option est activée, + # on vérifie que le négatif n'est pas atteint if user_solde: - if new_facture.paiement == Paiement.objects.get_or_create(moyen='solde')[0]: + if new_facture.paiement == Paiement.objects.get_or_create( + moyen='solde' + )[0]: prix_total = 0 for art_item in articles: if art_item.cleaned_data: - prix_total += art_item.cleaned_data['article'].prix*art_item.cleaned_data['quantity'] + prix_total += art_item.cleaned_data['article']\ + .prix*art_item.cleaned_data['quantity'] if float(user.solde) - float(prix_total) < solde_negatif: - messages.error(request, "Le solde est insuffisant pour effectuer l'opération") + messages.error(request, "Le solde est insuffisant pour\ + effectuer l'opération") return redirect("/users/profil/" + userid) with transaction.atomic(), reversion.create_revision(): new_facture.save() @@ -95,22 +112,47 @@ def new_facture(request, userid): if art_item.cleaned_data: article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] - new_vente = Vente.objects.create(facture=new_facture, name=article.name, prix=article.prix, iscotisation=article.iscotisation, duration=article.duration, number=quantity) + new_vente = Vente.objects.create( + facture=new_facture, + name=article.name, + prix=article.prix, + iscotisation=article.iscotisation, + duration=article.duration, + number=quantity + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) reversion.set_comment("Création") - if any(art_item.cleaned_data['article'].iscotisation for art_item in articles if art_item.cleaned_data): - messages.success(request, "La cotisation a été prolongée pour l'adhérent %s jusqu'au %s" % (user.pseudo, user.end_adhesion()) ) + if any(art_item.cleaned_data['article'].iscotisation + for art_item in articles if art_item.cleaned_data): + messages.success( + request, + "La cotisation a été prolongée\ + pour l'adhérent %s jusqu'au %s" % ( + user.pseudo, user.end_adhesion() + ) + ) else: messages.success(request, "La facture a été crée") return redirect("/users/profil/" + userid) - messages.error(request, u"Il faut au moins un article valide pour créer une facture" ) - return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request) + messages.error( + request, + u"Il faut au moins un article valide pour créer une facture" + ) + return form({ + 'factureform': facture_form, + 'venteform': article_formset, + 'articlelist': article_list + }, 'cotisations/new_facture.html', request) + @login_required @permission_required('tresorier') def new_facture_pdf(request): + """Permet de générer un pdf d'une facture. Réservée + au trésorier, permet d'emettre des factures sans objet + Vente ou Facture correspondant en bdd""" facture_form = NewFactureFormPdf(request.POST or None) if facture_form.is_valid(): options, created = AssoOption.objects.get_or_create() @@ -124,68 +166,129 @@ def new_facture_pdf(request): for a in article: tbl.append([a, quantite, a.prix * quantite]) prix_total = sum(a[2] for a in tbl) - user = {'name':destinataire, 'room':chambre} - return render_tex(request, 'cotisations/factures.tex', {'DATE' : timezone.now(),'dest':user,'fid':fid, 'article':tbl, 'total':prix_total, 'paid':paid, 'asso_name':options.name, 'line1':options.adresse1, 'line2':options.adresse2, 'siret':options.siret, 'email':options.contact, 'phone':options.telephone, 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)}) - return form({'factureform': facture_form}, 'cotisations/facture.html', request) + user = {'name': destinataire, 'room': chambre} + return render_tex(request, 'cotisations/factures.tex', { + 'DATE': timezone.now(), + 'dest': user, + 'fid': fid, + 'article': tbl, + 'total': prix_total, + 'paid': paid, + 'asso_name': options.name, + 'line1': options.adresse1, + 'line2': options.adresse2, + 'siret': options.siret, + 'email': options.contact, + 'phone': options.telephone, + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + }) + return form({ + 'factureform': facture_form + }, 'cotisations/facture.html', request) + @login_required def facture_pdf(request, factureid): + """Affiche en pdf une facture. Cree une ligne par Vente de la facture, + et génére une facture avec le total, le moyen de paiement, l'adresse + de l'adhérent, etc. Réservée à self pour un user sans droits, + les droits cableurs permettent d'afficher toute facture""" try: facture = Facture.objects.get(pk=factureid) except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante" ) + messages.error(request, u"Facture inexistante") return redirect("/cotisations/") - if not request.user.has_perms(('cableur',)) and facture.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher une facture ne vous appartenant pas sans droit cableur") + if not request.user.has_perms(('cableur',))\ + and facture.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher une facture ne vous\ + appartenant pas sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) if not facture.valid: - messages.error(request, "Vous ne pouvez pas afficher une facture non valide") + messages.error(request, "Vous ne pouvez pas afficher\ + une facture non valide") return redirect("/users/profil/" + str(request.user.id)) vente = Vente.objects.all().filter(facture=facture) ventes = [] options, created = AssoOption.objects.get_or_create() for v in vente: ventes.append([v, v.number, v.prix_total]) - return render_tex(request, 'cotisations/factures.tex', {'paid':True, 'fid':facture.id, 'DATE':facture.date,'dest':facture.user, 'article':ventes, 'total': facture.prix_total(), 'asso_name':options.name, 'line1': options.adresse1, 'line2':options.adresse2, 'siret':options.siret, 'email':options.contact, 'phone':options.telephone, 'tpl_path':os.path.join(settings.BASE_DIR, LOGO_PATH)}) + return render_tex(request, 'cotisations/factures.tex', { + 'paid': True, + 'fid': facture.id, + 'DATE': facture.date, + 'dest': facture.user, + 'article': ventes, + 'total': facture.prix_total(), + 'asso_name': options.name, + 'line1': options.adresse1, + 'line2': options.adresse2, + 'siret': options.siret, + 'email': options.contact, + 'phone': options.telephone, + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + }) + @login_required @permission_required('cableur') def edit_facture(request, factureid): + """Permet l'édition d'une facture. On peut y éditer les ventes + déjà effectuer, ou rendre une facture invalide (non payées, chèque + en bois etc). Mets à jour les durée de cotisation attenantes""" try: facture = Facture.objects.get(pk=factureid) except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante" ) + messages.error(request, u"Facture inexistante") return redirect("/cotisations/") if request.user.has_perms(['tresorier']): - facture_form = TrezEditFactureForm(request.POST or None, instance=facture) + facture_form = TrezEditFactureForm( + request.POST or None, + instance=facture + ) elif facture.control or not facture.valid: - messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier") + messages.error(request, "Vous ne pouvez pas editer une facture\ + controlée ou invalidée par le trésorier") return redirect("/cotisations/") else: facture_form = EditFactureForm(request.POST or None, instance=facture) ventes_objects = Vente.objects.filter(facture=facture) - vente_form_set = modelformset_factory(Vente, fields=('name','number'), extra=0, max_num=len(ventes_objects)) + vente_form_set = modelformset_factory( + Vente, + fields=('name', 'number'), + extra=0, + max_num=len(ventes_objects) + ) vente_form = vente_form_set(request.POST or None, queryset=ventes_objects) if facture_form.is_valid() and vente_form.is_valid(): with transaction.atomic(), reversion.create_revision(): facture_form.save() vente_form.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for form in vente_form for field in facture_form.changed_data + form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for form in vente_form for field + in facture_form.changed_data + form.changed_data) + ) messages.success(request, "La facture a bien été modifiée") return redirect("/cotisations/") - return form({'factureform': facture_form, 'venteform': vente_form}, 'cotisations/edit_facture.html', request) + return form({ + 'factureform': facture_form, + 'venteform': vente_form + }, 'cotisations/edit_facture.html', request) + @login_required @permission_required('cableur') def del_facture(request, factureid): + """Suppression d'une facture. Supprime en cascade les ventes + et cotisations filles""" try: facture = Facture.objects.get(pk=factureid) except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante" ) + messages.error(request, u"Facture inexistante") return redirect("/cotisations/") if (facture.control or not facture.valid): - messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier") + messages.error(request, "Vous ne pouvez pas editer une facture\ + controlée ou invalidée par le trésorier") return redirect("/cotisations/") if request.method == "POST": with transaction.atomic(), reversion.create_revision(): @@ -193,7 +296,11 @@ def del_facture(request, factureid): reversion.set_user(request.user) messages.success(request, "La facture a été détruite") return redirect("/cotisations/") - return form({'objet': facture, 'objet_name': 'facture'}, 'cotisations/delete.html', request) + return form({ + 'objet': facture, + 'objet_name': 'facture' + }, 'cotisations/delete.html', request) + @login_required @permission_required('cableur') @@ -202,7 +309,7 @@ def credit_solde(request, userid): try: user = User.objects.get(pk=userid) except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) + messages.error(request, u"Utilisateur inexistant") return redirect("/cotisations/") facture = CreditSoldeForm(request.POST or None) if facture.is_valid(): @@ -211,8 +318,15 @@ def credit_solde(request, userid): facture_instance.user = user facture_instance.save() reversion.set_user(request.user) - reversion.set_comment("Création") - new_vente = Vente.objects.create(facture=facture_instance, name="solde", prix=facture.cleaned_data['montant'], iscotisation=False, duration=0, number=1) + reversion.set_comment("Création") + new_vente = Vente.objects.create( + facture=facture_instance, + name="solde", + prix=facture.cleaned_data['montant'], + iscotisation=False, + duration=0, + number=1 + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) @@ -225,6 +339,13 @@ def credit_solde(request, userid): @login_required @permission_required('tresorier') def add_article(request): + """Ajoute un article. Champs : désignation, + prix, est-ce une cotisation et si oui sa durée + Réservé au trésorier + Nota bene : les ventes déjà effectuées ne sont pas reliées + aux articles en vente. La désignation, le prix... sont + copiés à la création de la facture. Un changement de prix n'a + PAS de conséquence sur les ventes déjà faites""" article = ArticleForm(request.POST or None) if article.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -235,27 +356,36 @@ def add_article(request): return redirect("/cotisations/index_article/") return form({'factureform': article}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def edit_article(request, articleid): + """Edition d'un article (designation, prix, etc) + Réservé au trésorier""" try: article_instance = Article.objects.get(pk=articleid) except Article.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/cotisations/index_article/") article = ArticleForm(request.POST or None, instance=article_instance) if article.is_valid(): with transaction.atomic(), reversion.create_revision(): article.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in article.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in article.changed_data + ) + ) messages.success(request, "Type d'article modifié") return redirect("/cotisations/index_article/") return form({'factureform': article}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def del_article(request): + """Suppression d'un article en vente""" article = DelArticleForm(request.POST or None) if article.is_valid(): article_del = article.cleaned_data['articles'] @@ -266,9 +396,12 @@ def del_article(request): return redirect("/cotisations/index_article") return form({'factureform': article}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def add_paiement(request): + """Ajoute un moyen de paiement. Relié aux factures + via foreign key""" paiement = PaiementForm(request.POST or None) if paiement.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -279,27 +412,35 @@ def add_paiement(request): return redirect("/cotisations/index_paiement/") return form({'factureform': paiement}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def edit_paiement(request, paiementid): + """Edition d'un moyen de paiement""" try: paiement_instance = Paiement.objects.get(pk=paiementid) except Paiement.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/cotisations/index_paiement/") paiement = PaiementForm(request.POST or None, instance=paiement_instance) if paiement.is_valid(): with transaction.atomic(), reversion.create_revision(): paiement.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in paiement.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in paiement.changed_data + ) + ) messages.success(request, "Type de paiement modifié") return redirect("/cotisations/index_paiement/") return form({'factureform': paiement}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def del_paiement(request): + """Suppression d'un moyen de paiement""" paiement = DelPaiementForm(request.POST or None) if paiement.is_valid(): paiement_dels = paiement.cleaned_data['paiements'] @@ -309,15 +450,24 @@ def del_paiement(request): paiement_del.delete() reversion.set_user(request.user) reversion.set_comment("Destruction") - messages.success(request, "Le moyen de paiement a été supprimé") + messages.success( + request, + "Le moyen de paiement a été supprimé" + ) except ProtectedError: - messages.error(request, "Le moyen de paiement %s est affecté à au moins une facture, vous ne pouvez pas le supprimer" % paiement_del) + messages.error( + request, + "Le moyen de paiement %s est affecté à au moins une\ + facture, vous ne pouvez pas le supprimer" % paiement_del + ) return redirect("/cotisations/index_paiement/") return form({'factureform': paiement}, 'cotisations/facture.html', request) + @login_required @permission_required('cableur') def add_banque(request): + """Ajoute une banque à la liste des banques""" banque = BanqueForm(request.POST or None) if banque.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -328,27 +478,35 @@ def add_banque(request): return redirect("/cotisations/index_banque/") return form({'factureform': banque}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def edit_banque(request, banqueid): + """Edite le nom d'une banque""" try: banque_instance = Banque.objects.get(pk=banqueid) except Banque.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/cotisations/index_banque/") banque = BanqueForm(request.POST or None, instance=banque_instance) if banque.is_valid(): with transaction.atomic(), reversion.create_revision(): banque.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in banque.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in banque.changed_data + ) + ) messages.success(request, "Banque modifiée") return redirect("/cotisations/index_banque/") return form({'factureform': banque}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def del_banque(request): + """Supprime une banque""" banque = DelBanqueForm(request.POST or None) if banque.is_valid(): banque_dels = banque.cleaned_data['banques'] @@ -360,17 +518,25 @@ def del_banque(request): reversion.set_comment("Destruction") messages.success(request, "La banque a été supprimée") except ProtectedError: - messages.error(request, "La banque %s est affectée à au moins une facture, vous ne pouvez pas la supprimer" % banque_del) + messages.error(request, "La banque %s est affectée à au moins\ + une facture, vous ne pouvez pas la supprimer" % banque_del) return redirect("/cotisations/index_banque/") return form({'factureform': banque}, 'cotisations/facture.html', request) + @login_required @permission_required('tresorier') def control(request): + """Pour le trésorier, vue pour controler en masse les + factures.Case à cocher, pratique""" options, created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number facture_list = Facture.objects.order_by('date').reverse() - controlform_set = modelformset_factory(Facture, fields=('control','valid'), extra=0) + controlform_set = modelformset_factory( + Facture, + fields=('control', 'valid'), + extra=0 + ) paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -379,7 +545,9 @@ def control(request): facture_list = paginator.page(1) except EmptyPage: facture_list = paginator.page(paginator.num.pages) - page_query = Facture.objects.order_by('date').reverse().filter(id__in=[facture.id for facture in facture_list]) + page_query = Facture.objects.order_by('date').reverse().filter( + id__in=[facture.id for facture in facture_list] + ) controlform = controlform_set(request.POST or None, queryset=page_query) if controlform.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -387,32 +555,50 @@ def control(request): reversion.set_user(request.user) reversion.set_comment("Controle trésorier") return redirect("/cotisations/control/") - return render(request, 'cotisations/control.html', {'facture_list': facture_list, 'controlform': controlform}) + return render(request, 'cotisations/control.html', { + 'facture_list': facture_list, + 'controlform': controlform + }) + @login_required @permission_required('cableur') def index_article(request): + """Affiche l'ensemble des articles en vente""" article_list = Article.objects.order_by('name') - return render(request, 'cotisations/index_article.html', {'article_list':article_list}) + return render(request, 'cotisations/index_article.html', { + 'article_list': article_list + }) + @login_required @permission_required('cableur') def index_paiement(request): + """Affiche l'ensemble des moyens de paiement en vente""" paiement_list = Paiement.objects.order_by('moyen') - return render(request, 'cotisations/index_paiement.html', {'paiement_list':paiement_list}) + return render(request, 'cotisations/index_paiement.html', { + 'paiement_list': paiement_list + }) + @login_required @permission_required('cableur') def index_banque(request): + """Affiche l'ensemble des banques""" banque_list = Banque.objects.order_by('name') - return render(request, 'cotisations/index_banque.html', {'banque_list':banque_list}) + return render(request, 'cotisations/index_banque.html', { + 'banque_list': banque_list + }) + @login_required @permission_required('cableur') def index(request): + """Affiche l'ensemble des factures, pour les cableurs et +""" options, created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - facture_list = Facture.objects.order_by('date').select_related('user').select_related('paiement').prefetch_related('vente_set').reverse() + facture_list = Facture.objects.order_by('date').select_related('user')\ + .select_related('paiement').prefetch_related('vente_set').reverse() paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -423,37 +609,43 @@ def index(request): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. facture_list = paginator.page(paginator.num_pages) - return render(request, 'cotisations/index.html', {'facture_list': facture_list}) + return render(request, 'cotisations/index.html', { + 'facture_list': facture_list + }) + @login_required def history(request, object, id): + """Affiche l'historique de chaque objet""" if object == 'facture': try: - object_instance = Facture.objects.get(pk=id) + object_instance = Facture.objects.get(pk=id) except Facture.DoesNotExist: - messages.error(request, "Facture inexistante") - return redirect("/cotisations/") - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'une facture d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) + messages.error(request, "Facture inexistante") + return redirect("/cotisations/") + if not request.user.has_perms(('cableur',))\ + and object_instance.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher l'historique\ + d'une facture d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) elif object == 'paiement' and request.user.has_perms(('cableur',)): try: - object_instance = Paiement.objects.get(pk=id) + object_instance = Paiement.objects.get(pk=id) except Paiement.DoesNotExist: - messages.error(request, "Paiement inexistant") - return redirect("/cotisations/") + messages.error(request, "Paiement inexistant") + return redirect("/cotisations/") elif object == 'article' and request.user.has_perms(('cableur',)): try: - object_instance = Article.objects.get(pk=id) + object_instance = Article.objects.get(pk=id) except Article.DoesNotExist: - messages.error(request, "Article inexistante") - return redirect("/cotisations/") + messages.error(request, "Article inexistante") + return redirect("/cotisations/") elif object == 'banque' and request.user.has_perms(('cableur',)): try: - object_instance = Banque.objects.get(pk=id) + object_instance = Banque.objects.get(pk=id) except Banque.DoesNotExist: - messages.error(request, "Banque inexistante") - return redirect("/cotisations/") + messages.error(request, "Banque inexistante") + return redirect("/cotisations/") else: messages.error(request, "Objet inconnu") return redirect("/cotisations/") @@ -470,4 +662,7 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render(request, 're2o/history.html', { + 'reversions': reversions, + 'object': object_instance + }) From 7039e7b0914e4ae94083be3574e0111ff1e535e2 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 05:24:57 +0200 Subject: [PATCH 024/138] Pep8 et doc strings --- cotisations/forms.py | 100 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 97bc82ab..b3742088 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -28,7 +28,10 @@ from django import forms from django.core.validators import MinValueValidator from .models import Article, Paiement, Facture, Banque, Vente + class NewFactureForm(ModelForm): + """Creation d'une facture, moyen de paiement, banque et numero + de cheque""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -36,58 +39,91 @@ class NewFactureForm(ModelForm): self.fields['banque'].required = False self.fields['cheque'].label = 'Numero de chèque' self.fields['banque'].empty_label = "Non renseigné" - self.fields['paiement'].empty_label = "Séléctionner un moyen de paiement" - self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects.filter(type_paiement=1).first().id + self.fields['paiement'].empty_label = "Séléctionner\ + un moyen de paiement" + self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects\ + .filter(type_paiement=1).first().id class Meta: model = Facture - fields = ['paiement','banque','cheque'] + fields = ['paiement', 'banque', 'cheque'] def clean(self): - cleaned_data=super(NewFactureForm, self).clean() + cleaned_data = super(NewFactureForm, self).clean() paiement = cleaned_data.get("paiement") cheque = cleaned_data.get("cheque") banque = cleaned_data.get("banque") if not paiement: - raise forms.ValidationError("Le moyen de paiement est obligatoire.") + raise forms.ValidationError("Le moyen de paiement est obligatoire") elif paiement.type_paiement == "check" and not (cheque and banque): - raise forms.ValidationError("Le numéro de chèque et la banque sont obligatoires.") + raise forms.ValidationError("Le numéro de chèque et\ + la banque sont obligatoires.") return cleaned_data + class CreditSoldeForm(NewFactureForm): + """Permet de faire des opérations sur le solde si il est activé""" class Meta(NewFactureForm.Meta): model = Facture - fields = ['paiement','banque','cheque'] + fields = ['paiement', 'banque', 'cheque'] def __init__(self, *args, **kwargs): super(CreditSoldeForm, self).__init__(*args, **kwargs) - self.fields['paiement'].queryset = Paiement.objects.exclude(moyen='solde').exclude(moyen="Solde") - + self.fields['paiement'].queryset = Paiement.objects.exclude( + moyen='solde' + ).exclude(moyen="Solde") montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) + class SelectArticleForm(Form): - article = forms.ModelChoiceField(queryset=Article.objects.all(), label="Article", required=True) - quantity = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)], required=True) + """Selection d'un article lors de la creation d'une facture""" + article = forms.ModelChoiceField( + queryset=Article.objects.all(), + label="Article", + required=True + ) + quantity = forms.IntegerField( + label="Quantité", + validators=[MinValueValidator(1)], + required=True + ) + class NewFactureFormPdf(Form): - article = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Article") - number = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)]) + """Creation d'un pdf facture par le trésorier""" + article = forms.ModelMultipleChoiceField( + queryset=Article.objects.all(), + label="Article" + ) + number = forms.IntegerField( + label="Quantité", + validators=[MinValueValidator(1)] + ) paid = forms.BooleanField(label="Payé", required=False) dest = forms.CharField(required=True, max_length=255, label="Destinataire") chambre = forms.CharField(required=False, max_length=10, label="Adresse") - fid = forms.CharField(required=True, max_length=10, label="Numéro de la facture") + fid = forms.CharField( + required=True, + max_length=10, + label="Numéro de la facture" + ) + class EditFactureForm(NewFactureForm): + """Edition d'une facture : moyen de paiement, banque, user parent""" class Meta(NewFactureForm.Meta): - fields = ['paiement','banque','cheque','user'] + fields = ['paiement', 'banque', 'cheque', 'user'] def __init__(self, *args, **kwargs): super(EditFactureForm, self).__init__(*args, **kwargs) self.fields['user'].label = 'Adherent' - self.fields['user'].empty_label = "Séléctionner l'adhérent propriétaire" + self.fields['user'].empty_label = "Séléctionner\ + l'adhérent propriétaire" + class TrezEditFactureForm(EditFactureForm): + """Vue pour édition controle trésorier""" class Meta(EditFactureForm.Meta): fields = '__all__' @@ -98,6 +134,7 @@ class TrezEditFactureForm(EditFactureForm): class ArticleForm(ModelForm): + """Creation d'un article. Champs : nom, cotisation, durée""" class Meta: model = Article fields = '__all__' @@ -107,10 +144,20 @@ class ArticleForm(ModelForm): super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = "Désignation de l'article" + class DelArticleForm(Form): - articles = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Articles actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs articles en vente. Choix + parmis les modèles""" + articles = forms.ModelMultipleChoiceField( + queryset=Article.objects.all(), + label="Articles actuels", + widget=forms.CheckboxSelectMultiple + ) + class PaiementForm(ModelForm): + """Creation d'un moyen de paiement, champ text moyen et type + permettant d'indiquer si il s'agit d'un chèque ou non pour le form""" class Meta: model = Paiement fields = ['moyen', 'type_paiement'] @@ -121,10 +168,19 @@ class PaiementForm(ModelForm): self.fields['moyen'].label = 'Moyen de paiement à ajouter' self.fields['type_paiement'].label = 'Type de paiement à ajouter' + class DelPaiementForm(Form): - paiements = forms.ModelMultipleChoiceField(queryset=Paiement.objects.all(), label="Moyens de paiement actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs moyens de paiements, selection + parmis les models""" + paiements = forms.ModelMultipleChoiceField( + queryset=Paiement.objects.all(), + label="Moyens de paiement actuels", + widget=forms.CheckboxSelectMultiple + ) + class BanqueForm(ModelForm): + """Creation d'une banque, field name""" class Meta: model = Banque fields = ['name'] @@ -134,5 +190,11 @@ class BanqueForm(ModelForm): super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Banque à ajouter' + class DelBanqueForm(Form): - banques = forms.ModelMultipleChoiceField(queryset=Banque.objects.all(), label="Banques actuelles", widget=forms.CheckboxSelectMultiple) + """Selection d'une ou plusieurs banques, pour suppression""" + banques = forms.ModelMultipleChoiceField( + queryset=Banque.objects.all(), + label="Banques actuelles", + widget=forms.CheckboxSelectMultiple + ) From 9ab06207bd8b0f10a95be01c3d0b75d28ccb6246 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 05:30:35 +0200 Subject: [PATCH 025/138] Pep8 --- cotisations/urls.py | 116 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/cotisations/urls.py b/cotisations/urls.py index 2cf86888..f59fd678 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -27,30 +27,96 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^new_facture/(?P[0-9]+)$', views.new_facture, name='new-facture'), - url(r'^edit_facture/(?P[0-9]+)$', views.edit_facture, name='edit-facture'), - url(r'^del_facture/(?P[0-9]+)$', views.del_facture, name='del-facture'), - url(r'^facture_pdf/(?P[0-9]+)$', views.facture_pdf, name='facture-pdf'), - url(r'^new_facture_pdf/$', views.new_facture_pdf, name='new-facture-pdf'), - url(r'^credit_solde/(?P[0-9]+)$', views.credit_solde, name='credit-solde'), - url(r'^add_article/$', views.add_article, name='add-article'), - url(r'^edit_article/(?P[0-9]+)$', views.edit_article, name='edit-article'), - url(r'^del_article/$', views.del_article, name='del-article'), - url(r'^add_paiement/$', views.add_paiement, name='add-paiement'), - url(r'^edit_paiement/(?P[0-9]+)$', views.edit_paiement, name='edit-paiement'), - url(r'^del_paiement/$', views.del_paiement, name='del-paiement'), - url(r'^add_banque/$', views.add_banque, name='add-banque'), - url(r'^edit_banque/(?P[0-9]+)$', views.edit_banque, name='edit-banque'), - url(r'^del_banque/$', views.del_banque, name='del-banque'), - url(r'^index_article/$', views.index_article, name='index-article'), - url(r'^index_banque/$', views.index_banque, name='index-banque'), - url(r'^index_paiement/$', views.index_paiement, name='index-paiement'), - url(r'^history/(?Pfacture)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Particle)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Ppaiement)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pbanque)/(?P[0-9]+)$', views.history, name='history'), - url(r'^control/$', views.control, name='control'), + url(r'^new_facture/(?P[0-9]+)$', + views.new_facture, + name='new-facture' + ), + url(r'^edit_facture/(?P[0-9]+)$', + views.edit_facture, + name='edit-facture' + ), + url(r'^del_facture/(?P[0-9]+)$', + views.del_facture, + name='del-facture' + ), + url(r'^facture_pdf/(?P[0-9]+)$', + views.facture_pdf, + name='facture-pdf' + ), + url(r'^new_facture_pdf/$', + views.new_facture_pdf, + name='new-facture-pdf' + ), + url(r'^credit_solde/(?P[0-9]+)$', + views.credit_solde, + name='credit-solde' + ), + url(r'^add_article/$', + views.add_article, + name='add-article' + ), + url(r'^edit_article/(?P[0-9]+)$', + views.edit_article, + name='edit-article' + ), + url(r'^del_article/$', + views.del_article, + name='del-article' + ), + url(r'^add_paiement/$', + views.add_paiement, + name='add-paiement' + ), + url(r'^edit_paiement/(?P[0-9]+)$', + views.edit_paiement, + name='edit-paiement' + ), + url(r'^del_paiement/$', + views.del_paiement, + name='del-paiement' + ), + url(r'^add_banque/$', + views.add_banque, + name='add-banque' + ), + url(r'^edit_banque/(?P[0-9]+)$', + views.edit_banque, + name='edit-banque' + ), + url(r'^del_banque/$', + views.del_banque, + name='del-banque' + ), + url(r'^index_article/$', + views.index_article, + name='index-article' + ), + url(r'^index_banque/$', + views.index_banque, + name='index-banque' + ), + url(r'^index_paiement/$', + views.index_paiement, + name='index-paiement' + ), + url(r'^history/(?Pfacture)/(?P[0-9]+)$', + views.history, + name='history' + ), + url(r'^history/(?Particle)/(?P[0-9]+)$', + views.history, + name='history' + ), + url(r'^history/(?Ppaiement)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pbanque)/(?P[0-9]+)$', + views.history, + name='history' + ), + url(r'^control/$', + views.control, + name='control' + ), url(r'^$', views.index, name='index'), ] - - From 0f47ab79ebd4cd9220f2ed5b05c1a18a128833f1 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 22:47:32 +0200 Subject: [PATCH 026/138] Passage d'un coup de pylint --- cotisations/admin.py | 19 ++++-- cotisations/views.py | 138 ++++++++++++++++++++----------------------- re2o/views.py | 6 +- 3 files changed, 79 insertions(+), 84 deletions(-) diff --git a/cotisations/admin.py b/cotisations/admin.py index 29e3285d..b3f91854 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -28,23 +28,30 @@ from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente + class FactureAdmin(VersionAdmin): - list_display = ('user','paiement','date','valid','control') + pass + class VenteAdmin(VersionAdmin): - list_display = ('facture','name','prix','number','iscotisation','duration') + pass + class ArticleAdmin(VersionAdmin): - list_display = ('name','prix','iscotisation','duration') + pass + class BanqueAdmin(VersionAdmin): - list_display = ('name',) + pass + class PaiementAdmin(VersionAdmin): - list_display = ('moyen','type_paiement') + pass + class CotisationAdmin(VersionAdmin): - list_display = ('vente','date_start','date_end') + pass + admin.site.register(Facture, FactureAdmin) admin.site.register(Article, ArticleAdmin) diff --git a/cotisations/views.py b/cotisations/views.py index 517eafd1..e44eee65 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -24,40 +24,29 @@ # Goulven Kermarec, Gabriel Détraz # Gplv2 from __future__ import unicode_literals - +import os from django.shortcuts import render, redirect -from django.shortcuts import get_object_or_404 -from django.template.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib.auth.decorators import login_required, permission_required from django.contrib import messages -from django.db.models import Max, ProtectedError +from django.db.models import ProtectedError from django.db import transaction from django.forms import modelformset_factory, formset_factory -import os +from django.utils import timezone from reversion import revisions as reversion from reversion.models import Version - -from .models import Facture, Article, Vente, Cotisation, Paiement, Banque +# Import des models, forms et fonctions re2o +from users.models import User +from re2o.settings import LOGO_PATH +from re2o import settings +from re2o.views import form +from preferences.models import OptionalUser, AssoOption, GeneralOption +from .models import Facture, Article, Vente, Paiement, Banque from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm from .forms import ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm from .forms import BanqueForm, DelBanqueForm, NewFactureFormPdf from .forms import SelectArticleForm, CreditSoldeForm -from users.models import User from .tex import render_tex -from re2o.settings import LOGO_PATH -from re2o import settings -from preferences.models import OptionalUser, AssoOption, GeneralOption - -from dateutil.relativedelta import relativedelta -from django.utils import timezone - - -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) @login_required @@ -82,18 +71,18 @@ def new_facture(request, userid): facture_form = NewFactureForm(request.POST or None, instance=facture) article_formset = formset_factory(SelectArticleForm)(request.POST or None) if facture_form.is_valid() and article_formset.is_valid(): - new_facture = facture_form.save(commit=False) + new_facture_instance = facture_form.save(commit=False) articles = article_formset # Si au moins un article est rempli if any(art.cleaned_data for art in articles): - options, created = OptionalUser.objects.get_or_create() + options, _created = OptionalUser.objects.get_or_create() user_solde = options.user_solde solde_negatif = options.solde_negatif # Si on paye par solde, que l'option est activée, # on vérifie que le négatif n'est pas atteint if user_solde: - if new_facture.paiement == Paiement.objects.get_or_create( - moyen='solde' + if new_facture_instance.paiement == Paiement.objects.get_or_create( + moyen='solde' )[0]: prix_total = 0 for art_item in articles: @@ -105,7 +94,7 @@ def new_facture(request, userid): effectuer l'opération") return redirect("/users/profil/" + userid) with transaction.atomic(), reversion.create_revision(): - new_facture.save() + new_facture_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") for art_item in articles: @@ -113,19 +102,19 @@ def new_facture(request, userid): article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] new_vente = Vente.objects.create( - facture=new_facture, - name=article.name, - prix=article.prix, - iscotisation=article.iscotisation, - duration=article.duration, - number=quantity - ) + facture=new_facture_instance, + name=article.name, + prix=article.prix, + iscotisation=article.iscotisation, + duration=article.duration, + number=quantity + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) reversion.set_comment("Création") if any(art_item.cleaned_data['article'].iscotisation - for art_item in articles if art_item.cleaned_data): + for art_item in articles if art_item.cleaned_data): messages.success( request, "La cotisation a été prolongée\ @@ -137,8 +126,8 @@ def new_facture(request, userid): messages.success(request, "La facture a été crée") return redirect("/users/profil/" + userid) messages.error( - request, - u"Il faut au moins un article valide pour créer une facture" + request, + u"Il faut au moins un article valide pour créer une facture" ) return form({ 'factureform': facture_form, @@ -155,7 +144,7 @@ def new_facture_pdf(request): Vente ou Facture correspondant en bdd""" facture_form = NewFactureFormPdf(request.POST or None) if facture_form.is_valid(): - options, created = AssoOption.objects.get_or_create() + options, _created = AssoOption.objects.get_or_create() tbl = [] article = facture_form.cleaned_data['article'] quantite = facture_form.cleaned_data['number'] @@ -163,8 +152,8 @@ def new_facture_pdf(request): destinataire = facture_form.cleaned_data['dest'] chambre = facture_form.cleaned_data['chambre'] fid = facture_form.cleaned_data['fid'] - for a in article: - tbl.append([a, quantite, a.prix * quantite]) + for art in article: + tbl.append([art, quantite, art.prix * quantite]) prix_total = sum(a[2] for a in tbl) user = {'name': destinataire, 'room': chambre} return render_tex(request, 'cotisations/factures.tex', { @@ -207,11 +196,11 @@ def facture_pdf(request, factureid): messages.error(request, "Vous ne pouvez pas afficher\ une facture non valide") return redirect("/users/profil/" + str(request.user.id)) - vente = Vente.objects.all().filter(facture=facture) + ventes_objects = Vente.objects.all().filter(facture=facture) ventes = [] - options, created = AssoOption.objects.get_or_create() - for v in vente: - ventes.append([v, v.number, v.prix_total]) + options, _created = AssoOption.objects.get_or_create() + for vente in ventes_objects: + ventes.append([vente, vente.number, vente.prix_total]) return render_tex(request, 'cotisations/factures.tex', { 'paid': True, 'fid': facture.id, @@ -253,11 +242,11 @@ def edit_facture(request, factureid): facture_form = EditFactureForm(request.POST or None, instance=facture) ventes_objects = Vente.objects.filter(facture=facture) vente_form_set = modelformset_factory( - Vente, - fields=('name', 'number'), - extra=0, - max_num=len(ventes_objects) - ) + Vente, + fields=('name', 'number'), + extra=0, + max_num=len(ventes_objects) + ) vente_form = vente_form_set(request.POST or None, queryset=ventes_objects) if facture_form.is_valid() and vente_form.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -266,8 +255,7 @@ def edit_facture(request, factureid): reversion.set_user(request.user) reversion.set_comment("Champs modifié(s) : %s" % ', '.join( field for form in vente_form for field - in facture_form.changed_data + form.changed_data) - ) + in facture_form.changed_data + form.changed_data)) messages.success(request, "La facture a bien été modifiée") return redirect("/cotisations/") return form({ @@ -286,7 +274,7 @@ def del_facture(request, factureid): except Facture.DoesNotExist: messages.error(request, u"Facture inexistante") return redirect("/cotisations/") - if (facture.control or not facture.valid): + if facture.control or not facture.valid: messages.error(request, "Vous ne pouvez pas editer une facture\ controlée ou invalidée par le trésorier") return redirect("/cotisations/") @@ -320,13 +308,13 @@ def credit_solde(request, userid): reversion.set_user(request.user) reversion.set_comment("Création") new_vente = Vente.objects.create( - facture=facture_instance, - name="solde", - prix=facture.cleaned_data['montant'], - iscotisation=False, - duration=0, - number=1 - ) + facture=facture_instance, + name="solde", + prix=facture.cleaned_data['montant'], + iscotisation=False, + duration=0, + number=1 + ) with transaction.atomic(), reversion.create_revision(): new_vente.save() reversion.set_user(request.user) @@ -429,7 +417,7 @@ def edit_paiement(request, paiementid): reversion.set_user(request.user) reversion.set_comment( "Champs modifié(s) : %s" % ', '.join( - field for field in paiement.changed_data + field for field in paiement.changed_data ) ) messages.success(request, "Type de paiement modifié") @@ -451,8 +439,8 @@ def del_paiement(request): reversion.set_user(request.user) reversion.set_comment("Destruction") messages.success( - request, - "Le moyen de paiement a été supprimé" + request, + "Le moyen de paiement a été supprimé" ) except ProtectedError: messages.error( @@ -529,14 +517,14 @@ def del_banque(request): def control(request): """Pour le trésorier, vue pour controler en masse les factures.Case à cocher, pratique""" - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number facture_list = Facture.objects.order_by('date').reverse() controlform_set = modelformset_factory( - Facture, - fields=('control', 'valid'), - extra=0 - ) + Facture, + fields=('control', 'valid'), + extra=0 + ) paginator = Paginator(facture_list, pagination_number) page = request.GET.get('page') try: @@ -546,8 +534,8 @@ def control(request): except EmptyPage: facture_list = paginator.page(paginator.num.pages) page_query = Facture.objects.order_by('date').reverse().filter( - id__in=[facture.id for facture in facture_list] - ) + id__in=[facture.id for facture in facture_list] + ) controlform = controlform_set(request.POST or None, queryset=page_query) if controlform.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -595,7 +583,7 @@ def index_banque(request): @permission_required('cableur') def index(request): """Affiche l'ensemble des factures, pour les cableurs et +""" - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number facture_list = Facture.objects.order_by('date').select_related('user')\ .select_related('paiement').prefetch_related('vente_set').reverse() @@ -615,11 +603,11 @@ def index(request): @login_required -def history(request, object, id): +def history(request, object, object_id): """Affiche l'historique de chaque objet""" if object == 'facture': try: - object_instance = Facture.objects.get(pk=id) + object_instance = Facture.objects.get(pk=object_id) except Facture.DoesNotExist: messages.error(request, "Facture inexistante") return redirect("/cotisations/") @@ -630,26 +618,26 @@ def history(request, object, id): return redirect("/users/profil/" + str(request.user.id)) elif object == 'paiement' and request.user.has_perms(('cableur',)): try: - object_instance = Paiement.objects.get(pk=id) + object_instance = Paiement.objects.get(pk=object_id) except Paiement.DoesNotExist: messages.error(request, "Paiement inexistant") return redirect("/cotisations/") elif object == 'article' and request.user.has_perms(('cableur',)): try: - object_instance = Article.objects.get(pk=id) + object_instance = Article.objects.get(pk=object_id) except Article.DoesNotExist: messages.error(request, "Article inexistante") return redirect("/cotisations/") elif object == 'banque' and request.user.has_perms(('cableur',)): try: - object_instance = Banque.objects.get(pk=id) + object_instance = Banque.objects.get(pk=object_id) except Banque.DoesNotExist: messages.error(request, "Banque inexistante") return redirect("/cotisations/") else: messages.error(request, "Objet inconnu") return redirect("/cotisations/") - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) diff --git a/re2o/views.py b/re2o/views.py index bd8077e1..77a36418 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -29,9 +29,9 @@ from django.template import Context, RequestContext, loader from preferences.models import Service def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) + context = ctx + context.update(csrf(request)) + return render(request, template, context) def index(request): From e211957e9ca94e80b97bfcd7b17175040c1e4dc0 Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 23:15:07 +0200 Subject: [PATCH 027/138] Pylint et docstring des fichiers --- cotisations/admin.py | 7 ++++ cotisations/forms.py | 18 ++++++++-- cotisations/models.py | 81 +++++++++++++++++++++++++++++-------------- 3 files changed, 78 insertions(+), 28 deletions(-) diff --git a/cotisations/admin.py b/cotisations/admin.py index b3f91854..8186e4e3 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,26 +30,33 @@ from .models import Facture, Article, Banque, Paiement, Cotisation, Vente class FactureAdmin(VersionAdmin): + """Class admin d'une facture, tous les champs""" pass class VenteAdmin(VersionAdmin): + """Class admin d'une vente, tous les champs (facture related)""" pass class ArticleAdmin(VersionAdmin): + """Class admin d'un article en vente""" pass class BanqueAdmin(VersionAdmin): + """Class admin de la liste des banques (facture related)""" pass class PaiementAdmin(VersionAdmin): + """Class admin d'un moyen de paiement (facture related""" pass class CotisationAdmin(VersionAdmin): + """Class admin d'une cotisation (date de debut et de fin), + Vente related""" pass diff --git a/cotisations/forms.py b/cotisations/forms.py index b3742088..76a67975 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -19,14 +19,28 @@ # 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. +""" +Forms de l'application cotisation de re2o. Dépendance avec les models, +importé par les views. +Permet de créer une nouvelle facture pour un user (NewFactureForm), +et de l'editer (soit l'user avec EditFactureForm, +soit le trésorier avec TrezEdit qui a plus de possibilités que self +notamment sur le controle trésorier) + +SelectArticleForm est utilisée lors de la creation d'une facture en +parrallèle de NewFacture pour le choix des articles désirés. +(la vue correspondante est unique) + +ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter, +éditer ou supprimer une banque/moyen de paiement ou un article +""" from __future__ import unicode_literals from django import forms from django.forms import ModelForm, Form -from django import forms from django.core.validators import MinValueValidator -from .models import Article, Paiement, Facture, Banque, Vente +from .models import Article, Paiement, Facture, Banque class NewFactureForm(ModelForm): diff --git a/cotisations/models.py b/cotisations/models.py index 6388b38c..54843076 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -20,20 +20,39 @@ # 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. +""" +Definition des models bdd pour les factures et cotisation. +Pièce maitresse : l'ensemble du code intelligent se trouve ici, +dans les clean et save des models ainsi que de leur methodes supplémentaires. + +Facture : reliée à un user, elle a un moyen de paiement, une banque (option), +une ou plusieurs ventes + +Article : liste des articles en vente, leur prix, etc + +Vente : ensemble des ventes effectuées, reliées à une facture (foreignkey) + +Banque : liste des banques existantes + +Cotisation : objets de cotisation, contenant un début et une fin. Reliées +aux ventes, en onetoone entre une vente et une cotisation. +Crées automatiquement au save des ventes. + +Post_save et Post_delete : sychronisation des services et régénération +des services d'accès réseau (ex dhcp) lors de la vente d'une cotisation +par exemple +""" from __future__ import unicode_literals +from dateutil.relativedelta import relativedelta from django.db import models - from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from dateutil.relativedelta import relativedelta from django.forms import ValidationError from django.core.validators import MinValueValidator - from django.db.models import Max from django.utils import timezone - from machines.models import regen @@ -47,32 +66,34 @@ class Facture(models.Model): user = models.ForeignKey('users.User', on_delete=models.PROTECT) paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) banque = models.ForeignKey( - 'Banque', - on_delete=models.PROTECT, - blank=True, - null=True) + 'Banque', + on_delete=models.PROTECT, + blank=True, + null=True) cheque = models.CharField(max_length=255, blank=True) date = models.DateTimeField(auto_now_add=True) valid = models.BooleanField(default=True) control = models.BooleanField(default=False) def prix(self): + """Renvoie le prix brut sans les quantités. Méthode + dépréciée""" prix = Vente.objects.filter( - facture=self - ).aggregate(models.Sum('prix'))['prix__sum'] + facture=self + ).aggregate(models.Sum('prix'))['prix__sum'] return prix def prix_total(self): """Prix total : somme des produits prix_unitaire et quantité des ventes de l'objet""" return Vente.objects.filter( - facture=self - ).aggregate( + facture=self + ).aggregate( total=models.Sum( models.F('prix')*models.F('number'), output_field=models.FloatField() - ) - )['total'] + ) + )['total'] def name(self): """String, somme des name des ventes de self""" @@ -95,6 +116,7 @@ def facture_post_save(sender, **kwargs): @receiver(post_delete, sender=Facture) def facture_post_delete(sender, **kwargs): + """Après la suppression d'une facture, on synchronise l'user ldap""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -111,19 +133,22 @@ class Vente(models.Model): prix = models.DecimalField(max_digits=5, decimal_places=2) iscotisation = models.BooleanField() duration = models.IntegerField( - help_text="Durée exprimée en mois entiers", - blank=True, - null=True) + help_text="Durée exprimée en mois entiers", + blank=True, + null=True) def prix_total(self): """Renvoie le prix_total de self (nombre*prix)""" return self.prix*self.number def update_cotisation(self): + """Mets à jour l'objet related cotisation de la vente, si + il existe : update la date de fin à partir de la durée de + la vente""" if hasattr(self, 'cotisation'): cotisation = self.cotisation cotisation.date_end = cotisation.date_start + relativedelta( - months=self.duration*self.number) + months=self.duration*self.number) return def create_cotis(self, date_start=False): @@ -134,13 +159,13 @@ class Vente(models.Model): cotisation = Cotisation(vente=self) if date_start: end_adhesion = Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.filter( - user=self.facture.user - ).exclude(valid=False)) - ).filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self.facture.user + ).exclude(valid=False)) + ).filter( date_start__lt=date_start - ).aggregate(Max('date_end'))['date_end__max'] + ).aggregate(Max('date_end'))['date_end__max'] else: end_adhesion = self.facture.user.end_adhesion() date_start = date_start or timezone.now() @@ -148,8 +173,8 @@ class Vente(models.Model): date_max = max(end_adhesion, date_start) cotisation.date_start = date_max cotisation.date_end = cotisation.date_start + relativedelta( - months=self.duration*self.number - ) + months=self.duration*self.number + ) return def save(self, *args, **kwargs): @@ -181,6 +206,8 @@ def vente_post_save(sender, **kwargs): @receiver(post_delete, sender=Vente) def vente_post_delete(sender, **kwargs): + """Après suppression d'une vente, on synchronise l'user ldap (ex + suppression d'une cotisation""" vente = kwargs['instance'] if vente.iscotisation: user = vente.facture.user @@ -258,6 +285,7 @@ class Cotisation(models.Model): @receiver(post_save, sender=Cotisation) def cotisation_post_save(sender, **kwargs): + """Après modification d'une cotisation, regeneration des services""" regen('dns') regen('dhcp') regen('mac_ip_list') @@ -266,6 +294,7 @@ def cotisation_post_save(sender, **kwargs): @receiver(post_delete, sender=Cotisation) def vente_post_delete(sender, **kwargs): + """Après suppression d'une vente, régénération des services""" cotisation = kwargs['instance'] regen('mac_ip_list') regen('mailing') From d0620328e5a2c579104c8a95eacad5def281a8fa Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 13 Oct 2017 23:42:37 +0200 Subject: [PATCH 028/138] Pylint, pep8 et doc sur forms et admin de topologie --- topologie/admin.py | 12 +++++++++++ topologie/forms.py | 53 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/topologie/admin.py b/topologie/admin.py index 8dcce849..bfc2a393 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -20,6 +20,9 @@ # 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. +""" +Fichier définissant les administration des models dans l'interface admin +""" from __future__ import unicode_literals @@ -28,18 +31,27 @@ from reversion.admin import VersionAdmin from .models import Port, Room, Switch, Stack + class StackAdmin(VersionAdmin): + """Administration d'une stack de switches (inclus des switches)""" pass + class SwitchAdmin(VersionAdmin): + """Administration d'un switch""" pass + class PortAdmin(VersionAdmin): + """Administration d'un port de switches""" pass + class RoomAdmin(VersionAdmin): + """Administration d'un chambre""" pass + admin.site.register(Port, PortAdmin) admin.site.register(Room, RoomAdmin) admin.site.register(Switch, SwitchAdmin) diff --git a/topologie/forms.py b/topologie/forms.py index 8c82afba..267d64b0 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -19,14 +19,27 @@ # 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. +""" +Un forms le plus simple possible pour les objets topologie de re2o. + +Permet de créer et supprimer : un Port de switch, relié à un switch. + +Permet de créer des stacks et d'y ajouter des switchs (StackForm) + +Permet de créer, supprimer et editer un switch (EditSwitchForm, +NewSwitchForm) +""" from __future__ import unicode_literals -from .models import Port, Switch, Room, Stack -from django.forms import ModelForm, Form from machines.models import Interface +from django.forms import ModelForm +from .models import Port, Switch, Room, Stack + class PortForm(ModelForm): + """Formulaire pour la création d'un port d'un switch + Relié directement au modèle port""" class Meta: model = Port fields = '__all__' @@ -35,25 +48,45 @@ class PortForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PortForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditPortForm(ModelForm): + """Form pour l'édition d'un port de switche : changement des reglages + radius ou vlan, ou attribution d'une chambre, autre port ou machine + + Un port est relié à une chambre, un autre port (uplink) ou une machine + (serveur ou borne), mutuellement exclusif + Optimisation sur les queryset pour machines et port_related pour + optimiser le temps de chargement avec select_related (vraiment + lent sans)""" class Meta(PortForm.Meta): - fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details'] + fields = ['room', 'related', 'machine_interface', 'radius', + 'vlan_force', 'details'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['machine_interface'].queryset = Interface.objects.all().select_related('domain__extension') - self.fields['related'].queryset = Port.objects.all().select_related('switch__switch_interface__domain__extension').order_by('switch', 'port') + self.fields['machine_interface'].queryset = Interface.objects.all()\ + .select_related('domain__extension') + self.fields['related'].queryset = Port.objects.all()\ + .select_related('switch__switch_interface__domain__extension')\ + .order_by('switch', 'port') + class AddPortForm(ModelForm): + """Permet d'ajouter un port de switch. Voir EditPortForm pour plus + d'informations""" class Meta(PortForm.Meta): - fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details'] + fields = ['port', 'room', 'machine_interface', 'related', + 'radius', 'vlan_force', 'details'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs) + class StackForm(ModelForm): + """Permet d'edition d'une stack : stack_id, et switches membres + de la stack""" class Meta: model = Stack fields = '__all__' @@ -62,7 +95,9 @@ class StackForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(StackForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditSwitchForm(ModelForm): + """Permet d'éditer un switch : nom et nombre de ports""" class Meta: model = Switch fields = '__all__' @@ -73,7 +108,10 @@ class EditSwitchForm(ModelForm): self.fields['location'].label = 'Localisation' self.fields['number'].label = 'Nombre de ports' + class NewSwitchForm(ModelForm): + """Permet de créer un switch : emplacement, paramètres machine, + membre d'un stack (option), nombre de ports (number)""" class Meta(EditSwitchForm.Meta): fields = ['location', 'number', 'details', 'stack', 'stack_member_id'] @@ -81,7 +119,9 @@ class NewSwitchForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditRoomForm(ModelForm): + """Permet d'éediter le nom et commentaire d'une prise murale""" class Meta: model = Room fields = '__all__' @@ -89,4 +129,3 @@ class EditRoomForm(ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditRoomForm, self).__init__(*args, prefix=prefix, **kwargs) - From 9660a3d0177f4dc613f34df428786dda654119ec Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 00:00:16 +0200 Subject: [PATCH 029/138] Documentation et pep8 sur models de topologie --- topologie/models.py | 142 +++++++++++++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 47 deletions(-) diff --git a/topologie/models.py b/topologie/models.py index c02c0c51..4924e31e 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -20,24 +20,31 @@ # 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. +""" +Definition des modèles de l'application topologie. + +On défini les models suivants : + +- stack (id, id_min, id_max et nom) regrouppant les switches +- switch : nom, nombre de port, et interface +machine correspondante (mac, ip, etc) (voir machines.models.interface) +- Port: relié à un switch parent par foreign_key, numero du port, +relié de façon exclusive à un autre port, une machine +(serveur ou borne) ou une prise murale +- room : liste des prises murales, nom et commentaire de l'état de +la prise +""" from __future__ import unicode_literals from django.db import models from django.db.models.signals import post_delete from django.dispatch import receiver -from django.forms import ModelForm, Form -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import ValidationError -import reversion - -from machines.models import Vlan - class Stack(models.Model): - """ Un objet stack. Regrouppe des switchs en foreign key - , contient une id de stack, un switch id min et max dans + """Un objet stack. Regrouppe des switchs en foreign key + ,contient une id de stack, un switch id min et max dans le stack""" PRETTY_NAME = "Stack de switchs" @@ -59,28 +66,40 @@ class Stack(models.Model): def clean(self): """ Verification que l'id_max < id_min""" if self.member_id_max < self.member_id_min: - raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"}) + raise ValidationError({'member_id_max':"L'id maximale est\ + inférieure à l'id minimale"}) class Switch(models.Model): - """ Definition d'un switch. Contient un nombre de ports (number), + """ Definition d'un switch. Contient un nombre de ports (number), un emplacement (location), un stack parent (optionnel, stack) et un id de membre dans le stack (stack_member_id) relié en onetoone à une interface - Pourquoi ne pas avoir fait hériter switch de interface ? + Pourquoi ne pas avoir fait hériter switch de interface ? Principalement par méconnaissance de la puissance de cette façon de faire. Ceci étant entendu, django crée en interne un onetoone, ce qui a un - effet identique avec ce que l'on fait ici""" + effet identique avec ce que l'on fait ici + + Validation au save que l'id du stack est bien dans le range id_min + id_max de la stack parente""" PRETTY_NAME = "Switch / Commutateur" - switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE) + switch_interface = models.OneToOneField( + 'machines.Interface', + on_delete=models.CASCADE + ) location = models.CharField(max_length=255) number = models.IntegerField() details = models.CharField(max_length=255, blank=True) - stack = models.ForeignKey(Stack, blank=True, null=True, on_delete=models.SET_NULL) + stack = models.ForeignKey( + Stack, + blank=True, + null=True, + on_delete=models.SET_NULL + ) stack_member_id = models.IntegerField(blank=True, null=True) class Meta: - unique_together = ('stack','stack_member_id') + unique_together = ('stack', 'stack_member_id') def __str__(self): return str(self.location) + ' ' + str(self.switch_interface) @@ -89,41 +108,65 @@ class Switch(models.Model): """ Verifie que l'id stack est dans le bon range""" if self.stack is not None: if self.stack_member_id is not None: - if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min): - raise ValidationError({'stack_member_id': "L'id de ce switch est en dehors des bornes permises pas la stack"}) + if (self.stack_member_id > self.stack.member_id_max) or\ + (self.stack_member_id < self.stack.member_id_min): + raise ValidationError({'stack_member_id': "L'id de ce\ + switch est en dehors des bornes permises pas la stack"}) else: - raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"}) + raise ValidationError({'stack_member_id': "L'id dans la stack\ + ne peut être nul"}) class Port(models.Model): - """ Definition d'un port. Relié à un switch(foreign_key), + """ Definition d'un port. Relié à un switch(foreign_key), un port peut etre relié de manière exclusive à : - une chambre (room) - une machine (serveur etc) (machine_interface) - un autre port (uplink) (related) - Champs supplémentaires : + Champs supplémentaires : - RADIUS (mode STRICT : connexion sur port uniquement si machine - d'un adhérent à jour de cotisation et que la chambre est également à jour de cotisation + d'un adhérent à jour de cotisation et que la chambre est également à + jour de cotisation mode COMMON : vérification uniquement du statut de la machine mode NO : accepte toute demande venant du port et place sur le vlan normal mode BLOQ : rejet de toute authentification - vlan_force : override la politique générale de placement vlan, permet - de forcer un port sur un vlan particulier. S'additionne à la politique + de forcer un port sur un vlan particulier. S'additionne à la politique RADIUS""" PRETTY_NAME = "Port de switch" STATES = ( - ('NO', 'NO'), - ('STRICT', 'STRICT'), - ('BLOQ', 'BLOQ'), - ('COMMON', 'COMMON'), - ) - + ('NO', 'NO'), + ('STRICT', 'STRICT'), + ('BLOQ', 'BLOQ'), + ('COMMON', 'COMMON'), + ) + switch = models.ForeignKey('Switch', related_name="ports") port = models.IntegerField() - room = models.ForeignKey('Room', on_delete=models.PROTECT, blank=True, null=True) - machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True) - related = models.OneToOneField('self', null=True, blank=True, related_name='related_port') + room = models.ForeignKey( + 'Room', + on_delete=models.PROTECT, + blank=True, + null=True + ) + machine_interface = models.ForeignKey( + 'machines.Interface', + on_delete=models.SET_NULL, + blank=True, + null=True + ) + related = models.OneToOneField( + 'self', + null=True, + blank=True, + related_name='related_port' + ) radius = models.CharField(max_length=32, choices=STATES, default='NO') - vlan_force = models.ForeignKey('machines.Vlan', on_delete=models.SET_NULL, blank=True, null=True) + vlan_force = models.ForeignKey( + 'machines.Vlan', + on_delete=models.SET_NULL, + blank=True, + null=True + ) details = models.CharField(max_length=255, blank=True) class Meta: @@ -134,7 +177,7 @@ class Port(models.Model): related_port = self.related related_port.related = self related_port.save() - + def clean_port_related(self): """ Supprime la relation related sur self""" related_port = self.related_port @@ -142,23 +185,27 @@ class Port(models.Model): related_port.save() def clean(self): - """ Verifie que un seul de chambre, interface_parent et related_port est rempli. - Verifie que le related n'est pas le port lui-même.... - Verifie que le related n'est pas déjà occupé par une machine ou une chambre. Si - ce n'est pas le cas, applique la relation related + """ Verifie que un seul de chambre, interface_parent et related_port + est rempli. Verifie que le related n'est pas le port lui-même.... + Verifie que le related n'est pas déjà occupé par une machine ou une + chambre. Si ce n'est pas le cas, applique la relation related Si un port related point vers self, on nettoie la relation - A priori pas d'autre solution que de faire ça à la main. A priori tout cela est dans - un bloc transaction, donc pas de problème de cohérence""" + A priori pas d'autre solution que de faire ça à la main. A priori + tout cela est dans un bloc transaction, donc pas de problème de + cohérence""" if hasattr(self, 'switch'): if self.port > self.switch.number: raise ValidationError("Ce port ne peut exister, numero trop élevé") - if self.room and self.machine_interface or self.room and self.related or self.machine_interface and self.related: - raise ValidationError("Chambre, interface et related_port sont mutuellement exclusifs") - if self.related==self: + if self.room and self.machine_interface or self.room and\ + self.related or self.machine_interface and self.related: + raise ValidationError("Chambre, interface et related_port sont\ + mutuellement exclusifs") + if self.related == self: raise ValidationError("On ne peut relier un port à lui même") if self.related and not self.related.related: if self.related.machine_interface or self.related.room: - raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation") + raise ValidationError("Le port relié est déjà occupé, veuillez\ + le libérer avant de créer une relation") else: self.make_port_related() elif hasattr(self, 'related_port'): @@ -168,7 +215,7 @@ class Port(models.Model): return str(self.switch) + " - " + str(self.port) class Room(models.Model): - """ Une chambre/local contenant une prise murale""" + """Une chambre/local contenant une prise murale""" PRETTY_NAME = "Chambre/ Prise murale" name = models.CharField(max_length=255, unique=True) @@ -176,10 +223,11 @@ class Room(models.Model): class Meta: ordering = ['name'] - + def __str__(self): return str(self.name) @receiver(post_delete, sender=Stack) def stack_post_delete(sender, **kwargs): - Switch.objects.filter(stack=None).update(stack_member_id = None) + """Vide les id des switches membres d'une stack supprimée""" + Switch.objects.filter(stack=None).update(stack_member_id=None) From e619e5a31cf19e7e8d8caa95ec1767d579b30428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Fri, 13 Oct 2017 23:04:05 +0000 Subject: [PATCH 030/138] Ajout du bft tag sur le form d'edit/ajout de port sur un switch --- topologie/templates/topologie/topo.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index fea858e4..bd07c2db 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load bootstrap_form_typeahead %} {% block title %}Création et modificationd 'utilisateur{% endblock %} @@ -32,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% bootstrap_form topoform %} + {% bootstrap_form_typeahead topoform 'room,related,machine_interface' %} {%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %}
From 95c32b464e58262034d40410ffb12cf659784f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Fri, 13 Oct 2017 23:36:04 +0000 Subject: [PATCH 031/138] Fix form topologie (new stack + affichage stack) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La création réussie d'une stack redirect sur la liste des stack et ne renvoie pas sur le formulaire de création La stack s'affichae même si il n'y a aucune interface liée --- topologie/templates/topologie/aff_stacks.html | 77 ++++++++++++------- topologie/views.py | 2 + 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/topologie/templates/topologie/aff_stacks.html b/topologie/templates/topologie/aff_stacks.html index 34e7b959..586fd90a 100644 --- a/topologie/templates/topologie/aff_stacks.html +++ b/topologie/templates/topologie/aff_stacks.html @@ -30,32 +30,53 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - {% for stack in stack_list %} - {% for switch in stack.switch_set.all %} - - {% if forloop.first %} - - - - {% endif %} - - {% if forloop.first %} - - {% endif %} - - {% endfor %} - {% endfor %} + + {% for stack in stack_list %} + {% for switch in stack.switch_set.all %} + + + {% if forloop.first %} + + + + {% endif %} + + {% if forloop.first %} + + {% endif %} + + {% empty %} + + + + + + + {% endfor %} + + {% endfor %}
Details Membres
{{stack.name}}{{stack.stack_id}}{{stack.details}}{{switch}} - - - - {% if is_infra %} - - - - - - - {% endif %} -
{{stack.name}}{{stack.stack_id}}{{stack.details}}{{switch}} + + + + {% if is_infra %} + + + + + + + {% endif %} +
{{stack.name}}{{stack.stack_id}}{{stack.details}}Aucun + + + + {% if is_infra %} + + + + + + + {% endif %} +
diff --git a/topologie/views.py b/topologie/views.py index f3dedff8..9fe37d6f 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -211,6 +211,8 @@ def new_stack(request): messages.success(request, "Stack crée") except: messages.error(request, "Cette stack existe déjà") + else: + return redirect('/topologie/index_stack') return form({'topoform':stack}, 'topologie/topo.html', request) From dc3096a3bfc50bfd82e8b995a9f13985d4dda086 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 01:43:01 +0200 Subject: [PATCH 032/138] Doc et respect de la pep8 --- topologie/models.py | 20 ++- topologie/urls.py | 39 ++++-- topologie/views.py | 295 ++++++++++++++++++++++++++++++-------------- 3 files changed, 248 insertions(+), 106 deletions(-) diff --git a/topologie/models.py b/topologie/models.py index 4924e31e..086e0aff 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -42,6 +42,7 @@ from django.db.models.signals import post_delete from django.dispatch import receiver from django.core.exceptions import ValidationError + class Stack(models.Model): """Un objet stack. Regrouppe des switchs en foreign key ,contient une id de stack, un switch id min et max dans @@ -66,9 +67,10 @@ class Stack(models.Model): def clean(self): """ Verification que l'id_max < id_min""" if self.member_id_max < self.member_id_min: - raise ValidationError({'member_id_max':"L'id maximale est\ + raise ValidationError({'member_id_max': "L'id maximale est\ inférieure à l'id minimale"}) + class Switch(models.Model): """ Definition d'un switch. Contient un nombre de ports (number), un emplacement (location), un stack parent (optionnel, stack) @@ -109,13 +111,16 @@ class Switch(models.Model): if self.stack is not None: if self.stack_member_id is not None: if (self.stack_member_id > self.stack.member_id_max) or\ - (self.stack_member_id < self.stack.member_id_min): - raise ValidationError({'stack_member_id': "L'id de ce\ - switch est en dehors des bornes permises pas la stack"}) + (self.stack_member_id < self.stack.member_id_min): + raise ValidationError( + {'stack_member_id': "L'id de ce switch est en\ + dehors des bornes permises pas la stack"} + ) else: raise ValidationError({'stack_member_id': "L'id dans la stack\ ne peut être nul"}) + class Port(models.Model): """ Definition d'un port. Relié à un switch(foreign_key), un port peut etre relié de manière exclusive à : @@ -195,9 +200,10 @@ class Port(models.Model): cohérence""" if hasattr(self, 'switch'): if self.port > self.switch.number: - raise ValidationError("Ce port ne peut exister, numero trop élevé") + raise ValidationError("Ce port ne peut exister,\ + numero trop élevé") if self.room and self.machine_interface or self.room and\ - self.related or self.machine_interface and self.related: + self.related or self.machine_interface and self.related: raise ValidationError("Chambre, interface et related_port sont\ mutuellement exclusifs") if self.related == self: @@ -214,6 +220,7 @@ class Port(models.Model): def __str__(self): return str(self.switch) + " - " + str(self.port) + class Room(models.Model): """Une chambre/local contenant une prise murale""" PRETTY_NAME = "Chambre/ Prise murale" @@ -227,6 +234,7 @@ class Room(models.Model): def __str__(self): return str(self.name) + @receiver(post_delete, sender=Stack) def stack_post_delete(sender, **kwargs): """Vide les id des switches membres d'une stack supprimée""" diff --git a/topologie/urls.py b/topologie/urls.py index f4537ac5..4d0a6779 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -19,6 +19,12 @@ # 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. +""" +Definition des urls de l'application topologie. +Inclu dans urls de re2o. + +Fait référence aux fonctions du views +""" from __future__ import unicode_literals @@ -33,18 +39,33 @@ urlpatterns = [ url(r'^new_room/$', views.new_room, name='new-room'), url(r'^edit_room/(?P[0-9]+)$', views.edit_room, name='edit-room'), url(r'^del_room/(?P[0-9]+)$', views.del_room, name='del-room'), - url(r'^switch/(?P[0-9]+)$', views.index_port, name='index-port'), - url(r'^history/(?Pswitch)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pport)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Proom)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pstack)/(?P[0-9]+)$', views.history, name='history'), + url(r'^switch/(?P[0-9]+)$', + views.index_port, + name='index-port'), + url(r'^history/(?Pswitch)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pport)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Proom)/(?P[0-9]+)$', + views.history, + name='history'), + url(r'^history/(?Pstack)/(?P[0-9]+)$', + views.history, + name='history'), url(r'^edit_port/(?P[0-9]+)$', views.edit_port, name='edit-port'), url(r'^new_port/(?P[0-9]+)$', views.new_port, name='new-port'), url(r'^del_port/(?P[0-9]+)$', views.del_port, name='del-port'), - url(r'^edit_switch/(?P[0-9]+)$', views.edit_switch, name='edit-switch'), + url(r'^edit_switch/(?P[0-9]+)$', + views.edit_switch, + name='edit-switch'), url(r'^new_stack/$', views.new_stack, name='new-stack'), url(r'^index_stack/$', views.index_stack, name='index-stack'), - url(r'^edit_stack/(?P[0-9]+)$', views.edit_stack, name='edit-stack'), - url(r'^del_stack/(?P[0-9]+)$', views.del_stack, name='del-stack'), + url(r'^edit_stack/(?P[0-9]+)$', + views.edit_stack, + name='edit-stack'), + url(r'^del_stack/(?P[0-9]+)$', + views.del_stack, + name='del-stack'), ] - diff --git a/topologie/views.py b/topologie/views.py index 42cd09e7..08ada8d0 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -19,7 +19,20 @@ # 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. +""" +Page des vues de l'application topologie +Permet de créer, modifier et supprimer : +- un port (add_port, edit_port, del_port) +- un switch : les vues d'ajout et d'édition font appel aux forms de creation +de switch, mais aussi aux forms de machines.forms (domain, interface et +machine). Le views les envoie et les save en même temps. TODO : rationaliser +et faire que la creation de machines (interfaces, domain etc) soit gérée +coté models et forms de topologie +- une chambre (new_room, edit_room, del_room) +- une stack +- l'historique de tous les objets cités +""" from __future__ import unicode_literals from django.shortcuts import render, redirect @@ -33,11 +46,12 @@ from reversion import revisions as reversion from reversion.models import Version from topologie.models import Switch, Port, Room, Stack -from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm, AddPortForm, EditRoomForm, StackForm +from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm +from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form -from users.models import User -from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from machines.forms import AliasForm, NewMachineForm, EditMachineForm +from machines.forms import EditInterfaceForm, AddInterfaceForm from preferences.models import AssoOption, GeneralOption @@ -45,41 +59,52 @@ from preferences.models import AssoOption, GeneralOption @permission_required('cableur') def index(request): """ Vue d'affichage de tous les swicthes""" - switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack') - return render(request, 'topologie/index.html', {'switch_list': switch_list}) + switch_list = Switch.objects.order_by( + 'stack', + 'stack_member_id', + 'location' + )\ + .select_related('switch_interface__domain__extension')\ + .select_related('switch_interface__ipv4')\ + .select_related('switch_interface__domain')\ + .select_related('stack') + return render(request, 'topologie/index.html', { + 'switch_list': switch_list + }) + @login_required @permission_required('cableur') -def history(request, object, id): +def history(request, object_name, object_id): """ Vue générique pour afficher l'historique complet d'un objet""" - if object == 'switch': + if object_name == 'switch': try: - object_instance = Switch.objects.get(pk=id) + object_instance = Switch.objects.get(pk=object_id) except Switch.DoesNotExist: - messages.error(request, "Switch inexistant") - return redirect("/topologie/") - elif object == 'port': + messages.error(request, "Switch inexistant") + return redirect("/topologie/") + elif object_name == 'port': try: - object_instance = Port.objects.get(pk=id) + object_instance = Port.objects.get(pk=object_id) except Port.DoesNotExist: - messages.error(request, "Port inexistant") - return redirect("/topologie/") - elif object == 'room': + messages.error(request, "Port inexistant") + return redirect("/topologie/") + elif object_name == 'room': try: - object_instance = Room.objects.get(pk=id) + object_instance = Room.objects.get(pk=object_id) except Room.DoesNotExist: - messages.error(request, "Chambre inexistante") - return redirect("/topologie/") - elif object == 'stack': + messages.error(request, "Chambre inexistante") + return redirect("/topologie/") + elif object_name == 'stack': try: - object_instance = Stack.objects.get(pk=id) + object_instance = Stack.objects.get(pk=object_id) except Room.DoesNotExist: - messages.error(request, "Stack inexistante") - return redirect("/topologie/") + messages.error(request, "Stack inexistante") + return redirect("/topologie/") else: messages.error(request, "Objet inconnu") return redirect("/topologie/") - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) @@ -92,7 +117,11 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render(request, 're2o/history.html', { + 'reversions': reversions, + 'object': object_instance + }) + @login_required @permission_required('cableur') @@ -103,15 +132,25 @@ def index_port(request, switch_id): except Switch.DoesNotExist: messages.error(request, u"Switch inexistant") return redirect("/topologie/") - port_list = Port.objects.filter(switch = switch).select_related('room').select_related('machine_interface__domain__extension').select_related('related').select_related('switch').order_by('port') - return render(request, 'topologie/index_p.html', {'port_list':port_list, 'id_switch':switch_id, 'nom_switch':switch}) + port_list = Port.objects.filter(switch=switch)\ + .select_related('room')\ + .select_related('machine_interface__domain__extension')\ + .select_related('related')\ + .select_related('switch')\ + .order_by('port') + return render(request, 'topologie/index_p.html', { + 'port_list': port_list, + 'id_switch': switch_id, + 'nom_switch': switch + }) + @login_required @permission_required('cableur') def index_room(request): """ Affichage de l'ensemble des chambres""" room_list = Room.objects.order_by('name') - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number paginator = Paginator(room_list, pagination_number) page = request.GET.get('page') @@ -123,13 +162,20 @@ def index_room(request): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. room_list = paginator.page(paginator.num_pages) - return render(request, 'topologie/index_room.html', {'room_list': room_list}) + return render(request, 'topologie/index_room.html', { + 'room_list': room_list + }) + @login_required @permission_required('infra') def index_stack(request): - stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension') - return render(request, 'topologie/index_stack.html', {'stack_list': stack_list}) + """Affichage de la liste des stacks (affiche l'ensemble des switches)""" + stack_list = Stack.objects.order_by('name')\ + .prefetch_related('switch_set__switch_interface__domain__extension') + return render(request, 'topologie/index_stack.html', { + 'stack_list': stack_list + }) @login_required @@ -152,16 +198,24 @@ def new_port(request, switch_id): reversion.set_comment("Création") messages.success(request, "Port ajouté") except IntegrityError: - messages.error(request,"Ce port existe déjà" ) + messages.error(request, "Ce port existe déjà") return redirect("/topologie/switch/" + switch_id) - return form({'topoform':port}, 'topologie/topo.html', request) + return form({'topoform': port}, 'topologie/topo.html', request) + @login_required @permission_required('infra') def edit_port(request, port_id): - """ Edition d'un port. Permet de changer le switch parent et l'affectation du port""" + """ Edition d'un port. Permet de changer le switch parent et + l'affectation du port""" try: - port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id) + port_object = Port.objects\ + .select_related('switch__switch_interface__domain__extension')\ + .select_related('machine_interface__domain__extension')\ + .select_related('machine_interface__switch')\ + .select_related('room')\ + .select_related('related')\ + .get(pk=port_id) except Port.DoesNotExist: messages.error(request, u"Port inexistant") return redirect("/topologie/") @@ -170,14 +224,17 @@ def edit_port(request, port_id): with transaction.atomic(), reversion.create_revision(): port.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in port.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in port.changed_data + )) messages.success(request, "Le port a bien été modifié") return redirect("/topologie/switch/" + str(port_object.switch.id)) - return form({'topoform':port}, 'topologie/topo.html', request) + return form({'topoform': port}, 'topologie/topo.html', request) + @login_required @permission_required('infra') -def del_port(request,port_id): +def del_port(request, port_id): """ Supprime le port""" try: port = Port.objects.get(pk=port_id) @@ -192,30 +249,30 @@ def del_port(request,port_id): reversion.set_comment("Destruction") messages.success(request, "Le port a eté détruit") except ProtectedError: - messages.error(request, "Le port %s est affecté à un autre objet, impossible de le supprimer" % port) + messages.error(request, "Le port %s est affecté à un autre objet,\ + impossible de le supprimer" % port) return redirect('/topologie/switch/' + str(port.switch.id)) - return form({'objet':port}, 'topologie/delete.html', request) + return form({'objet': port}, 'topologie/delete.html', request) + @login_required @permission_required('infra') def new_stack(request): + """Ajoute un nouveau stack : stack_id_min, max, et nombre de switches""" stack = StackForm(request.POST or None) - #if stack.is_valid(): - if request.POST: - try: - with transaction.atomic(), reversion.create_revision(): - stack.save() - reversion.set_user(request.user) - reversion.set_comment("Création") - messages.success(request, "Stack crée") - except: - messages.error(request, "Cette stack existe déjà") - return form({'topoform':stack}, 'topologie/topo.html', request) + if stack.is_valid(): + with transaction.atomic(), reversion.create_revision(): + stack.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Stack crée") + return form({'topoform': stack}, 'topologie/topo.html', request) @login_required @permission_required('infra') -def edit_stack(request,stack_id): +def edit_stack(request, stack_id): + """Edition d'un stack (nombre de switches, nom...)""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -226,13 +283,19 @@ def edit_stack(request,stack_id): with transaction.atomic(), reversion.create_revision(): stack.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in stack.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in stack.changed_data + ) + ) return redirect('/topologie/index_stack') - return form({'topoform':stack}, 'topologie/topo.html', request) + return form({'topoform': stack}, 'topologie/topo.html', request) + @login_required @permission_required('infra') -def del_stack(request,stack_id): +def del_stack(request, stack_id): + """Supprime un stack""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -246,13 +309,16 @@ def del_stack(request,stack_id): reversion.set_comment("Destruction") messages.success(request, "La stack a eté détruite") except ProtectedError: - messages.error(request, "La stack %s est affectée à un autre objet, impossible de la supprimer" % stack) + messages.error(request, "La stack %s est affectée à un autre\ + objet, impossible de la supprimer" % stack) return redirect('/topologie/index_stack') - return form({'objet':stack}, 'topologie/delete.html', request) + return form({'objet': stack}, 'topologie/delete.html', request) + @login_required @permission_required('infra') -def edit_switchs_stack(request,stack_id): +def edit_switchs_stack(request, stack_id): + """Permet d'éditer la liste des switches dans une stack et l'ajouter""" try: stack = Stack.objects.get(pk=stack_id) except Stack.DoesNotExist: @@ -264,30 +330,36 @@ def edit_switchs_stack(request,stack_id): context = {'stack': stack} context['switchs_stack'] = stack.switchs_set.all() context['switchs_autres'] = Switch.object.filter(stack=None) - pass @login_required @permission_required('infra') def new_switch(request): - """ Creation d'un switch. Cree en meme temps l'interface et la machine associée. - Vue complexe. Appelle successivement les 4 models forms adaptés : machine, - interface, domain et switch""" + """ Creation d'un switch. Cree en meme temps l'interface et la machine + associée. Vue complexe. Appelle successivement les 4 models forms + adaptés : machine, interface, domain et switch""" switch = NewSwitchForm(request.POST or None) machine = NewMachineForm(request.POST or None) - interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) - domain = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',))) + interface = AddInterfaceForm( + request.POST or None, + infra=request.user.has_perms(('infra',)) + ) + domain = AliasForm( + request.POST or None, + infra=request.user.has_perms(('infra',)) + ) if switch.is_valid() and machine.is_valid() and interface.is_valid(): - options, created = AssoOption.objects.get_or_create() + options, _created = AssoOption.objects.get_or_create() user = options.utilisateur_asso if not user: - messages.error(request, "L'user association n'existe pas encore, veuillez le créer ou le linker dans preferences") + messages.error(request, "L'user association n'existe pas encore,\ + veuillez le créer ou le linker dans preferences") return redirect("/topologie/") new_machine = machine.save(commit=False) new_machine.user = user new_interface = interface.save(commit=False) - new_switch = switch.save(commit=False) - new_domain = domain.save(commit=False) + new_switch_instance = switch.save(commit=False) + new_domain_instance = domain.save(commit=False) with transaction.atomic(), reversion.create_revision(): new_machine.save() reversion.set_user(request.user) @@ -297,58 +369,91 @@ def new_switch(request): new_interface.save() reversion.set_user(request.user) reversion.set_comment("Création") - new_domain.interface_parent = new_interface + new_domain_instance.interface_parent = new_interface with transaction.atomic(), reversion.create_revision(): - new_domain.save() + new_domain_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") - new_switch.switch_interface = new_interface + new_switch_instance.switch_interface = new_interface with transaction.atomic(), reversion.create_revision(): - new_switch.save() + new_switch_instance.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Le switch a été crée") return redirect("/topologie/") - return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'topologie/switch.html', request) + return form({ + 'topoform': switch, + 'machineform': machine, + 'interfaceform': interface, + 'domainform': domain + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') def edit_switch(request, switch_id): - """ Edition d'un switch. Permet de chambre nombre de ports, place dans le stack, - interface et machine associée""" + """ Edition d'un switch. Permet de chambre nombre de ports, + place dans le stack, interface et machine associée""" try: switch = Switch.objects.get(pk=switch_id) except Switch.DoesNotExist: messages.error(request, u"Switch inexistant") return redirect("/topologie/") switch_form = EditSwitchForm(request.POST or None, instance=switch) - machine_form = EditMachineForm(request.POST or None, instance=switch.switch_interface.machine) - interface_form = EditInterfaceForm(request.POST or None, instance=switch.switch_interface) - domain_form = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',)), instance=switch.switch_interface.domain) - if switch_form.is_valid() and machine_form.is_valid() and interface_form.is_valid(): + machine_form = EditMachineForm( + request.POST or None, + instance=switch.switch_interface.machine + ) + interface_form = EditInterfaceForm( + request.POST or None, + instance=switch.switch_interface + ) + domain_form = AliasForm( + request.POST or None, + infra=request.user.has_perms(('infra',)), + instance=switch.switch_interface.domain + ) + if switch_form.is_valid() and machine_form.is_valid()\ + and interface_form.is_valid(): new_interface = interface_form.save(commit=False) new_machine = machine_form.save(commit=False) - new_switch = switch_form.save(commit=False) + new_switch_instance = switch_form.save(commit=False) new_domain = domain_form.save(commit=False) with transaction.atomic(), reversion.create_revision(): new_machine.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machine_form.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in machine_form.changed_data + ) + ) with transaction.atomic(), reversion.create_revision(): new_interface.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in interface_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in interface_form.changed_data) + ) with transaction.atomic(), reversion.create_revision(): new_domain.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in domain_form.changed_data) + ) with transaction.atomic(), reversion.create_revision(): - new_switch.save() + new_switch_instance.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in switch_form.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in switch_form.changed_data) + ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'topologie/switch.html', request) + return form({ + 'topoform': switch_form, + 'machineform': machine_form, + 'interfaceform': interface_form, + 'domainform': domain_form + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') @@ -362,7 +467,8 @@ def new_room(request): reversion.set_comment("Création") messages.success(request, "La chambre a été créé") return redirect("/topologie/index_room/") - return form({'topoform':room}, 'topologie/topo.html', request) + return form({'topoform': room}, 'topologie/topo.html', request) + @login_required @permission_required('infra') @@ -378,10 +484,13 @@ def edit_room(request, room_id): with transaction.atomic(), reversion.create_revision(): room.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in room.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in room.changed_data) + ) messages.success(request, "La chambre a bien été modifiée") return redirect("/topologie/index_room/") - return form({'topoform':room}, 'topologie/topo.html', request) + return form({'topoform': room}, 'topologie/topo.html', request) + @login_required @permission_required('infra') @@ -390,7 +499,7 @@ def del_room(request, room_id): try: room = Room.objects.get(pk=room_id) except Room.DoesNotExist: - messages.error(request, u"Chambre inexistante" ) + messages.error(request, u"Chambre inexistante") return redirect("/topologie/index_room/") if request.method == "POST": try: @@ -400,6 +509,10 @@ def del_room(request, room_id): reversion.set_comment("Destruction") messages.success(request, "La chambre/prise a été détruite") except ProtectedError: - messages.error(request, "La chambre %s est affectée à un autre objet, impossible de la supprimer (switch ou user)" % room) + messages.error(request, "La chambre %s est affectée à un autre objet,\ + impossible de la supprimer (switch ou user)" % room) return redirect("/topologie/index_room/") - return form({'objet': room, 'objet_name': 'Chambre'}, 'topologie/delete.html', request) + return form({ + 'objet': room, + 'objet_name': 'Chambre' + }, 'topologie/delete.html', request) From 77ce677c5b8565902d91f0e585a5b2f9aa123a4f Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 01:56:14 +0200 Subject: [PATCH 033/138] Fix pep8 --- topologie/views.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/topologie/views.py b/topologie/views.py index ceb06f0a..12732422 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -50,7 +50,8 @@ from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form -from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from machines.forms import AliasForm, NewMachineForm, EditMachineForm +from machines.forms import EditInterfaceForm, AddInterfaceForm from machines.views import generate_ipv4_bft_param from preferences.models import AssoOption, GeneralOption @@ -381,8 +382,15 @@ def new_switch(request): reversion.set_comment("Création") messages.success(request, "Le switch a été créé") return redirect("/topologie/") - i_bft_param = generate_ipv4_bft_param( interface, False ) - return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) + i_bft_param = generate_ipv4_bft_param(interface, False) + return form({ + 'topoform': switch, + 'machineform': machine, + 'interfaceform': interface, + 'domainform': domain, + 'i_bft_param': i_bft_param + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') @@ -442,8 +450,15 @@ def edit_switch(request, switch_id): ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) + i_bft_param = generate_ipv4_bft_param(interface_form, False) + return form({ + 'topoform': switch_form, + 'machineform': machine_form, + 'interfaceform': interface_form, + 'domainform': domain_form, + 'i_bft_param': i_bft_param + }, 'topologie/switch.html', request) + @login_required @permission_required('infra') From 54fad989b3fbc6ac326a13c8d2f650601076c6fb Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 04:17:42 +0200 Subject: [PATCH 034/138] Docstrings et pep8 sur logs --- logs/urls.py | 9 +- logs/views.py | 338 +++++++++++++++++++++++++++++++------------------- 2 files changed, 215 insertions(+), 132 deletions(-) diff --git a/logs/urls.py b/logs/urls.py index 3bb41c4a..11009835 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -19,7 +19,10 @@ # 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. - +""" +Urls de l'application logs, pointe vers les fonctions de views. +Inclu dans le re2o.urls +""" from __future__ import unicode_literals from django.conf.urls import url @@ -29,7 +32,9 @@ from . import views urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^stats_logs$', views.stats_logs, name='stats-logs'), - url(r'^revert_action/(?P[0-9]+)$', views.revert_action, name='revert-action'), + url(r'^revert_action/(?P[0-9]+)$', + views.revert_action, + name='revert-action'), url(r'^stats_general/$', views.stats_general, name='stats-general'), url(r'^stats_models/$', views.stats_models, name='stats-models'), url(r'^stats_users/$', views.stats_users, name='stats-users'), diff --git a/logs/views.py b/logs/views.py index d84a2f43..13879c86 100644 --- a/logs/views.py +++ b/logs/views.py @@ -23,62 +23,68 @@ # App de gestion des statistiques pour re2o # Gabriel Détraz # Gplv2 +""" +Vues des logs et statistiques générales. + +La vue index générale affiche une selection des dernières actions, +classées selon l'importance, avec date, et user formatés. + +Stats_logs renvoie l'ensemble des logs. + +Les autres vues sont thématiques, ensemble des statistiques et du +nombre d'objets par models, nombre d'actions par user, etc +""" from __future__ import unicode_literals -from django.http import HttpResponse from django.shortcuts import render, redirect -from django.shortcuts import get_object_or_404 -from django.template.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import ProtectedError -from django.forms import ValidationError -from django.db import transaction from django.db.models import Count from reversion.models import Revision from reversion.models import Version, ContentType -from users.models import User, ServiceUser, Right, School, ListRight, ListShell, Ban, Whitelist -from users.models import all_has_access, all_whitelisted, all_baned, all_adherent -from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation -from machines.models import Machine, MachineType, IpType, Extension, Interface, Domain, IpList -from machines.views import all_active_assigned_interfaces_count, all_active_interfaces_count +from users.models import User, ServiceUser, Right, School, ListRight, ListShell +from users.models import Ban, Whitelist, all_has_access +from users.models import all_whitelisted, all_baned, all_adherent +from cotisations.models import Facture, Vente, Article, Banque, Paiement +from cotisations.models import Cotisation +from machines.models import Machine, MachineType, IpType, Extension, Interface +from machines.models import Domain, IpList +from machines.views import all_active_assigned_interfaces_count +from machines.views import all_active_interfaces_count from topologie.models import Switch, Port, Room from preferences.models import GeneralOption +from re2o.views import form -from django.utils import timezone -from dateutil.relativedelta import relativedelta STATS_DICT = { - 0 : ["Tout", 36], - 1 : ["1 mois", 1], - 2 : ["2 mois", 2], - 3 : ["6 mois", 6], - 4 : ["1 an", 12], - 5 : ["2 an", 24], + 0: ["Tout", 36], + 1: ["1 mois", 1], + 2: ["2 mois", 2], + 3: ["6 mois", 6], + 4: ["1 an", 12], + 5: ["2 an", 24], } -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) @login_required @permission_required('cableur') def index(request): - options, created = GeneralOption.objects.get_or_create() + """Affiche les logs affinés, date reformatées, selectionne + les event importants (ajout de droits, ajout de ban/whitelist)""" + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - # The types of content kept for display - content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user'] - + content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user'] # Select only wanted versions - versions = Version.objects.filter(content_type__in=ContentType.objects.filter(model__in=content_type_filter)).order_by('revision__date_created').reverse().select_related('revision') - + versions = Version.objects.filter( + content_type__in=ContentType.objects.filter( + model__in=content_type_filter + ) + ).order_by('revision__date_created').reverse().select_related('revision') paginator = Paginator(versions, pagination_number) page = request.GET.get('page') try: @@ -87,7 +93,7 @@ def index(request): # If page is not an integer, deliver first page. versions = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. + # If page is out of range (e.g. 9999), deliver last page of results. versions = paginator.page(paginator.num_pages) # Force to have a list instead of QuerySet @@ -95,30 +101,38 @@ def index(request): # Items to remove later because invalid to_remove = [] # Parse every item (max = pagination_number) - for i in range( len( versions.object_list ) ): - if versions.object_list[i].object : - v = versions.object_list[i] + for i in range(len(versions.object_list)): + if versions.object_list[i].object: + version = versions.object_list[i] versions.object_list[i] = { - 'rev_id' : v.revision.id, - 'comment': v.revision.comment, - 'datetime': v.revision.date_created.strftime('%d/%m/%y %H:%M:%S'), - 'username': v.revision.user.get_username() if v.revision.user else '?', - 'user_id': v.revision.user_id, - 'version': v } - else : - to_remove.insert(0,i) + 'rev_id': version.revision.id, + 'comment': version.revision.comment, + 'datetime': version.revision.date_created.strftime( + '%d/%m/%y %H:%M:%S' + ), + 'username': + version.revision.user.get_username() + if version.revision.user else '?', + 'user_id': version.revision.user_id, + 'version': version} + else: + to_remove.insert(0, i) # Remove all tagged invalid items - for i in to_remove : + for i in to_remove: versions.object_list.pop(i) - return render(request, 'logs/index.html', {'versions_list': versions}) + @login_required @permission_required('cableur') def stats_logs(request): - options, created = GeneralOption.objects.get_or_create() + """Affiche l'ensemble des logs et des modifications sur les objets, + classés par date croissante, en vrac""" + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - revisions = Revision.objects.all().order_by('date_created').reverse().select_related('user').prefetch_related('version_set__object') + revisions = Revision.objects.all().order_by('date_created')\ + .reverse().select_related('user')\ + .prefetch_related('version_set__object') paginator = Paginator(revisions, pagination_number) page = request.GET.get('page') try: @@ -127,9 +141,12 @@ def stats_logs(request): # If page is not an integer, deliver first page. revisions = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. + # If page is out of range (e.g. 9999), deliver last page of results. revisions = paginator.page(paginator.num_pages) - return render(request, 'logs/stats_logs.html', {'revisions_list': revisions}) + return render(request, 'logs/stats_logs.html', { + 'revisions_list': revisions + }) + @login_required @permission_required('bureau') @@ -138,121 +155,182 @@ def revert_action(request, revision_id): try: revision = Revision.objects.get(id=revision_id) except Revision.DoesNotExist: - messages.error(request, u"Revision inexistante" ) + messages.error(request, u"Revision inexistante") if request.method == "POST": revision.revert() messages.success(request, "L'action a été supprimée") return redirect("/logs/") - return form({'objet': revision, 'objet_name': revision.__class__.__name__ }, 'logs/delete.html', request) + return form({ + 'objet': revision, + 'objet_name': revision.__class__.__name__ + }, 'logs/delete.html', request) + @login_required @permission_required('cableur') def stats_general(request): - all_active_users = User.objects.filter(state=User.STATE_ACTIVE) - ip = dict() + """Statistiques générales affinées sur les ip, activées, utilisées par + range, et les statistiques générales sur les users : users actifs, + cotisants, activés, archivés, etc""" + ip_dict = dict() for ip_range in IpType.objects.all(): all_ip = IpList.objects.filter(ip_type=ip_range) used_ip = Interface.objects.filter(ipv4__in=all_ip).count() - active_ip = all_active_assigned_interfaces_count().filter(ipv4__in=IpList.objects.filter(ip_type=ip_range)).count() - ip[ip_range] = [ip_range, all_ip.count(), used_ip, active_ip, all_ip.count()-used_ip] + active_ip = all_active_assigned_interfaces_count().filter( + ipv4__in=IpList.objects.filter(ip_type=ip_range) + ).count() + ip_dict[ip_range] = [ip_range, all_ip.count(), + used_ip, active_ip, all_ip.count()-used_ip] stats = [ - [["Categorie", "Nombre d'utilisateurs"], { - 'active_users' : ["Users actifs", User.objects.filter(state=User.STATE_ACTIVE).count()], - 'inactive_users' : ["Users désactivés", User.objects.filter(state=User.STATE_DISABLED).count()], - 'archive_users' : ["Users archivés", User.objects.filter(state=User.STATE_ARCHIVE).count()], - 'adherent_users' : ["Adhérents à l'association", all_adherent().count()], - 'connexion_users' : ["Utilisateurs bénéficiant d'une connexion", all_has_access().count()], - 'ban_users' : ["Utilisateurs bannis", all_baned().count()], - 'whitelisted_user' : ["Utilisateurs bénéficiant d'une connexion gracieuse", all_whitelisted().count()], - 'actives_interfaces' : ["Interfaces actives (ayant accès au reseau)", all_active_interfaces_count().count()], - 'actives_assigned_interfaces' : ["Interfaces actives et assignées ipv4", all_active_assigned_interfaces_count().count()] - }], - [["Range d'ip", "Nombre d'ip totales", "Ip assignées", "Ip assignées à une machine active", "Ip non assignées"] ,ip] - ] + [["Categorie", "Nombre d'utilisateurs"], { + 'active_users': [ + "Users actifs", + User.objects.filter(state=User.STATE_ACTIVE).count()], + 'inactive_users': [ + "Users désactivés", + User.objects.filter(state=User.STATE_DISABLED).count()], + 'archive_users': [ + "Users archivés", + User.objects.filter(state=User.STATE_ARCHIVE).count()], + 'adherent_users': [ + "Adhérents à l'association", + all_adherent().count()], + 'connexion_users': [ + "Utilisateurs bénéficiant d'une connexion", + all_has_access().count()], + 'ban_users': [ + "Utilisateurs bannis", + all_baned().count()], + 'whitelisted_user': [ + "Utilisateurs bénéficiant d'une connexion gracieuse", + all_whitelisted().count()], + 'actives_interfaces': [ + "Interfaces actives (ayant accès au reseau)", + all_active_interfaces_count().count()], + 'actives_assigned_interfaces': [ + "Interfaces actives et assignées ipv4", + all_active_assigned_interfaces_count().count()] + }], + [["Range d'ip", "Nombre d'ip totales", "Ip assignées", + "Ip assignées à une machine active", "Ip non assignées"], ip_dict] + ] return render(request, 'logs/stats_general.html', {'stats_list': stats}) @login_required @permission_required('cableur') def stats_models(request): - all_active_users = User.objects.filter(state=User.STATE_ACTIVE) + """Statistiques générales, affiche les comptages par models: + nombre d'users, d'écoles, de droits, de bannissements, + de factures, de ventes, de banque, de machines, etc""" stats = { - 'Users' : { - 'users' : [User.PRETTY_NAME, User.objects.count()], - 'serviceuser' : [ServiceUser.PRETTY_NAME, ServiceUser.objects.count()], - 'right' : [Right.PRETTY_NAME, Right.objects.count()], - 'school' : [School.PRETTY_NAME, School.objects.count()], - 'listright' : [ListRight.PRETTY_NAME, ListRight.objects.count()], - 'listshell' : [ListShell.PRETTY_NAME, ListShell.objects.count()], - 'ban' : [Ban.PRETTY_NAME, Ban.objects.count()], - 'whitelist' : [Whitelist.PRETTY_NAME, Whitelist.objects.count()] - }, - 'Cotisations' : { - 'factures' : [Facture.PRETTY_NAME, Facture.objects.count()], - 'vente' : [Vente.PRETTY_NAME, Vente.objects.count()], - 'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()], - 'article' : [Article.PRETTY_NAME, Article.objects.count()], - 'banque' : [Banque.PRETTY_NAME, Banque.objects.count()], - 'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.objects.count()], - }, - 'Machines' : { - 'machine' : [Machine.PRETTY_NAME, Machine.objects.count()], - 'typemachine' : [MachineType.PRETTY_NAME, MachineType.objects.count()], - 'typeip' : [IpType.PRETTY_NAME, IpType.objects.count()], - 'extension' : [Extension.PRETTY_NAME, Extension.objects.count()], - 'interface' : [Interface.PRETTY_NAME, Interface.objects.count()], - 'alias' : [Domain.PRETTY_NAME, Domain.objects.exclude(cname=None).count()], - 'iplist' : [IpList.PRETTY_NAME, IpList.objects.count()], - }, - 'Topologie' : { - 'switch' : [Switch.PRETTY_NAME, Switch.objects.count()], - 'port' : [Port.PRETTY_NAME, Port.objects.count()], - 'chambre' : [Room.PRETTY_NAME, Room.objects.count()], - }, - 'Actions effectuées sur la base' : - { - 'revision' : ["Nombre d'actions", Revision.objects.count()], - }, + 'Users': { + 'users': [User.PRETTY_NAME, User.objects.count()], + 'serviceuser': [ServiceUser.PRETTY_NAME, + ServiceUser.objects.count()], + 'right': [Right.PRETTY_NAME, Right.objects.count()], + 'school': [School.PRETTY_NAME, School.objects.count()], + 'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()], + 'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()], + 'ban': [Ban.PRETTY_NAME, Ban.objects.count()], + 'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()] + }, + 'Cotisations': { + 'factures': [Facture.PRETTY_NAME, Facture.objects.count()], + 'vente': [Vente.PRETTY_NAME, Vente.objects.count()], + 'cotisation': [Cotisation.PRETTY_NAME, Cotisation.objects.count()], + 'article': [Article.PRETTY_NAME, Article.objects.count()], + 'banque': [Banque.PRETTY_NAME, Banque.objects.count()], + }, + 'Machines': { + 'machine': [Machine.PRETTY_NAME, Machine.objects.count()], + 'typemachine': [MachineType.PRETTY_NAME, + MachineType.objects.count()], + 'typeip': [IpType.PRETTY_NAME, IpType.objects.count()], + 'extension': [Extension.PRETTY_NAME, Extension.objects.count()], + 'interface': [Interface.PRETTY_NAME, Interface.objects.count()], + 'alias': [Domain.PRETTY_NAME, + Domain.objects.exclude(cname=None).count()], + 'iplist': [IpList.PRETTY_NAME, IpList.objects.count()], + }, + 'Topologie': { + 'switch': [Switch.PRETTY_NAME, Switch.objects.count()], + 'port': [Port.PRETTY_NAME, Port.objects.count()], + 'chambre': [Room.PRETTY_NAME, Room.objects.count()], + }, + 'Actions effectuées sur la base': + { + 'revision': ["Nombre d'actions", Revision.objects.count()], + }, } - return render(request, 'logs/stats_models.html', {'stats_list': stats}) + return render(request, 'logs/stats_models.html', {'stats_list': stats}) + @login_required @permission_required('cableur') def stats_users(request): + """Affiche les statistiques base de données aggrégées par user : + nombre de machines par user, d'etablissements par user, + de moyens de paiements par user, de banque par user, + de bannissement par user, etc""" onglet = request.GET.get('onglet') try: - search_field = STATS_DICT[onglet] - except: - search_field = STATS_DICT[0] + _search_field = STATS_DICT[onglet] + except KeyError: + _search_field = STATS_DICT[0] onglet = 0 - start_date = timezone.now() + relativedelta(months=-search_field[1]) stats = { - 'Utilisateur' : { - 'Machines' : User.objects.annotate(num=Count('machine')).order_by('-num')[:10], - 'Facture' : User.objects.annotate(num=Count('facture')).order_by('-num')[:10], - 'Bannissement' : User.objects.annotate(num=Count('ban')).order_by('-num')[:10], - 'Accès gracieux' : User.objects.annotate(num=Count('whitelist')).order_by('-num')[:10], - 'Droits' : User.objects.annotate(num=Count('right')).order_by('-num')[:10], - }, - 'Etablissement' : { - 'Utilisateur' : School.objects.annotate(num=Count('user')).order_by('-num')[:10], - }, - 'Moyen de paiement' : { - 'Utilisateur' : Paiement.objects.annotate(num=Count('facture')).order_by('-num')[:10], - }, - 'Banque' : { - 'Utilisateur' : Banque.objects.annotate(num=Count('facture')).order_by('-num')[:10], - }, + 'Utilisateur': { + 'Machines': User.objects.annotate( + num=Count('machine') + ).order_by('-num')[:10], + 'Facture': User.objects.annotate( + num=Count('facture') + ).order_by('-num')[:10], + 'Bannissement': User.objects.annotate( + num=Count('ban') + ).order_by('-num')[:10], + 'Accès gracieux': User.objects.annotate( + num=Count('whitelist') + ).order_by('-num')[:10], + 'Droits': User.objects.annotate( + num=Count('right') + ).order_by('-num')[:10], + }, + 'Etablissement': { + 'Utilisateur': School.objects.annotate( + num=Count('user') + ).order_by('-num')[:10], + }, + 'Moyen de paiement': { + 'Utilisateur': Paiement.objects.annotate( + num=Count('facture') + ).order_by('-num')[:10], + }, + 'Banque': { + 'Utilisateur': Banque.objects.annotate( + num=Count('facture') + ).order_by('-num')[:10], + }, } - return render(request, 'logs/stats_users.html', {'stats_list': stats, 'stats_dict' : STATS_DICT, 'active_field': onglet}) + return render(request, 'logs/stats_users.html', { + 'stats_list': stats, + 'stats_dict': STATS_DICT, + 'active_field': onglet + }) + @login_required @permission_required('cableur') def stats_actions(request): - onglet = request.GET.get('onglet') + """Vue qui affiche les statistiques de modifications d'objets par + utilisateurs. + Affiche le nombre de modifications aggrégées par utilisateurs""" stats = { - 'Utilisateur' : { - 'Action' : User.objects.annotate(num=Count('revision')).order_by('-num')[:40], - }, + 'Utilisateur': { + 'Action': User.objects.annotate( + num=Count('revision') + ).order_by('-num')[:40], + }, } return render(request, 'logs/stats_users.html', {'stats_list': stats}) From 6cac4b20a2647deead31fe0a00758dc38ee563d6 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 04:48:43 +0200 Subject: [PATCH 035/138] Pylintage again --- re2o/context_processors.py | 14 +++++++++----- re2o/urls.py | 10 ++++++++-- re2o/views.py | 11 +++++++---- re2o/wsgi.py | 5 +++-- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/re2o/context_processors.py b/re2o/context_processors.py index ed4769b5..e562a347 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -19,15 +19,19 @@ # 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. +"""Fonction de context, variables renvoyées à toutes les vues""" + from __future__ import unicode_literals -from machines.models import Interface, Machine from preferences.models import GeneralOption, OptionalMachine + def context_user(request): - general_options, created = GeneralOption.objects.get_or_create() - machine_options, created = OptionalMachine.objects.get_or_create() + """Fonction de context lorsqu'un user est logué (ou non), + renvoie les infos sur l'user, la liste de ses droits, ses machines""" + general_options, _created = GeneralOption.objects.get_or_create() + machine_options, _created = OptionalMachine.objects.get_or_create() user = request.user if user.is_authenticated(): interfaces = user.user_interfaces() @@ -52,8 +56,8 @@ def context_user(request): 'is_bofh': is_bofh, 'is_trez': is_trez, 'is_infra': is_infra, - 'is_admin' : is_admin, + 'is_admin': is_admin, 'interfaces': interfaces, 'site_name': general_options.site_name, - 'ipv6_enabled' : machine_options.ipv6, + 'ipv6_enabled': machine_options.ipv6, } diff --git a/re2o/urls.py b/re2o/urls.py index 5fd45f85..775b87ec 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -49,10 +49,16 @@ urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^users/', include('users.urls', namespace='users')), url(r'^search/', include('search.urls', namespace='search')), - url(r'^cotisations/', include('cotisations.urls', namespace='cotisations')), + url( + r'^cotisations/', + include('cotisations.urls', namespace='cotisations') + ), url(r'^machines/', include('machines.urls', namespace='machines')), url(r'^topologie/', include('topologie.urls', namespace='topologie')), url(r'^logs/', include('logs.urls', namespace='logs')), - url(r'^preferences/', include('preferences.urls', namespace='preferences')), + url( + r'^preferences/', + include('preferences.urls', namespace='preferences') + ), ] diff --git a/re2o/views.py b/re2o/views.py index 77a36418..9cab6273 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -19,25 +19,28 @@ # 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. +""" +Fonctions de la page d'accueil et diverses fonctions utiles pour tous +les views +""" from __future__ import unicode_literals from django.shortcuts import render -from django.shortcuts import get_object_or_404 from django.template.context_processors import csrf -from django.template import Context, RequestContext, loader from preferences.models import Service + def form(ctx, template, request): + """Form générique, raccourci importé par les fonctions views du site""" context = ctx context.update(csrf(request)) return render(request, template, context) def index(request): - i = 0 + """Affiche la liste des services sur la page d'accueil de re2o""" services = [[], [], []] for indice, serv in enumerate(Service.objects.all()): services[indice % 3].append(serv) - return form({'services_urls': services}, 're2o/index.html', request) diff --git a/re2o/wsgi.py b/re2o/wsgi.py index 70108566..deb6b330 100644 --- a/re2o/wsgi.py +++ b/re2o/wsgi.py @@ -32,9 +32,10 @@ https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ from __future__ import unicode_literals import os -from django.core.wsgi import get_wsgi_application -from os.path import dirname import sys +from os.path import dirname +from django.core.wsgi import get_wsgi_application + sys.path.append(dirname(dirname(__file__))) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") From 4c0d9b9a407e4aefbfa5b0501f546d67dd3ebaf7 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 06:03:53 +0200 Subject: [PATCH 036/138] Pep8 et nettoyage, et doc pour l'app preferences --- preferences/admin.py | 22 +++++++- preferences/forms.py | 113 +++++++++++++++++++++++++++++--------- preferences/models.py | 74 ++++++++++++++++++++----- preferences/urls.py | 51 +++++++++++++++--- preferences/views.py | 123 ++++++++++++++++++++++++++++-------------- 5 files changed, 295 insertions(+), 88 deletions(-) diff --git a/preferences/admin.py b/preferences/admin.py index a8ce9335..96b4d9e1 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -20,35 +20,53 @@ # 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. - +""" +Classes admin pour les models de preferences +""" from __future__ import unicode_literals from django.contrib import admin from reversion.admin import VersionAdmin -from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, Service, AssoOption, MailMessageOption +from .models import OptionalUser, OptionalMachine, OptionalTopologie +from .models import GeneralOption, Service, AssoOption, MailMessageOption + class OptionalUserAdmin(VersionAdmin): + """Class admin options user""" pass + class OptionalTopologieAdmin(VersionAdmin): + """Class admin options topologie""" pass + class OptionalMachineAdmin(VersionAdmin): + """Class admin options machines""" pass + class GeneralOptionAdmin(VersionAdmin): + """Class admin options générales""" pass + class ServiceAdmin(VersionAdmin): + """Class admin gestion des services de la page d'accueil""" pass + class AssoOptionAdmin(VersionAdmin): + """Class admin options de l'asso""" pass + class MailMessageOptionAdmin(VersionAdmin): + """Class admin options mail""" pass + admin.site.register(OptionalUser, OptionalUserAdmin) admin.site.register(OptionalMachine, OptionalMachineAdmin) admin.site.register(OptionalTopologie, OptionalTopologieAdmin) diff --git a/preferences/forms.py b/preferences/forms.py index 887d768d..51cbb885 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -19,71 +19,116 @@ # 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. +""" +Formulaire d'edition des réglages : user, machine, topologie, asso... +""" from __future__ import unicode_literals -from django.forms import ModelForm, Form, ValidationError +from django.forms import ModelForm, Form from django import forms -from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, AssoOption, MailMessageOption, Service -from django.db.models import Q +from .models import OptionalUser, OptionalMachine, OptionalTopologie +from .models import GeneralOption, AssoOption, MailMessageOption, Service + class EditOptionalUserForm(ModelForm): + """Formulaire d'édition des options de l'user. (solde, telephone..)""" class Meta: model = OptionalUser fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditOptionalUserForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['is_tel_mandatory'].label = 'Exiger un numéro de téléphone' - self.fields['user_solde'].label = 'Activation du solde pour les utilisateurs' + super(EditOptionalUserForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['is_tel_mandatory'].label = 'Exiger un numéro de\ + téléphone' + self.fields['user_solde'].label = 'Activation du solde pour\ + les utilisateurs' + class EditOptionalMachineForm(ModelForm): + """Options machines (max de machines, etc)""" class Meta: model = OptionalMachine fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditOptionalMachineForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['password_machine'].label = "Possibilité d'attribuer un mot de passe par interface" - self.fields['max_lambdauser_interfaces'].label = "Maximum d'interfaces autorisées pour un user normal" - self.fields['max_lambdauser_aliases'].label = "Maximum d'alias dns autorisés pour un user normal" + super(EditOptionalMachineForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['password_machine'].label = "Possibilité d'attribuer\ + un mot de passe par interface" + self.fields['max_lambdauser_interfaces'].label = "Maximum\ + d'interfaces autorisées pour un user normal" + self.fields['max_lambdauser_aliases'].label = "Maximum d'alias\ + dns autorisés pour un user normal" + class EditOptionalTopologieForm(ModelForm): + """Options de topologie, formulaire d'edition (vlan par default etc)""" class Meta: model = OptionalTopologie fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditOptionalTopologieForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['vlan_decision_ok'].label = "Vlan où placer les machines après acceptation RADIUS" - self.fields['vlan_decision_nok'].label = "Vlan où placer les machines après rejet RADIUS" + super(EditOptionalTopologieForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['vlan_decision_ok'].label = "Vlan où placer les\ + machines après acceptation RADIUS" + self.fields['vlan_decision_nok'].label = "Vlan où placer les\ + machines après rejet RADIUS" + class EditGeneralOptionForm(ModelForm): + """Options générales (affichages de résultats de recherche, etc)""" class Meta: model = GeneralOption fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditGeneralOptionForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['search_display_page'].label = 'Resultats affichés dans une recherche' - self.fields['pagination_number'].label = 'Items par page, taille normale (ex users)' - self.fields['pagination_large_number'].label = 'Items par page, taille élevée (machines)' - self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien de reinitialisation de mot de passe (en heures)' + super(EditGeneralOptionForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['search_display_page'].label = 'Resultats\ + affichés dans une recherche' + self.fields['pagination_number'].label = 'Items par page,\ + taille normale (ex users)' + self.fields['pagination_large_number'].label = 'Items par page,\ + taille élevée (machines)' + self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien\ + de reinitialisation de mot de passe (en heures)' self.fields['site_name'].label = 'Nom du site web' - self.fields['email_from'].label = 'Adresse mail d\'expedition automatique' + self.fields['email_from'].label = "Adresse mail d\ + 'expedition automatique" + class EditAssoOptionForm(ModelForm): + """Options de l'asso (addresse, telephone, etc)""" class Meta: model = AssoOption fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditAssoOptionForm, self).__init__(*args, prefix=prefix, **kwargs) + super(EditAssoOptionForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) self.fields['name'].label = 'Nom de l\'asso' self.fields['siret'].label = 'SIRET' self.fields['adresse1'].label = 'Adresse (ligne 1)' @@ -91,20 +136,31 @@ class EditAssoOptionForm(ModelForm): self.fields['contact'].label = 'Email de contact' self.fields['telephone'].label = 'Numéro de téléphone' self.fields['pseudo'].label = 'Pseudo d\'usage' - self.fields['utilisateur_asso'].label = 'Compte utilisé pour faire les modifications depuis /admin' + self.fields['utilisateur_asso'].label = 'Compte utilisé pour\ + faire les modifications depuis /admin' + class EditMailMessageOptionForm(ModelForm): + """Formulaire d'edition des messages de bienvenue personnalisés""" class Meta: model = MailMessageOption fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditMailMessageOptionForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['welcome_mail_fr'].label = 'Message dans le mail de bienvenue en français' - self.fields['welcome_mail_en'].label = 'Message dans le mail de bienvenue en anglais' + super(EditMailMessageOptionForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) + self.fields['welcome_mail_fr'].label = 'Message dans le\ + mail de bienvenue en français' + self.fields['welcome_mail_en'].label = 'Message dans le\ + mail de bienvenue en anglais' + class ServiceForm(ModelForm): + """Edition, ajout de services sur la page d'accueil""" class Meta: model = Service fields = '__all__' @@ -113,6 +169,11 @@ class ServiceForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) -class DelServiceForm(Form): - services = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), label="Enregistrements service actuels", widget=forms.CheckboxSelectMultiple) +class DelServiceForm(Form): + """Suppression de services sur la page d'accueil""" + services = forms.ModelMultipleChoiceField( + queryset=Service.objects.all(), + label="Enregistrements service actuels", + widget=forms.CheckboxSelectMultiple + ) diff --git a/preferences/models.py b/preferences/models.py index 34c4c0b1..dc1412e7 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -20,26 +20,38 @@ # 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. - +""" +Reglages généraux, machines, utilisateurs, mail, general pour l'application. +""" from __future__ import unicode_literals from django.db import models from cotisations.models import Paiement -from machines.models import Vlan + class OptionalUser(models.Model): + """Options pour l'user : obligation ou nom du telephone, + activation ou non du solde, autorisation du negatif, fingerprint etc""" PRETTY_NAME = "Options utilisateur" is_tel_mandatory = models.BooleanField(default=True) user_solde = models.BooleanField(default=False) - solde_negatif = models.DecimalField(max_digits=5, decimal_places=2, default=0) + solde_negatif = models.DecimalField( + max_digits=5, + decimal_places=2, + default=0 + ) gpg_fingerprint = models.BooleanField(default=True) def clean(self): + """Creation du mode de paiement par solde""" if self.user_solde: Paiement.objects.get_or_create(moyen="Solde") + class OptionalMachine(models.Model): + """Options pour les machines : maximum de machines ou d'alias par user + sans droit, activation de l'ipv6""" PRETTY_NAME = "Options machines" password_machine = models.BooleanField(default=False) @@ -47,21 +59,43 @@ class OptionalMachine(models.Model): max_lambdauser_aliases = models.IntegerField(default=10) ipv6 = models.BooleanField(default=False) + class OptionalTopologie(models.Model): + """Reglages pour la topologie : mode d'accès radius, vlan où placer + les machines en accept ou reject""" PRETTY_NAME = "Options topologie" MACHINE = 'MACHINE' DEFINED = 'DEFINED' CHOICE_RADIUS = ( - (MACHINE, 'Sur le vlan de la plage ip machine'), - (DEFINED, 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"'), + (MACHINE, 'Sur le vlan de la plage ip machine'), + (DEFINED, 'Prédéfini dans "Vlan où placer les machines\ + après acceptation RADIUS"'), + ) + + 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 ) - 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) - class GeneralOption(models.Model): + """Options générales : nombre de resultats par page, nom du site, + temps où les liens sont valides""" PRETTY_NAME = "Options générales" search_display_page = models.IntegerField(default=15) @@ -71,30 +105,44 @@ class GeneralOption(models.Model): site_name = models.CharField(max_length=32, default="Re2o") email_from = models.EmailField(default="www-data@serveur.net") + class Service(models.Model): + """Liste des services affichés sur la page d'accueil : url, description, + image et nom""" name = models.CharField(max_length=32) url = models.URLField() description = models.TextField() - image = models.ImageField(upload_to='logo', blank=True) + image = models.ImageField(upload_to='logo', blank=True) def __str__(self): return str(self.name) + class AssoOption(models.Model): + """Options générales de l'asso : siret, addresse, nom, etc""" PRETTY_NAME = "Options de l'association" - name = models.CharField(default="Association réseau école machin", max_length=256) + name = models.CharField( + default="Association réseau école machin", + max_length=256 + ) siret = models.CharField(default="00000000000000", max_length=32) adresse1 = models.CharField(default="1 Rue de exemple", max_length=128) adresse2 = models.CharField(default="94230 Cachan", max_length=128) contact = models.EmailField(default="contact@example.org") telephone = models.CharField(max_length=15, default="0000000000") pseudo = models.CharField(default="Asso", max_length=32) - utilisateur_asso = models.OneToOneField('users.User', on_delete=models.PROTECT, blank=True, null=True) + utilisateur_asso = models.OneToOneField( + 'users.User', + on_delete=models.PROTECT, + blank=True, + null=True + ) + class MailMessageOption(models.Model): + """Reglages, mail de bienvenue et autre""" PRETTY_NAME = "Options de corps de mail" welcome_mail_fr = models.TextField(default="") welcome_mail_en = models.TextField(default="") - diff --git a/preferences/urls.py b/preferences/urls.py index 624d2e75..2169f83c 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -19,6 +19,9 @@ # 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. +""" +Urls de l'application preferences, pointant vers les fonctions de views +""" from __future__ import unicode_literals @@ -28,15 +31,47 @@ from . import views urlpatterns = [ - url(r'^edit_options/(?P
OptionalUser)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
OptionalMachine)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
OptionalTopologie)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
GeneralOption)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
AssoOption)$', views.edit_options, name='edit-options'), - url(r'^edit_options/(?P
MailMessageOption)$', views.edit_options, name='edit-options'), + url( + r'^edit_options/(?P
OptionalUser)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
OptionalMachine)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
OptionalTopologie)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
GeneralOption)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
AssoOption)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
MailMessageOption)$', + views.edit_options, + name='edit-options' + ), url(r'^add_services/$', views.add_services, name='add-services'), - url(r'^edit_services/(?P[0-9]+)$', views.edit_services, name='edit-services'), + url( + r'^edit_services/(?P[0-9]+)$', + views.edit_services, + name='edit-services' + ), url(r'^del_services/$', views.del_services, name='del-services'), - url(r'^history/(?Pservice)/(?P[0-9]+)$', views.history, name='history'), + url( + r'^history/(?Pservice)/(?P[0-9]+)$', + views.history, + name='history' + ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 5fe1cff5..1e2c433e 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -23,48 +23,53 @@ # App de gestion des machines pour re2o # Gabriel Détraz, Augustin Lemesle # Gplv2 +""" +Vue d'affichage, et de modification des réglages (réglages machine, +topologie, users, service...) +""" from __future__ import unicode_literals -from django.shortcuts import render -from django.shortcuts import get_object_or_404, render, redirect -from django.template.context_processors import csrf +from django.shortcuts import render, redirect from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import Max, ProtectedError -from django.db import IntegrityError -from django.core.mail import send_mail -from django.utils import timezone -from django.core.urlresolvers import reverse +from django.db.models import ProtectedError from django.db import transaction from reversion.models import Version from reversion import revisions as reversion +from re2o.views import form from .forms import ServiceForm, DelServiceForm -from .models import Service, OptionalUser, OptionalMachine, AssoOption, MailMessageOption, GeneralOption, OptionalTopologie +from .models import Service, OptionalUser, OptionalMachine, AssoOption +from .models import MailMessageOption, GeneralOption, OptionalTopologie from . import models from . import forms -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) - @login_required @permission_required('cableur') def display_options(request): - useroptions, created = OptionalUser.objects.get_or_create() - machineoptions, created = OptionalMachine.objects.get_or_create() - topologieoptions, created = OptionalTopologie.objects.get_or_create() - generaloptions, created = GeneralOption.objects.get_or_create() - assooptions, created = AssoOption.objects.get_or_create() - mailmessageoptions, created = MailMessageOption.objects.get_or_create() + """Vue pour affichage des options (en vrac) classé selon les models + correspondants dans un tableau""" + useroptions, _created = OptionalUser.objects.get_or_create() + machineoptions, _created = OptionalMachine.objects.get_or_create() + topologieoptions, _created = OptionalTopologie.objects.get_or_create() + generaloptions, _created = GeneralOption.objects.get_or_create() + assooptions, _created = AssoOption.objects.get_or_create() + mailmessageoptions, _created = MailMessageOption.objects.get_or_create() service_list = Service.objects.all() - return form({'useroptions': useroptions, 'machineoptions': machineoptions, 'topologieoptions': topologieoptions, 'generaloptions': generaloptions, 'assooptions' : assooptions, 'mailmessageoptions' : mailmessageoptions, 'service_list':service_list}, 'preferences/display_preferences.html', request) + return form({ + 'useroptions': useroptions, + 'machineoptions': machineoptions, + 'topologieoptions': topologieoptions, + 'generaloptions': generaloptions, + 'assooptions': assooptions, + 'mailmessageoptions': mailmessageoptions, + 'service_list': service_list + }, 'preferences/display_preferences.html', request) + @login_required @permission_required('admin') @@ -73,23 +78,36 @@ def edit_options(request, section): model = getattr(models, section, None) form_instance = getattr(forms, 'Edit' + section + 'Form', None) if model and form: - options_instance, created = model.objects.get_or_create() - options = form_instance(request.POST or None, instance=options_instance) + options_instance, _created = model.objects.get_or_create() + options = form_instance( + request.POST or None, + instance=options_instance + ) if options.is_valid(): with transaction.atomic(), reversion.create_revision(): options.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in options.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in options.changed_data + ) + ) messages.success(request, "Préférences modifiées") return redirect("/preferences/") - return form({'options': options}, 'preferences/edit_preferences.html', request) + return form( + {'options': options}, + 'preferences/edit_preferences.html', + request + ) else: messages.error(request, "Objet inconnu") return redirect("/preferences/") + @login_required @permission_required('admin') def add_services(request): + """Ajout d'un service de la page d'accueil""" services = ServiceForm(request.POST or None) if services.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -98,29 +116,45 @@ def add_services(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement ns a été ajouté") return redirect("/preferences/") - return form({'preferenceform': services}, 'preferences/preferences.html', request) + return form( + {'preferenceform': services}, + 'preferences/preferences.html', + request + ) + @login_required @permission_required('admin') def edit_services(request, servicesid): + """Edition des services affichés sur la page d'accueil""" try: services_instance = Service.objects.get(pk=servicesid) except Service.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/preferences/") services = ServiceForm(request.POST or None, instance=services_instance) if services.is_valid(): with transaction.atomic(), reversion.create_revision(): services.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in services.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in services.changed_data + ) + ) messages.success(request, "Service modifié") return redirect("/preferences/") - return form({'preferenceform': services}, 'preferences/preferences.html', request) + return form( + {'preferenceform': services}, + 'preferences/preferences.html', + request + ) + @login_required @permission_required('admin') def del_services(request): + """Suppression d'un service de la page d'accueil""" services = DelServiceForm(request.POST or None) if services.is_valid(): services_dels = services.cleaned_data['services'] @@ -131,20 +165,28 @@ def del_services(request): reversion.set_user(request.user) messages.success(request, "Le services a été supprimée") except ProtectedError: - messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % services_del) + messages.error(request, "Erreur le service\ + suivant %s ne peut être supprimé" % services_del) return redirect("/preferences/") - return form({'preferenceform': services}, 'preferences/preferences.html', request) + return form( + {'preferenceform': services}, + 'preferences/preferences.html', + request + ) + @login_required @permission_required('cableur') -def history(request, object, id): - if object == 'service': +def history(request, object_name, object_id): + """Historique de creation et de modification d'un service affiché sur + la page d'accueil""" + if object_name == 'service': try: - object_instance = Service.objects.get(pk=id) + object_instance = Service.objects.get(pk=object_id) except Service.DoesNotExist: - messages.error(request, "Service inexistant") - return redirect("/preferences/") - options, created = GeneralOption.objects.get_or_create() + messages.error(request, "Service inexistant") + return redirect("/preferences/") + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) @@ -157,4 +199,7 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render(request, 're2o/history.html', { + 'reversions': reversions, + 'object': object_instance + }) From b84f654fb397e920108c3fcc0a64437d913be473 Mon Sep 17 00:00:00 2001 From: LEVY-FALK Hugo Date: Sat, 14 Oct 2017 12:40:22 +0200 Subject: [PATCH 037/138] Factorisation de code. --- machines/views.py | 113 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 17 deletions(-) diff --git a/machines/views.py b/machines/views.py index 5c9e31e6..7ac2bb63 100644 --- a/machines/views.py +++ b/machines/views.py @@ -43,31 +43,107 @@ from django.contrib.auth import authenticate, login from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer -from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer, OuverturePortsSerializer +from machines.serializers import ( FullInterfaceSerializer, + InterfaceSerializer, + TypeSerializer, + DomainSerializer, + TextSerializer, + MxSerializer, + ExtensionSerializer, + ServiceServersSerializer, + NsSerializer, + OuverturePortsSerializer +) from reversion import revisions as reversion from reversion.models import Version import re -from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm -from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm +from .forms import ( + NewMachineForm, + EditMachineForm, + EditInterfaceForm, + AddInterfaceForm, + MachineTypeForm, + DelMachineTypeForm, + ExtensionForm, + DelExtensionForm, + BaseEditInterfaceForm, + BaseEditMachineForm +) +from .forms import ( + EditIpTypeForm, + IpTypeForm, + DelIpTypeForm, + DomainForm, + AliasForm, + DelAliasForm, + NsForm, + DelNsForm, + TextForm, + DelTextForm, + MxForm, + DelMxForm, + VlanForm, + DelVlanForm, + ServiceForm, + DelServiceForm, + NasForm, + DelNasForm +) from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm -from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort +from .models import ( + IpType, + Machine, + Interface, + IpList, + MachineType, + Extension, + Mx, + Ns, + Domain, + Service, + Service_link, + Vlan, + Nas, + Text, + OuverturePortList, + OuverturePort +) from users.models import User from users.models import all_has_access from preferences.models import GeneralOption, OptionalMachine from .templatetags.bootstrap_form_typeahead import hidden_id, input_id +def filter_active_interfaces(q): + """Filtre les machines autorisées à sortir sur internet dans une requête""" + return q.filter( + machine__in=Machine.objects.filter( + user__in=all_has_access() + ).filter(active=True)) \ + .select_related('domain') \ + .select_related('machine') \ + .select_related('type') \ + .select_related('ipv4') \ + .select_related('domain__extension') \ + .select_related('ipv4__ip_type').distinct() + def all_active_interfaces(): """Renvoie l'ensemble des machines autorisées à sortir sur internet """ - return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)).select_related('domain').select_related('machine').select_related('type').select_related('ipv4').select_related('domain__extension').select_related('ipv4__ip_type').distinct() + return filter_active_interfaces(Interface.objects) def all_active_assigned_interfaces(): - """ Renvoie l'ensemble des machines qui ont une ipv4 assignées et disposant de l'accès internet""" + """ + Renvoie l'ensemble des machines qui ont une ipv4 assignées et disposant de + l'accès internet + """ return all_active_interfaces().filter(ipv4__isnull=False) def all_active_interfaces_count(): """ Version light seulement pour compter""" - return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)) + return Interface.objects.filter( + machine__in=Machine.objects.filter(user__in=all_has_access())\ + .filter(active=True) + ) def all_active_assigned_interfaces_count(): """ Version light seulement pour compter""" @@ -92,7 +168,8 @@ def generate_ipv4_choices( form ) : choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},' mtype_id = -1 - for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id')).order_by('mtype_id', 'id') : + for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id'))\ + .order_by('mtype_id', 'id') : if mtype_id != ip.mtype_id : mtype_id = ip.mtype_id used_mtype_id.append(mtype_id) @@ -159,8 +236,8 @@ def generate_ipv4_bft_param( form, is_type_tt ): @login_required def new_machine(request, userid): - """ Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain - à partir de model forms. + """ Fonction de creation d'une machine. Cree l'objet machine, + le sous objet interface et l'objet domain à partir de model forms. Trop complexe, devrait être simplifié""" try: user = User.objects.get(pk=userid) @@ -171,7 +248,9 @@ def new_machine(request, userid): max_lambdauser_interfaces = options.max_lambdauser_interfaces if not request.user.has_perms(('cableur',)): if user != request.user: - messages.error(request, "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit") + messages.error( + request, + "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit") return redirect("/users/profil/" + str(request.user.id)) if user.user_interfaces().count() >= max_lambdauser_interfaces: messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces) @@ -1205,14 +1284,14 @@ def service_servers(request): @permission_required('serveur') def ouverture_ports(request): r = {'ipv4':{}, 'ipv6':{}} - for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set'): + for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'): pl = { - "tcp_in":set(map(str,o.tcp_ports_in())), - "tcp_out":set(map(str,o.tcp_ports_out())), - "udp_in":set(map(str,o.udp_ports_in())), - "udp_out":set(map(str,o.udp_ports_out())), + "tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))), + "tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))), + "udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))), + "udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))), } - for i in o.interface_set.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)).select_related('domain').select_related('machine').select_related('type').select_related('ipv4').select_related('domain__extension').select_related('ipv4__ip_type').distinct(): + for i in filter_active_interfaces(o.interface_set): if i.may_have_port_open(): d = r['ipv4'].get(i.ipv4.ipv4, {}) d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) From a92eaae633fff3b249c38cbb7bf9f88d1946885c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sat, 14 Oct 2017 13:27:56 +0000 Subject: [PATCH 038/138] Renomme bft en mfb (massive_bootstrap_form) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plus adapté aux nouvelles fonctions incoming --- .../templates/cotisations/edit_facture.html | 4 +- machines/templates/machines/machine.html | 14 +-- machines/views.py | 28 ++--- .../preferences/edit_preferences.html | 4 +- ...typeahead.py => massive_bootstrap_form.py} | 105 ++++++++++-------- topologie/templates/topologie/switch.html | 10 +- topologie/templates/topologie/topo.html | 4 +- topologie/views.py | 6 +- users/templates/users/user.html | 4 +- 9 files changed, 93 insertions(+), 86 deletions(-) rename re2o/templatetags/{bootstrap_form_typeahead.py => massive_bootstrap_form.py} (81%) diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index 11e454f5..f1af2b8b 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load staticfiles%} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de factures{% endblock %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Editer la facture

- {% bootstrap_form_typeahead factureform 'user' %} + {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }}

Articles de la facture

diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 4f68b6ee..9f5e93b5 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de machines{% endblock %} @@ -78,10 +78,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if interfaceform %}

Interface

- {% if i_bft_param %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% if i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %} @@ -98,15 +98,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if extensionform %}

Extension

- {% bootstrap_form_typeahead extensionform 'origin' %} + {% massive_bootstrap_form extensionform 'origin' %} {% endif %} {% if mxform %}

Enregistrement MX

- {% bootstrap_form_typeahead mxform 'name' %} + {% massive_bootstrap_form mxform 'name' %} {% endif %} {% if nsform %}

Enregistrement NS

- {% bootstrap_form_typeahead nsform 'ns' %} + {% massive_bootstrap_form nsform 'ns' %} {% endif %} {% if txtform %}

Enregistrement TXT

diff --git a/machines/views.py b/machines/views.py index 0e00dc67..5ea4b8b3 100644 --- a/machines/views.py +++ b/machines/views.py @@ -55,7 +55,7 @@ from .models import IpType, Machine, Interface, IpList, MachineType, Extension, from users.models import User from users.models import all_has_access from preferences.models import GeneralOption, OptionalMachine -from re2o.templatetags.bootstrap_form_typeahead import hidden_id, input_id +from re2o.templatetags.massive_bootstrap_form import hidden_id, input_id def all_active_interfaces(): """Renvoie l'ensemble des machines autorisées à sortir sur internet """ @@ -85,7 +85,7 @@ def f_type_id( is_type_tt ): return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type' def generate_ipv4_choices( form ) : - """ Generate the parameter choices for the bootstrap_form_typeahead tag + """ Generate the parameter choices for the massive_bootstrap_form tag """ f_ipv4 = form.fields['ipv4'] used_mtype_id = [] @@ -112,7 +112,7 @@ def generate_ipv4_choices( form ) : return choices def generate_ipv4_engine( is_type_tt ) : - """ Generate the parameter engine for the bootstrap_form_typeahead tag + """ Generate the parameter engine for the massive_bootstrap_form tag """ return ( 'new Bloodhound( {{' @@ -126,7 +126,7 @@ def generate_ipv4_engine( is_type_tt ) : ) def generate_ipv4_match_func( is_type_tt ) : - """ Generate the parameter match_func for the bootstrap_form_typeahead tag + """ Generate the parameter match_func for the massive_bootstrap_form tag """ return ( 'function(q, sync) {{' @@ -142,20 +142,20 @@ def generate_ipv4_match_func( is_type_tt ) : type_id = f_type_id( is_type_tt ) ) -def generate_ipv4_bft_param( form, is_type_tt ): - """ Generate all the parameters to use with the bootstrap_form_typeahead +def generate_ipv4_mbf_param( form, is_type_tt ): + """ Generate all the parameters to use with the massive_bootstrap_form tag """ i_choices = { 'ipv4': generate_ipv4_choices( form ) } i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) } i_update_on = { 'ipv4': [f_type_id( is_type_tt )] } - i_bft_param = { + i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, 'update_on': i_update_on } - return i_bft_param + return i_mbf_param @login_required def new_machine(request, userid): @@ -203,8 +203,8 @@ def new_machine(request, userid): reversion.set_comment("Création") messages.success(request, "La machine a été créée") return redirect("/users/profil/" + str(user.id)) - i_bft_param = generate_ipv4_bft_param( interface, False ) - return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface, False ) + return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def edit_interface(request, interfaceid): @@ -243,8 +243,8 @@ def edit_interface(request, interfaceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) messages.success(request, "La machine a été modifiée") return redirect("/users/profil/" + str(interface.machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_machine(request, machineid): @@ -302,8 +302,8 @@ def new_interface(request, machineid): reversion.set_comment("Création") messages.success(request, "L'interface a été ajoutée") return redirect("/users/profil/" + str(machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_interface(request, interfaceid): diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 610889dd..02f006c1 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification des préférences{% endblock %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} -{% bootstrap_form_typeahead options 'utilisateur_asso' %} +{% massive_bootstrap_form options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/re2o/templatetags/bootstrap_form_typeahead.py b/re2o/templatetags/massive_bootstrap_form.py similarity index 81% rename from re2o/templatetags/bootstrap_form_typeahead.py rename to re2o/templatetags/massive_bootstrap_form.py index 4c665361..f35c43e7 100644 --- a/re2o/templatetags/bootstrap_form_typeahead.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -29,32 +29,40 @@ from bootstrap3.forms import render_field register = template.Library() @register.simple_tag -def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): +def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): """ - Render a form where some specific fields are rendered using Typeahead. - Using Typeahead really improves the performance, the speed and UX when - dealing with very large datasets (select with 50k+ elts for instance). + Render a form where some specific fields are rendered using Twitter + Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the + speed and UX when dealing with very large datasets (select with 50k+ elts + for instance). + When the fields specified should normally be rendered as a select with + single selectable option, Twitter Typeahead is used for a better display + and the matching query engine. When dealing with multiple selectable + options, sliptree's Bootstrap Tokenfield in addition with Typeahead. For convenience, it accepts the same parameters as a standard bootstrap can accept. **Tag name**:: - bootstrap_form_typeahead + massive_bootstrap_form **Parameters**: - form + form (required) The form that is to be rendered - typeahead_fields + mbf_fields (optional) A list of field names (comma separated) that should be rendered - with typeahead instead of the default bootstrap renderer. + with Typeahead/Tokenfield instead of the default bootstrap + renderer. + If not specified, all fields will be rendered as a normal bootstrap + field. - bft_param - A dict of parameters for the bootstrap_form_typeahead tag. The + mbf_param (optional) + A dict of parameters for the massive_bootstrap_form tag. The possible parameters are the following. - choices + choices (optional) A dict of strings representing the choices in JS. The keys of the dict are the names of the concerned fields. The choices must be an array of objects. Each of those objects must at @@ -71,7 +79,7 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): ... } - engine + engine (optional) A dict of strings representating the engine used for matching queries and possible values with typeahead. The keys of the dict are the names of the concerned fields. The string is valid @@ -81,7 +89,7 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): Example : 'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} - match_func + match_func (optional) A dict of strings representing a valid JS function used in the dataset to overload the matching engine. The keys of the dict are the names of the concerned fields. This function is used @@ -100,7 +108,7 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): ... } - update_on + update_on (optional) A dict of list of ids that the values depends on. The engine and the typeahead properties are recalculated and reapplied. Example : @@ -114,10 +122,10 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): **Usage**:: - {% bootstrap_form_typeahead + {% massive_bootstrap_form form [ '[,[,...]]' ] - [ { + [ mbf_param = { [ 'choices': { [ '': '' [, '': '' @@ -144,56 +152,55 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): **Example**: - {% bootstrap_form_typeahead form 'ipv4' choices='[...]' %} + {% massive_bootstrap_form form 'ipv4' choices='[...]' %} """ - t_fields = typeahead_fields.split(',') - params = kwargs.get('bft_param', {}) - exclude = params.get('exclude', None) - exclude = exclude.split(',') if exclude else [] - t_choices = params.get('choices', {}) - t_engine = params.get('engine', {}) - t_match_func = params.get('match_func', {}) - t_update_on = params.get('update_on', {}) - hidden = [h.name for h in django_form.hidden_fields()] + fields = mbf_fields.split(',') + param = kwargs.pop('mbf_param', {}) + exclude = param.get('exclude', '').split(',') + choices = param.get('choices', {}) + engine = param.get('engine', {}) + match_func = param.get('match_func', {}) + update_on = param.get('update_on', {}) + hidden_fields = [h.name for h in form.hidden_fields()] - form = '' - for f_name, f_value in django_form.fields.items() : + html = '' + for f_name, f_value in form.fields.items() : if not f_name in exclude : - if f_name in t_fields and not f_name in hidden : - f_bound = f_value.get_bound_field( django_form, f_name ) + if f_name in fields and not f_name in hidden_fields : + f_bound = f_value.get_bound_field( form, f_name ) f_value.widget = TextInput( attrs={ - 'name': 'typeahead_'+f_name, + 'name': 'mbf_'+f_name, 'placeholder': f_value.empty_label } ) - form += render_field( - f_value.get_bound_field( django_form, f_name ), + html += render_field( + f_value.get_bound_field( form, f_name ), *args, **kwargs ) - form += render_tag( + html += render_tag( 'div', content = hidden_tag( f_bound, f_name ) + - typeahead_js( + mbf_js( f_name, f_value, f_bound, - t_choices, - t_engine, - t_match_func, - t_update_on + choices, + engine, + match_func, + update_on ) ) else: - form += render_field( - f_value.get_bound_field(django_form, f_name), + html += render_field( + f_value.get_bound_field( form, f_name ), *args, **kwargs ) - return mark_safe( form ) + return mark_safe( html ) def input_id( f_bound ) : """ The id of the HTML input element """ @@ -215,20 +222,20 @@ def hidden_tag( f_bound, f_name ): } ) -def typeahead_js( f_name, f_value, f_bound, - t_choices, t_engine, t_match_func, t_update_on ) : +def mbf_js( f_name, f_value, f_bound, + choices_, engine_, match_func_, update_on_ ) : """ The whole script to use """ - choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \ + choices = mark_safe( choices_[f_name] ) if f_name in choices_.keys() \ else default_choices( f_value ) - engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \ + engine = mark_safe( engine_[f_name] ) if f_name in engine_.keys() \ else default_engine ( f_name ) - match_func = mark_safe(t_match_func[f_name]) \ - if f_name in t_match_func.keys() else default_match_func( f_name ) + match_func = mark_safe( match_func_[f_name] ) \ + if f_name in match_func_.keys() else default_match_func( f_name ) - update_on = t_update_on[f_name] if f_name in t_update_on.keys() else [] + update_on = update_on_[f_name] if f_name in update_on_.keys() else [] js_content = ( 'var choices_{f_name} = {choices};' diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index cb84e846..fe224678 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'un switch{% endblock %} @@ -47,16 +47,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if topoform %} - {% bootstrap_form_typeahead topoform 'switch_interface' %} + {% massive_bootstrap_form topoform 'switch_interface' %} {% endif %} {% if machineform %} - {% bootstrap_form_typeahead machineform 'user' %} + {% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %} {% if i_bft_param %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_bft_param %} {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %} diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index bd07c2db..e14b72a7 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modificationd 'utilisateur{% endblock %} @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} - {% bootstrap_form_typeahead topoform 'room,related,machine_interface' %} + {% massive_bootstrap_form topoform 'room,related,machine_interface' %} {%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %}
diff --git a/topologie/views.py b/topologie/views.py index ceb06f0a..1fccae75 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -51,7 +51,7 @@ from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm -from machines.views import generate_ipv4_bft_param +from machines.views import generate_ipv4_mbf_param from preferences.models import AssoOption, GeneralOption @@ -381,7 +381,7 @@ def new_switch(request): reversion.set_comment("Création") messages.success(request, "Le switch a été créé") return redirect("/topologie/") - i_bft_param = generate_ipv4_bft_param( interface, False ) + i_bft_param = generate_ipv4_mbf_param( interface, False ) return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) @login_required @@ -442,7 +442,7 @@ def edit_switch(request, switch_id): ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - i_bft_param = generate_ipv4_bft_param( interface_form, False ) + i_bft_param = generate_ipv4_mbf_param( interface_form, False ) return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) @login_required diff --git a/users/templates/users/user.html b/users/templates/users/user.html index 62d05146..756b4153 100644 --- a/users/templates/users/user.html +++ b/users/templates/users/user.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'utilisateur{% endblock %} @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% bootstrap_form_typeahead userform 'room' %} + {% massive_bootstrap_form userform 'room' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
From 1083f8d199f495bc366a51d07b5e95ca7e939574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sat, 14 Oct 2017 18:32:17 +0000 Subject: [PATCH 039/138] Support de typeahead pour les select multiples avec tokenfield --- machines/templates/machines/machine.html | 2 +- re2o/templatetags/massive_bootstrap_form.py | 289 +++-- static/css/bootstrap-tokenfield.css | 210 ++++ static/js/bootstrap-tokenfield/LICENSE.md | 23 + .../bootstrap-tokenfield.js | 1042 +++++++++++++++++ templates/base.html | 4 + 6 files changed, 1499 insertions(+), 71 deletions(-) create mode 100644 static/css/bootstrap-tokenfield.css create mode 100644 static/js/bootstrap-tokenfield/LICENSE.md create mode 100644 static/js/bootstrap-tokenfield/bootstrap-tokenfield.js diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 9f5e93b5..86bf7b90 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if serviceform %}

Service

- {% bootstrap_form serviceform %} + {% massive_bootstrap_form serviceform 'servers' %} {% endif %} {% if vlanform %}

Vlan

diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index f35c43e7..cf6c01fe 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -22,6 +22,7 @@ from django import template from django.utils.safestring import mark_safe from django.forms import TextInput +from django.forms.widgets import Select from bootstrap3.templatetags.bootstrap3 import bootstrap_form from bootstrap3.utils import render_tag from bootstrap3.forms import render_field @@ -165,34 +166,64 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): hidden_fields = [h.name for h in form.hidden_fields()] html = '' + for f_name, f_value in form.fields.items() : if not f_name in exclude : if f_name in fields and not f_name in hidden_fields : - f_bound = f_value.get_bound_field( form, f_name ) - f_value.widget = TextInput( - attrs={ - 'name': 'mbf_'+f_name, - 'placeholder': f_value.empty_label - } - ) - html += render_field( - f_value.get_bound_field( form, f_name ), - *args, - **kwargs - ) - html += render_tag( - 'div', - content = hidden_tag( f_bound, f_name ) + - mbf_js( - f_name, - f_value, - f_bound, - choices, - engine, - match_func, - update_on + + if not isinstance(f_value.widget, Select) : + raise ValueError( + ('Field named {f_name} from {form} is not a Select and' + 'can\'t be rendered with massive_bootstrap_form.' + ).format( + f_name=f_name, + form=form ) ) + + multiple = f_value.widget.allow_multiple_selected + f_bound = f_value.get_bound_field( form, f_name ) + + f_value.widget = TextInput( + attrs = { + 'name': 'mbf_'+f_name, + 'placeholder': f_value.empty_label + } + ) + html += render_field( + f_value.get_bound_field( form, f_name ), + *args, + **kwargs + ) + + if multiple : + content = mbf_js( + f_name, + f_value, + f_bound, + multiple, + choices, + engine, + match_func, + update_on + ) + else : + content = hidden_tag( f_bound, f_name ) + mbf_js( + f_name, + f_value, + f_bound, + multiple, + choices, + engine, + match_func, + update_on + ) + html += render_tag( + 'div', + content = content, + attrs = { 'id': custom_div_id( f_bound ) } + ) + else: html += render_field( f_value.get_bound_field( form, f_name ), @@ -208,7 +239,11 @@ def input_id( f_bound ) : def hidden_id( f_bound ): """ The id of the HTML hidden input element """ - return input_id( f_bound ) +'_hidden' + return input_id( f_bound ) + '_hidden' + +def custom_div_id( f_bound ): + """ The id of the HTML div element containing values and script """ + return input_id( f_bound ) + '_div' def hidden_tag( f_bound, f_name ): """ The HTML hidden input element """ @@ -222,61 +257,101 @@ def hidden_tag( f_bound, f_name ): } ) -def mbf_js( f_name, f_value, f_bound, +def mbf_js( f_name, f_value, f_bound, multiple, choices_, engine_, match_func_, update_on_ ) : """ The whole script to use """ - choices = mark_safe( choices_[f_name] ) if f_name in choices_.keys() \ - else default_choices( f_value ) + choices = ( mark_safe( choices_[f_name] ) if f_name in choices_.keys() + else default_choices( f_value ) ) - engine = mark_safe( engine_[f_name] ) if f_name in engine_.keys() \ - else default_engine ( f_name ) + engine = ( mark_safe( engine_[f_name] ) if f_name in engine_.keys() + else default_engine ( f_name ) ) - match_func = mark_safe( match_func_[f_name] ) \ - if f_name in match_func_.keys() else default_match_func( f_name ) + match_func = ( mark_safe( match_func_[f_name] ) + if f_name in match_func_.keys() else default_match_func( f_name ) ) update_on = update_on_[f_name] if f_name in update_on_.keys() else [] - js_content = ( - 'var choices_{f_name} = {choices};' - 'var engine_{f_name};' - 'var setup_{f_name} = function() {{' - 'engine_{f_name} = {engine};' - '$( "#{input_id}" ).typeahead( "destroy" );' - '$( "#{input_id}" ).typeahead( {datasets} );' - '}};' - '$( "#{input_id}" ).bind( "typeahead:select", {updater} );' - '$( "#{input_id}" ).bind( "typeahead:change", {change} );' - '{updates}' - '$( "#{input_id}" ).ready( function() {{' - 'setup_{f_name}();' - '{init_input}' - '}} );' - ).format( - f_name = f_name, - choices = choices, - engine = engine, - input_id = input_id( f_bound ), - datasets = default_datasets( f_name, match_func ), - updater = typeahead_updater( f_bound ), - change = typeahead_change( f_bound ), - updates = ''.join( [ ( - '$( "#{u_id}" ).change( function() {{' - 'setup_{f_name}();' - '{reset_input}' - '}} );' - ).format( - u_id = u_id, - reset_input = reset_input( f_bound ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = init_input( f_name, f_bound ), - ) + if multiple : + js_content = ( + 'var choices_{f_name} = {choices};' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).tokenfield( "destroy" );' + '$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});' + '}};' + '$( "#{input_id}" ).bind( "tokenfield:createtoken", {create} );' + '$( "#{input_id}" ).bind( "tokenfield:edittoken", {edit} );' + '$( "#{input_id}" ).bind( "tokenfield:removetoken", {remove} );' + '{updates}' + '$( "#{input_id}" ).ready( function() {{' + 'setup_{f_name}();' + '{init_input}' + '}} );' + ).format( + f_name = f_name, + choices = choices, + engine = engine, + input_id = input_id( f_bound ), + datasets = default_datasets( f_name, match_func ), + create = tokenfield_create( f_name, f_bound ), + edit = tokenfield_edit( f_bound ), + remove = tokenfield_remove( f_bound ), + updates = ''.join( [ ( + '$( "#{u_id}" ).change( function() {{' + 'setup_{f_name}();' + '{reset_input}' + '}} );' + ).format( + u_id = u_id, + reset_input = tokenfield_reset_input( f_bound ), + f_name = f_name + ) for u_id in update_on ] + ), + init_input = tokenfield_init_input( f_name, f_bound ), + ) + else : + js_content = ( + 'var choices_{f_name} = {choices};' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).typeahead( "destroy" );' + '$( "#{input_id}" ).typeahead( {datasets} );' + '}};' + '$( "#{input_id}" ).bind( "typeahead:select", {select} );' + '$( "#{input_id}" ).bind( "typeahead:change", {change} );' + '{updates}' + '$( "#{input_id}" ).ready( function() {{' + 'setup_{f_name}();' + '{init_input}' + '}} );' + ).format( + f_name = f_name, + choices = choices, + engine = engine, + input_id = input_id( f_bound ), + datasets = default_datasets( f_name, match_func ), + select = typeahead_select( f_bound ), + change = typeahead_change( f_bound ), + updates = ''.join( [ ( + '$( "#{u_id}" ).change( function() {{' + 'setup_{f_name}();' + '{reset_input}' + '}} );' + ).format( + u_id = u_id, + reset_input = typeahead_reset_input( f_bound ), + f_name = f_name + ) for u_id in update_on ] + ), + init_input = typeahead_init_input( f_name, f_bound ), + ) return render_tag( 'script', content=mark_safe( js_content ) ) -def init_input( f_name, f_bound ) : +def typeahead_init_input( f_name, f_bound ) : """ The JS script to init the fields values """ init_key = f_bound.value() or '""' return ( @@ -293,7 +368,7 @@ def init_input( f_name, f_bound ) : hidden_id = hidden_id( f_bound ) ) -def reset_input( f_bound ) : +def typeahead_reset_input( f_bound ) : """ The JS script to reset the fields values """ return ( '$( "#{input_id}" ).typeahead("val", "");' @@ -303,6 +378,31 @@ def reset_input( f_bound ) : hidden_id = hidden_id( f_bound ) ) +def tokenfield_init_input( f_name, f_bound ) : + """ The JS script to init the fields values """ + init_key = f_bound.value() or '""' + return ( + '$( "#{input_id}" ).tokenfield("setTokens", {init_val});' + ).format( + input_id = input_id( f_bound ), + init_val = '""' if init_key == '""' else ( + 'engine_{f_name}.get( {init_key} ).map(' + 'function(o) {{ return o.value; }}' + ')').format( + f_name = f_name, + init_key = init_key + ), + init_key = init_key, + ) + +def tokenfield_reset_input( f_bound ) : + """ The JS script to reset the fields values """ + return ( + '$( "#{input_id}" ).tokenfield("setTokens", "");' + ).format( + input_id = input_id( f_bound ), + ) + def default_choices( f_value ) : """ The JS script creating the variable choices_ """ return '[{objects}]'.format( @@ -362,7 +462,7 @@ def default_match_func ( f_name ) : f_name = f_name ) -def typeahead_updater( f_bound ): +def typeahead_select( f_bound ): """ The JS script creating the function triggered when an item is selected through typeahead """ return ( @@ -391,3 +491,52 @@ def typeahead_change( f_bound ): hidden_id = hidden_id( f_bound ) ) +def tokenfield_create( f_name, f_bound ): + """ The JS script triggered when a new token is created in tokenfield. """ + return ( + 'function(evt) {{' + 'var data = evt.attrs.value;' + 'var i = 0;' + 'while ( i= 0) + this._delimiters[whitespace] = '\\s' + + if (dash >= 0) { + delete this._delimiters[dash] + this._delimiters.unshift('-') + } + + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] + $.each(this._delimiters, function (index, character) { + var pos = $.inArray(character, specialCharacters) + if (pos >= 0) _self._delimiters[index] = '\\' + character; + }); + + // Store original input width + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null + , elStyleWidth = element.style.width + , elCSSWidth + , elWidth = this.$element.width() + + if (elRules) { + $.each( elRules, function (i, rule) { + if (rule.style.width) { + elCSSWidth = rule.style.width; + } + }); + } + + // Move original input out of the way + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', + originalStyles = { position: this.$element.css('position') }; + originalStyles[hidingPosition] = this.$element.css(hidingPosition); + + this.$element + .data('original-styles', originalStyles) + .data('original-tabindex', this.$element.prop('tabindex')) + .css('position', 'absolute') + .css(hidingPosition, '-10000px') + .prop('tabindex', -1) + + // Create a wrapper + this.$wrapper = $('
') + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') + + // Create a new input + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) + this.$input = $('') + .appendTo( this.$wrapper ) + .prop( 'placeholder', this.$element.prop('placeholder') ) + .prop( 'id', id + '-tokenfield' ) + .prop( 'tabindex', this.$element.data('original-tabindex') ) + + // Re-route original input label to new input + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$input.prop('id') ) + } + + // Set up a copy helper to handle copy & paste + this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) + + // Set wrapper width + if (elStyleWidth) { + this.$wrapper.css('width', elStyleWidth); + } + else if (elCSSWidth) { + this.$wrapper.css('width', elCSSWidth); + } + // If input is inside inline-form with no width set, set fixed width + else if (this.$element.parents('.form-inline').length) { + this.$wrapper.width( elWidth ) + } + + // Set tokenfield disabled, if original or fieldset input is disabled + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { + this.disable(); + } + + // Set tokenfield readonly, if original input is readonly + if (this.$element.prop('readonly')) { + this.readonly(); + } + + // Set up mirror for input auto-sizing + this.$mirror = $(''); + this.$input.css('min-width', this.options.minWidth + 'px') + $.each([ + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'letterSpacing', + 'textTransform', + 'wordSpacing', + 'textIndent' + ], function (i, val) { + _self.$mirror[0].style[val] = _self.$input.css(val); + }); + this.$mirror.appendTo( 'body' ) + + // Insert tokenfield to HTML + this.$wrapper.insertBefore( this.$element ) + this.$element.prependTo( this.$wrapper ) + + // Calculate inner input width + this.update() + + // Create initial tokens, if any + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) + + // Start listening to events + this.listen() + + // Initialize autocomplete, if necessary + if ( ! $.isEmptyObject( this.options.autocomplete ) ) { + var side = this.textDirection === 'rtl' ? 'right' : 'left' + , autocompleteOptions = $.extend({ + minLength: this.options.showAutocompleteOnFocus ? 0 : null, + position: { my: side + " top", at: side + " bottom", of: this.$wrapper } + }, this.options.autocomplete ) + + this.$input.autocomplete( autocompleteOptions ) + } + + // Initialize typeahead, if necessary + if ( ! $.isEmptyObject( this.options.typeahead ) ) { + + var typeaheadOptions = this.options.typeahead + , defaults = { + minLength: this.options.showAutocompleteOnFocus ? 0 : null + } + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] + + args[0] = $.extend( {}, defaults, args[0] ) + + this.$input.typeahead.apply( this.$input, args ) + this.typeahead = true + } + } + + Tokenfield.prototype = { + + constructor: Tokenfield + + , createToken: function (attrs, triggerChange) { + var _self = this + + if (typeof attrs === 'string') { + attrs = { value: attrs, label: attrs } + } else { + // Copy objects to prevent contamination of data sources. + attrs = $.extend( {}, attrs ) + } + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + // Normalize label and value + attrs.value = $.trim(attrs.value.toString()); + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value + + // Bail out if has no value or label, or label is too short + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return + + // Bail out if maximum number of tokens is reached + if (this.options.limit && this.getTokens().length >= this.options.limit) return + + // Allow changing token data before creating it + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) + this.$element.trigger(createEvent) + + // Bail out if there if attributes are empty or event was defaultPrevented + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return + + var $token = $('
') + .append('') + .append('×') + .data('attrs', attrs) + + // Insert token into HTML + if (this.$input.hasClass('tt-input')) { + // If the input has typeahead enabled, insert token before it's parent + this.$input.parent().before( $token ) + } else { + this.$input.before( $token ) + } + + // Temporarily set input width to minimum + this.$input.css('width', this.options.minWidth + 'px') + + var $tokenLabel = $token.find('.token-label') + , $closeButton = $token.find('.close') + + // Determine maximum possible token label width + if (!this.maxTokenWidth) { + this.maxTokenWidth = + this.$wrapper.width() - $closeButton.outerWidth() - + parseInt($closeButton.css('margin-left'), 10) - + parseInt($closeButton.css('margin-right'), 10) - + parseInt($token.css('border-left-width'), 10) - + parseInt($token.css('border-right-width'), 10) - + parseInt($token.css('padding-left'), 10) - + parseInt($token.css('padding-right'), 10) + parseInt($tokenLabel.css('border-left-width'), 10) - + parseInt($tokenLabel.css('border-right-width'), 10) - + parseInt($tokenLabel.css('padding-left'), 10) - + parseInt($tokenLabel.css('padding-right'), 10) + parseInt($tokenLabel.css('margin-left'), 10) - + parseInt($tokenLabel.css('margin-right'), 10) + } + + $tokenLabel.css('max-width', this.maxTokenWidth) + if (this.options.html) + $tokenLabel.html(attrs.label) + else + $tokenLabel.text(attrs.label) + + // Listen to events on token + $token + .on('mousedown', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = true + }) + .on('click', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = false + + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + return _self.toggle( $token ) + } + + _self.activate( $token, e.shiftKey, e.shiftKey ) + }) + .on('dblclick', function (e) { + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false + _self.edit( $token ) + }) + + $closeButton + .on('click', $.proxy(this.remove, this)) + + // Trigger createdtoken event on the original field + // indicating that the token is now in the DOM + this.$element.trigger($.Event('tokenfield:createdtoken', { + attrs: attrs, + relatedTarget: $token.get(0) + })) + + // Trigger change event on the original field + if (triggerChange) { + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) + } + + // Update tokenfield dimensions + var _self = this + setTimeout(function () { + _self.update() + }, 0) + + // Return original element + return this.$element.get(0) + } + + , setTokens: function (tokens, add, triggerChange) { + if (!add) this.$wrapper.find('.token').remove() + + if (!tokens) return + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + if (typeof tokens === 'string') { + if (this._delimiters.length) { + // Split based on delimiters + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) + } else { + tokens = [tokens]; + } + } + + var _self = this + $.each(tokens, function (i, attrs) { + _self.createToken(attrs, triggerChange) + }) + + return this.$element.get(0) + } + + , getTokenData: function($token) { + var data = $token.map(function() { + var $token = $(this); + return $token.data('attrs') + }).get(); + + if (data.length == 1) { + data = data[0]; + } + + return data; + } + + , getTokens: function(active) { + var self = this + , tokens = [] + , activeClass = active ? '.active' : '' // get active tokens only + this.$wrapper.find( '.token' + activeClass ).each( function() { + tokens.push( self.getTokenData( $(this) ) ) + }) + return tokens + } + + , getTokensList: function(delimiter, beautify, active) { + delimiter = delimiter || this._firstDelimiter + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify + + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') + return $.map( this.getTokens(active), function (token) { + return token.value + }).join(separator) + } + + , getInput: function() { + return this.$input.val() + } + + , setInput: function (val) { + if (this.$input.hasClass('tt-input')) { + // Typeahead acts weird when simply setting input value to empty, + // so we set the query to empty instead + this.$input.typeahead('val', val) + } else { + this.$input.val(val) + } + } + + , listen: function () { + var _self = this + + this.$element + .on('change', $.proxy(this.change, this)) + + this.$wrapper + .on('mousedown',$.proxy(this.focusInput, this)) + + this.$input + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('paste', $.proxy(this.paste, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + this.$copyHelper + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keyup', $.proxy(this.keyup, this)) + + // Secondary listeners for input width calculation + this.$input + .on('keypress', $.proxy(this.update, this)) + .on('keyup', $.proxy(this.update, this)) + + this.$input + .on('autocompletecreate', function() { + // Set minimum autocomplete menu width + var $_menuElement = $(this).data('ui-autocomplete').menu.element + + var minWidth = _self.$wrapper.outerWidth() - + parseInt( $_menuElement.css('border-left-width'), 10 ) - + parseInt( $_menuElement.css('border-right-width'), 10 ) + + $_menuElement.css( 'min-width', minWidth + 'px' ) + }) + .on('autocompleteselect', function (e, ui) { + if (_self.createToken( ui.item )) { + _self.$input.val('') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + return false + }) + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { + // Create token + if (_self.createToken( datum )) { + _self.$input.typeahead('val', '') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + }) + + // Listen to window resize + $(window).on('resize', $.proxy(this.update, this )) + + } + + , keydown: function (e) { + + if (!this.focused) return + + var _self = this + + switch(e.keyCode) { + case 8: // backspace + if (!this.$input.is(document.activeElement)) break + this.lastInputValue = this.$input.val() + break + + case 37: // left arrow + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) + break + + case 38: // up arrow + upDown('prev') + break + + case 39: // right arrow + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) + break + + case 40: // down arrow + upDown('next') + break + + case 65: // a (to handle ctrl + a) + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break + this.activateAll() + e.preventDefault() + break + + case 9: // tab + case 13: // enter + + // We will handle creating tokens from autocomplete in autocomplete events + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break + + // We will handle creating tokens from typeahead in typeahead events + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break + + // Create token + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { + return this.createTokensFromInput(e, this.$input.data('edit')); + } + + // Edit token + if (e.keyCode === 13) { + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break + if (!_self.options.allowEditing) break + this.edit( this.$wrapper.find('.token.active') ) + } + } + + function leftRight(direction) { + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + direction += 'All' + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') + if (!$token.length) return + + _self.preventInputFocus = true + _self.preventDeactivation = true + + _self.activate( $token ) + e.preventDefault() + + } else { + _self[direction]( e.shiftKey ) + e.preventDefault() + } + } + + function upDown(direction) { + if (!e.shiftKey) return + + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') + if (!$token.length) return + + _self.activate( $token ) + } + + var opposite = direction === 'prev' ? 'next' : 'prev' + , position = direction === 'prev' ? 'first' : 'last' + + _self.$firstActiveToken[opposite + 'All']('.token').each(function() { + _self.deactivate( $(this) ) + }) + + _self.activate( _self.$wrapper.find('.token:' + position), true, true ) + e.preventDefault() + } + + this.lastKeyDown = e.keyCode + } + + , keypress: function(e) { + + // Comma + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { + if (this.$input.val()) { + this.createTokensFromInput(e) + } + return false; + } + } + + , keyup: function (e) { + this.preventInputFocus = false + + if (!this.focused) return + + switch(e.keyCode) { + case 8: // backspace + if (this.$input.is(document.activeElement)) { + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break + + this.preventDeactivation = true + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') + + if (!$prevToken.length) break + + this.activate( $prevToken ) + } else { + this.remove(e) + } + break + + case 46: // delete + this.remove(e, 'next') + break + } + this.lastKeyUp = e.keyCode + } + + , focus: function (e) { + this.focused = true + this.$wrapper.addClass('focus') + + if (this.$input.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + + if (this.options.showAutocompleteOnFocus) { + this.search() + } + } + } + + , blur: function (e) { + + this.focused = false + this.$wrapper.removeClass('focus') + + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + } + + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { + this.createTokensFromInput(e) + } + + this.preventDeactivation = false + this.preventCreateTokens = false + } + + , paste: function (e) { + var _self = this + + // Add tokens to existing ones + if (_self.options.allowPasting) { + setTimeout(function () { + _self.createTokensFromInput(e) + }, 1) + } + } + + , change: function (e) { + if ( e.initiator === 'tokenfield' ) return // Prevent loops + + this.setTokens( this.$element.val() ) + } + + , createTokensFromInput: function (e, focus) { + if (this.$input.val().length < this.options.minLength) + return // No input, simply return + + var tokensBefore = this.getTokensList() + this.setTokens( this.$input.val(), true ) + + if (tokensBefore == this.getTokensList() && this.$input.val().length) + return false // No tokens were added, do nothing (prevent form submit) + + this.setInput('') + + if (this.$input.data( 'edit' )) { + this.unedit(focus) + } + + return false // Prevent form being submitted + } + + , next: function (add) { + if (add) { + var $firstActiveToken = this.$wrapper.find('.active:first') + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $firstActiveToken ) + } + + var $lastActiveToken = this.$wrapper.find('.active:last') + , $nextToken = $lastActiveToken.nextAll('.token:first') + + if (!$nextToken.length) { + this.$input.focus() + return + } + + this.activate($nextToken, add) + } + + , prev: function (add) { + + if (add) { + var $lastActiveToken = this.$wrapper.find('.active:last') + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $lastActiveToken ) + } + + var $firstActiveToken = this.$wrapper.find('.active:first') + , $prevToken = $firstActiveToken.prevAll('.token:first') + + if (!$prevToken.length) { + $prevToken = this.$wrapper.find('.token:first') + } + + if (!$prevToken.length && !add) { + this.$input.focus() + return + } + + this.activate( $prevToken, add ) + } + + , activate: function ($token, add, multi, remember) { + + if (!$token) return + + if (typeof remember === 'undefined') var remember = true + + if (multi) var add = true + + this.$copyHelper.focus() + + if (!add) { + this.$wrapper.find('.active').removeClass('active') + if (remember) { + this.$firstActiveToken = $token + } else { + delete this.$firstActiveToken + } + } + + if (multi && this.$firstActiveToken) { + // Determine first active token and the current tokens indicies + // Account for the 1 hidden textarea by subtracting 1 from both + var i = this.$firstActiveToken.index() - 2 + , a = $token.index() - 2 + , _self = this + + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { + _self.activate( $(this), true ) + }) + } + + $token.addClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , activateAll: function() { + var _self = this + + this.$wrapper.find('.token').each( function (i) { + _self.activate($(this), i !== 0, false, false) + }) + } + + , deactivate: function($token) { + if (!$token) return + + $token.removeClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , toggle: function($token) { + if (!$token) return + + $token.toggleClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , edit: function ($token) { + if (!$token) return + + var attrs = $token.data('attrs') + + // Allow changing input value before editing + var options = { attrs: attrs, relatedTarget: $token.get(0) } + var editEvent = $.Event('tokenfield:edittoken', options) + this.$element.trigger( editEvent ) + + // Edit event can be cancelled if default is prevented + if (editEvent.isDefaultPrevented()) return + + $token.find('.token-label').text(attrs.value) + var tokenWidth = $token.outerWidth() + + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + + $token.replaceWith( $_input ) + + this.preventCreateTokens = true + + this.$input.val( attrs.value ) + .select() + .data( 'edit', true ) + .width( tokenWidth ) + + this.update(); + + // Indicate that token is now being edited, and is replaced with an input field in the DOM + this.$element.trigger($.Event('tokenfield:editedtoken', options )) + } + + , unedit: function (focus) { + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + $_input.appendTo( this.$wrapper ) + + this.$input.data('edit', false) + this.$mirror.text('') + + this.update() + + // Because moving the input element around in DOM + // will cause it to lose focus, we provide an option + // to re-focus the input after appending it to the wrapper + if (focus) { + var _self = this + setTimeout(function () { + _self.$input.focus() + }, 1) + } + } + + , remove: function (e, direction) { + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return + + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') + + if (e.type !== 'click') { + if (!direction) var direction = 'prev' + this[direction]() + + // Was it the first token? + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 + } + + // Prepare events and their options + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } + , removeEvent = $.Event('tokenfield:removetoken', options) + + this.$element.trigger(removeEvent); + + // Remove event can be intercepted and cancelled + if (removeEvent.isDefaultPrevented()) return + + var removedEvent = $.Event('tokenfield:removedtoken', options) + , changeEvent = $.Event('change', { initiator: 'tokenfield' }) + + // Remove token from DOM + $token.remove() + + // Trigger events + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) + + // Focus, when necessary: + // When there are no more tokens, or if this was the first token + // and it was removed with backspace or it was clicked on + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() + + // Adjust input width + this.$input.css('width', this.options.minWidth + 'px') + this.update() + + // Cancel original event handlers + e.preventDefault() + e.stopPropagation() + } + + /** + * Update tokenfield dimensions + */ + , update: function (e) { + var value = this.$input.val() + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) + , inputPadding = inputPaddingLeft + inputPaddingRight + + if (this.$input.data('edit')) { + + if (!value) { + value = this.$input.prop("placeholder") + } + if (value === this.$mirror.text()) return + + this.$mirror.text(value) + + var mirrorWidth = this.$mirror.width() + 10; + if ( mirrorWidth > this.$wrapper.width() ) { + return this.$input.width( this.$wrapper.width() ) + } + + this.$input.width( mirrorWidth ) + } + else { + //temporary reset width to minimal value to get proper results + this.$input.width(this.options.minWidth); + + var w = (this.textDirection === 'rtl') + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; + // + // some usecases pre-render widget before attaching to DOM, + // dimensions returned by jquery will be NaN -> we default to 100% + // so placeholder won't be cut off. + isNaN(w) ? this.$input.width('100%') : this.$input.width(w); + } + } + + , focusInput: function (e) { + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return + // Focus only after the current call stack has cleared, + // otherwise has no effect. + // Reason: mousedown is too early - input will lose focus + // after mousedown. However, since the input may be moved + // in DOM, there may be no click or mouseup event triggered. + var _self = this + setTimeout(function() { + _self.$input.focus() + }, 0) + } + + , search: function () { + if ( this.$input.data('ui-autocomplete') ) { + this.$input.autocomplete('search') + } + } + + , disable: function () { + this.setProperty('disabled', true); + } + + , enable: function () { + this.setProperty('disabled', false); + } + + , readonly: function () { + this.setProperty('readonly', true); + } + + , writeable: function () { + this.setProperty('readonly', false); + } + + , setProperty: function(property, value) { + this['_' + property] = value; + this.$input.prop(property, value); + this.$element.prop(property, value); + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); + } + + , destroy: function() { + // Set field value + this.$element.val( this.getTokensList() ); + // Restore styles and properties + this.$element.css( this.$element.data('original-styles') ); + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); + + // Re-route tokenfield label to original input + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$element.prop('id') ) + } + + // Move original element outside of tokenfield wrapper + this.$element.insertBefore( this.$wrapper ); + + // Remove tokenfield-related data + this.$element.removeData('original-styles') + .removeData('original-tabindex') + .removeData('bs.tokenfield'); + + // Remove tokenfield from DOM + this.$wrapper.remove(); + this.$mirror.remove(); + + var $_element = this.$element; + + return $_element; + } + + } + + + /* TOKENFIELD PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.tokenfield + + $.fn.tokenfield = function (option, param) { + var value + , args = [] + + Array.prototype.push.apply( args, arguments ); + + var elements = this.each(function () { + var $this = $(this) + , data = $this.data('bs.tokenfield') + , options = typeof option == 'object' && option + + if (typeof option === 'string' && data && data[option]) { + args.shift() + value = data[option].apply(data, args) + } else { + if (!data && typeof option !== 'string' && !param) { + $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) + $this.trigger('tokenfield:initialize') + } + } + }) + + return typeof value !== 'undefined' ? value : elements; + } + + $.fn.tokenfield.defaults = { + minWidth: 60, + minLength: 0, + html: true, + allowEditing: true, + allowPasting: true, + limit: 0, + autocomplete: {}, + typeahead: {}, + showAutocompleteOnFocus: false, + createTokensOnBlur: false, + delimiter: ',', + beautify: true, + inputType: 'text' + } + + $.fn.tokenfield.Constructor = Tokenfield + + + /* TOKENFIELD NO CONFLICT + * ================== */ + + $.fn.tokenfield.noConflict = function () { + $.fn.tokenfield = old + return this + } + + return Tokenfield; + +})); diff --git a/templates/base.html b/templates/base.html index 42505b23..dfaca5eb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -33,12 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Load CSS and JavaScript #} {% bootstrap_css %} + + {% comment %}{% endcomment %} {% bootstrap_javascript %} + + {% comment %}{% endcomment %} {{ site_name }} : {% block title %}Accueil{% endblock %} From aae6c8f0182220636567e2c80c1894eaa3d682e0 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 20:18:12 +0200 Subject: [PATCH 040/138] Doc et grosse review pep8 --- users/forms.py | 215 +++++++++++++--- users/models.py | 650 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 665 insertions(+), 200 deletions(-) diff --git a/users/forms.py b/users/forms.py index a8b1a219..fd81b426 100644 --- a/users/forms.py +++ b/users/forms.py @@ -20,8 +20,16 @@ # 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. +""" +Definition des forms pour l'application users. -# -*- coding: utf-8 -*- +Modification, creation de : + - un user (informations personnelles) + - un bannissement + - le mot de passe d'un user + - une whiteliste + - un user de service +""" from __future__ import unicode_literals @@ -29,17 +37,34 @@ from django import forms from django.forms import ModelForm, Form from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.core.validators import MinLengthValidator -from preferences.models import OptionalUser from django.utils import timezone -from .models import User, ServiceUser, Right, School, ListRight, Whitelist, Ban, Request, remove_user_room -from .models import get_admin_right +from preferences.models import OptionalUser +from .models import User, ServiceUser, Right, School, ListRight, Whitelist +from .models import Ban, remove_user_room + +NOW = timezone.now() + class PassForm(forms.Form): - passwd1 = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput) - passwd2 = forms.CharField(label=u'Saisir à nouveau le mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput) + """Formulaire de changement de mot de passe. Verifie que les 2 + nouveaux mots de passe renseignés sont identiques et respectent + une norme""" + passwd1 = forms.CharField( + label=u'Nouveau mot de passe', + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput + ) + passwd2 = forms.CharField( + label=u'Saisir à nouveau le mot de passe', + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput + ) def clean_passwd2(self): + """Verifie que passwd1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("passwd1") password2 = self.cleaned_data.get("passwd2") @@ -47,11 +72,26 @@ class PassForm(forms.Form): raise forms.ValidationError("Passwords don't match") return password2 + class UserCreationForm(forms.ModelForm): """A form for creating new users. Includes all the required - fields, plus a repeated password.""" - password1 = forms.CharField(label='Password', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255) - password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255) + fields, plus a repeated password. + + Formulaire pour la création d'un user. N'est utilisé que pour + l'admin, lors de la creation d'un user par admin. Inclu tous les + champs obligatoires""" + password1 = forms.CharField( + label='Password', + widget=forms.PasswordInput, + validators=[MinLengthValidator(8)], + max_length=255 + ) + password2 = forms.CharField( + label='Password confirmation', + widget=forms.PasswordInput, + validators=[MinLengthValidator(8)], + max_length=255 + ) is_admin = forms.BooleanField(label='is admin') def __init__(self, *args, **kwargs): @@ -63,6 +103,7 @@ class UserCreationForm(forms.ModelForm): fields = ('pseudo', 'name', 'surname', 'email') def clean_password2(self): + """Verifie que password1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") @@ -78,21 +119,40 @@ class UserCreationForm(forms.ModelForm): user.is_admin = self.cleaned_data.get("is_admin") return user + class ServiceUserCreationForm(forms.ModelForm): """A form for creating new users. Includes all the required - fields, plus a repeated password.""" - password1 = forms.CharField(label='Password', widget=forms.PasswordInput, min_length=8, max_length=255) - password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, min_length=8, max_length=255) + fields, plus a repeated password. + + Formulaire pour la creation de nouveaux serviceusers. + Requiert seulement un mot de passe; et un pseudo""" + password1 = forms.CharField( + label='Password', + widget=forms.PasswordInput, + min_length=8, + max_length=255 + ) + password2 = forms.CharField( + label='Password confirmation', + widget=forms.PasswordInput, + min_length=8, + max_length=255 + ) def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(ServiceUserCreationForm, self).__init__(*args, prefix=prefix, **kwargs) + super(ServiceUserCreationForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) class Meta: model = ServiceUser fields = ('pseudo',) def clean_password2(self): + """Verifie que password1 et 2 sont indentiques""" # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") @@ -107,10 +167,13 @@ class ServiceUserCreationForm(forms.ModelForm): user.save() return user + class UserChangeForm(forms.ModelForm): """A form for updating users. Includes all the fields on the user, but replaces the password field with admin's password hash display field. + + Formulaire pour la modification d'un user coté admin """ password = ReadOnlyPasswordHashField() is_admin = forms.BooleanField(label='is admin', required=False) @@ -126,6 +189,7 @@ class UserChangeForm(forms.ModelForm): self.initial['is_admin'] = kwargs['instance'].is_admin def clean_password(self): + """Dummy fun""" # Regardless of what the user provides, return the initial value. # This is done here, rather than on the field, because the # field does not have access to the initial value @@ -139,42 +203,59 @@ class UserChangeForm(forms.ModelForm): user.save() return user + class ServiceUserChangeForm(forms.ModelForm): """A form for updating users. Includes all the fields on the user, but replaces the password field with admin's password hash display field. + + Formulaire pour l'edition des service users coté admin """ password = ReadOnlyPasswordHashField() def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(ServiceUserChangeForm, self).__init__(*args, prefix=prefix, **kwargs) + super(ServiceUserChangeForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) class Meta: model = ServiceUser fields = ('pseudo',) def clean_password(self): - # Regardless of what the user provides, return the initial value. - # This is done here, rather than on the field, because the - # field does not have access to the initial value + """Dummy fun""" return self.initial["password"] + class ResetPasswordForm(forms.Form): + """Formulaire de demande de reinitialisation de mot de passe, + mdp oublié""" pseudo = forms.CharField(label=u'Pseudo', max_length=255) email = forms.EmailField(max_length=255) + class MassArchiveForm(forms.Form): + """Formulaire d'archivage des users inactif. Prend en argument + du formulaire la date de depart avant laquelle archiver les + users""" date = forms.DateTimeField(help_text='%d/%m/%y') def clean(self): - cleaned_data=super(MassArchiveForm, self).clean() + cleaned_data = super(MassArchiveForm, self).clean() date = cleaned_data.get("date") if date: - if date>timezone.now(): - raise forms.ValidationError("Impossible d'archiver des utilisateurs dont la fin d'accès se situe dans le futur !") + if date > NOW: + raise forms.ValidationError("Impossible d'archiver des\ + utilisateurs dont la fin d'accès se situe dans le futur !") + class BaseInfoForm(ModelForm): + """Formulaire de base d'edition d'un user. Formulaire de base, utilisé + pour l'edition de self par self ou un cableur. On formate les champs + avec des label plus jolis""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(BaseInfoForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -200,13 +281,21 @@ class BaseInfoForm(ModelForm): ] def clean_telephone(self): + """Verifie que le tel est présent si 'option est validée + dans preferences""" telephone = self.cleaned_data['telephone'] - preferences, created = OptionalUser.objects.get_or_create() + preferences, _created = OptionalUser.objects.get_or_create() if not telephone and preferences.is_tel_mandatory: - raise forms.ValidationError("Un numéro de téléphone valide est requis") + raise forms.ValidationError( + "Un numéro de téléphone valide est requis" + ) return telephone + class EditInfoForm(BaseInfoForm): + """Edition complète d'un user. Utilisé par admin, + permet d'editer normalement la chambre, ou le shell + Herite de la base""" class Meta(BaseInfoForm.Meta): fields = [ 'name', @@ -220,22 +309,33 @@ class EditInfoForm(BaseInfoForm): 'telephone', ] + class InfoForm(EditInfoForm): - """ Utile pour forcer un déménagement quand il y a déjà un user en place""" - force = forms.BooleanField(label="Forcer le déménagement ?", initial=False, required=False) + """ Utile pour forcer un déménagement quand il y a déjà un user en place + Formuaire utilisé pour la creation initiale""" + force = forms.BooleanField( + label="Forcer le déménagement ?", + 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""" if self.cleaned_data.get('force', False): remove_user_room(self.cleaned_data.get('room')) return + class UserForm(InfoForm): """ Model form general""" class Meta(InfoForm.Meta): fields = '__all__' + class PasswordForm(ModelForm): - """ Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement""" + """ Formulaire de changement brut de mot de passe. + Ne pas utiliser sans traitement""" class Meta: model = User fields = ['password', 'pwd_ntlm'] @@ -244,21 +344,32 @@ class PasswordForm(ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs) + class ServiceUserForm(ModelForm): """ Modification d'un service user""" - password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False) + password = forms.CharField( + label=u'Nouveau mot de passe', + max_length=255, + validators=[MinLengthValidator(8)], + widget=forms.PasswordInput, + required=False + ) class Meta: model = ServiceUser - fields = ('pseudo','access_group') + fields = ('pseudo', 'access_group') def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) + class EditServiceUserForm(ServiceUserForm): + """Formulaire d'edition de base d'un service user. Ne permet + d'editer que son group d'acl et son commentaire""" class Meta(ServiceUserForm.Meta): - fields = ['access_group','comment'] + fields = ['access_group', 'comment'] + class StateForm(ModelForm): """ Changement de l'état d'un user""" @@ -272,6 +383,7 @@ class StateForm(ModelForm): class SchoolForm(ModelForm): + """Edition, creation d'un école""" class Meta: model = School fields = ['name'] @@ -281,7 +393,10 @@ class SchoolForm(ModelForm): super(SchoolForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['name'].label = 'Établissement' + class ListRightForm(ModelForm): + """Edition, d'un groupe , équivalent à un droit + Ne peremet pas d'editer le gid, car il sert de primary key""" class Meta: model = ListRight fields = ['listright', 'details'] @@ -291,21 +406,38 @@ class ListRightForm(ModelForm): super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['listright'].label = 'Nom du droit/groupe' + class NewListRightForm(ListRightForm): + """Ajout d'un groupe/list de droit """ class Meta(ListRightForm.Meta): fields = '__all__' def __init__(self, *args, **kwargs): super(NewListRightForm, self).__init__(*args, **kwargs) - self.fields['gid'].label = 'Gid, attention, cet attribut ne doit pas être modifié après création' + self.fields['gid'].label = 'Gid, attention, cet attribut ne doit\ + pas être modifié après création' + class DelListRightForm(Form): - listrights = forms.ModelMultipleChoiceField(queryset=ListRight.objects.all(), label="Droits actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'un ou plusieurs groupes""" + listrights = forms.ModelMultipleChoiceField( + queryset=ListRight.objects.all(), + label="Droits actuels", + widget=forms.CheckboxSelectMultiple + ) + class DelSchoolForm(Form): - schools = forms.ModelMultipleChoiceField(queryset=School.objects.all(), label="Etablissements actuels", widget=forms.CheckboxSelectMultiple) + """Suppression d'une ou plusieurs écoles""" + schools = forms.ModelMultipleChoiceField( + queryset=School.objects.all(), + label="Etablissements actuels", + widget=forms.CheckboxSelectMultiple + ) + class RightForm(ModelForm): + """Assignation d'un droit à un user""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(RightForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -318,13 +450,19 @@ class RightForm(ModelForm): class DelRightForm(Form): - rights = forms.ModelMultipleChoiceField(queryset=Right.objects.all(), widget=forms.CheckboxSelectMultiple) + """Suppression d'un droit d'un user""" + rights = forms.ModelMultipleChoiceField( + queryset=Right.objects.all(), + widget=forms.CheckboxSelectMultiple + ) def __init__(self, right, *args, **kwargs): super(DelRightForm, self).__init__(*args, **kwargs) self.fields['rights'].queryset = Right.objects.filter(right=right) + class BanForm(ModelForm): + """Creation, edition d'un objet bannissement""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(BanForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -335,13 +473,16 @@ class BanForm(ModelForm): exclude = ['user'] def clean_date_end(self): + """Verification que date_end est après now""" date_end = self.cleaned_data['date_end'] - if date_end < timezone.now(): - raise forms.ValidationError("Triple buse, la date de fin ne peut pas être avant maintenant... Re2o ne voyage pas dans le temps") + if date_end < NOW: + raise forms.ValidationError("Triple buse, la date de fin ne peut\ + pas être avant maintenant... Re2o ne voyage pas dans le temps") return date_end class WhitelistForm(ModelForm): + """Creation, edition d'un objet whitelist""" def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -352,7 +493,9 @@ class WhitelistForm(ModelForm): exclude = ['user'] def clean_date_end(self): + """Verification que la date_end est posterieur à now""" date_end = self.cleaned_data['date_end'] - if date_end < timezone.now(): - raise forms.ValidationError("Triple buse, la date de fin ne peut pas être avant maintenant... Re2o ne voyage pas dans le temps") + if date_end < NOW: + raise forms.ValidationError("Triple buse, la date de fin ne peut pas\ + être avant maintenant... Re2o ne voyage pas dans le temps") return date_end diff --git a/users/models.py b/users/models.py index 78b76156..a0ef29f8 100644 --- a/users/models.py +++ b/users/models.py @@ -1,7 +1,7 @@ # -*- 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. +# 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 @@ -20,44 +20,66 @@ # 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. +""" +Models de l'application users. + +On défini ici des models django classiques: +- users, qui hérite de l'abstract base user de django. Permet de définit +un utilisateur du site (login, passwd, chambre, adresse, etc) +- les whiteslist +- les bannissements +- les établissements d'enseignement (school) +- les droits (right et listright) +- les utilisateurs de service (pour connexion automatique) + +On défini aussi des models qui héritent de django-ldapdb : +- ldapuser +- ldapgroup +- ldapserviceuser + +Ces utilisateurs ldap sont synchronisés à partir des objets +models sql classiques. Seuls certains champs essentiels sont +dupliqués. +""" + from __future__ import unicode_literals +import re +import uuid +import datetime + from django.db import models from django.db.models import Q from django import forms from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.utils.functional import cached_property -from django.template import Context, RequestContext, loader +from django.template import Context, loader from django.core.mail import send_mail from django.core.urlresolvers import reverse +from django.db import transaction +from django.utils import timezone +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.core.validators import RegexValidator from reversion import revisions as reversion -from django.db import transaction import ldapdb.models import ldapdb.models.fields -from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES,UID_RANGES -import re, uuid -import datetime +from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES, UID_RANGES from re2o.login import hashNT -from django.utils import timezone -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager - -from django.core.validators import MinLengthValidator -from django.core.validators import RegexValidator -from topologie.models import Room from cotisations.models import Cotisation, Facture, Paiement, Vente -from machines.models import Domain, Interface, MachineType, Machine, Nas, MachineType, Extension, regen -from preferences.models import GeneralOption, AssoOption, OptionalUser, OptionalMachine, MailMessageOption +from machines.models import Domain, Interface, Machine, regen +from preferences.models import GeneralOption, AssoOption, OptionalUser +from preferences.models import OptionalMachine, MailMessageOption -now = timezone.now() +DT_NOW = timezone.now() -#### Utilitaires généraux +# Utilitaires généraux def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ @@ -76,33 +98,42 @@ def linux_user_check(login): def linux_user_validator(login): - """ Retourne une erreur de validation si le login ne respecte + """ Retourne une erreur de validation si le login ne respecte pas les contraintes unix (maj, min, chiffres ou tiret)""" if not linux_user_check(login): raise forms.ValidationError( - ", ce pseudo ('%(label)s') contient des carractères interdits", - params={'label': login}, + ", ce pseudo ('%(label)s') contient des carractères interdits", + params={'label': login}, ) + def get_fresh_user_uid(): """ Renvoie le plus petit uid non pris. Fonction très paresseuse """ - uids = list(range(int(min(UID_RANGES['users'])),int(max(UID_RANGES['users'])))) + uids = list(range( + int(min(UID_RANGES['users'])), + int(max(UID_RANGES['users'])) + )) try: used_uids = list(User.objects.values_list('uid_number', flat=True)) except: used_uids = [] - free_uids = [ id for id in uids if id not in used_uids] + free_uids = [id for id in uids if id not in used_uids] return min(free_uids) + def get_fresh_gid(): """ Renvoie le plus petit gid libre """ - gids = list(range(int(min(GID_RANGES['posix'])),int(max(GID_RANGES['posix'])))) + gids = list(range( + int(min(GID_RANGES['posix'])), + int(max(GID_RANGES['posix'])) + )) used_gids = list(ListRight.objects.values_list('gid', flat=True)) - free_gids = [ id for id in gids if id not in used_gids] + free_gids = [id for id in gids if id not in used_gids] return min(free_gids) + def get_admin_right(): - """ Renvoie l'instance droit admin. La crée si elle n'existe pas + """ Renvoie l'instance droit admin. La crée si elle n'existe pas Lui attribue un gid libre""" try: admin_right = ListRight.objects.get(listright="admin") @@ -112,25 +143,74 @@ def get_admin_right(): admin_right.save() return admin_right -def all_adherent(search_time=now): - """ Fonction renvoyant tous les users adherents. Optimisee pour n'est qu'une seule requete sql - Inspecte les factures de l'user et ses cotisation, regarde si elles sont posterieur à now (end_time)""" - return User.objects.filter(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))).distinct() -def all_baned(search_time=now): +def all_adherent(search_time=DT_NOW): + """ Fonction renvoyant tous les users adherents. Optimisee pour n'est + qu'une seule requete sql + Inspecte les factures de l'user et ses cotisation, regarde si elles + sont posterieur à now (end_time)""" + return User.objects.filter( + facture__in=Facture.objects.filter( + vente__in=Vente.objects.filter( + cotisation__in=Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.all().exclude(valid=False) + ) + ).filter(date_end__gt=search_time) + ) + ) + ).distinct() + + +def all_baned(search_time=DT_NOW): """ Fonction renvoyant tous les users bannis """ - return User.objects.filter(ban__in=Ban.objects.filter(date_end__gt=search_time)).distinct() + return User.objects.filter( + ban__in=Ban.objects.filter( + date_end__gt=search_time + ) + ).distinct() -def all_whitelisted(search_time=now): + +def all_whitelisted(search_time=DT_NOW): """ Fonction renvoyant tous les users whitelistes """ - return User.objects.filter(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)).distinct() + return User.objects.filter( + whitelist__in=Whitelist.objects.filter( + date_end__gt=search_time + ) + ).distinct() + + +def all_has_access(search_time=DT_NOW): + """ Renvoie tous les users beneficiant d'une connexion + : user adherent ou whiteliste et non banni """ + 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(facture__in=Facture.objects.filter( + vente__in=Vente.objects.filter( + cotisation__in=Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.all() + .exclude(valid=False) + ) + ).filter(date_end__gt=search_time) + ) + ))) + ).distinct() -def all_has_access(search_time=now): - """ Renvoie tous les users beneficiant d'une connexion : user adherent ou whiteliste et non banni """ - return User.objects.filter(Q(state=User.STATE_ACTIVE) & ~Q(ban__in=Ban.objects.filter(date_end__gt=timezone.now())) & (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=timezone.now())) | Q(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))))).distinct() class UserManager(BaseUserManager): - def _create_user(self, pseudo, name, surname, email, password=None, su=False): + """User manager basique de django""" + def _create_user( + self, + pseudo, + name, + surname, + email, + password=None, + su=False + ): if not pseudo: raise ValueError('Users must have an username') @@ -174,28 +254,53 @@ class User(AbstractBaseUser): STATE_DISABLED = 1 STATE_ARCHIVE = 2 STATES = ( - (0, 'STATE_ACTIVE'), - (1, 'STATE_DISABLED'), - (2, 'STATE_ARCHIVE'), - ) + (0, 'STATE_ACTIVE'), + (1, 'STATE_DISABLED'), + (2, 'STATE_ARCHIVE'), + ) def auto_uid(): + """Renvoie un uid libre""" return get_fresh_user_uid() name = models.CharField(max_length=255) surname = models.CharField(max_length=255) - pseudo = models.CharField(max_length=32, unique=True, help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", validators=[linux_user_validator]) + pseudo = models.CharField( + max_length=32, + unique=True, + help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", + validators=[linux_user_validator] + ) email = models.EmailField() - school = models.ForeignKey('School', on_delete=models.PROTECT, null=True, blank=True) - shell = models.ForeignKey('ListShell', on_delete=models.PROTECT, null=True, blank=True) - comment = models.CharField(help_text="Commentaire, promo", max_length=255, blank=True) - room = models.OneToOneField('topologie.Room', on_delete=models.PROTECT, blank=True, null=True) + school = models.ForeignKey( + 'School', + on_delete=models.PROTECT, + null=True, + blank=True + ) + shell = models.ForeignKey( + 'ListShell', + on_delete=models.PROTECT, + null=True, + blank=True + ) + comment = models.CharField( + help_text="Commentaire, promo", + max_length=255, + blank=True + ) + room = models.OneToOneField( + 'topologie.Room', + on_delete=models.PROTECT, + blank=True, + null=True + ) pwd_ntlm = models.CharField(max_length=255) state = models.IntegerField(choices=STATES, default=STATE_ACTIVE) registered = models.DateTimeField(auto_now_add=True) telephone = models.CharField(max_length=15, blank=True, null=True) uid_number = models.IntegerField(default=auto_uid, unique=True) - rezo_rez_uid = models.IntegerField(unique=True, blank=True, null=True) + rezo_rez_uid = models.IntegerField(unique=True, blank=True, null=True) USERNAME_FIELD = 'pseudo' REQUIRED_FIELDS = ['name', 'surname', 'email'] @@ -223,7 +328,8 @@ class User(AbstractBaseUser): @is_admin.setter def is_admin(self, value): - """ Change la valeur de admin à true ou false suivant la valeur de value""" + """ Change la valeur de admin à true ou false suivant la valeur de + value""" if value and not self.is_admin: self.make_admin() elif not value and self.is_admin: @@ -247,7 +353,7 @@ class User(AbstractBaseUser): for right in RIGHTS_LINK[perm]: query = query | Q(right__listright=right) if Right.objects.filter(Q(user=self) & query): - return True + return True try: Right.objects.get(user=self, right__listright=perm) except Right.DoesNotExist: @@ -255,17 +361,20 @@ class User(AbstractBaseUser): return True def has_perm(self, perm, obj=None): + """Ne sert à rien""" return True - def has_right(self, right): - """ Renvoie si un user a un right donné. Crée le right si il n'existe pas""" + """ Renvoie si un user a un right donné. Crée le right si il n'existe + pas""" try: list_right = ListRight.objects.get(listright=right) except: list_right = ListRight(listright=right, gid=get_fresh_gid()) list_right.save() - return Right.objects.filter(user=self).filter(right=list_right).exists() + return Right.objects.filter(user=self).filter( + right=list_right + ).exists() @cached_property def is_bureau(self): @@ -279,9 +388,10 @@ class User(AbstractBaseUser): @cached_property def is_cableur(self): - """ True si l'user a les droits cableur + """ True si l'user a les droits cableur (également true si bureau, infra ou bofh)""" - return self.has_right('cableur') or self.has_right('bureau') or self.has_right('infra') or self.has_right('bofh') + return self.has_right('cableur') or self.has_right('bureau') or\ + self.has_right('infra') or self.has_right('bofh') @cached_property def is_trez(self): @@ -296,15 +406,22 @@ class User(AbstractBaseUser): def end_adhesion(self): """ Renvoie la date de fin d'adhésion d'un user. Examine les objets cotisation""" - date_max = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self).exclude(valid=False))).aggregate(models.Max('date_end'))['date_end__max'] + date_max = Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self + ).exclude(valid=False) + ) + ).aggregate(models.Max('date_end'))['date_end__max'] return date_max def is_adherent(self): - """ Renvoie True si l'user est adhérent : si self.end_adhesion()>now""" + """ Renvoie True si l'user est adhérent : si + self.end_adhesion()>now""" end = self.end_adhesion() if not end: return False - elif end < timezone.now(): + elif end < DT_NOW: return False else: return True @@ -312,13 +429,17 @@ class User(AbstractBaseUser): @cached_property def end_ban(self): """ Renvoie la date de fin de ban d'un user, False sinon """ - date_max = Ban.objects.filter(user=self).aggregate(models.Max('date_end'))['date_end__max'] + date_max = Ban.objects.filter( + user=self + ).aggregate(models.Max('date_end'))['date_end__max'] return date_max @cached_property def end_whitelist(self): """ Renvoie la date de fin de whitelist d'un user, False sinon """ - date_max = Whitelist.objects.filter(user=self).aggregate(models.Max('date_end'))['date_end__max'] + date_max = Whitelist.objects.filter( + user=self + ).aggregate(models.Max('date_end'))['date_end__max'] return date_max @cached_property @@ -327,7 +448,7 @@ class User(AbstractBaseUser): end = self.end_ban if not end: return False - elif end < timezone.now(): + elif end < DT_NOW: return False else: return True @@ -338,14 +459,14 @@ class User(AbstractBaseUser): end = self.end_whitelist if not end: return False - elif end < timezone.now(): + elif end < DT_NOW: return False else: return True def has_access(self): """ Renvoie si un utilisateur a accès à internet """ - return self.state == User.STATE_ACTIVE \ + return self.state == User.STATE_ACTIVE\ and not self.is_ban and (self.is_adherent() or self.is_whitelisted) def end_access(self): @@ -358,27 +479,50 @@ class User(AbstractBaseUser): else: if not self.end_whitelist: return self.end_adhesion() - else: + else: return max(self.end_adhesion(), self.end_whitelist) @cached_property def solde(self): - """ Renvoie le solde d'un user. Vérifie que l'option solde est activé, retourne 0 sinon. + """ Renvoie le solde d'un user. Vérifie que l'option solde est + activé, retourne 0 sinon. Somme les crédits de solde et retire les débit payés par solde""" - options, created = OptionalUser.objects.get_or_create() + options, _created = OptionalUser.objects.get_or_create() user_solde = options.user_solde if user_solde: - solde_object, created=Paiement.objects.get_or_create(moyen='Solde') - somme_debit = Vente.objects.filter(facture__in=Facture.objects.filter(user=self, paiement=solde_object)).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] or 0 - somme_credit =Vente.objects.filter(facture__in=Facture.objects.filter(user=self), name="solde").aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] or 0 + solde_object, _created = Paiement.objects.get_or_create( + moyen='Solde' + ) + somme_debit = Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self, + paiement=solde_object + ) + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + somme_credit = Vente.objects.filter( + facture__in=Facture.objects.filter(user=self), + name="solde" + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 return somme_credit - somme_debit else: return 0 def user_interfaces(self, active=True): - """ Renvoie toutes les interfaces dont les machines appartiennent à self - Par defaut ne prend que les interfaces actives""" - return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active)).select_related('domain__extension') + """ Renvoie toutes les interfaces dont les machines appartiennent à + self. Par defaut ne prend que les interfaces actives""" + return Interface.objects.filter( + machine__in=Machine.objects.filter(user=self, active=active) + ).select_related('domain__extension') def assign_ips(self): """ Assign une ipv4 aux machines d'un user """ @@ -400,17 +544,19 @@ class User(AbstractBaseUser): interface.save() def archive(self): - """ Archive l'user : appelle unassign_ips() puis passe state à ARCHIVE""" + """ Archive l'user : appelle unassign_ips() puis passe state à + ARCHIVE""" self.unassign_ips() - self.state = User.STATE_ARCHIVE + self.state = User.STATE_ARCHIVE def unarchive(self): - """ Désarchive l'user : réassigne ses ip et le passe en state ACTIVE""" + """ Désarchive l'user : réassigne ses ip et le passe en state + ACTIVE""" self.assign_ips() self.state = User.STATE_ACTIVE def has_module_perms(self, app_label): - # Simplest version again + """True, a toutes les permissions de module""" return True def make_admin(self): @@ -419,16 +565,20 @@ class User(AbstractBaseUser): user_admin_right.save() def un_admin(self): + """Supprime les droits admin d'un user""" try: - user_right = Right.objects.get(user=self,right=get_admin_right()) + user_right = Right.objects.get(user=self, right=get_admin_right()) except Right.DoesNotExist: return user_right.delete() def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True): - """ Synchronisation du ldap. Synchronise dans le ldap les attributs de self - Options : base : synchronise tous les attributs de base - nom, prenom, mail, password, shell, home - access_refresh : synchronise le dialup_access notant si l'user a accès aux services + """ Synchronisation du ldap. Synchronise dans le ldap les attributs de + self + Options : base : synchronise tous les attributs de base - nom, prenom, + mail, password, shell, home + access_refresh : synchronise le dialup_access notant si l'user a accès + aux services mac_refresh : synchronise les machines de l'user""" self.refresh_from_db() try: @@ -441,7 +591,8 @@ class User(AbstractBaseUser): user_ldap.dialupAccess = str(self.has_access()) user_ldap.home_directory = '/home/' + self.pseudo user_ldap.mail = self.email - user_ldap.given_name = self.surname.lower() + '_' + self.name.lower()[:3] + user_ldap.given_name = self.surname.lower() + '_'\ + + self.name.lower()[:3] user_ldap.gid = LDAP['user_gid'] user_ldap.user_password = self.password[:6] + self.password[7:] user_ldap.sambat_nt_password = self.pwd_ntlm.upper() @@ -454,7 +605,10 @@ class User(AbstractBaseUser): if access_refresh: user_ldap.dialupAccess = str(self.has_access()) if mac_refresh: - user_ldap.macs = [inter.mac_bare() for inter in Interface.objects.filter(machine__in=Machine.objects.filter(user=self))] + user_ldap.macs = [inter.mac_bare() for inter in + Interface.objects.filter( + machine__in=Machine.objects.filter(user=self) + )] user_ldap.save() def ldap_del(self): @@ -467,53 +621,69 @@ class User(AbstractBaseUser): def notif_inscription(self): """ Prend en argument un objet user, envoie un mail de bienvenue """ - t = loader.get_template('users/email_welcome') - assooptions, created = AssoOption.objects.get_or_create() - mailmessageoptions, created = MailMessageOption.objects.get_or_create() - general_options, created = GeneralOption.objects.get_or_create() - c = Context({ + template = loader.get_template('users/email_welcome') + assooptions, _created = AssoOption.objects.get_or_create() + mailmessageoptions, _created = MailMessageOption\ + .objects.get_or_create() + general_options, _created = GeneralOption.objects.get_or_create() + context = Context({ 'nom': str(self.name) + ' ' + str(self.surname), 'asso_name': assooptions.name, 'asso_email': assooptions.contact, - 'welcome_mail_fr' : mailmessageoptions.welcome_mail_fr, - 'welcome_mail_en' : mailmessageoptions.welcome_mail_en, - 'pseudo':self.pseudo, + 'welcome_mail_fr': mailmessageoptions.welcome_mail_fr, + 'welcome_mail_en': mailmessageoptions.welcome_mail_en, + 'pseudo': self.pseudo, }) - send_mail('Bienvenue au %(name)s / Welcome to %(name)s' % {'name': assooptions.name }, '', - general_options.email_from, [self.email], html_message=t.render(c)) + send_mail( + 'Bienvenue au %(name)s / Welcome to %(name)s' % { + 'name': assooptions.name + }, + '', + general_options.email_from, + [self.email], + html_message=template.render(context) + ) return def reset_passwd_mail(self, request): - """ Prend en argument un request, envoie un mail de réinitialisation de mot de pass """ + """ Prend en argument un request, envoie un mail de + réinitialisation de mot de pass """ req = Request() req.type = Request.PASSWD req.user = self req.save() - t = loader.get_template('users/email_passwd_request') - options, created = AssoOption.objects.get_or_create() - general_options, created = GeneralOption.objects.get_or_create() - c = { + template = loader.get_template('users/email_passwd_request') + options, _created = AssoOption.objects.get_or_create() + general_options, _created = GeneralOption.objects.get_or_create() + context = { 'name': str(req.user.name) + ' ' + str(req.user.surname), 'asso': options.name, 'asso_mail': options.contact, 'site_name': general_options.site_name, 'url': request.build_absolute_uri( - reverse('users:process', kwargs={'token': req.token})), + reverse('users:process', kwargs={'token': req.token})), 'expire_in': str(general_options.req_expire_hrs) + ' heures', } - send_mail('Changement de mot de passe du %(name)s / Password renewal for %(name)s' % {'name': options.name }, t.render(c), - general_options.email_from, [req.user.email], fail_silently=False) + send_mail( + 'Changement de mot de passe du %(name)s / Password\ + renewal for %(name)s' % {'name': options.name}, + template.render(context), + general_options.email_from, + [req.user.email], + fail_silently=False + ) return 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""" + """ 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) - options, created = OptionalMachine.objects.get_or_create() + options, _created = OptionalMachine.objects.get_or_create() if all_interfaces.count() > options.max_lambdauser_interfaces: return False, "Maximum de machines enregistrees atteinte" if not nas_type: - return False, "Re2o ne sait pas à quel machinetype affecter cette machine" + return False, "Re2o ne sait pas à quel machinetype affecter cette\ + machine" machine_type_cible = nas_type.machine_type try: machine_parent = Machine() @@ -533,12 +703,12 @@ class User(AbstractBaseUser): domain.interface_parent = interface_cible domain.clean() domain.save() - except Exception as e: - return False, e + except Exception as error: + return False, error return True, "Ok" def set_user_password(self, password): - """ A utiliser de préférence, set le password en hash courrant et + """ A utiliser de préférence, set le password en hash courrant et dans la version ntlm""" self.set_password(password) self.pwd_ntlm = hashNT(password) @@ -547,23 +717,28 @@ class User(AbstractBaseUser): def get_next_domain_name(self): """Look for an available name for a new interface for this user by trying "pseudo0", "pseudo1", "pseudo2", ... + + Recherche un nom disponible, pour une machine. Doit-être + unique, concatène le nom, le pseudo et le numero de machine """ def simple_pseudo(): + """Renvoie le pseudo sans underscore (compat dns)""" return self.pseudo.replace('_', '-').lower() - def composed_pseudo( n ): - return simple_pseudo() + str(n) + def composed_pseudo(name): + """Renvoie le resultat de simplepseudo et rajoute le nom""" + return simple_pseudo() + str(name) num = 0 - while Domain.objects.filter(name=composed_pseudo(num)) : + while Domain.objects.filter(name=composed_pseudo(num)): num += 1 return composed_pseudo(num) - def __str__(self): return self.pseudo + @receiver(post_save, sender=User) def user_post_save(sender, **kwargs): """ Synchronisation post_save : envoie le mail de bienvenue si creation @@ -575,29 +750,44 @@ def user_post_save(sender, **kwargs): user.ldap_sync(base=True, access_refresh=True, mac_refresh=False) regen('mailing') + @receiver(post_delete, sender=User) def user_post_delete(sender, **kwargs): + """Post delete d'un user, on supprime son instance ldap""" user = kwargs['instance'] user.ldap_del() regen('mailing') + class ServiceUser(AbstractBaseUser): """ Classe des users daemons, règle leurs accès au ldap""" readonly = 'readonly' ACCESS = ( - ('auth', 'auth'), - ('readonly', 'readonly'), - ('usermgmt', 'usermgmt'), - ) + ('auth', 'auth'), + ('readonly', 'readonly'), + ('usermgmt', 'usermgmt'), + ) PRETTY_NAME = "Utilisateurs de service" - pseudo = models.CharField(max_length=32, unique=True, help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", validators=[linux_user_validator]) - access_group = models.CharField(choices=ACCESS, default=readonly, max_length=32) - comment = models.CharField(help_text="Commentaire", max_length=255, blank=True) + pseudo = models.CharField( + max_length=32, + unique=True, + help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", + validators=[linux_user_validator] + ) + access_group = models.CharField( + choices=ACCESS, + default=readonly, + max_length=32 + ) + comment = models.CharField( + help_text="Commentaire", + max_length=255, + blank=True + ) USERNAME_FIELD = 'pseudo' - objects = UserManager() def ldap_sync(self): @@ -611,6 +801,7 @@ class ServiceUser(AbstractBaseUser): self.serviceuser_group_sync() def ldap_del(self): + """Suppression de l'instance ldap d'un service user""" try: user_ldap = LdapServiceUser.objects.get(name=self.pseudo) user_ldap.delete() @@ -619,30 +810,38 @@ class ServiceUser(AbstractBaseUser): self.serviceuser_group_sync() def serviceuser_group_sync(self): + """Synchronise le groupe et les droits de groupe dans le ldap""" try: group = LdapServiceUserGroup.objects.get(name=self.access_group) except: group = LdapServiceUserGroup(name=self.access_group) - group.members = list(LdapServiceUser.objects.filter(name__in=[user.pseudo for user in ServiceUser.objects.filter(access_group=self.access_group)]).values_list('dn', flat=True)) + group.members = list(LdapServiceUser.objects.filter( + name__in=[user.pseudo for user in ServiceUser.objects.filter( + access_group=self.access_group + )]).values_list('dn', flat=True)) group.save() def __str__(self): return self.pseudo + @receiver(post_save, sender=ServiceUser) def service_user_post_save(sender, **kwargs): """ Synchronise un service user ldap après modification django""" service_user = kwargs['instance'] service_user.ldap_sync() + @receiver(post_delete, sender=ServiceUser) def service_user_post_delete(sender, **kwargs): """ Supprime un service user ldap après suppression django""" service_user = kwargs['instance'] service_user.ldap_del() + class Right(models.Model): - """ Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un manytomany + """ Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un + manytomany Ceci dit le résultat aurait été le même avec une table intermediaire""" PRETTY_NAME = "Droits affectés à des users" @@ -655,18 +854,21 @@ class Right(models.Model): def __str__(self): return str(self.user) + @receiver(post_save, sender=Right) def right_post_save(sender, **kwargs): """ Synchronise les users ldap groups avec les groupes de droits""" right = kwargs['instance'].right right.ldap_sync() + @receiver(post_delete, sender=Right) def right_post_delete(sender, **kwargs): """ Supprime l'user du groupe""" right = kwargs['instance'].right right.ldap_sync() + class School(models.Model): """ Etablissement d'enseignement""" PRETTY_NAME = "Etablissements enregistrés" @@ -678,46 +880,69 @@ class School(models.Model): class ListRight(models.Model): - """ Ensemble des droits existants. Chaque droit crée un groupe ldap synchronisé, avec gid. + """ Ensemble des droits existants. Chaque droit crée un groupe + ldap synchronisé, avec gid. Permet de gérer facilement les accès serveurs et autres - La clef de recherche est le gid, pour cette raison là il n'est plus modifiable après creation""" + La clef de recherche est le gid, pour cette raison là + il n'est plus modifiable après creation""" PRETTY_NAME = "Liste des droits existants" - listright = models.CharField(max_length=255, unique=True, validators=[RegexValidator('^[a-z]+$', message="Les groupes unix ne peuvent contenir que des lettres minuscules")]) + listright = models.CharField( + max_length=255, + unique=True, + validators=[RegexValidator( + '^[a-z]+$', + message="Les groupes unix ne peuvent contenir\ + que des lettres minuscules" + )] + ) gid = models.IntegerField(unique=True, null=True) - details = models.CharField(help_text="Description", max_length=255, blank=True) + details = models.CharField( + help_text="Description", + max_length=255, + blank=True + ) def __str__(self): return self.listright def ldap_sync(self): + """Sychronise les groups ldap avec le model listright coté django""" try: group_ldap = LdapUserGroup.objects.get(gid=self.gid) except LdapUserGroup.DoesNotExist: group_ldap = LdapUserGroup(gid=self.gid) group_ldap.name = self.listright - group_ldap.members = [right.user.pseudo for right in Right.objects.filter(right=self)] + group_ldap.members = [right.user.pseudo for right + in Right.objects.filter(right=self)] group_ldap.save() def ldap_del(self): + """Supprime un groupe ldap""" try: group_ldap = LdapUserGroup.objects.get(gid=self.gid) group_ldap.delete() except LdapUserGroup.DoesNotExist: pass + @receiver(post_save, sender=ListRight) def listright_post_save(sender, **kwargs): """ Synchronise le droit ldap quand il est modifié""" right = kwargs['instance'] right.ldap_sync() + @receiver(post_delete, sender=ListRight) def listright_post_delete(sender, **kwargs): + """Suppression d'un groupe ldap après suppression coté django""" right = kwargs['instance'] right.ldap_del() + class ListShell(models.Model): + """Un shell possible. Pas de check si ce shell existe, les + admin sont des grands""" PRETTY_NAME = "Liste des shells disponibles" shell = models.CharField(max_length=255, unique=True) @@ -725,6 +950,7 @@ class ListShell(models.Model): def __str__(self): return self.shell + class Ban(models.Model): """ Bannissement. Actuellement a un effet tout ou rien. Gagnerait à être granulaire""" @@ -734,38 +960,45 @@ class Ban(models.Model): STATE_SOFT = 1 STATE_BRIDAGE = 2 STATES = ( - (0, 'HARD (aucun accès)'), - (1, 'SOFT (accès local seulement)'), - (2, 'BRIDAGE (bridage du débit)'), - ) + (0, 'HARD (aucun accès)'), + (1, 'SOFT (accès local seulement)'), + (2, 'BRIDAGE (bridage du débit)'), + ) user = models.ForeignKey('User', on_delete=models.PROTECT) raison = models.CharField(max_length=255) date_start = models.DateTimeField(auto_now_add=True) date_end = models.DateTimeField(help_text='%d/%m/%y %H:%M:%S') - state = models.IntegerField(choices=STATES, default=STATE_HARD) + state = models.IntegerField(choices=STATES, default=STATE_HARD) def notif_ban(self): """ Prend en argument un objet ban, envoie un mail de notification """ - general_options, created = GeneralOption.objects.get_or_create() - t = loader.get_template('users/email_ban_notif') - options, created = AssoOption.objects.get_or_create() - c = Context({ + general_options, _created = GeneralOption.objects.get_or_create() + template = loader.get_template('users/email_ban_notif') + options, _created = AssoOption.objects.get_or_create() + context = Context({ 'name': str(self.user.name) + ' ' + str(self.user.surname), 'raison': self.raison, 'date_end': self.date_end, - 'asso_name' : options.name, + 'asso_name': options.name, }) - send_mail('Deconnexion disciplinaire', t.render(c), - general_options.email_from, [self.user.email], fail_silently=False) + send_mail( + 'Deconnexion disciplinaire', + template.render(context), + general_options.email_from, + [self.user.email], + fail_silently=False + ) return def is_active(self): - return self.date_end > now + """Ce ban est-il actif?""" + return self.date_end > DT_NOW def __str__(self): return str(self.user) + ' ' + str(self.raison) + @receiver(post_save, sender=Ban) def ban_post_save(sender, **kwargs): """ Regeneration de tous les services après modification d'un ban""" @@ -782,6 +1015,7 @@ def ban_post_save(sender, **kwargs): regen('dhcp') regen('mac_ip_list') + @receiver(post_delete, sender=Ban) def ban_post_delete(sender, **kwargs): """ Regen de tous les services après suppression d'un ban""" @@ -791,7 +1025,11 @@ def ban_post_delete(sender, **kwargs): regen('dhcp') regen('mac_ip_list') + class Whitelist(models.Model): + """Accès à titre gracieux. L'utilisateur ne paye pas; se voit + accorder un accès internet pour une durée défini. Moins + fort qu'un ban quel qu'il soit""" PRETTY_NAME = "Liste des accès gracieux" user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -800,13 +1038,16 @@ class Whitelist(models.Model): date_end = models.DateTimeField(help_text='%d/%m/%y %H:%M:%S') def is_active(self): - return self.date_end > now + return self.date_end > DT_NOW def __str__(self): return str(self.user) + ' ' + str(self.raison) + @receiver(post_save, sender=Whitelist) def whitelist_post_save(sender, **kwargs): + """Après modification d'une whitelist, on synchronise les services + et on lui permet d'avoir internet""" whitelist = kwargs['instance'] user = whitelist.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -819,17 +1060,21 @@ def whitelist_post_save(sender, **kwargs): regen('dhcp') regen('mac_ip_list') + @receiver(post_delete, sender=Whitelist) def whitelist_post_delete(sender, **kwargs): + """Après suppression d'une whitelist, on supprime l'accès internet + en forçant la régénration""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) regen('mailing') regen('dhcp') regen('mac_ip_list') + class Request(models.Model): """ Objet request, générant une url unique de validation. - Utilisé par exemple pour la generation du mot de passe et + Utilisé par exemple pour la generation du mot de passe et sa réinitialisation""" PASSWD = 'PW' EMAIL = 'EM' @@ -845,38 +1090,86 @@ class Request(models.Model): def save(self): if not self.expires_at: - options, created = GeneralOption.objects.get_or_create() - self.expires_at = timezone.now() \ + options, _created = GeneralOption.objects.get_or_create() + self.expires_at = DT_NOW \ + datetime.timedelta(hours=options.req_expire_hrs) if not self.token: self.token = str(uuid.uuid4()).replace('-', '') # remove hyphens super(Request, self).save() + class LdapUser(ldapdb.models.Model): """ Class for representing an LDAP user entry. """ # LDAP meta-data base_dn = LDAP['base_user_dn'] - object_classes = ['inetOrgPerson','top','posixAccount','sambaSamAccount','radiusprofile', 'shadowAccount'] + object_classes = ['inetOrgPerson', 'top', 'posixAccount', + 'sambaSamAccount', 'radiusprofile', + 'shadowAccount'] # attributes gid = ldapdb.models.fields.IntegerField(db_column='gidNumber') - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) uid = ldapdb.models.fields.CharField(db_column='uid', max_length=200) - uidNumber = ldapdb.models.fields.IntegerField(db_column='uidNumber', unique=True) + uidNumber = ldapdb.models.fields.IntegerField( + db_column='uidNumber', + unique=True + ) sn = ldapdb.models.fields.CharField(db_column='sn', max_length=200) - login_shell = ldapdb.models.fields.CharField(db_column='loginShell', max_length=200, blank=True, null=True) - mail = ldapdb.models.fields.CharField(db_column='mail', max_length=200) - given_name = ldapdb.models.fields.CharField(db_column='givenName', max_length=200) - home_directory = ldapdb.models.fields.CharField(db_column='homeDirectory', max_length=200) - display_name = ldapdb.models.fields.CharField(db_column='displayName', max_length=200, blank=True, null=True) + login_shell = ldapdb.models.fields.CharField( + db_column='loginShell', + max_length=200, + blank=True, + null=True + ) + mail = ldapdb.models.fields.CharField(db_column='mail', max_length=200) + given_name = ldapdb.models.fields.CharField( + db_column='givenName', + max_length=200 + ) + home_directory = ldapdb.models.fields.CharField( + db_column='homeDirectory', + max_length=200 + ) + display_name = ldapdb.models.fields.CharField( + db_column='displayName', + max_length=200, + blank=True, + null=True + ) dialupAccess = ldapdb.models.fields.CharField(db_column='dialupAccess') - sambaSID = ldapdb.models.fields.IntegerField(db_column='sambaSID', unique=True) - user_password = ldapdb.models.fields.CharField(db_column='userPassword', max_length=200, blank=True, null=True) - sambat_nt_password = ldapdb.models.fields.CharField(db_column='sambaNTPassword', max_length=200, blank=True, null=True) - macs = ldapdb.models.fields.ListField(db_column='radiusCallingStationId', max_length=200, blank=True, null=True) - shadowexpire = ldapdb.models.fields.CharField(db_column='shadowExpire', blank=True, null=True) + sambaSID = ldapdb.models.fields.IntegerField( + db_column='sambaSID', + unique=True + ) + user_password = ldapdb.models.fields.CharField( + db_column='userPassword', + max_length=200, + blank=True, + null=True + ) + sambat_nt_password = ldapdb.models.fields.CharField( + db_column='sambaNTPassword', + max_length=200, + blank=True, + null=True + ) + macs = ldapdb.models.fields.ListField( + db_column='radiusCallingStationId', + max_length=200, + blank=True, + null=True + ) + shadowexpire = ldapdb.models.fields.CharField( + db_column='shadowExpire', + blank=True, + null=True + ) def __str__(self): return self.name @@ -890,9 +1183,12 @@ class LdapUser(ldapdb.models.Model): self.sambaSID = self.uidNumber super(LdapUser, self).save(*args, **kwargs) + class LdapUserGroup(ldapdb.models.Model): """ - Class for representing an LDAP user entry. + Class for representing an LDAP group entry. + + Un groupe ldap """ # LDAP meta-data base_dn = LDAP['base_usergroup_dn'] @@ -901,38 +1197,64 @@ class LdapUserGroup(ldapdb.models.Model): # attributes gid = ldapdb.models.fields.IntegerField(db_column='gidNumber') members = ldapdb.models.fields.ListField(db_column='memberUid', blank=True) - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) def __str__(self): return self.name + class LdapServiceUser(ldapdb.models.Model): """ Class for representing an LDAP userservice entry. + + Un user de service coté ldap """ # LDAP meta-data base_dn = LDAP['base_userservice_dn'] - object_classes = ['applicationProcess','simpleSecurityObject'] + object_classes = ['applicationProcess', 'simpleSecurityObject'] # attributes - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) - user_password = ldapdb.models.fields.CharField(db_column='userPassword', max_length=200, blank=True, null=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) + user_password = ldapdb.models.fields.CharField( + db_column='userPassword', + max_length=200, + blank=True, + null=True + ) def __str__(self): return self.name + class LdapServiceUserGroup(ldapdb.models.Model): """ Class for representing an LDAP userservice entry. + + Un group user de service coté ldap. Dans userservicegroupdn + (voir dans settings_local.py) """ # LDAP meta-data base_dn = LDAP['base_userservicegroup_dn'] object_classes = ['groupOfNames'] # attributes - name = ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True) - members = ldapdb.models.fields.ListField(db_column='member', blank=True) + name = ldapdb.models.fields.CharField( + db_column='cn', + max_length=200, + primary_key=True + ) + members = ldapdb.models.fields.ListField( + db_column='member', + blank=True + ) def __str__(self): return self.name - From 102521397f93aa0ae0398d9c8312deab3d9dcb13 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 22:10:07 +0200 Subject: [PATCH 041/138] Corrige les vues (docstring et autres) --- users/views.py | 377 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 255 insertions(+), 122 deletions(-) diff --git a/users/views.py b/users/views.py index aa72517b..e553f5b8 100644 --- a/users/views.py +++ b/users/views.py @@ -23,20 +23,25 @@ # App de gestion des users pour re2o # Goulven Kermarec, Gabriel Détraz, Lemesle Augustin # Gplv2 +""" +Module des views. + +On définit les vues pour l'ajout, l'edition des users : infos personnelles, +mot de passe, etc + +Permet aussi l'ajout, edition et suppression des droits, des bannissements, +des whitelist, des services users et des écoles +""" from __future__ import unicode_literals from django.shortcuts import get_object_or_404, render, redirect -from django.template.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import Max, ProtectedError +from django.db.models import ProtectedError from django.db import IntegrityError -from django.core.mail import send_mail from django.utils import timezone -from django.core.urlresolvers import reverse from django.db import transaction from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt @@ -47,22 +52,21 @@ from rest_framework.renderers import JSONRenderer from reversion.models import Version from reversion import revisions as reversion from users.serializers import MailSerializer -from users.models import User, Right, Ban, Whitelist, School, ListRight, Request, ServiceUser, all_has_access -from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm, DelListRightForm, NewListRightForm -from users.forms import EditInfoForm, InfoForm, BaseInfoForm, StateForm, RightForm, SchoolForm, EditServiceUserForm, ServiceUserForm, ListRightForm -from cotisations.models import Facture -from machines.models import Machine, Interface +from users.models import User, Right, Ban, Whitelist, School, ListRight +from users.models import Request, ServiceUser, all_has_access +from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm +from users.forms import DelListRightForm, NewListRightForm +from users.forms import InfoForm, BaseInfoForm, StateForm +from users.forms import RightForm, SchoolForm, EditServiceUserForm +from users.forms import ServiceUserForm, ListRightForm from users.forms import MassArchiveForm, PassForm, ResetPasswordForm -from preferences.models import OptionalUser, AssoOption, GeneralOption +from cotisations.models import Facture +from machines.models import Machine +from preferences.models import OptionalUser, GeneralOption -from re2o.login import hashNT +from re2o.views import form -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) - def password_change_action(u_form, user, request, req=False): """ Fonction qui effectue le changeemnt de mdp bdd""" user.set_user_password(u_form.cleaned_data['passwd1']) @@ -75,10 +79,12 @@ def password_change_action(u_form, user, request, req=False): return redirect("/") return redirect("/users/profil/" + str(user.id)) + @login_required @permission_required('cableur') def new_user(request): - """ Vue de création d'un nouvel utilisateur, envoie un mail pour le mot de passe""" + """ Vue de création d'un nouvel utilisateur, + envoie un mail pour le mot de passe""" user = InfoForm(request.POST or None) if user.is_valid(): user = user.save(commit=False) @@ -87,21 +93,25 @@ def new_user(request): reversion.set_user(request.user) reversion.set_comment("Création") user.reset_passwd_mail(request) - messages.success(request, "L'utilisateur %s a été crée, un mail pour l'initialisation du mot de passe a été envoyé" % user.pseudo) + messages.success(request, "L'utilisateur %s a été crée, un mail\ + pour l'initialisation du mot de passe a été envoyé" % user.pseudo) return redirect("/users/profil/" + str(user.id)) return form({'userform': user}, 'users/user.html', request) + @login_required def edit_info(request, userid): - """ Edite un utilisateur à partir de son id, - si l'id est différent de request.user, vérifie la possession du droit cableur """ + """ Edite un utilisateur à partir de son id, + si l'id est différent de request.user, vérifie la + possession du droit cableur """ try: user = User.objects.get(pk=userid) except User.DoesNotExist: messages.error(request, "Utilisateur inexistant") return redirect("/users/") if not request.user.has_perms(('cableur',)) and user != request.user: - messages.error(request, "Vous ne pouvez pas modifier un autre user que vous sans droit cableur") + messages.error(request, "Vous ne pouvez pas modifier un autre\ + user que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) if not request.user.has_perms(('cableur',)): user = BaseInfoForm(request.POST or None, instance=user) @@ -111,15 +121,19 @@ def edit_info(request, userid): with transaction.atomic(), reversion.create_revision(): user.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in user.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in user.changed_data + )) messages.success(request, "L'user a bien été modifié") return redirect("/users/profil/" + userid) return form({'userform': user}, 'users/user.html', request) + @login_required @permission_required('bureau') def state(request, userid): - """ Changer l'etat actif/desactivé/archivé d'un user, need droit bureau """ + """ Changer l'etat actif/desactivé/archivé d'un user, + need droit bureau """ try: user = User.objects.get(pk=userid) except User.DoesNotExist: @@ -135,12 +149,15 @@ def state(request, userid): elif state.cleaned_data['state'] == User.STATE_DISABLED: user.state = User.STATE_DISABLED reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in state.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in state.changed_data + )) user.save() messages.success(request, "Etat changé avec succès") return redirect("/users/profil/" + userid) return form({'userform': state}, 'users/user.html', request) + @login_required def password(request, userid): """ Reinitialisation d'un mot de passe à partir de l'userid, @@ -152,16 +169,20 @@ def password(request, userid): messages.error(request, "Utilisateur inexistant") return redirect("/users/") if not request.user.has_perms(('cableur',)) and user != request.user: - messages.error(request, "Vous ne pouvez pas modifier un autre user que vous sans droit cableur") + messages.error(request, "Vous ne pouvez pas modifier un\ + autre user que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) - if not request.user.has_perms(('bureau',)) and user != request.user and Right.objects.filter(user=user): - messages.error(request, "Il faut les droits bureau pour modifier le mot de passe d'un membre actif") + if not request.user.has_perms(('bureau',)) and user != request.user\ + and Right.objects.filter(user=user): + messages.error(request, "Il faut les droits bureau pour modifier le\ + mot de passe d'un membre actif") return redirect("/users/profil/" + str(request.user.id)) u_form = PassForm(request.POST or None) if u_form.is_valid(): return password_change_action(u_form, user, request) return form({'userform': u_form}, 'users/user.html', request) + @login_required @permission_required('infra') def new_serviceuser(request): @@ -174,15 +195,20 @@ def new_serviceuser(request): user_object.save() reversion.set_user(request.user) reversion.set_comment("Création") - messages.success(request, "L'utilisateur %s a été crée" % user_object.pseudo) + messages.success( + request, + "L'utilisateur %s a été crée" % user_object.pseudo + ) return redirect("/users/index_serviceusers/") return form({'userform': user}, 'users/user.html', request) + @login_required @permission_required('infra') def edit_serviceuser(request, userid): - """ Edite un utilisateur à partir de son id, - si l'id est différent de request.user, vérifie la possession du droit cableur """ + """ Edite un utilisateur à partir de son id, + si l'id est différent de request.user, + vérifie la possession du droit cableur """ try: user = ServiceUser.objects.get(pk=userid) except ServiceUser.DoesNotExist: @@ -196,18 +222,22 @@ def edit_serviceuser(request, userid): user_object.set_password(user.cleaned_data['password']) user_object.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in user.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in user.changed_data + )) messages.success(request, "L'user a bien été modifié") return redirect("/users/index_serviceusers") return form({'userform': user}, 'users/user.html', request) + @login_required @permission_required('infra') def del_serviceuser(request, userid): + """Suppression d'un ou plusieurs serviceusers""" try: user = ServiceUser.objects.get(pk=userid) except ServiceUser.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) + messages.error(request, u"Utilisateur inexistant") return redirect("/users/") if request.method == "POST": with transaction.atomic(), reversion.create_revision(): @@ -215,7 +245,12 @@ def del_serviceuser(request, userid): reversion.set_user(request.user) messages.success(request, "L'user a été détruite") return redirect("/users/index_serviceusers/") - return form({'objet': user, 'objet_name': 'serviceuser'}, 'users/delete.html', request) + return form( + {'objet': user, 'objet_name': 'serviceuser'}, + 'users/delete.html', + request + ) + @login_required @permission_required('bureau') @@ -241,28 +276,33 @@ def add_right(request, userid): return redirect("/users/profil/" + userid) return form({'userform': right}, 'users/user.html', request) + @login_required @permission_required('bureau') def del_right(request): """ Supprimer un droit à un user, need droit bureau """ user_right_list = dict() for right in ListRight.objects.all(): - user_right_list[right]= DelRightForm(right, request.POST or None) - for keys, right_item in user_right_list.items(): + user_right_list[right] = DelRightForm(right, request.POST or None) + for _keys, right_item in user_right_list.items(): if right_item.is_valid(): right_del = right_item.cleaned_data['rights'] with transaction.atomic(), reversion.create_revision(): reversion.set_user(request.user) - reversion.set_comment("Retrait des droit %s" % ','.join(str(deleted_right) for deleted_right in right_del)) + reversion.set_comment("Retrait des droit %s" % ','.join( + str(deleted_right) for deleted_right in right_del + )) right_del.delete() messages.success(request, "Droit retiré avec succès") return redirect("/users/") return form({'userform': user_right_list}, 'users/del_right.html', request) + @login_required @permission_required('bofh') def add_ban(request, userid): - """ Ajouter un banissement, nécessite au moins le droit bofh (a fortiori bureau) + """ Ajouter un banissement, nécessite au moins le droit bofh + (a fortiori bureau) Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" try: user = User.objects.get(pk=userid) @@ -273,7 +313,7 @@ def add_ban(request, userid): ban = BanForm(request.POST or None, instance=ban_instance) if ban.is_valid(): with transaction.atomic(), reversion.create_revision(): - ban_object = ban.save() + _ban_object = ban.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Bannissement ajouté") @@ -285,10 +325,12 @@ def add_ban(request, userid): ) return form({'userform': ban}, 'users/user.html', request) + @login_required @permission_required('bofh') def edit_ban(request, banid): - """ Editer un bannissement, nécessite au moins le droit bofh (a fortiori bureau) + """ Editer un bannissement, nécessite au moins le droit bofh + (a fortiori bureau) Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" try: ban_instance = Ban.objects.get(pk=banid) @@ -300,23 +342,31 @@ def edit_ban(request, banid): with transaction.atomic(), reversion.create_revision(): ban.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ban.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in ban.changed_data + )) messages.success(request, "Bannissement modifié") return redirect("/users/") return form({'userform': ban}, 'users/user.html', request) + @login_required @permission_required('cableur') def add_whitelist(request, userid): - """ Accorder un accès gracieux, temporaire ou permanent. Need droit cableur - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, raison obligatoire""" + """ Accorder un accès gracieux, temporaire ou permanent. + Need droit cableur + Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, + raison obligatoire""" try: user = User.objects.get(pk=userid) except User.DoesNotExist: messages.error(request, "Utilisateur inexistant") return redirect("/users/") whitelist_instance = Whitelist(user=user) - whitelist = WhitelistForm(request.POST or None, instance=whitelist_instance) + whitelist = WhitelistForm( + request.POST or None, + instance=whitelist_instance + ) if whitelist.is_valid(): with transaction.atomic(), reversion.create_revision(): whitelist.save() @@ -331,30 +381,40 @@ def add_whitelist(request, userid): ) return form({'userform': whitelist}, 'users/user.html', request) + @login_required @permission_required('cableur') def edit_whitelist(request, whitelistid): - """ Editer un accès gracieux, temporaire ou permanent. Need droit cableur - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, raison obligatoire""" + """ Editer un accès gracieux, temporaire ou permanent. + Need droit cableur + Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, + raison obligatoire""" try: whitelist_instance = Whitelist.objects.get(pk=whitelistid) except Whitelist.DoesNotExist: messages.error(request, "Entrée inexistante") return redirect("/users/") - whitelist = WhitelistForm(request.POST or None, instance=whitelist_instance) + whitelist = WhitelistForm( + request.POST or None, + instance=whitelist_instance + ) if whitelist.is_valid(): with transaction.atomic(), reversion.create_revision(): whitelist.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in whitelist.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in whitelist.changed_data + )) messages.success(request, "Whitelist modifiée") return redirect("/users/") return form({'userform': whitelist}, 'users/user.html', request) + @login_required @permission_required('cableur') def add_school(request): - """ Ajouter un établissement d'enseignement à la base de donnée, need cableur""" + """ Ajouter un établissement d'enseignement à la base de donnée, + need cableur""" school = SchoolForm(request.POST or None) if school.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -365,30 +425,37 @@ def add_school(request): return redirect("/users/index_school/") return form({'userform': school}, 'users/user.html', request) + @login_required @permission_required('cableur') def edit_school(request, schoolid): - """ Editer un établissement d'enseignement à partir du schoolid dans la base de donnée, need cableur""" + """ Editer un établissement d'enseignement à partir du schoolid dans + la base de donnée, need cableur""" try: school_instance = School.objects.get(pk=schoolid) except School.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/users/") school = SchoolForm(request.POST or None, instance=school_instance) if school.is_valid(): with transaction.atomic(), reversion.create_revision(): school.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in school.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in school.changed_data + )) messages.success(request, "Établissement modifié") return redirect("/users/index_school/") return form({'userform': school}, 'users/user.html', request) + @login_required @permission_required('cableur') def del_school(request): - """ Supprimer un établissement d'enseignement à la base de donnée, need cableur - Objet protégé, possible seulement si aucun user n'est affecté à l'établissement """ + """ Supprimer un établissement d'enseignement à la base de donnée, + need cableur + Objet protégé, possible seulement si aucun user n'est affecté à + l'établissement """ school = DelSchoolForm(request.POST or None) if school.is_valid(): school_dels = school.cleaned_data['schools'] @@ -406,6 +473,7 @@ def del_school(request): return redirect("/users/index_school/") return form({'userform': school}, 'users/user.html', request) + @login_required @permission_required('bureau') def add_listright(request): @@ -421,29 +489,38 @@ def add_listright(request): return redirect("/users/index_listright/") return form({'userform': listright}, 'users/user.html', request) + @login_required @permission_required('bureau') def edit_listright(request, listrightid): - """ Editer un groupe/droit, necessite droit bureau, à partir du listright id """ + """ Editer un groupe/droit, necessite droit bureau, + à partir du listright id """ try: listright_instance = ListRight.objects.get(pk=listrightid) except ListRight.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) + messages.error(request, u"Entrée inexistante") return redirect("/users/") - listright = ListRightForm(request.POST or None, instance=listright_instance) + listright = ListRightForm( + request.POST or None, + instance=listright_instance + ) if listright.is_valid(): with transaction.atomic(), reversion.create_revision(): listright.save() reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in listright.changed_data)) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join( + field for field in listright.changed_data + )) messages.success(request, "Droit modifié") return redirect("/users/index_listright/") return form({'userform': listright}, 'users/user.html', request) + @login_required @permission_required('bureau') def del_listright(request): - """ Supprimer un ou plusieurs groupe, possible si il est vide, need droit bureau """ + """ Supprimer un ou plusieurs groupe, possible si il est vide, need droit + bureau """ listright = DelListRightForm(request.POST or None) if listright.is_valid(): listright_dels = listright.cleaned_data['listrights'] @@ -461,6 +538,7 @@ def del_listright(request): return redirect("/users/index_listright/") return form({'userform': listright}, 'users/user.html', request) + @login_required @permission_required('bureau') def mass_archive(request): @@ -469,7 +547,10 @@ def mass_archive(request): to_archive_list = [] if to_archive_date.is_valid(): date = to_archive_date.cleaned_data['date'] - to_archive_list = [user for user in User.objects.exclude(state=User.STATE_ARCHIVE) if not user.end_access() or user.end_access() < date] + to_archive_list = [user for user in + User.objects.exclude(state=User.STATE_ARCHIVE) + if not user.end_access() + or user.end_access() < date] if "valider" in request.POST: for user in to_archive_list: with transaction.atomic(), reversion.create_revision(): @@ -477,15 +558,22 @@ def mass_archive(request): user.save() reversion.set_user(request.user) reversion.set_comment("Archivage") - messages.success(request, "%s users ont été archivés" % len(to_archive_list)) - return redirect("/users/") - return form({'userform': to_archive_date, 'to_archive_list': to_archive_list}, 'users/mass_archive.html', request) + messages.success(request, "%s users ont été archivés" % len( + to_archive_list + )) + return redirect("/users/") + return form( + {'userform': to_archive_date, 'to_archive_list': to_archive_list}, + 'users/mass_archive.html', + request + ) + @login_required @permission_required('cableur') def index(request): """ Affiche l'ensemble des users, need droit cableur """ - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number users_list = User.objects.select_related('room').order_by('state', 'name') paginator = Paginator(users_list, pagination_number) @@ -500,13 +588,15 @@ def index(request): users_list = paginator.page(paginator.num_pages) return render(request, 'users/index.html', {'users_list': users_list}) + @login_required @permission_required('cableur') def index_ban(request): """ Affiche l'ensemble des ban, need droit cableur """ - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - ban_list = Ban.objects.order_by('date_start').select_related('user').reverse() + ban_list = Ban.objects.order_by('date_start')\ + .select_related('user').reverse() paginator = Paginator(ban_list, pagination_number) page = request.GET.get('page') try: @@ -515,17 +605,19 @@ def index_ban(request): # If page isn't an integer, deliver first page ban_list = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - ban_list = paginator.page(paginator.num_pages) + # If page is out of range (e.g. 9999), deliver last page of results. + ban_list = paginator.page(paginator.num_pages) return render(request, 'users/index_ban.html', {'ban_list': ban_list}) + @login_required @permission_required('cableur') def index_white(request): """ Affiche l'ensemble des whitelist, need droit cableur """ - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number - white_list = Whitelist.objects.select_related('user').order_by('date_start') + white_list = Whitelist.objects.select_related('user')\ + .order_by('date_start') paginator = Paginator(white_list, pagination_number) page = request.GET.get('page') try: @@ -534,92 +626,114 @@ def index_white(request): # If page isn't an integer, deliver first page white_list = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - white_list = paginator.page(paginator.num_pages) + # If page is out of range (e.g. 9999), deliver last page of results. + white_list = paginator.page(paginator.num_pages) return render( request, 'users/index_whitelist.html', {'white_list': white_list} ) + @login_required @permission_required('cableur') def index_school(request): """ Affiche l'ensemble des établissement, need droit cableur """ school_list = School.objects.order_by('name') - return render(request, 'users/index_schools.html', {'school_list':school_list}) + return render( + request, + 'users/index_schools.html', + {'school_list': school_list} + ) + @login_required @permission_required('cableur') def index_listright(request): """ Affiche l'ensemble des droits , need droit cableur """ listright_list = ListRight.objects.order_by('listright') - return render(request, 'users/index_listright.html', {'listright_list':listright_list}) + return render( + request, + 'users/index_listright.html', + {'listright_list': listright_list} + ) + @login_required @permission_required('cableur') def index_serviceusers(request): """ Affiche les users de services (pour les accès ldap)""" serviceusers_list = ServiceUser.objects.order_by('pseudo') - return render(request, 'users/index_serviceusers.html', {'serviceusers_list':serviceusers_list}) + return render( + request, + 'users/index_serviceusers.html', + {'serviceusers_list': serviceusers_list} + ) + @login_required -def history(request, object, id): +def history(request, object_name, object_id): """ Affichage de l'historique : (acl, argument) user : self or cableur, userid, ban : self or cableur, banid, whitelist : self or cableur, whitelistid, school : cableur, schoolid, listright : cableur, listrightid """ - if object == 'user': + if object_name == 'user': try: - object_instance = User.objects.get(pk=id) + object_instance = User.objects.get(pk=object_id) except User.DoesNotExist: - messages.error(request, "Utilisateur inexistant") - return redirect("/users/") - if not request.user.has_perms(('cableur',)) and object_instance != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) - elif object == 'serviceuser' and request.user.has_perms(('cableur',)): + messages.error(request, "Utilisateur inexistant") + return redirect("/users/") + if not request.user.has_perms(('cableur',)) and\ + object_instance != request.user: + messages.error(request, "Vous ne pouvez pas afficher\ + l'historique d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) + elif object_name == 'serviceuser' and request.user.has_perms(('cableur',)): try: - object_instance = ServiceUser.objects.get(pk=id) + object_instance = ServiceUser.objects.get(pk=object_id) except ServiceUser.DoesNotExist: - messages.error(request, "User service inexistant") - return redirect("/users/") - elif object == 'ban': + messages.error(request, "User service inexistant") + return redirect("/users/") + elif object_name == 'ban': try: - object_instance = Ban.objects.get(pk=id) + object_instance = Ban.objects.get(pk=object_id) except Ban.DoesNotExist: - messages.error(request, "Bannissement inexistant") - return redirect("/users/") - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher les bans d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) - elif object == 'whitelist': + messages.error(request, "Bannissement inexistant") + return redirect("/users/") + if not request.user.has_perms(('cableur',)) and\ + object_instance.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher les bans\ + d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) + elif object_name == 'whitelist': try: - object_instance = Whitelist.objects.get(pk=id) - except Whiltelist.DoesNotExist: - messages.error(request, "Whitelist inexistant") - return redirect("/users/") - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher les whitelist d'un autre user que vous sans droit cableur") - return redirect("/users/profil/" + str(request.user.id)) - elif object == 'school' and request.user.has_perms(('cableur',)): + object_instance = Whitelist.objects.get(pk=object_id) + except Whitelist.DoesNotExist: + messages.error(request, "Whitelist inexistant") + return redirect("/users/") + if not request.user.has_perms(('cableur',)) and\ + object_instance.user != request.user: + messages.error(request, "Vous ne pouvez pas afficher les\ + whitelist d'un autre user que vous sans droit cableur") + return redirect("/users/profil/" + str(request.user.id)) + elif object_name == 'school' and request.user.has_perms(('cableur',)): try: - object_instance = School.objects.get(pk=id) + object_instance = School.objects.get(pk=object_id) except School.DoesNotExist: - messages.error(request, "Ecole inexistante") - return redirect("/users/") - elif object == 'listright' and request.user.has_perms(('cableur',)): + messages.error(request, "Ecole inexistante") + return redirect("/users/") + elif object_name == 'listright' and request.user.has_perms(('cableur',)): try: - object_instance = ListRight.objects.get(pk=id) + object_instance = ListRight.objects.get(pk=object_id) except ListRight.DoesNotExist: - messages.error(request, "Droit inexistant") - return redirect("/users/") + messages.error(request, "Droit inexistant") + return redirect("/users/") else: messages.error(request, "Objet inconnu") return redirect("/users/") - options, created = GeneralOption.objects.get_or_create() + options, _created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number reversions = Version.objects.get_for_object(object_instance) paginator = Paginator(reversions, pagination_number) @@ -632,7 +746,11 @@ def history(request, object, id): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) + return render( + request, + 're2o/history.html', + {'reversions': reversions, 'object': object_instance} + ) @login_required @@ -640,6 +758,7 @@ def mon_profil(request): """ Lien vers profil, renvoie request.id à la fonction """ return redirect("/users/profil/" + str(request.user.id)) + @login_required def profil(request, userid): """ Affiche un profil, self or cableur, prend un userid en argument """ @@ -649,14 +768,19 @@ def profil(request, userid): messages.error(request, "Utilisateur inexistant") return redirect("/users/") if not request.user.has_perms(('cableur',)) and users != request.user: - messages.error(request, "Vous ne pouvez pas afficher un autre user que vous sans droit cableur") + messages.error(request, "Vous ne pouvez pas afficher un autre user\ + que vous sans droit cableur") return redirect("/users/profil/" + str(request.user.id)) - machines = Machine.objects.filter(user=users).select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type__extension').prefetch_related('interface_set__type').prefetch_related('interface_set__domain__related_domain__extension') + machines = Machine.objects.filter(user=users).select_related('user')\ + .prefetch_related('interface_set__domain__extension')\ + .prefetch_related('interface_set__ipv4__ip_type__extension')\ + .prefetch_related('interface_set__type')\ + .prefetch_related('interface_set__domain__related_domain__extension') factures = Facture.objects.filter(user=users) bans = Ban.objects.filter(user=users) whitelists = Whitelist.objects.filter(user=users) list_droits = Right.objects.filter(user=users) - options, created = OptionalUser.objects.get_or_create() + options, _created = OptionalUser.objects.get_or_create() user_solde = options.user_solde return render( request, @@ -672,46 +796,56 @@ def profil(request, userid): } ) + def reset_password(request): """ Reintialisation du mot de passe si mdp oublié """ userform = ResetPasswordForm(request.POST or None) if userform.is_valid(): try: - user = User.objects.get(pseudo=userform.cleaned_data['pseudo'],email=userform.cleaned_data['email']) + user = User.objects.get( + pseudo=userform.cleaned_data['pseudo'], + email=userform.cleaned_data['email'] + ) except User.DoesNotExist: messages.error(request, "Cet utilisateur n'existe pas") return form({'userform': userform}, 'users/user.html', request) user.reset_passwd_mail(request) - messages.success(request, "Un mail pour l'initialisation du mot de passe a été envoyé") - redirect("/") + messages.success(request, "Un mail pour l'initialisation du mot\ + de passe a été envoyé") + redirect("/") return form({'userform': userform}, 'users/user.html', request) + def process(request, token): + """Process, lien pour la reinitialisation du mot de passe""" valid_reqs = Request.objects.filter(expires_at__gt=timezone.now()) req = get_object_or_404(valid_reqs, token=token) if req.type == Request.PASSWD: return process_passwd(request, req) - elif req.type == Request.EMAIL: - return process_email(request, req=req) else: messages.error(request, "Entrée incorrecte, contactez un admin") redirect("/") + def process_passwd(request, req): + """Process le changeemnt de mot de passe, renvoie le formulaire + demandant le nouveau password""" u_form = PassForm(request.POST or None) user = req.user if u_form.is_valid(): return password_change_action(u_form, user, request, req=req) return form({'userform': u_form}, 'users/user.html', request) -""" Framework Rest """ + class JSONResponse(HttpResponse): + """ Framework Rest """ def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' super(JSONResponse, self).__init__(content, **kwargs) + @csrf_exempt @login_required @permission_required('serveur') @@ -721,4 +855,3 @@ def mailing(request): mails = all_has_access().values('email').distinct() seria = MailSerializer(mails, many=True) return JSONResponse(seria.data) - From 9da900b6cb04519917153b5816753a02b78c273a Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 22:20:23 +0200 Subject: [PATCH 042/138] Fix urls.py pep8 --- users/urls.py | 84 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/users/urls.py b/users/urls.py index 43054fe5..531e0826 100644 --- a/users/urls.py +++ b/users/urls.py @@ -19,6 +19,9 @@ # 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. +""" +Definition des urls, pointant vers les views +""" from __future__ import unicode_literals @@ -32,39 +35,88 @@ urlpatterns = [ url(r'^state/(?P[0-9]+)$', views.state, name='state'), url(r'^password/(?P[0-9]+)$', views.password, name='password'), url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'), - url(r'^edit_serviceuser/(?P[0-9]+)$', views.edit_serviceuser, name='edit-serviceuser'), - url(r'^del_serviceuser/(?P[0-9]+)$', views.del_serviceuser, name='del-serviceuser'), + url( + r'^edit_serviceuser/(?P[0-9]+)$', + views.edit_serviceuser, + name='edit-serviceuser' + ), + url( + r'^del_serviceuser/(?P[0-9]+)$', + views.del_serviceuser, + name='del-serviceuser' + ), url(r'^add_ban/(?P[0-9]+)$', views.add_ban, name='add-ban'), url(r'^edit_ban/(?P[0-9]+)$', views.edit_ban, name='edit-ban'), - url(r'^add_whitelist/(?P[0-9]+)$', views.add_whitelist, name='add-whitelist'), - url(r'^edit_whitelist/(?P[0-9]+)$', views.edit_whitelist, name='edit-whitelist'), + url( + r'^add_whitelist/(?P[0-9]+)$', + views.add_whitelist, + name='add-whitelist' + ), + url( + r'^edit_whitelist/(?P[0-9]+)$', + views.edit_whitelist, + name='edit-whitelist' + ), url(r'^add_right/(?P[0-9]+)$', views.add_right, name='add-right'), url(r'^del_right/$', views.del_right, name='del-right'), url(r'^add_school/$', views.add_school, name='add-school'), - url(r'^edit_school/(?P[0-9]+)$', views.edit_school, name='edit-school'), + url( + r'^edit_school/(?P[0-9]+)$', + views.edit_school, + name='edit-school' + ), url(r'^del_school/$', views.del_school, name='del-school'), url(r'^add_listright/$', views.add_listright, name='add-listright'), - url(r'^edit_listright/(?P[0-9]+)$', views.edit_listright, name='edit-listright'), + url( + r'^edit_listright/(?P[0-9]+)$', + views.edit_listright, + name='edit-listright' + ), url(r'^del_listright/$', views.del_listright, name='del-listright'), url(r'^profil/(?P[0-9]+)$', views.profil, name='profil'), url(r'^index_ban/$', views.index_ban, name='index-ban'), url(r'^index_white/$', views.index_white, name='index-white'), url(r'^index_school/$', views.index_school, name='index-school'), url(r'^index_listright/$', views.index_listright, name='index-listright'), - url(r'^index_serviceusers/$', views.index_serviceusers, name='index-serviceusers'), + url( + r'^index_serviceusers/$', + views.index_serviceusers, + name='index-serviceusers' + ), url(r'^mon_profil/$', views.mon_profil, name='mon-profil'), url(r'^process/(?P[a-z0-9]{32})/$', views.process, name='process'), url(r'^reset_password/$', views.reset_password, name='reset-password'), url(r'^mass_archive/$', views.mass_archive, name='mass-archive'), - url(r'^history/(?Puser)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pban)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pwhitelist)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pschool)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Plistright)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pserviceuser)/(?P[0-9]+)$', views.history, name='history'), + url( + r'^history/(?Puser)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pban)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pwhitelist)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pschool)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Plistright)/(?P[0-9]+)$', + views.history, + name='history' + ), + url( + r'^history/(?Pserviceuser)/(?P[0-9]+)$', + views.history, + name='history' + ), url(r'^$', views.index, name='index'), url(r'^rest/mailing/$', views.mailing, name='mailing'), - ] - - From e11b1623ca072e4c75e40bc6c3258491a9544f02 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 14 Oct 2017 22:46:21 +0200 Subject: [PATCH 043/138] Fix admin pep8 users --- users/admin.py | 101 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 19 deletions(-) diff --git a/users/admin.py b/users/admin.py index 6a1e0e74..0c71064a 100644 --- a/users/admin.py +++ b/users/admin.py @@ -20,6 +20,10 @@ # 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. +""" +Definition des vues pour les admin. Classique, sauf pour users, +où on fait appel à UserChange et ServiceUserChange, forms custom +""" from __future__ import unicode_literals @@ -28,11 +32,15 @@ from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from reversion.admin import VersionAdmin -from .models import User, ServiceUser, School, Right, ListRight, ListShell, Ban, Whitelist, Request, LdapUser, LdapServiceUser, LdapServiceUserGroup, LdapUserGroup -from .forms import UserChangeForm, UserCreationForm, ServiceUserChangeForm, ServiceUserCreationForm +from .models import User, ServiceUser, School, Right, ListRight, ListShell +from .models import Ban, Whitelist, Request, LdapUser, LdapServiceUser +from .models import LdapServiceUserGroup, LdapUserGroup +from .forms import UserChangeForm, UserCreationForm +from .forms import ServiceUserChangeForm, ServiceUserCreationForm class UserAdmin(admin.ModelAdmin): + """Administration d'un user""" list_display = ( 'name', 'surname', @@ -43,51 +51,73 @@ class UserAdmin(admin.ModelAdmin): 'shell', 'state' ) - search_fields = ('name','surname','pseudo','room') + search_fields = ('name', 'surname', 'pseudo', 'room') class LdapUserAdmin(admin.ModelAdmin): - list_display = ('name','uidNumber','login_shell') - exclude = ('user_password','sambat_nt_password') + """Administration du ldapuser""" + list_display = ('name', 'uidNumber', 'login_shell') + exclude = ('user_password', 'sambat_nt_password') search_fields = ('name',) + class LdapServiceUserAdmin(admin.ModelAdmin): + """Administration du ldapserviceuser""" list_display = ('name',) exclude = ('user_password',) search_fields = ('name',) + class LdapUserGroupAdmin(admin.ModelAdmin): - list_display = ('name','members','gid') + """Administration du ldapusergroupe""" + list_display = ('name', 'members', 'gid') search_fields = ('name',) + class LdapServiceUserGroupAdmin(admin.ModelAdmin): + """Administration du ldap serviceusergroup""" list_display = ('name',) search_fields = ('name',) + class SchoolAdmin(VersionAdmin): - list_display = ('name',) + """Administration, gestion des écoles""" + pass + class ListRightAdmin(VersionAdmin): + """Gestion de la liste des droits existants + Ne permet pas l'edition du gid (primarykey pour ldap)""" list_display = ('listright',) + class ListShellAdmin(VersionAdmin): - list_display = ('shell',) + """Gestion de la liste des shells coté admin""" + pass + class RightAdmin(VersionAdmin): - list_display = ('user', 'right') + """Gestion de la liste des droits affectés""" + pass + class RequestAdmin(admin.ModelAdmin): + """Gestion des request objet, ticket pour lien de reinit mot de passe""" list_display = ('user', 'type', 'created_at', 'expires_at') + class BanAdmin(VersionAdmin): - list_display = ('user', 'raison', 'date_start', 'date_end') + """Gestion des bannissements""" + pass class WhitelistAdmin(VersionAdmin): - list_display = ('user', 'raison', 'date_start', 'date_end') + """Gestion des whitelist""" + pass class UserAdmin(VersionAdmin, BaseUserAdmin): + """Gestion d'un user : modification des champs perso, mot de passe, etc""" # The forms to add and change user instances form = UserChangeForm add_form = UserCreationForm @@ -95,27 +125,56 @@ class UserAdmin(VersionAdmin, BaseUserAdmin): # The fields to be used in displaying the User model. # These override the definitions on the base UserAdmin # that reference specific fields on auth.User. - list_display = ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'shell') + list_display = ( + 'pseudo', + 'name', + 'surname', + 'email', + 'school', + 'is_admin', + 'shell' + ) list_display = ('pseudo',) list_filter = () fieldsets = ( (None, {'fields': ('pseudo', 'password')}), - ('Personal info', {'fields': ('name', 'surname', 'email', 'school','shell', 'uid_number')}), + ( + 'Personal info', + { + 'fields': + ('name', 'surname', 'email', 'school', 'shell', 'uid_number') + } + ), ('Permissions', {'fields': ('is_admin', )}), ) # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin # overrides get_fieldsets to use this attribute when creating a user. add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'password1', 'password2')} + ( + None, + { + 'classes': ('wide',), + 'fields': ( + 'pseudo', + 'name', + 'surname', + 'email', + 'school', + 'is_admin', + 'password1', + 'password2' + ) + } ), ) search_fields = ('pseudo',) ordering = ('pseudo',) filter_horizontal = () + class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): + """Gestion d'un service user admin : champs personnels, + mot de passe; etc""" # The forms to add and change user instances form = ServiceUserChangeForm add_form = ServiceUserCreationForm @@ -131,15 +190,19 @@ class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin # overrides get_fieldsets to use this attribute when creating a user. add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('pseudo', 'password1', 'password2')} + ( + None, + { + 'classes': ('wide',), + 'fields': ('pseudo', 'password1', 'password2') + } ), ) search_fields = ('pseudo',) ordering = ('pseudo',) filter_horizontal = () + admin.site.register(User, UserAdmin) admin.site.register(ServiceUser, ServiceUserAdmin) admin.site.register(LdapUser, LdapUserAdmin) From e51b4847314fafde8adf99563a9051b2c633ffb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sat, 14 Oct 2017 21:38:38 +0000 Subject: [PATCH 044/138] =?UTF-8?q?Utilise=20l'id=20des=20objets=20plut?= =?UTF-8?q?=C3=B4t=20que=20leur=20nom=20pour=20les=20id=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/templatetags/massive_bootstrap_form.py | 62 +++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index cf6c01fe..df7edc1f 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -296,8 +296,8 @@ def mbf_js( f_name, f_value, f_bound, multiple, input_id = input_id( f_bound ), datasets = default_datasets( f_name, match_func ), create = tokenfield_create( f_name, f_bound ), - edit = tokenfield_edit( f_bound ), - remove = tokenfield_remove( f_bound ), + edit = tokenfield_edit( f_name, f_bound ), + remove = tokenfield_remove( f_name, f_bound ), updates = ''.join( [ ( '$( "#{u_id}" ).change( function() {{' 'setup_{f_name}();' @@ -495,17 +495,21 @@ def tokenfield_create( f_name, f_bound ): """ The JS script triggered when a new token is created in tokenfield. """ return ( 'function(evt) {{' - 'var data = evt.attrs.value;' - 'var i = 0;' - 'while ( i Date: Sun, 15 Oct 2017 00:18:08 +0000 Subject: [PATCH 045/138] Fix bug id avec JS et paiement + utilisation bootstrap --- .../templates/cotisations/new_facture.html | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index 2dfa52c2..f2586e8b 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -38,18 +38,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ venteform.management_form }}

Articles de la facture

-
+
{% for form in venteform.forms %} -
-

- {{ form.as_table }} -

+
+ Article :   + {% bootstrap_form form label_class='sr-only' %} +   +
{% endfor %}
-

-

Prix total : 0,00

@@ -63,19 +65,23 @@ with this program; if not, write to the Free Software Foundation, Inc., prices[{{ article.id|escapejs }}] = {{ article.prix }}; {% endfor %} - var template = `

{{ venteform.empty_form.as_table }}

`; + var template = `Article :   + {% bootstrap_form venteform.empty_form label_class='sr-only' %} +   + ` function add_article(){ // Index start at 0 => new_index = number of items var new_index = document.getElementsByClassName('product_to_sell').length; - document.getElementById('id_form-TOTAL_FORMS').value = - parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1; + document.getElementById('id_form-TOTAL_FORMS').value ++; var new_article = document.createElement('div'); - new_article.className = 'product_to_sell'; + new_article.className = 'product_to_sell form-inline'; new_article.innerHTML = template.replace(/__prefix__/g, new_index); - document.getElementById('form_set') - .appendChild(new_article); + document.getElementById('form_set').appendChild(new_article); add_listenner_for_id(new_index); } @@ -106,18 +112,28 @@ with this program; if not, write to the Free Software Foundation, Inc., .addEventListener("onkeypress", update_price, true); document.getElementById('id_form-' + i.toString() + '-quantity') .addEventListener("change", update_price, true); + document.getElementById('id_form-' + i.toString() + '-article-remove') + .addEventListener("click", function(event) { + var article = event.target.parentNode; + article.parentNode.removeChild(article); + document.getElementById('id_form-TOTAL_FORMS').value --; + update_price(); + } + ) } - function set_cheque_info_visibility(){ - var visible = document.getElementById("id_paiement").value == document.getElementById("id_paiement").getAttribute('data-cheque'); - p = document.getElementById("id_paiement") - console.log(p); + function set_cheque_info_visibility() { + var paiement = document.getElementById("id_Facture-paiement"); + var visible = paiement.value == paiement.getAttribute('data-cheque'); + p = document.getElementById("id_Facture-paiement"); var display = 'none'; if (visible) { display = 'block'; } - document.getElementById("id_cheque").parentNode.style.display = display; - document.getElementById("id_banque").parentNode.style.display = display; + document.getElementById("id_Facture-cheque") + .parentNode.style.display = display; + document.getElementById("id_Facture-banque") + .parentNode.style.display = display; } // Add events manager when DOM is fully loaded @@ -129,7 +145,7 @@ with this program; if not, write to the Free Software Foundation, Inc., for (i = 0; i < product_count; ++i){ add_listenner_for_id(i); } - document.getElementById("id_paiement") + document.getElementById("id_Facture-paiement") .addEventListener("change", set_cheque_info_visibility, true); set_cheque_info_visibility(); update_price(); From 1263784154b9b665257a1376e1d759099a18eba9 Mon Sep 17 00:00:00 2001 From: chirac Date: Sun, 15 Oct 2017 03:47:17 +0200 Subject: [PATCH 046/138] Menage --- logs/views.py | 9 ++- machines/views.py | 24 +------- re2o/utils.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++ users/models.py | 56 ------------------- users/views.py | 4 +- 5 files changed, 144 insertions(+), 85 deletions(-) create mode 100644 re2o/utils.py diff --git a/logs/views.py b/logs/views.py index 13879c86..e21d4de6 100644 --- a/logs/views.py +++ b/logs/views.py @@ -47,18 +47,17 @@ from reversion.models import Revision from reversion.models import Version, ContentType from users.models import User, ServiceUser, Right, School, ListRight, ListShell -from users.models import Ban, Whitelist, all_has_access -from users.models import all_whitelisted, all_baned, all_adherent +from users.models import Ban, Whitelist from cotisations.models import Facture, Vente, Article, Banque, Paiement from cotisations.models import Cotisation from machines.models import Machine, MachineType, IpType, Extension, Interface from machines.models import Domain, IpList -from machines.views import all_active_assigned_interfaces_count -from machines.views import all_active_interfaces_count from topologie.models import Switch, Port, Room from preferences.models import GeneralOption from re2o.views import form - +from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent +from re2o.utils import all_active_assigned_interfaces_count +from re2o.utils import all_active_interfaces_count STATS_DICT = { 0: ["Tout", 36], diff --git a/machines/views.py b/machines/views.py index 0e00dc67..0bfe36f0 100644 --- a/machines/views.py +++ b/machines/views.py @@ -53,30 +53,10 @@ from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasF from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort from users.models import User -from users.models import all_has_access from preferences.models import GeneralOption, OptionalMachine from re2o.templatetags.bootstrap_form_typeahead import hidden_id, input_id - -def all_active_interfaces(): - """Renvoie l'ensemble des machines autorisées à sortir sur internet """ - return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)).select_related('domain').select_related('machine').select_related('type').select_related('ipv4').select_related('domain__extension').select_related('ipv4__ip_type').distinct() - -def all_active_assigned_interfaces(): - """ Renvoie l'ensemble des machines qui ont une ipv4 assignées et disposant de l'accès internet""" - return all_active_interfaces().filter(ipv4__isnull=False) - -def all_active_interfaces_count(): - """ Version light seulement pour compter""" - return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)) - -def all_active_assigned_interfaces_count(): - """ Version light seulement pour compter""" - return all_active_interfaces_count().filter(ipv4__isnull=False) - -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) +from re2o.utils import all_active_assigned_interfaces, all_has_access +from re2o.views import form def f_type_id( is_type_tt ): """ The id that will be used in HTML to store the value of the field diff --git a/re2o/utils.py b/re2o/utils.py new file mode 100644 index 00000000..e2ca6db9 --- /dev/null +++ b/re2o/utils.py @@ -0,0 +1,136 @@ +# -*- 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 © 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. + +# -*- coding: utf-8 -*- +# David Sinquin, Gabriel Détraz, Goulven Kermarec +""" +Regroupe les fonctions transversales utiles + +Fonction : + - récupérer tous les utilisateurs actifs + - récupérer toutes les machines + - récupérer tous les bans + etc +""" + + +from __future__ import unicode_literals + + +from django.utils import timezone +from django.db.models import Q + +from cotisations.models import Cotisation, Facture, Paiement, Vente +from machines.models import Domain, Interface, Machine +from users.models import User, Ban, Whitelist +from preferences.models import Service + +DT_NOW = timezone.now() + + +def all_adherent(search_time=DT_NOW): + """ Fonction renvoyant tous les users adherents. Optimisee pour n'est + qu'une seule requete sql + Inspecte les factures de l'user et ses cotisation, regarde si elles + sont posterieur à now (end_time)""" + return User.objects.filter( + facture__in=Facture.objects.filter( + vente__in=Vente.objects.filter( + cotisation__in=Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.all().exclude(valid=False) + ) + ).filter(date_end__gt=search_time) + ) + ) + ).distinct() + + +def all_baned(search_time=DT_NOW): + """ Fonction renvoyant tous les users bannis """ + return User.objects.filter( + ban__in=Ban.objects.filter( + date_end__gt=search_time + ) + ).distinct() + + +def all_whitelisted(search_time=DT_NOW): + """ Fonction renvoyant tous les users whitelistes """ + return User.objects.filter( + whitelist__in=Whitelist.objects.filter( + date_end__gt=search_time + ) + ).distinct() + + +def all_has_access(search_time=DT_NOW): + """ Renvoie tous les users beneficiant d'une connexion + : user adherent ou whiteliste et non banni """ + 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(facture__in=Facture.objects.filter( + vente__in=Vente.objects.filter( + cotisation__in=Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.all() + .exclude(valid=False) + ) + ).filter(date_end__gt=search_time) + ) + ))) + ).distinct() + + +def all_active_interfaces(): + """Renvoie l'ensemble des machines autorisées à sortir sur internet """ + return Interface.objects.filter( + machine__in=Machine.objects.filter( + user__in=all_has_access() + ).filter(active=True) + ).select_related('domain').select_related('machine')\ + .select_related('type').select_related('ipv4')\ + .select_related('domain__extension').select_related('ipv4__ip_type')\ + .distinct() + + +def all_active_assigned_interfaces(): + """ Renvoie l'ensemble des machines qui ont une ipv4 assignées et + disposant de l'accès internet""" + return all_active_interfaces().filter(ipv4__isnull=False) + + +def all_active_interfaces_count(): + """ Version light seulement pour compter""" + return Interface.objects.filter( + machine__in=Machine.objects.filter( + user__in=all_has_access() + ).filter(active=True) + ) + + +def all_active_assigned_interfaces_count(): + """ Version light seulement pour compter""" + return all_active_interfaces_count().filter(ipv4__isnull=False) diff --git a/users/models.py b/users/models.py index a0ef29f8..2f8f888f 100644 --- a/users/models.py +++ b/users/models.py @@ -144,62 +144,6 @@ def get_admin_right(): return admin_right -def all_adherent(search_time=DT_NOW): - """ Fonction renvoyant tous les users adherents. Optimisee pour n'est - qu'une seule requete sql - Inspecte les factures de l'user et ses cotisation, regarde si elles - sont posterieur à now (end_time)""" - return User.objects.filter( - facture__in=Facture.objects.filter( - vente__in=Vente.objects.filter( - cotisation__in=Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.all().exclude(valid=False) - ) - ).filter(date_end__gt=search_time) - ) - ) - ).distinct() - - -def all_baned(search_time=DT_NOW): - """ Fonction renvoyant tous les users bannis """ - return User.objects.filter( - ban__in=Ban.objects.filter( - date_end__gt=search_time - ) - ).distinct() - - -def all_whitelisted(search_time=DT_NOW): - """ Fonction renvoyant tous les users whitelistes """ - return User.objects.filter( - whitelist__in=Whitelist.objects.filter( - date_end__gt=search_time - ) - ).distinct() - - -def all_has_access(search_time=DT_NOW): - """ Renvoie tous les users beneficiant d'une connexion - : user adherent ou whiteliste et non banni """ - 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(facture__in=Facture.objects.filter( - vente__in=Vente.objects.filter( - cotisation__in=Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.all() - .exclude(valid=False) - ) - ).filter(date_end__gt=search_time) - ) - ))) - ).distinct() - - class UserManager(BaseUserManager): """User manager basique de django""" def _create_user( diff --git a/users/views.py b/users/views.py index e553f5b8..5b0b3910 100644 --- a/users/views.py +++ b/users/views.py @@ -53,7 +53,7 @@ from reversion.models import Version from reversion import revisions as reversion from users.serializers import MailSerializer from users.models import User, Right, Ban, Whitelist, School, ListRight -from users.models import Request, ServiceUser, all_has_access +from users.models import Request, ServiceUser from users.forms import DelRightForm, BanForm, WhitelistForm, DelSchoolForm from users.forms import DelListRightForm, NewListRightForm from users.forms import InfoForm, BaseInfoForm, StateForm @@ -65,7 +65,7 @@ from machines.models import Machine from preferences.models import OptionalUser, GeneralOption from re2o.views import form - +from re2o.utils import all_has_access def password_change_action(u_form, user, request, req=False): """ Fonction qui effectue le changeemnt de mdp bdd""" From fc7e5d0ad4be3eec6f7f30b6069ec831d8ddf2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sun, 15 Oct 2017 14:47:22 +0000 Subject: [PATCH 047/138] Affichage plus lisible pour la suppression des droits --- users/templates/users/del_right.html | 31 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/users/templates/users/del_right.html b/users/templates/users/del_right.html index 740778a2..30edf666 100644 --- a/users/templates/users/del_right.html +++ b/users/templates/users/del_right.html @@ -35,19 +35,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
- - - {% for key, values in userform.items %} - - {% endfor %} - - - + {% for key, values in userform.items %} - {% bootstrap_form_errors values %} - - {% endfor %} + + + + + + {% endfor %} +
{{ key }}
{{ values.rights }}
+ +
+
+
    + {% for user in values.rights %} +
  • {{ user }}
  • + {% endfor %} +
+
+
{% bootstrap_button "Modifier" button_type="submit" icon="star" %} From 54cb9f46ad58a24def1d6625396c76ac63eef82d Mon Sep 17 00:00:00 2001 From: Mael Kervella Date: Sun, 15 Oct 2017 15:27:13 +0000 Subject: [PATCH 048/138] =?UTF-8?q?Fix=20:=20merge=20fait=20=C3=A0=20l'arr?= =?UTF-8?q?ache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- topologie/templates/topologie/switch.html | 2 +- topologie/views.py | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index fe224678..249097e7 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -54,7 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if interfaceform %} {% if i_bft_param %} - {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_bft_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% else %} {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} diff --git a/topologie/views.py b/topologie/views.py index fb7550db..f1e8740c 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -381,20 +381,15 @@ def new_switch(request): reversion.set_comment("Création") messages.success(request, "Le switch a été créé") return redirect("/topologie/") -<<<<<<< HEAD - i_bft_param = generate_ipv4_mbf_param( interface, False ) - return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) -======= - i_bft_param = generate_ipv4_bft_param(interface, False) + i_mbf_param = generate_ipv4_mbf_param( interface, False ) return form({ 'topoform': switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, - 'i_bft_param': i_bft_param + 'i_mbf_param': i_mbf_param }, 'topologie/switch.html', request) ->>>>>>> master @login_required @permission_required('infra') @@ -454,20 +449,15 @@ def edit_switch(request, switch_id): ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") -<<<<<<< HEAD - i_bft_param = generate_ipv4_mbf_param( interface_form, False ) - return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) -======= - i_bft_param = generate_ipv4_bft_param(interface_form, False) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) return form({ 'topoform': switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, - 'i_bft_param': i_bft_param + 'i_mbf_param': i_mbf_param }, 'topologie/switch.html', request) ->>>>>>> master @login_required @permission_required('infra') From 3b686cfd2981797aa596db691f524ad74b974e0c Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 15 Oct 2017 20:37:21 +0200 Subject: [PATCH 049/138] =?UTF-8?q?Borde=20un=20certain=20nombre=20d'integ?= =?UTF-8?q?er=20qui=20ont=20des=20range=20de=20valeur=20d=C3=A9fini=20dans?= =?UTF-8?q?=20les=20RFC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0024_auto_20171015_2033.py | 26 ++++++++++++ cotisations/models.py | 4 +- .../migrations/0061_auto_20171015_2033.py | 36 +++++++++++++++++ machines/models.py | 13 +++--- .../migrations/0031_auto_20171015_2033.py | 40 +++++++++++++++++++ topologie/models.py | 10 ++--- users/migrations/0056_auto_20171015_2033.py | 37 +++++++++++++++++ users/models.py | 6 +-- 8 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 cotisations/migrations/0024_auto_20171015_2033.py create mode 100644 machines/migrations/0061_auto_20171015_2033.py create mode 100644 topologie/migrations/0031_auto_20171015_2033.py create mode 100644 users/migrations/0056_auto_20171015_2033.py diff --git a/cotisations/migrations/0024_auto_20171015_2033.py b/cotisations/migrations/0024_auto_20171015_2033.py new file mode 100644 index 00000000..b52dad62 --- /dev/null +++ b/cotisations/migrations/0024_auto_20171015_2033.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0023_auto_20170902_1303'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='duration', + field=models.PositiveIntegerField(blank=True, help_text='Durée exprimée en mois entiers', null=True, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name='vente', + name='duration', + field=models.PositiveIntegerField(blank=True, help_text='Durée exprimée en mois entiers', null=True), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 54843076..0b3aef35 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -132,7 +132,7 @@ class Vente(models.Model): name = models.CharField(max_length=255) prix = models.DecimalField(max_digits=5, decimal_places=2) iscotisation = models.BooleanField() - duration = models.IntegerField( + duration = models.PositiveIntegerField( help_text="Durée exprimée en mois entiers", blank=True, null=True) @@ -222,7 +222,7 @@ class Article(models.Model): name = models.CharField(max_length=255, unique=True) prix = models.DecimalField(max_digits=5, decimal_places=2) iscotisation = models.BooleanField() - duration = models.IntegerField( + duration = models.PositiveIntegerField( help_text="Durée exprimée en mois entiers", blank=True, null=True, diff --git a/machines/migrations/0061_auto_20171015_2033.py b/machines/migrations/0061_auto_20171015_2033.py new file mode 100644 index 00000000..6153bbd0 --- /dev/null +++ b/machines/migrations/0061_auto_20171015_2033.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0060_iptype_ouverture_ports'), + ] + + operations = [ + migrations.AlterField( + model_name='mx', + name='priority', + field=models.PositiveIntegerField(unique=True), + ), + migrations.AlterField( + model_name='ouvertureport', + name='begin', + field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(65535)]), + ), + migrations.AlterField( + model_name='ouvertureport', + name='end', + field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(65535)]), + ), + migrations.AlterField( + model_name='vlan', + name='vlan_id', + field=models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(4095)]), + ), + ] diff --git a/machines/models.py b/machines/models.py index 7444996c..0c1686ba 100644 --- a/machines/models.py +++ b/machines/models.py @@ -151,10 +151,11 @@ class IpType(models.Model): return self.type class Vlan(models.Model): - """ Un vlan : vlan_id et nom""" + """ Un vlan : vlan_id et nom + On limite le vlan id entre 0 et 4096, comme défini par la norme""" PRETTY_NAME = "Vlans" - vlan_id = models.IntegerField() + vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)]) name = models.CharField(max_length=256) comment = models.CharField(max_length=256, blank=True) @@ -205,7 +206,7 @@ class Mx(models.Model): PRETTY_NAME = "Enregistrements MX" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) - priority = models.IntegerField(unique=True) + priority = models.PositiveIntegerField(unique=True) name = models.OneToOneField('Domain', on_delete=models.PROTECT) @cached_property @@ -512,13 +513,15 @@ class OuverturePort(models.Model): Les ports de la plage sont compris entre begin et en inclus. Si begin == end alors on ne représente qu'un seul port. + + On limite les ports entre 0 et 65535, tels que défini par la RFC """ TCP = 'T' UDP = 'U' IN = 'I' OUT = 'O' - begin = models.IntegerField() - end = models.IntegerField() + begin = models.PositiveIntegerField(validators=[MaxValueValidator(65535)]) + end = models.PositiveIntegerField(validators=[MaxValueValidator(65535)]) port_list = models.ForeignKey('OuverturePortList', on_delete=models.CASCADE) protocole = models.CharField( max_length=1, diff --git a/topologie/migrations/0031_auto_20171015_2033.py b/topologie/migrations/0031_auto_20171015_2033.py new file mode 100644 index 00000000..674af6c6 --- /dev/null +++ b/topologie/migrations/0031_auto_20171015_2033.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0030_auto_20171004_0235'), + ] + + operations = [ + migrations.AlterField( + model_name='port', + name='port', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='stack', + name='member_id_max', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='stack', + name='member_id_min', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='switch', + name='number', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='switch', + name='stack_member_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index 086e0aff..66c8fe94 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -52,8 +52,8 @@ class Stack(models.Model): name = models.CharField(max_length=32, blank=True, null=True) stack_id = models.CharField(max_length=32, unique=True) details = models.CharField(max_length=255, blank=True, null=True) - member_id_min = models.IntegerField() - member_id_max = models.IntegerField() + member_id_min = models.PositiveIntegerField() + member_id_max = models.PositiveIntegerField() def __str__(self): return " ".join([self.name, self.stack_id]) @@ -90,7 +90,7 @@ class Switch(models.Model): on_delete=models.CASCADE ) location = models.CharField(max_length=255) - number = models.IntegerField() + number = models.PositiveIntegerField() details = models.CharField(max_length=255, blank=True) stack = models.ForeignKey( Stack, @@ -98,7 +98,7 @@ class Switch(models.Model): null=True, on_delete=models.SET_NULL ) - stack_member_id = models.IntegerField(blank=True, null=True) + stack_member_id = models.PositiveIntegerField(blank=True, null=True) class Meta: unique_together = ('stack', 'stack_member_id') @@ -146,7 +146,7 @@ class Port(models.Model): ) switch = models.ForeignKey('Switch', related_name="ports") - port = models.IntegerField() + port = models.PositiveIntegerField() room = models.ForeignKey( 'Room', on_delete=models.PROTECT, diff --git a/users/migrations/0056_auto_20171015_2033.py b/users/migrations/0056_auto_20171015_2033.py new file mode 100644 index 00000000..a47aca6a --- /dev/null +++ b/users/migrations/0056_auto_20171015_2033.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-15 18:33 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import users.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0055_auto_20171003_0556'), + ] + + operations = [ + migrations.AlterField( + model_name='listright', + name='gid', + field=models.PositiveIntegerField(null=True, unique=True), + ), + migrations.AlterField( + model_name='listright', + name='listright', + field=models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[a-z]+$', message='Les groupes unix ne peuvent contenir que des lettres minuscules')]), + ), + migrations.AlterField( + model_name='user', + name='rezo_rez_uid', + field=models.PositiveIntegerField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='user', + name='uid_number', + field=models.PositiveIntegerField(default=users.models.User.auto_uid, unique=True), + ), + ] diff --git a/users/models.py b/users/models.py index 2f8f888f..550bc770 100644 --- a/users/models.py +++ b/users/models.py @@ -243,8 +243,8 @@ class User(AbstractBaseUser): state = models.IntegerField(choices=STATES, default=STATE_ACTIVE) registered = models.DateTimeField(auto_now_add=True) telephone = models.CharField(max_length=15, blank=True, null=True) - uid_number = models.IntegerField(default=auto_uid, unique=True) - rezo_rez_uid = models.IntegerField(unique=True, blank=True, null=True) + uid_number = models.PositiveIntegerField(default=auto_uid, unique=True) + rezo_rez_uid = models.PositiveIntegerField(unique=True, blank=True, null=True) USERNAME_FIELD = 'pseudo' REQUIRED_FIELDS = ['name', 'surname', 'email'] @@ -840,7 +840,7 @@ class ListRight(models.Model): que des lettres minuscules" )] ) - gid = models.IntegerField(unique=True, null=True) + gid = models.PositiveIntegerField(unique=True, null=True) details = models.CharField( help_text="Description", max_length=255, From 919537f278028c1fd04d24e54497ffaa659986d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sun, 15 Oct 2017 18:47:14 +0000 Subject: [PATCH 050/138] Valable sans JS --- machines/views.py | 4 +- re2o/templatetags/massive_bootstrap_form.py | 310 +++++++++++++------- 2 files changed, 211 insertions(+), 103 deletions(-) diff --git a/machines/views.py b/machines/views.py index 3cc906c0..6a87a9f8 100644 --- a/machines/views.py +++ b/machines/views.py @@ -129,11 +129,13 @@ def generate_ipv4_mbf_param( form, is_type_tt ): i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) } i_update_on = { 'ipv4': [f_type_id( is_type_tt )] } + i_gen_select = { 'ipv4': False } i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, - 'update_on': i_update_on + 'update_on': i_update_on, + 'gen_select': i_gen_select } return i_mbf_param diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index df7edc1f..ae4d5cdc 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -113,12 +113,29 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): A dict of list of ids that the values depends on. The engine and the typeahead properties are recalculated and reapplied. Example : - 'addition' : { + 'update_on' : { 'field_A' : [ 'id0', 'id1', ... ] , 'field_B' : ... , ... } + gen_select (optional) + A dict of boolean telling if the form should either generate + the normal select (set to true) and then use it to generate + the possible choices and then remove it or either (set to + false) generate the choices variable in this tag and do not + send any select. + Sending the select before can be usefull to permit the use + without any JS enabled but it will execute more code locally + for the client so the loading might be slower. + If not specified, this variable is set to true for each field + Example : + 'gen_select' : { + 'field_A': True , + 'field_B': ... , + ... + } + See boostrap_form_ for other arguments **Usage**:: @@ -146,6 +163,11 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): [ '': '' [, '': '' [, ... ] ] ] + } ], + [, 'gen_select': { + [ '': '' + [, '': '' + [, ... ] ] ] } ] } ] [ ] @@ -163,6 +185,7 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): engine = param.get('engine', {}) match_func = param.get('match_func', {}) update_on = param.get('update_on', {}) + gen_select = param.get('gen_select', {}) hidden_fields = [h.name for h in form.hidden_fields()] html = '' @@ -184,40 +207,43 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): multiple = f_value.widget.allow_multiple_selected f_bound = f_value.get_bound_field( form, f_name ) + if gen_select.get(f_name, True) : + html += render_field( + f_bound, + *args, + **kwargs + ) + f_value.widget = TextInput( attrs = { 'name': 'mbf_'+f_name, 'placeholder': f_value.empty_label } ) - html += render_field( + replace_input = render_field( f_value.get_bound_field( form, f_name ), *args, **kwargs ) - if multiple : - content = mbf_js( - f_name, - f_value, - f_bound, - multiple, - choices, - engine, - match_func, - update_on - ) - else : - content = hidden_tag( f_bound, f_name ) + mbf_js( - f_name, - f_value, - f_bound, - multiple, - choices, - engine, - match_func, - update_on - ) + if not gen_select.get(f_name, True) : + html += replace_input + + content = mbf_js( + f_name, + f_value, + f_bound, + multiple, + replace_input, + choices, + engine, + match_func, + update_on, + gen_select + ) + if not multiple and not gen_select.get(f_name, True) : + content += hidden_tag( f_bound, f_name ) + html += render_tag( 'div', content = content, @@ -257,12 +283,14 @@ def hidden_tag( f_bound, f_name ): } ) -def mbf_js( f_name, f_value, f_bound, multiple, - choices_, engine_, match_func_, update_on_ ) : +def mbf_js( f_name, f_value, f_bound, multiple, replace_input, + choices_, engine_, match_func_, update_on_, gen_select_ ) : """ The whole script to use """ + gen_select = gen_select_.get( f_name, True ) + choices = ( mark_safe( choices_[f_name] ) if f_name in choices_.keys() - else default_choices( f_value ) ) + else default_choices( f_value, f_bound, gen_select ) ) engine = ( mark_safe( engine_[f_name] ) if f_name in engine_.keys() else default_engine ( f_name ) ) @@ -272,82 +300,120 @@ def mbf_js( f_name, f_value, f_bound, multiple, update_on = update_on_[f_name] if f_name in update_on_.keys() else [] - if multiple : - js_content = ( - 'var choices_{f_name} = {choices};' - 'var engine_{f_name};' - 'var setup_{f_name} = function() {{' - 'engine_{f_name} = {engine};' - '$( "#{input_id}" ).tokenfield( "destroy" );' - '$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});' - '}};' - '$( "#{input_id}" ).bind( "tokenfield:createtoken", {create} );' - '$( "#{input_id}" ).bind( "tokenfield:edittoken", {edit} );' - '$( "#{input_id}" ).bind( "tokenfield:removetoken", {remove} );' - '{updates}' - '$( "#{input_id}" ).ready( function() {{' - 'setup_{f_name}();' - '{init_input}' - '}} );' - ).format( - f_name = f_name, - choices = choices, - engine = engine, - input_id = input_id( f_bound ), - datasets = default_datasets( f_name, match_func ), - create = tokenfield_create( f_name, f_bound ), - edit = tokenfield_edit( f_name, f_bound ), - remove = tokenfield_remove( f_name, f_bound ), - updates = ''.join( [ ( - '$( "#{u_id}" ).change( function() {{' - 'setup_{f_name}();' - '{reset_input}' - '}} );' - ).format( - u_id = u_id, - reset_input = tokenfield_reset_input( f_bound ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = tokenfield_init_input( f_name, f_bound ), + if gen_select : + if multiple : + js_content = ( + '$( "#{input_id}" ).ready( function() {{' + 'var choices_{f_name} = {choices};' + '{del_select}' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).tokenfield( "destroy" );' + '$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});' + '}};' + '$( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );' + '$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );' + '$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );' + '{tok_updates}' + 'setup_{f_name}();' + '{tok_init_input}' + '}} );' + ) + else : + js_content = ( + '$( "#{input_id}" ).ready( function() {{' + 'var choices_{f_name} = {choices};' + '{del_select}' + '{gen_hidden}' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).typeahead( "destroy" );' + '$( "#{input_id}" ).typeahead( {datasets} );' + '}};' + '$( "#{input_id}" ).bind( "typeahead:select", {typ_select} );' + '$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );' + '{typ_updates}' + 'setup_{f_name}();' + '{typ_init_input}' + '}} );' ) else : - js_content = ( - 'var choices_{f_name} = {choices};' - 'var engine_{f_name};' - 'var setup_{f_name} = function() {{' - 'engine_{f_name} = {engine};' - '$( "#{input_id}" ).typeahead( "destroy" );' - '$( "#{input_id}" ).typeahead( {datasets} );' - '}};' - '$( "#{input_id}" ).bind( "typeahead:select", {select} );' - '$( "#{input_id}" ).bind( "typeahead:change", {change} );' - '{updates}' - '$( "#{input_id}" ).ready( function() {{' - 'setup_{f_name}();' - '{init_input}' - '}} );' - ).format( - f_name = f_name, - choices = choices, - engine = engine, - input_id = input_id( f_bound ), - datasets = default_datasets( f_name, match_func ), - select = typeahead_select( f_bound ), - change = typeahead_change( f_bound ), - updates = ''.join( [ ( - '$( "#{u_id}" ).change( function() {{' - 'setup_{f_name}();' - '{reset_input}' - '}} );' - ).format( - u_id = u_id, - reset_input = typeahead_reset_input( f_bound ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = typeahead_init_input( f_name, f_bound ), + if multiple : + js_content = ( + 'var choices_{f_name} = {choices};' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).tokenfield( "destroy" );' + '$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});' + '}};' + '$( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );' + '$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );' + '$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );' + '{tok_updates}' + '$( "#{input_id}" ).ready( function() {{' + 'setup_{f_name}();' + '{tok_init_input}' + '}} );' ) + else : + js_content = ( + 'var choices_{f_name} = {choices};' + 'var engine_{f_name};' + 'var setup_{f_name} = function() {{' + 'engine_{f_name} = {engine};' + '$( "#{input_id}" ).typeahead( "destroy" );' + '$( "#{input_id}" ).typeahead( {datasets} );' + '}};' + '$( "#{input_id}" ).bind( "typeahead:select", {typ_select} );' + '$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );' + '{typ_updates}' + '$( "#{input_id}" ).ready( function() {{' + 'setup_{f_name}();' + '{typ_init_input}' + '}} );' + ) + + js_content = js_content.format( + f_name = f_name, + choices = choices, + del_select = del_select( f_bound, replace_input ), + gen_hidden = gen_hidden( f_bound ), + engine = engine, + input_id = input_id( f_bound ), + datasets = default_datasets( f_name, match_func ), + typ_select = typeahead_select( f_bound ), + typ_change = typeahead_change( f_bound ), + tok_create = tokenfield_create( f_name, f_bound ), + tok_edit = tokenfield_edit( f_name, f_bound ), + tok_remove = tokenfield_remove( f_name, f_bound ), + typ_updates = ''.join( [ ( + '$( "#{u_id}" ).change( function() {{' + 'setup_{f_name}();' + '{reset_input}' + '}} );' + ).format( + u_id = u_id, + reset_input = typeahead_reset_input( f_bound ), + f_name = f_name + ) for u_id in update_on ] + ), + tok_updates = ''.join( [ ( + '$( "#{u_id}" ).change( function() {{' + 'setup_{f_name}();' + '{reset_input}' + '}} );' + ).format( + u_id = u_id, + reset_input = tokenfield_reset_input( f_bound ), + f_name = f_name + ) for u_id in update_on ] + ), + tok_init_input = tokenfield_init_input( f_name, f_bound ), + typ_init_input = typeahead_init_input( f_name, f_bound ), + ) return render_tag( 'script', content=mark_safe( js_content ) ) @@ -403,9 +469,21 @@ def tokenfield_reset_input( f_bound ) : input_id = input_id( f_bound ), ) -def default_choices( f_value ) : +def default_choices( f_value, f_bound, gen_select ) : """ The JS script creating the variable choices_ """ - return '[{objects}]'.format( + if gen_select : + c = ( 'function plop(o) {{' + 'var c = [];' + 'for( let i=0 ; i