mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-28 07:32:25 +00:00
Merge branch 'dev' of https://gitlab.federez.net/federez/re2o into anonymisation
This commit is contained in:
commit
8035f14e1b
81 changed files with 3109 additions and 1025 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -5,7 +5,7 @@ __pycache__/
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
#*.mo TODO
|
*.mo
|
||||||
*.pot
|
*.pot
|
||||||
|
|
||||||
# Django stuff
|
# Django stuff
|
||||||
|
|
|
@ -391,13 +391,25 @@ class OptionalTopologieSerializer(NamespacedHMSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = preferences.OptionalTopologie
|
model = preferences.OptionalTopologie
|
||||||
fields = ('radius_general_policy', 'vlan_decision_ok',
|
fields = ('switchs_ip_type', 'switchs_web_management',
|
||||||
'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management',
|
|
||||||
'switchs_web_management_ssl', 'switchs_rest_management',
|
'switchs_web_management_ssl', 'switchs_rest_management',
|
||||||
'switchs_management_utils', 'switchs_management_interface_ip',
|
'switchs_management_utils', 'switchs_management_interface_ip',
|
||||||
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
|
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusOptionSerializer(NamespacedHMSerializer):
|
||||||
|
"""Serialize `preferences.models.RadiusOption` objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = preferences.RadiusOption
|
||||||
|
fields = ('radius_general_policy', 'unknown_machine',
|
||||||
|
'unknown_machine_vlan', 'unknown_port',
|
||||||
|
'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan',
|
||||||
|
'non_member', 'non_member_vlan', 'banned', 'banned_vlan',
|
||||||
|
'vlan_decision_ok')
|
||||||
|
|
||||||
|
|
||||||
class GeneralOptionSerializer(NamespacedHMSerializer):
|
class GeneralOptionSerializer(NamespacedHMSerializer):
|
||||||
"""Serialize `preferences.models.GeneralOption` objects.
|
"""Serialize `preferences.models.GeneralOption` objects.
|
||||||
"""
|
"""
|
||||||
|
@ -407,9 +419,8 @@ class GeneralOptionSerializer(NamespacedHMSerializer):
|
||||||
fields = ('general_message_fr', 'general_message_en',
|
fields = ('general_message_fr', 'general_message_en',
|
||||||
'search_display_page', 'pagination_number',
|
'search_display_page', 'pagination_number',
|
||||||
'pagination_large_number', 'req_expire_hrs',
|
'pagination_large_number', 'req_expire_hrs',
|
||||||
'site_name', 'email_from', 'GTU_sum_up',
|
'site_name', 'main_site_url', 'email_from',
|
||||||
'GTU')
|
'GTU_sum_up', 'GTU')
|
||||||
|
|
||||||
|
|
||||||
class HomeServiceSerializer(NamespacedHMSerializer):
|
class HomeServiceSerializer(NamespacedHMSerializer):
|
||||||
"""Serialize `preferences.models.Service` objects.
|
"""Serialize `preferences.models.Service` objects.
|
||||||
|
@ -633,9 +644,8 @@ class AdherentSerializer(NamespacedHMSerializer):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class HomeCreationSerializer(NamespacedHMSerializer):
|
class BasicUserSerializer(NamespacedHMSerializer):
|
||||||
"""Serialize 'users.models.User' minimal infos to create home
|
"""Serialize 'users.models.User' minimal infos"""
|
||||||
"""
|
|
||||||
uid = serializers.IntegerField(source='uid_number')
|
uid = serializers.IntegerField(source='uid_number')
|
||||||
gid = serializers.IntegerField(source='gid_number')
|
gid = serializers.IntegerField(source='gid_number')
|
||||||
|
|
||||||
|
@ -813,7 +823,8 @@ class SwitchPortSerializer(serializers.ModelSerializer):
|
||||||
model = topologie.Switch
|
model = topologie.Switch
|
||||||
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
|
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
|
||||||
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
|
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
|
||||||
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value')
|
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value',
|
||||||
|
'list_modules')
|
||||||
|
|
||||||
# LOCAL EMAILS
|
# LOCAL EMAILS
|
||||||
|
|
||||||
|
@ -1001,6 +1012,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer):
|
||||||
model = machines.Domain
|
model = machines.Domain
|
||||||
fields = ('alias', 'hostname')
|
fields = ('alias', 'hostname')
|
||||||
|
|
||||||
|
class DNAMERecordSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serialize `machines.models.Domain` objects with the data needed to
|
||||||
|
generate a DNAME DNS record.
|
||||||
|
"""
|
||||||
|
alias = serializers.CharField(read_only=True)
|
||||||
|
zone = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = machines.DName
|
||||||
|
fields = ('alias', 'zone')
|
||||||
|
|
||||||
|
|
||||||
class DNSZonesSerializer(serializers.ModelSerializer):
|
class DNSZonesSerializer(serializers.ModelSerializer):
|
||||||
"""Serialize the data about DNS Zones.
|
"""Serialize the data about DNS Zones.
|
||||||
|
@ -1015,14 +1037,14 @@ class DNSZonesSerializer(serializers.ModelSerializer):
|
||||||
a_records = ARecordSerializer(many=True, source='get_associated_a_records')
|
a_records = ARecordSerializer(many=True, source='get_associated_a_records')
|
||||||
aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records')
|
aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records')
|
||||||
cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records')
|
cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records')
|
||||||
|
dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records')
|
||||||
sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records')
|
sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = machines.Extension
|
model = machines.Extension
|
||||||
fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6',
|
fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6',
|
||||||
'mx_records', 'txt_records', 'srv_records', 'a_records',
|
'mx_records', 'txt_records', 'srv_records', 'a_records',
|
||||||
'aaaa_records', 'cname_records', 'sshfp_records')
|
'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records')
|
||||||
|
|
||||||
#REMINDER
|
#REMINDER
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ router.register_viewset(r'machines/role', views.RoleViewSet)
|
||||||
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
|
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
|
||||||
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
|
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
|
||||||
router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView),
|
router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView),
|
||||||
|
router.register_view(r'preferences/radiusoption', views.RadiusOptionView),
|
||||||
router.register_view(r'preferences/generaloption', views.GeneralOptionView),
|
router.register_view(r'preferences/generaloption', views.GeneralOptionView),
|
||||||
router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
|
router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
|
||||||
router.register_view(r'preferences/assooption', views.AssoOptionView),
|
router.register_view(r'preferences/assooption', views.AssoOptionView),
|
||||||
|
@ -88,6 +89,8 @@ router.register(r'topologie/portprofile', views.PortProfileViewSet)
|
||||||
# USERS
|
# USERS
|
||||||
router.register_viewset(r'users/user', views.UserViewSet, base_name='user')
|
router.register_viewset(r'users/user', views.UserViewSet, base_name='user')
|
||||||
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation')
|
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation')
|
||||||
|
router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser')
|
||||||
|
router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser')
|
||||||
router.register_viewset(r'users/club', views.ClubViewSet)
|
router.register_viewset(r'users/club', views.ClubViewSet)
|
||||||
router.register_viewset(r'users/adherent', views.AdherentViewSet)
|
router.register_viewset(r'users/adherent', views.AdherentViewSet)
|
||||||
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)
|
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)
|
||||||
|
|
35
api/views.py
35
api/views.py
|
@ -292,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView):
|
||||||
return preferences.OptionalTopologie.objects.first()
|
return preferences.OptionalTopologie.objects.first()
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusOptionView(generics.RetrieveAPIView):
|
||||||
|
"""Exposes details of `preferences.models.OptionalTopologie` settings.
|
||||||
|
"""
|
||||||
|
permission_classes = (ACLPermission,)
|
||||||
|
perms_map = {'GET': [preferences.RadiusOption.can_view_all]}
|
||||||
|
serializer_class = serializers.RadiusOptionSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return preferences.RadiusOption.objects.first()
|
||||||
|
|
||||||
|
|
||||||
class GeneralOptionView(generics.RetrieveAPIView):
|
class GeneralOptionView(generics.RetrieveAPIView):
|
||||||
"""Exposes details of `preferences.models.GeneralOption` settings.
|
"""Exposes details of `preferences.models.GeneralOption` settings.
|
||||||
"""
|
"""
|
||||||
|
@ -445,7 +456,19 @@ class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""Exposes infos of `users.models.Users` objects to create homes.
|
"""Exposes infos of `users.models.Users` objects to create homes.
|
||||||
"""
|
"""
|
||||||
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
|
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
|
||||||
serializer_class = serializers.HomeCreationSerializer
|
serializer_class = serializers.BasicUserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class NormalUserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""Exposes infos of `users.models.Users`without specific rights objects."""
|
||||||
|
queryset = users.User.objects.exclude(groups__listright__critical=True).distinct()
|
||||||
|
serializer_class = serializers.BasicUserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""Exposes infos of `users.models.Users`without specific rights objects."""
|
||||||
|
queryset = users.User.objects.filter(groups__listright__critical=True).distinct()
|
||||||
|
serializer_class = serializers.BasicUserSerializer
|
||||||
|
|
||||||
|
|
||||||
class ClubViewSet(viewsets.ReadOnlyModelViewSet):
|
class ClubViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
@ -541,8 +564,8 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
|
||||||
# Config des switches
|
# Config des switches
|
||||||
|
|
||||||
class SwitchPortView(generics.ListAPIView):
|
class SwitchPortView(generics.ListAPIView):
|
||||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
"""Output each port of a switch, to be serialized with
|
||||||
order to build the DHCP lease files.
|
additionnal informations (profiles etc)
|
||||||
"""
|
"""
|
||||||
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
|
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
|
||||||
|
|
||||||
|
@ -551,16 +574,14 @@ class SwitchPortView(generics.ListAPIView):
|
||||||
# Rappel fin adhésion
|
# Rappel fin adhésion
|
||||||
|
|
||||||
class ReminderView(generics.ListAPIView):
|
class ReminderView(generics.ListAPIView):
|
||||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
"""Output for users to remind an end of their subscription.
|
||||||
order to build the DHCP lease files.
|
|
||||||
"""
|
"""
|
||||||
queryset = preferences.Reminder.objects.all()
|
queryset = preferences.Reminder.objects.all()
|
||||||
serializer_class = serializers.ReminderSerializer
|
serializer_class = serializers.ReminderSerializer
|
||||||
|
|
||||||
|
|
||||||
class RoleView(generics.ListAPIView):
|
class RoleView(generics.ListAPIView):
|
||||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
"""Output of roles for each server
|
||||||
order to build the DHCP lease files.
|
|
||||||
"""
|
"""
|
||||||
queryset = machines.Role.objects.all().prefetch_related('servers')
|
queryset = machines.Role.objects.all().prefetch_related('servers')
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
|
|
|
@ -17,3 +17,4 @@ libjs-bootstrap
|
||||||
fonts-font-awesome
|
fonts-font-awesome
|
||||||
graphviz
|
graphviz
|
||||||
git
|
git
|
||||||
|
gettext
|
||||||
|
|
|
@ -30,7 +30,7 @@ from django.contrib import admin
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
|
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
|
||||||
from .models import CustomInvoice
|
from .models import CustomInvoice, CostEstimate
|
||||||
|
|
||||||
|
|
||||||
class FactureAdmin(VersionAdmin):
|
class FactureAdmin(VersionAdmin):
|
||||||
|
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CostEstimateAdmin(VersionAdmin):
|
||||||
|
"""Admin class for cost estimates."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CustomInvoiceAdmin(VersionAdmin):
|
class CustomInvoiceAdmin(VersionAdmin):
|
||||||
"""Admin class for custom invoices."""
|
"""Admin class for custom invoices."""
|
||||||
pass
|
pass
|
||||||
|
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
|
||||||
admin.site.register(Vente, VenteAdmin)
|
admin.site.register(Vente, VenteAdmin)
|
||||||
admin.site.register(Cotisation, CotisationAdmin)
|
admin.site.register(Cotisation, CotisationAdmin)
|
||||||
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
|
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
|
||||||
|
admin.site.register(CostEstimate, CostEstimateAdmin)
|
||||||
|
|
|
@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from re2o.field_permissions import FieldPermissionFormMixin
|
from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
from .models import Article, Paiement, Facture, Banque, CustomInvoice
|
from .models import (
|
||||||
|
Article, Paiement, Facture, Banque,
|
||||||
|
CustomInvoice, Vente, CostEstimate
|
||||||
|
)
|
||||||
from .payment_methods import balance
|
from .payment_methods import balance
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,7 +107,44 @@ class SelectArticleForm(FormRevMixin, Form):
|
||||||
user = kwargs.pop('user')
|
user = kwargs.pop('user')
|
||||||
target_user = kwargs.pop('target_user', None)
|
target_user = kwargs.pop('target_user', None)
|
||||||
super(SelectArticleForm, self).__init__(*args, **kwargs)
|
super(SelectArticleForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user)
|
self.fields['article'].queryset = Article.find_allowed_articles(
|
||||||
|
user, target_user)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscountForm(Form):
|
||||||
|
"""
|
||||||
|
Form used in oder to create a discount on an invoice.
|
||||||
|
"""
|
||||||
|
is_relative = forms.BooleanField(
|
||||||
|
label=_("Discount is on percentage"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
discount = forms.DecimalField(
|
||||||
|
label=_("Discount"),
|
||||||
|
max_value=100,
|
||||||
|
min_value=0,
|
||||||
|
max_digits=5,
|
||||||
|
decimal_places=2,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_to_invoice(self, invoice):
|
||||||
|
invoice_price = invoice.prix_total()
|
||||||
|
discount = self.cleaned_data['discount']
|
||||||
|
is_relative = self.cleaned_data['is_relative']
|
||||||
|
if is_relative:
|
||||||
|
amount = discount/100 * invoice_price
|
||||||
|
else:
|
||||||
|
amount = discount
|
||||||
|
if amount:
|
||||||
|
name = _("{}% discount") if is_relative else _("{}€ discount")
|
||||||
|
name = name.format(discount)
|
||||||
|
Vente.objects.create(
|
||||||
|
facture=invoice,
|
||||||
|
name=name,
|
||||||
|
prix=-amount,
|
||||||
|
number=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomInvoiceForm(FormRevMixin, ModelForm):
|
class CustomInvoiceForm(FormRevMixin, ModelForm):
|
||||||
|
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class CostEstimateForm(FormRevMixin, ModelForm):
|
||||||
|
"""
|
||||||
|
Form used to create a cost estimate.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = CostEstimate
|
||||||
|
exclude = ['paid', 'final_invoice']
|
||||||
|
|
||||||
|
|
||||||
class ArticleForm(FormRevMixin, ModelForm):
|
class ArticleForm(FormRevMixin, ModelForm):
|
||||||
"""
|
"""
|
||||||
Form used to create an article.
|
Form used to create an article.
|
||||||
|
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
|
||||||
super(RechargeForm, self).__init__(*args, **kwargs)
|
super(RechargeForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['payment'].empty_label = \
|
self.fields['payment'].empty_label = \
|
||||||
_("Select a payment method")
|
_("Select a payment method")
|
||||||
self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True)
|
self.fields['payment'].queryset = Paiement.find_allowed_payments(
|
||||||
|
user_source).exclude(is_balance=True)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
|
@ -266,4 +316,3 @@ class RechargeForm(FormRevMixin, Form):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
Binary file not shown.
20
cotisations/migrations/0036_custominvoice_remark.py
Normal file
20
cotisations/migrations/0036_custominvoice_remark.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-29 14:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cotisations', '0035_notepayment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='custominvoice',
|
||||||
|
name='remark',
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name='Remark'),
|
||||||
|
),
|
||||||
|
]
|
28
cotisations/migrations/0037_costestimate.py
Normal file
28
cotisations/migrations/0037_costestimate.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-29 21:03
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cotisations', '0036_custominvoice_remark'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CostEstimate',
|
||||||
|
fields=[
|
||||||
|
('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')),
|
||||||
|
('validity', models.DurationField(verbose_name='Period of validity')),
|
||||||
|
('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'permissions': (('view_costestimate', 'Can view a cost estimate object'),),
|
||||||
|
},
|
||||||
|
bases=('cotisations.custominvoice',),
|
||||||
|
),
|
||||||
|
]
|
31
cotisations/migrations/0038_auto_20181231_1657.py
Normal file
31
cotisations/migrations/0038_auto_20181231_1657.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-31 22:57
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cotisations', '0037_costestimate'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='costestimate',
|
||||||
|
name='final_invoice',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='costestimate',
|
||||||
|
name='validity',
|
||||||
|
field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custominvoice',
|
||||||
|
name='paid',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Paid'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -284,8 +284,65 @@ class CustomInvoice(BaseInvoice):
|
||||||
verbose_name=_("Address")
|
verbose_name=_("Address")
|
||||||
)
|
)
|
||||||
paid = models.BooleanField(
|
paid = models.BooleanField(
|
||||||
verbose_name=_("Paid")
|
verbose_name=_("Paid"),
|
||||||
|
default=False
|
||||||
)
|
)
|
||||||
|
remark = models.TextField(
|
||||||
|
verbose_name=_("Remark"),
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CostEstimate(CustomInvoice):
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_costestimate', _("Can view a cost estimate object")),
|
||||||
|
)
|
||||||
|
validity = models.DurationField(
|
||||||
|
verbose_name=_("Period of validity"),
|
||||||
|
help_text="DD HH:MM:SS"
|
||||||
|
)
|
||||||
|
final_invoice = models.ForeignKey(
|
||||||
|
CustomInvoice,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="origin_cost_estimate",
|
||||||
|
primary_key=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_invoice(self):
|
||||||
|
"""Create a CustomInvoice from the CostEstimate."""
|
||||||
|
if self.final_invoice is not None:
|
||||||
|
return self.final_invoice
|
||||||
|
invoice = CustomInvoice()
|
||||||
|
invoice.recipient = self.recipient
|
||||||
|
invoice.payment = self.payment
|
||||||
|
invoice.address = self.address
|
||||||
|
invoice.paid = False
|
||||||
|
invoice.remark = self.remark
|
||||||
|
invoice.date = timezone.now()
|
||||||
|
invoice.save()
|
||||||
|
self.final_invoice = invoice
|
||||||
|
self.save()
|
||||||
|
for sale in self.vente_set.all():
|
||||||
|
Vente.objects.create(
|
||||||
|
facture=invoice,
|
||||||
|
name=sale.name,
|
||||||
|
prix=sale.prix,
|
||||||
|
number=sale.number,
|
||||||
|
)
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
def can_delete(self, user_request, *args, **kwargs):
|
||||||
|
if not user_request.has_perm('cotisations.delete_costestimate'):
|
||||||
|
return False, _("You don't have the right "
|
||||||
|
"to delete a cost estimate.")
|
||||||
|
if self.final_invoice is not None:
|
||||||
|
return False, _("The cost estimate has an "
|
||||||
|
"invoice and cannot be deleted.")
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
# TODO : change Vente to Purchase
|
# TODO : change Vente to Purchase
|
||||||
|
@ -624,7 +681,7 @@ class Article(RevMixin, AclMixin, models.Model):
|
||||||
objects_pool = cls.objects.filter(
|
objects_pool = cls.objects.filter(
|
||||||
Q(type_user='All') | Q(type_user='Adherent')
|
Q(type_user='All') | Q(type_user='Adherent')
|
||||||
)
|
)
|
||||||
if not target_user.is_adherent():
|
if target_user is not None and not target_user.is_adherent():
|
||||||
objects_pool = objects_pool.filter(
|
objects_pool = objects_pool.filter(
|
||||||
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
|
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
|
||||||
)
|
)
|
||||||
|
|
101
cotisations/templates/cotisations/aff_cost_estimate.html
Normal file
101
cotisations/templates/cotisations/aff_cost_estimate.html
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2018 Hugo Levy-Falk
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
{% if cost_estimate_list.paginator %}
|
||||||
|
{% include 'pagination.html' with list=cost_estimate_list%}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% trans "Recipient" as tr_recip %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Designation" %}</th>
|
||||||
|
<th>{% trans "Total price" %}</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Payment method" as tr_payment_method %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Date" as tr_date %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Validity" as tr_validity %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Cost estimate ID" as tr_estimate_id %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Invoice created" as tr_invoice_created%}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %}
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for estimate in cost_estimate_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ estimate.recipient }}</td>
|
||||||
|
<td>{{ estimate.name }}</td>
|
||||||
|
<td>{{ estimate.prix_total }}</td>
|
||||||
|
<td>{{ estimate.payment }}</td>
|
||||||
|
<td>{{ estimate.date }}</td>
|
||||||
|
<td>{{ estimate.validity }}</td>
|
||||||
|
<td>{{ estimate.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if estimate.final_invoice %}
|
||||||
|
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
|
||||||
|
{% else %}
|
||||||
|
<i style="color: #D10115;" class="fa fa-times"></i>'
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% can_edit estimate %}
|
||||||
|
{% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %}
|
||||||
|
{% acl_end %}
|
||||||
|
{% history_button estimate %}
|
||||||
|
{% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-to-invoice' estimate.id %}">
|
||||||
|
<i class="fa fa-file"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-pdf' estimate.id %}">
|
||||||
|
<i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if custom_invoice_list.paginator %}
|
||||||
|
{% include 'pagination.html' with list=custom_invoice_list %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
|
@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% if title %}
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
{% else %}
|
||||||
<h3>{% trans "Edit the invoice" %}</h3>
|
<h3>{% trans "Edit the invoice" %}</h3>
|
||||||
|
{% endif %}
|
||||||
{% massive_bootstrap_form factureform 'user' %}
|
{% massive_bootstrap_form factureform 'user' %}
|
||||||
{{ venteform.management_form }}
|
{{ venteform.management_form }}
|
||||||
<h3>{% trans "Articles" %}</h3>
|
<h3>{% trans "Articles" %}</h3>
|
||||||
|
|
|
@ -44,6 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
|
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if factureform %}
|
||||||
|
{% bootstrap_form_errors factureform %}
|
||||||
|
{% endif %}
|
||||||
|
{% if discount_form %}
|
||||||
|
{% bootstrap_form_errors discount_form %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -68,6 +74,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
|
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
|
||||||
|
<h3>{% trans "Discount" %}</h3>
|
||||||
|
{% if discount_form %}
|
||||||
|
{% bootstrap_form discount_form %}
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
|
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -78,20 +88,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if articlesformset or payment_method%}
|
{% if articlesformset or payment_method%}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% if articlesformset %}
|
{% if articlesformset %}
|
||||||
var prices = {};
|
var prices = {};
|
||||||
{% for article in articlelist %}
|
{% for article in articlelist %}
|
||||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
var template = `Article :
|
var template = `Article :
|
||||||
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
||||||
|
|
||||||
<button class="btn btn-danger btn-sm"
|
<button class="btn btn-danger btn-sm"
|
||||||
id="id_form-__prefix__-article-remove" type="button">
|
id="id_form-__prefix__-article-remove" type="button">
|
||||||
<span class="fa fa-times"></span>
|
<span class="fa fa-times"></span>
|
||||||
</button>`
|
</button>`
|
||||||
|
|
||||||
function add_article(){
|
function add_article(){
|
||||||
// Index start at 0 => new_index = number of items
|
// Index start at 0 => new_index = number of items
|
||||||
var new_index =
|
var new_index =
|
||||||
document.getElementsByClassName('product_to_sell').length;
|
document.getElementsByClassName('product_to_sell').length;
|
||||||
|
@ -101,9 +111,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
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);
|
add_listenner_for_id(new_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_price(){
|
function update_price(){
|
||||||
var price = 0;
|
var price = 0;
|
||||||
var product_count =
|
var product_count =
|
||||||
document.getElementsByClassName('product_to_sell').length;
|
document.getElementsByClassName('product_to_sell').length;
|
||||||
|
@ -119,11 +129,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
'id_form-' + i.toString() + '-quantity').value;
|
'id_form-' + i.toString() + '-quantity').value;
|
||||||
price += article_price * quantity;
|
price += article_price * quantity;
|
||||||
}
|
}
|
||||||
|
{% if discount_form %}
|
||||||
|
var relative_discount = document.getElementById('id_is_relative').checked;
|
||||||
|
var discount = document.getElementById('id_discount').value;
|
||||||
|
if(relative_discount) {
|
||||||
|
discount = discount/100 * price;
|
||||||
|
}
|
||||||
|
price -= discount;
|
||||||
|
{% endif %}
|
||||||
document.getElementById('total_price').innerHTML =
|
document.getElementById('total_price').innerHTML =
|
||||||
price.toFixed(2).toString().replace('.', ',');
|
price.toFixed(2).toString().replace('.', ',');
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_listenner_for_id(i){
|
function add_listenner_for_id(i){
|
||||||
document.getElementById('id_form-' + i.toString() + '-article')
|
document.getElementById('id_form-' + i.toString() + '-article')
|
||||||
.addEventListener("change", update_price, true);
|
.addEventListener("change", update_price, true);
|
||||||
document.getElementById('id_form-' + i.toString() + '-article')
|
document.getElementById('id_form-' + i.toString() + '-article')
|
||||||
|
@ -137,10 +155,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
document.getElementById('id_form-TOTAL_FORMS').value --;
|
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||||
update_price();
|
update_price();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add events manager when DOM is fully loaded
|
// Add events manager when DOM is fully loaded
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
document.getElementById("add_one")
|
document.getElementById("add_one")
|
||||||
.addEventListener("click", add_article, true);
|
.addEventListener("click", add_article, true);
|
||||||
var product_count =
|
var product_count =
|
||||||
|
@ -148,21 +166,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
for (i = 0; i < product_count; ++i){
|
for (i = 0; i < product_count; ++i){
|
||||||
add_listenner_for_id(i);
|
add_listenner_for_id(i);
|
||||||
}
|
}
|
||||||
|
document.getElementById('id_discount')
|
||||||
|
.addEventListener('change', update_price, true);
|
||||||
|
document.getElementById('id_is_relative')
|
||||||
|
.addEventListener('click', update_price, true);
|
||||||
update_price();
|
update_price();
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if payment_method.templates %}
|
{% if payment_method.templates %}
|
||||||
var TEMPLATES = [
|
var TEMPLATES = [
|
||||||
"",
|
"",
|
||||||
{% for t in payment_method.templates %}
|
{% for t in payment_method.templates %}
|
||||||
{% if t %}
|
{% if t %}
|
||||||
`{% bootstrap_form t %}`,
|
`{% bootstrap_form t %}`,
|
||||||
{% else %}
|
{% else %}
|
||||||
"",
|
"",
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
function update_payment_method_form(){
|
function update_payment_method_form(){
|
||||||
var method = document.getElementById('paymentMethodSelect').value;
|
var method = document.getElementById('paymentMethodSelect').value;
|
||||||
if(method==""){
|
if(method==""){
|
||||||
method=0;
|
method=0;
|
||||||
|
@ -175,8 +197,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
var html = TEMPLATES[method];
|
var html = TEMPLATES[method];
|
||||||
|
|
||||||
document.getElementById('paymentMethod').innerHTML = html;
|
document.getElementById('paymentMethod').innerHTML = html;
|
||||||
}
|
}
|
||||||
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -75,8 +75,12 @@
|
||||||
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
|
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
|
||||||
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
|
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
|
||||||
{% if fid is not None %}
|
{% if fid is not None %}
|
||||||
|
{% if is_estimate %}
|
||||||
|
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
|
||||||
|
{% else %}
|
||||||
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
|
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
\end{tabular*}
|
\end{tabular*}
|
||||||
\\
|
\\
|
||||||
|
|
||||||
|
@ -104,12 +108,30 @@
|
||||||
\begin{tabular}{|l|r|}
|
\begin{tabular}{|l|r|}
|
||||||
\hline
|
\hline
|
||||||
\textbf{Total} & {{total|floatformat:2}} \euro \\
|
\textbf{Total} & {{total|floatformat:2}} \euro \\
|
||||||
|
{% if not is_estimate %}
|
||||||
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
|
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
|
||||||
\doublehline
|
\doublehline
|
||||||
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
|
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
|
||||||
|
{% endif %}
|
||||||
\hline
|
\hline
|
||||||
\end{tabular}
|
\end{tabular}
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
\begin{tabularx}{\textwidth}{r X}
|
||||||
|
\hline
|
||||||
|
\textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\
|
||||||
|
\hline
|
||||||
|
{% if remark %}
|
||||||
|
\textbf{Remarque} & {{remark|safe}} \\
|
||||||
|
\hline
|
||||||
|
{% endif %}
|
||||||
|
{% if end_validity %}
|
||||||
|
\textbf{Validité} & Jusqu'au {{end_validity}} \\
|
||||||
|
\hline
|
||||||
|
{% endif %}
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
|
|
||||||
|
|
||||||
|
|
36
cotisations/templates/cotisations/index_cost_estimate.html
Normal file
36
cotisations/templates/cotisations/index_cost_estimate.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "cotisations/sidebar.html" %}
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2017 Gabriel Détraz
|
||||||
|
Copyright © 2017 Goulven Kermarec
|
||||||
|
Copyright © 2017 Augustin Lemesle
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Cost estimates" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% trans "Cost estimates list" %}</h2>
|
||||||
|
{% can_create CostEstimate %}
|
||||||
|
{% include "buttons/add.html" with href='cotisations:new-cost-estimate'%}
|
||||||
|
{% acl_end %}
|
||||||
|
{% include 'cotisations/aff_cost_estimate.html' %}
|
||||||
|
{% endblock %}
|
|
@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
|
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
|
||||||
</a>
|
</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
|
{% can_view_all CostEstimate %}
|
||||||
|
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-cost-estimate" %}">
|
||||||
|
<i class="fa fa-list-ul"></i> {% trans "Cost estimate" %}
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
{% can_view_all Article %}
|
{% can_view_all Article %}
|
||||||
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
|
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
|
||||||
<i class="fa fa-list-ul"></i> {% trans "Available articles" %}
|
<i class="fa fa-list-ul"></i> {% trans "Available articles" %}
|
||||||
|
|
|
@ -36,6 +36,7 @@ from django.template import Context
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
|
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
|
||||||
|
@ -48,8 +49,9 @@ def render_invoice(_request, ctx={}):
|
||||||
Render an invoice using some available information such as the current
|
Render an invoice using some available information such as the current
|
||||||
date, the user, the articles, the prices, ...
|
date, the user, the articles, the prices, ...
|
||||||
"""
|
"""
|
||||||
|
is_estimate = ctx.get('is_estimate', False)
|
||||||
filename = '_'.join([
|
filename = '_'.join([
|
||||||
'invoice',
|
'cost_estimate' if is_estimate else 'invoice',
|
||||||
slugify(ctx.get('asso_name', "")),
|
slugify(ctx.get('asso_name', "")),
|
||||||
slugify(ctx.get('recipient_name', "")),
|
slugify(ctx.get('recipient_name', "")),
|
||||||
str(ctx.get('DATE', datetime.now()).year),
|
str(ctx.get('DATE', datetime.now()).year),
|
||||||
|
@ -93,6 +95,20 @@ def create_pdf(template, ctx={}):
|
||||||
return pdf
|
return pdf
|
||||||
|
|
||||||
|
|
||||||
|
def escape_chars(string):
|
||||||
|
"""Escape the '%' and the '€' signs to avoid messing with LaTeX"""
|
||||||
|
if not isinstance(string, str):
|
||||||
|
return string
|
||||||
|
mapping = (
|
||||||
|
('€', r'\euro'),
|
||||||
|
('%', r'\%'),
|
||||||
|
)
|
||||||
|
r = str(string)
|
||||||
|
for k, v in mapping:
|
||||||
|
r = r.replace(k, v)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def render_tex(_request, template, ctx={}):
|
def render_tex(_request, template, ctx={}):
|
||||||
"""Creates a PDF from a LaTex templates using pdflatex.
|
"""Creates a PDF from a LaTex templates using pdflatex.
|
||||||
|
|
||||||
|
|
|
@ -51,11 +51,41 @@ urlpatterns = [
|
||||||
views.facture_pdf,
|
views.facture_pdf,
|
||||||
name='facture-pdf'
|
name='facture-pdf'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^new_cost_estimate/$',
|
||||||
|
views.new_cost_estimate,
|
||||||
|
name='new-cost-estimate'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^index_cost_estimate/$',
|
||||||
|
views.index_cost_estimate,
|
||||||
|
name='index-cost-estimate'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.cost_estimate_pdf,
|
||||||
|
name='cost-estimate-pdf',
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'^index_custom_invoice/$',
|
r'^index_custom_invoice/$',
|
||||||
views.index_custom_invoice,
|
views.index_custom_invoice,
|
||||||
name='index-custom-invoice'
|
name='index-custom-invoice'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^edit_cost_estimate/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.edit_cost_estimate,
|
||||||
|
name='edit-cost-estimate'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^cost_estimate_to_invoice/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.cost_estimate_to_invoice,
|
||||||
|
name='cost-estimate-to-invoice'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^del_cost_estimate/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.del_cost_estimate,
|
||||||
|
name='del-cost-estimate'
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'^new_custom_invoice/$',
|
r'^new_custom_invoice/$',
|
||||||
views.new_custom_invoice,
|
views.new_custom_invoice,
|
||||||
|
|
|
@ -47,7 +47,10 @@ from users.models import User
|
||||||
from re2o.settings import LOGO_PATH
|
from re2o.settings import LOGO_PATH
|
||||||
from re2o import settings
|
from re2o import settings
|
||||||
from re2o.views import form
|
from re2o.views import form
|
||||||
from re2o.utils import SortTable, re2o_paginator
|
from re2o.base import (
|
||||||
|
SortTable,
|
||||||
|
re2o_paginator,
|
||||||
|
)
|
||||||
from re2o.acl import (
|
from re2o.acl import (
|
||||||
can_create,
|
can_create,
|
||||||
can_edit,
|
can_edit,
|
||||||
|
@ -65,7 +68,8 @@ from .models import (
|
||||||
Paiement,
|
Paiement,
|
||||||
Banque,
|
Banque,
|
||||||
CustomInvoice,
|
CustomInvoice,
|
||||||
BaseInvoice
|
BaseInvoice,
|
||||||
|
CostEstimate
|
||||||
)
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
FactureForm,
|
FactureForm,
|
||||||
|
@ -77,9 +81,11 @@ from .forms import (
|
||||||
DelBanqueForm,
|
DelBanqueForm,
|
||||||
SelectArticleForm,
|
SelectArticleForm,
|
||||||
RechargeForm,
|
RechargeForm,
|
||||||
CustomInvoiceForm
|
CustomInvoiceForm,
|
||||||
|
DiscountForm,
|
||||||
|
CostEstimateForm,
|
||||||
)
|
)
|
||||||
from .tex import render_invoice
|
from .tex import render_invoice, escape_chars
|
||||||
from .payment_methods.forms import payment_method_factory
|
from .payment_methods.forms import payment_method_factory
|
||||||
from .utils import find_payment_method
|
from .utils import find_payment_method
|
||||||
|
|
||||||
|
@ -175,7 +181,58 @@ def new_facture(request, user, userid):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO : change facture to invoice
|
@login_required
|
||||||
|
@can_create(CostEstimate)
|
||||||
|
def new_cost_estimate(request):
|
||||||
|
"""
|
||||||
|
View used to generate a custom invoice. It's mainly used to
|
||||||
|
get invoices that are not taken into account, for the administrative
|
||||||
|
point of view.
|
||||||
|
"""
|
||||||
|
# The template needs the list of articles (for the JS part)
|
||||||
|
articles = Article.objects.filter(
|
||||||
|
Q(type_user='All') | Q(type_user=request.user.class_name)
|
||||||
|
)
|
||||||
|
# Building the invocie form and the article formset
|
||||||
|
cost_estimate_form = CostEstimateForm(request.POST or None)
|
||||||
|
|
||||||
|
articles_formset = formset_factory(SelectArticleForm)(
|
||||||
|
request.POST or None,
|
||||||
|
form_kwargs={'user': request.user}
|
||||||
|
)
|
||||||
|
discount_form = DiscountForm(request.POST or None)
|
||||||
|
|
||||||
|
if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
|
||||||
|
cost_estimate_instance = cost_estimate_form.save()
|
||||||
|
for art_item in articles_formset:
|
||||||
|
if art_item.cleaned_data:
|
||||||
|
article = art_item.cleaned_data['article']
|
||||||
|
quantity = art_item.cleaned_data['quantity']
|
||||||
|
Vente.objects.create(
|
||||||
|
facture=cost_estimate_instance,
|
||||||
|
name=article.name,
|
||||||
|
prix=article.prix,
|
||||||
|
type_cotisation=article.type_cotisation,
|
||||||
|
duration=article.duration,
|
||||||
|
number=quantity
|
||||||
|
)
|
||||||
|
discount_form.apply_to_invoice(cost_estimate_instance)
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The cost estimate was created.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||||
|
|
||||||
|
return form({
|
||||||
|
'factureform': cost_estimate_form,
|
||||||
|
'action_name': _("Confirm"),
|
||||||
|
'articlesformset': articles_formset,
|
||||||
|
'articlelist': articles,
|
||||||
|
'discount_form': discount_form,
|
||||||
|
'title': _("Cost estimate"),
|
||||||
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_create(CustomInvoice)
|
@can_create(CustomInvoice)
|
||||||
def new_custom_invoice(request):
|
def new_custom_invoice(request):
|
||||||
|
@ -195,8 +252,9 @@ def new_custom_invoice(request):
|
||||||
request.POST or None,
|
request.POST or None,
|
||||||
form_kwargs={'user': request.user}
|
form_kwargs={'user': request.user}
|
||||||
)
|
)
|
||||||
|
discount_form = DiscountForm(request.POST or None)
|
||||||
|
|
||||||
if invoice_form.is_valid() and articles_formset.is_valid():
|
if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
|
||||||
new_invoice_instance = invoice_form.save()
|
new_invoice_instance = invoice_form.save()
|
||||||
for art_item in articles_formset:
|
for art_item in articles_formset:
|
||||||
if art_item.cleaned_data:
|
if art_item.cleaned_data:
|
||||||
|
@ -210,6 +268,7 @@ def new_custom_invoice(request):
|
||||||
duration=article.duration,
|
duration=article.duration,
|
||||||
number=quantity
|
number=quantity
|
||||||
)
|
)
|
||||||
|
discount_form.apply_to_invoice(new_invoice_instance)
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("The custom invoice was created.")
|
_("The custom invoice was created.")
|
||||||
|
@ -220,7 +279,8 @@ def new_custom_invoice(request):
|
||||||
'factureform': invoice_form,
|
'factureform': invoice_form,
|
||||||
'action_name': _("Confirm"),
|
'action_name': _("Confirm"),
|
||||||
'articlesformset': articles_formset,
|
'articlesformset': articles_formset,
|
||||||
'articlelist': articles
|
'articlelist': articles,
|
||||||
|
'discount_form': discount_form
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -263,7 +323,8 @@ def facture_pdf(request, facture, **_kwargs):
|
||||||
'siret': AssoOption.get_cached_value('siret'),
|
'siret': AssoOption.get_cached_value('siret'),
|
||||||
'email': AssoOption.get_cached_value('contact'),
|
'email': AssoOption.get_cached_value('contact'),
|
||||||
'phone': AssoOption.get_cached_value('telephone'),
|
'phone': AssoOption.get_cached_value('telephone'),
|
||||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||||
|
'payment_method': facture.paiement.moyen,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -328,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
|
||||||
}, 'cotisations/delete.html', request)
|
}, 'cotisations/delete.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(CostEstimate)
|
||||||
|
def edit_cost_estimate(request, invoice, **kwargs):
|
||||||
|
# Building the invocie form and the article formset
|
||||||
|
invoice_form = CostEstimateForm(
|
||||||
|
request.POST or None,
|
||||||
|
instance=invoice
|
||||||
|
)
|
||||||
|
purchases_objects = Vente.objects.filter(facture=invoice)
|
||||||
|
purchase_form_set = modelformset_factory(
|
||||||
|
Vente,
|
||||||
|
fields=('name', 'number'),
|
||||||
|
extra=0,
|
||||||
|
max_num=len(purchases_objects)
|
||||||
|
)
|
||||||
|
purchase_form = purchase_form_set(
|
||||||
|
request.POST or None,
|
||||||
|
queryset=purchases_objects
|
||||||
|
)
|
||||||
|
if invoice_form.is_valid() and purchase_form.is_valid():
|
||||||
|
if invoice_form.changed_data:
|
||||||
|
invoice_form.save()
|
||||||
|
purchase_form.save()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The cost estimate was edited.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||||
|
|
||||||
|
return form({
|
||||||
|
'factureform': invoice_form,
|
||||||
|
'venteform': purchase_form,
|
||||||
|
'title': "Edit the cost estimate"
|
||||||
|
}, 'cotisations/edit_facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(CostEstimate)
|
||||||
|
@can_create(CustomInvoice)
|
||||||
|
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
|
||||||
|
"""Create a custom invoice from a cos estimate"""
|
||||||
|
cost_estimate.create_invoice()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("An invoice was successfully created from your cost estimate.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-custom-invoice'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_edit(CustomInvoice)
|
@can_edit(CustomInvoice)
|
||||||
def edit_custom_invoice(request, invoice, **kwargs):
|
def edit_custom_invoice(request, invoice, **kwargs):
|
||||||
|
@ -364,10 +474,10 @@ def edit_custom_invoice(request, invoice, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_view(CustomInvoice)
|
@can_view(CostEstimate)
|
||||||
def custom_invoice_pdf(request, invoice, **_kwargs):
|
def cost_estimate_pdf(request, invoice, **_kwargs):
|
||||||
"""
|
"""
|
||||||
View used to generate a PDF file from an existing invoice in database
|
View used to generate a PDF file from an existing cost estimate in database
|
||||||
Creates a line for each Purchase (thus article sold) and generate the
|
Creates a line for each Purchase (thus article sold) and generate the
|
||||||
invoice with the total price, the payment method, the address and the
|
invoice with the total price, the payment method, the address and the
|
||||||
legal information for the user.
|
legal information for the user.
|
||||||
|
@ -379,7 +489,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||||
purchases_info = []
|
purchases_info = []
|
||||||
for purchase in purchases_objects:
|
for purchase in purchases_objects:
|
||||||
purchases_info.append({
|
purchases_info.append({
|
||||||
'name': purchase.name,
|
'name': escape_chars(purchase.name),
|
||||||
'price': purchase.prix,
|
'price': purchase.prix,
|
||||||
'quantity': purchase.number,
|
'quantity': purchase.number,
|
||||||
'total_price': purchase.prix_total
|
'total_price': purchase.prix_total
|
||||||
|
@ -398,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||||
'siret': AssoOption.get_cached_value('siret'),
|
'siret': AssoOption.get_cached_value('siret'),
|
||||||
'email': AssoOption.get_cached_value('contact'),
|
'email': AssoOption.get_cached_value('contact'),
|
||||||
'phone': AssoOption.get_cached_value('telephone'),
|
'phone': AssoOption.get_cached_value('telephone'),
|
||||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||||
|
'payment_method': invoice.payment,
|
||||||
|
'remark': invoice.remark,
|
||||||
|
'end_validity': invoice.date + invoice.validity,
|
||||||
|
'is_estimate': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_delete(CostEstimate)
|
||||||
|
def del_cost_estimate(request, estimate, **_kwargs):
|
||||||
|
"""
|
||||||
|
View used to delete an existing invocie.
|
||||||
|
"""
|
||||||
|
if request.method == "POST":
|
||||||
|
estimate.delete()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The cost estimate was deleted.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||||
|
return form({
|
||||||
|
'objet': estimate,
|
||||||
|
'objet_name': _("Cost Estimate")
|
||||||
|
}, 'cotisations/delete.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_view(CustomInvoice)
|
||||||
|
def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||||
|
"""
|
||||||
|
View used to generate a PDF file from an existing invoice in database
|
||||||
|
Creates a line for each Purchase (thus article sold) and generate the
|
||||||
|
invoice with the total price, the payment method, the address and the
|
||||||
|
legal information for the user.
|
||||||
|
"""
|
||||||
|
# TODO : change vente to purchase
|
||||||
|
purchases_objects = Vente.objects.all().filter(facture=invoice)
|
||||||
|
# Get the article list and build an list out of it
|
||||||
|
# contiaining (article_name, article_price, quantity, total_price)
|
||||||
|
purchases_info = []
|
||||||
|
for purchase in purchases_objects:
|
||||||
|
purchases_info.append({
|
||||||
|
'name': escape_chars(purchase.name),
|
||||||
|
'price': purchase.prix,
|
||||||
|
'quantity': purchase.number,
|
||||||
|
'total_price': purchase.prix_total
|
||||||
|
})
|
||||||
|
return render_invoice(request, {
|
||||||
|
'paid': invoice.paid,
|
||||||
|
'fid': invoice.id,
|
||||||
|
'DATE': invoice.date,
|
||||||
|
'recipient_name': invoice.recipient,
|
||||||
|
'address': invoice.address,
|
||||||
|
'article': purchases_info,
|
||||||
|
'total': invoice.prix_total(),
|
||||||
|
'asso_name': AssoOption.get_cached_value('name'),
|
||||||
|
'line1': AssoOption.get_cached_value('adresse1'),
|
||||||
|
'line2': AssoOption.get_cached_value('adresse2'),
|
||||||
|
'siret': AssoOption.get_cached_value('siret'),
|
||||||
|
'email': AssoOption.get_cached_value('contact'),
|
||||||
|
'phone': AssoOption.get_cached_value('telephone'),
|
||||||
|
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||||
|
'payment_method': invoice.payment,
|
||||||
|
'remark': invoice.remark,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# TODO : change facture to invoice
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_delete(CustomInvoice)
|
@can_delete(CustomInvoice)
|
||||||
def del_custom_invoice(request, invoice, **_kwargs):
|
def del_custom_invoice(request, invoice, **_kwargs):
|
||||||
|
@ -753,12 +926,35 @@ def index_banque(request):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_view_all(CustomInvoice)
|
||||||
|
def index_cost_estimate(request):
|
||||||
|
"""View used to display every custom invoice."""
|
||||||
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
|
cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
|
||||||
|
cost_estimate_list = SortTable.sort(
|
||||||
|
cost_estimate_list,
|
||||||
|
request.GET.get('col'),
|
||||||
|
request.GET.get('order'),
|
||||||
|
SortTable.COTISATIONS_CUSTOM
|
||||||
|
)
|
||||||
|
cost_estimate_list = re2o_paginator(
|
||||||
|
request,
|
||||||
|
cost_estimate_list,
|
||||||
|
pagination_number,
|
||||||
|
)
|
||||||
|
return render(request, 'cotisations/index_cost_estimate.html', {
|
||||||
|
'cost_estimate_list': cost_estimate_list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_view_all(CustomInvoice)
|
@can_view_all(CustomInvoice)
|
||||||
def index_custom_invoice(request):
|
def index_custom_invoice(request):
|
||||||
"""View used to display every custom invoice."""
|
"""View used to display every custom invoice."""
|
||||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
|
cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
|
||||||
|
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
|
||||||
custom_invoice_list = SortTable.sort(
|
custom_invoice_list = SortTable.sort(
|
||||||
custom_invoice_list,
|
custom_invoice_list,
|
||||||
request.GET.get('col'),
|
request.GET.get('col'),
|
||||||
|
|
|
@ -57,14 +57,9 @@ application = get_wsgi_application()
|
||||||
from machines.models import Interface, IpList, Nas, Domain
|
from machines.models import Interface, IpList, Nas, Domain
|
||||||
from topologie.models import Port, Switch
|
from topologie.models import Port, Switch
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from preferences.models import OptionalTopologie
|
from preferences.models import RadiusOption
|
||||||
|
|
||||||
|
|
||||||
options, created = OptionalTopologie.objects.get_or_create()
|
|
||||||
VLAN_NOK = options.vlan_decision_nok.vlan_id
|
|
||||||
VLAN_OK = options.vlan_decision_ok.vlan_id
|
|
||||||
RADIUS_POLICY = options.radius_general_policy
|
|
||||||
|
|
||||||
#: Serveur radius de test (pas la prod)
|
#: Serveur radius de test (pas la prod)
|
||||||
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
|
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
|
||||||
|
|
||||||
|
@ -287,6 +282,7 @@ def find_nas_from_request(nas_id):
|
||||||
.select_related('machine__switch__stack'))
|
.select_related('machine__switch__stack'))
|
||||||
return nas.first()
|
return nas.first()
|
||||||
|
|
||||||
|
|
||||||
def check_user_machine_and_register(nas_type, username, mac_address):
|
def check_user_machine_and_register(nas_type, username, mac_address):
|
||||||
"""Verifie le username et la mac renseignee. L'enregistre si elle est
|
"""Verifie le username et la mac renseignee. L'enregistre si elle est
|
||||||
inconnue.
|
inconnue.
|
||||||
|
@ -331,28 +327,46 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
||||||
"""Fonction de placement vlan pour un switch en radius filaire auth par
|
"""Fonction de placement vlan pour un switch en radius filaire auth par
|
||||||
mac.
|
mac.
|
||||||
Plusieurs modes :
|
Plusieurs modes :
|
||||||
- nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK
|
- tous les modes:
|
||||||
- pas de radius sur le port : VLAN_OK
|
- nas inconnu: VLAN_OK
|
||||||
- bloq : VLAN_NOK
|
- port inconnu: Politique définie dans RadiusOption
|
||||||
- force : placement sur le vlan indiqué dans la bdd
|
- pas de radius sur le port: VLAN_OK
|
||||||
- mode strict :
|
- force: placement sur le vlan indiqué dans la bdd
|
||||||
- pas de chambre associée : VLAN_NOK
|
- mode strict:
|
||||||
- pas d'utilisateur dans la chambre : VLAN_NOK
|
- pas de chambre associée: Politique définie
|
||||||
- cotisation non à jour : VLAN_NOK
|
dans RadiusOption
|
||||||
|
- pas d'utilisateur dans la chambre : Rejet
|
||||||
|
(redirection web si disponible)
|
||||||
|
- utilisateur de la chambre banni ou désactivé : Rejet
|
||||||
|
(redirection web si disponible)
|
||||||
|
- utilisateur de la chambre non cotisant et non whiteslist:
|
||||||
|
Politique définie dans RadiusOption
|
||||||
|
|
||||||
- sinon passe à common (ci-dessous)
|
- sinon passe à common (ci-dessous)
|
||||||
- mode common :
|
- mode common :
|
||||||
- interface connue (macaddress):
|
- interface connue (macaddress):
|
||||||
- utilisateur proprio non cotisant ou banni : VLAN_NOK
|
- utilisateur proprio non cotisant / machine désactivée:
|
||||||
- user à jour : VLAN_OK
|
Politique définie dans RadiusOption
|
||||||
|
- utilisateur proprio banni :
|
||||||
|
Politique définie dans RadiusOption
|
||||||
|
- user à jour : VLAN_OK (réassignation de l'ipv4 au besoin)
|
||||||
- interface inconnue :
|
- interface inconnue :
|
||||||
- register mac désactivé : VLAN_NOK
|
- register mac désactivé : Politique définie
|
||||||
- register mac activé -> redirection vers webauth
|
dans RadiusOption
|
||||||
|
- register mac activé: redirection vers webauth
|
||||||
|
Returns:
|
||||||
|
tuple avec :
|
||||||
|
- Nom du switch (str)
|
||||||
|
- chambre (str)
|
||||||
|
- raison de la décision (str)
|
||||||
|
- vlan_id (int)
|
||||||
|
- decision (bool)
|
||||||
"""
|
"""
|
||||||
# Get port from switch and port number
|
# Get port from switch and port number
|
||||||
extra_log = ""
|
extra_log = ""
|
||||||
# Si le NAS est inconnu, on place sur le vlan defaut
|
# Si le NAS est inconnu, on place sur le vlan defaut
|
||||||
if not nas_machine:
|
if not nas_machine:
|
||||||
return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK, True)
|
return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True)
|
||||||
|
|
||||||
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
|
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
|
||||||
|
|
||||||
|
@ -367,7 +381,13 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
||||||
# Aucune information particulière ne permet de déterminer quelle
|
# Aucune information particulière ne permet de déterminer quelle
|
||||||
# politique à appliquer sur ce port
|
# politique à appliquer sur ce port
|
||||||
if not port:
|
if not port:
|
||||||
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK, True)
|
return (
|
||||||
|
sw_name,
|
||||||
|
"Chambre inconnue",
|
||||||
|
u'Port inconnu',
|
||||||
|
getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None),
|
||||||
|
RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT
|
||||||
|
)
|
||||||
|
|
||||||
# On récupère le profil du port
|
# On récupère le profil du port
|
||||||
port_profile = port.get_port_profile
|
port_profile = port.get_port_profile
|
||||||
|
@ -378,11 +398,11 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
||||||
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
|
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
|
||||||
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
|
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
|
||||||
else:
|
else:
|
||||||
DECISION_VLAN = VLAN_OK
|
DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok')
|
||||||
|
|
||||||
# Si le port est désactivé, on rejette sur le vlan de déconnexion
|
# Si le port est désactivé, on rejette la connexion
|
||||||
if not port.state:
|
if not port.state:
|
||||||
return (sw_name, port.room, u'Port desactivé', VLAN_NOK, True)
|
return (sw_name, port.room, u'Port desactivé', None, False)
|
||||||
|
|
||||||
# Si radius est désactivé, on laisse passer
|
# Si radius est désactivé, on laisse passer
|
||||||
if port_profile.radius_type == 'NO':
|
if port_profile.radius_type == 'NO':
|
||||||
|
@ -392,33 +412,68 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
||||||
DECISION_VLAN,
|
DECISION_VLAN,
|
||||||
True)
|
True)
|
||||||
|
|
||||||
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
|
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a
|
||||||
|
# été accept précédemment
|
||||||
# Par conséquent, on laisse passer sur le bon vlan
|
# Par conséquent, on laisse passer sur le bon vlan
|
||||||
if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
|
if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'):
|
||||||
room = port.room or "Chambre/local inconnu"
|
room = port.room or "Chambre/local inconnu"
|
||||||
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN, True)
|
return (
|
||||||
|
sw_name,
|
||||||
|
room,
|
||||||
|
u'Acceptation authentification 802.1X',
|
||||||
|
DECISION_VLAN,
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
# Sinon, cela veut dire qu'on fait de l'auth radius par mac
|
# Sinon, cela veut dire qu'on fait de l'auth radius par mac
|
||||||
# Si le port est en mode strict, on vérifie que tous les users
|
# Si le port est en mode strict, on vérifie que tous les users
|
||||||
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
|
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette
|
||||||
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
|
# (anti squattage)
|
||||||
# dedans
|
# Il n'est pas possible de se connecter sur une prise strict sans adhérent
|
||||||
|
# à jour de cotis dedans
|
||||||
if port_profile.radius_mode == 'STRICT':
|
if port_profile.radius_mode == 'STRICT':
|
||||||
room = port.room
|
room = port.room
|
||||||
if not room:
|
if not room:
|
||||||
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK, True)
|
return (
|
||||||
|
sw_name,
|
||||||
|
"Inconnue",
|
||||||
|
u'Chambre inconnue',
|
||||||
|
getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None),
|
||||||
|
RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT
|
||||||
|
)
|
||||||
|
|
||||||
room_user = User.objects.filter(
|
room_user = User.objects.filter(
|
||||||
Q(club__room=port.room) | Q(adherent__room=port.room)
|
Q(club__room=port.room) | Q(adherent__room=port.room)
|
||||||
)
|
)
|
||||||
if not room_user:
|
if not room_user:
|
||||||
return (sw_name, room, u'Chambre non cotisante -> Web redirect', None, False)
|
return (
|
||||||
|
sw_name,
|
||||||
|
room,
|
||||||
|
u'Chambre non cotisante -> Web redirect',
|
||||||
|
None,
|
||||||
|
False
|
||||||
|
)
|
||||||
for user in room_user:
|
for user in room_user:
|
||||||
if not user.has_access():
|
if user.is_ban() or user.state != User.STATE_ACTIVE:
|
||||||
return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False)
|
return (
|
||||||
|
sw_name,
|
||||||
|
room,
|
||||||
|
u'Utilisateur banni ou désactivé -> Web redirect',
|
||||||
|
None,
|
||||||
|
False
|
||||||
|
)
|
||||||
|
elif not (user.is_connected() or user.is_whitelisted()):
|
||||||
|
return (
|
||||||
|
sw_name,
|
||||||
|
room,
|
||||||
|
u'Utilisateur non cotisant',
|
||||||
|
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
|
||||||
|
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
|
||||||
|
)
|
||||||
# else: user OK, on passe à la verif MAC
|
# else: user OK, on passe à la verif MAC
|
||||||
|
|
||||||
# Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
|
# Si on fait de l'auth par mac, on cherche l'interface
|
||||||
|
# via sa mac dans la bdd
|
||||||
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
|
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
|
||||||
# Authentification par mac
|
# Authentification par mac
|
||||||
interface = (Interface.objects
|
interface = (Interface.objects
|
||||||
|
@ -428,38 +483,67 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
||||||
.first())
|
.first())
|
||||||
if not interface:
|
if not interface:
|
||||||
room = port.room
|
room = port.room
|
||||||
# On essaye de register la mac, si l'autocapture a été activée
|
# On essaye de register la mac, si l'autocapture a été activée,
|
||||||
# Sinon on rejette sur vlan_nok
|
# on rejette pour faire une redirection web si possible.
|
||||||
if not nas_type.autocapture_mac:
|
if nas_type.autocapture_mac:
|
||||||
return (sw_name, "", u'Machine inconnue', VLAN_NOK, True)
|
return (
|
||||||
# On rejette pour basculer sur du webauth
|
sw_name,
|
||||||
|
room,
|
||||||
|
u'Machine Inconnue -> Web redirect',
|
||||||
|
None,
|
||||||
|
False
|
||||||
|
)
|
||||||
|
# Sinon on bascule sur la politique définie dans les options
|
||||||
|
# radius.
|
||||||
else:
|
else:
|
||||||
return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False)
|
return (
|
||||||
|
sw_name,
|
||||||
|
"",
|
||||||
|
u'Machine inconnue',
|
||||||
|
getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None),
|
||||||
|
RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT
|
||||||
|
)
|
||||||
|
|
||||||
# L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
|
# L'interface a été trouvée, on vérifie qu'elle est active,
|
||||||
|
# sinon on reject
|
||||||
# Si elle n'a pas d'ipv4, on lui en met une
|
# Si elle n'a pas d'ipv4, on lui en met une
|
||||||
# Enfin on laisse passer sur le vlan pertinent
|
# Enfin on laisse passer sur le vlan pertinent
|
||||||
else:
|
else:
|
||||||
room = port.room
|
room = port.room
|
||||||
|
if interface.machine.user.is_ban():
|
||||||
|
return (
|
||||||
|
sw_name,
|
||||||
|
room,
|
||||||
|
u'Adherent banni',
|
||||||
|
getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None),
|
||||||
|
RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT
|
||||||
|
)
|
||||||
if not interface.is_active:
|
if not interface.is_active:
|
||||||
return (sw_name,
|
return (
|
||||||
|
sw_name,
|
||||||
room,
|
room,
|
||||||
u'Machine non active / adherent non cotisant',
|
u'Machine non active / adherent non cotisant',
|
||||||
VLAN_NOK,
|
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
|
||||||
True)
|
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
|
||||||
## Si on choisi de placer les machines sur le vlan correspondant à leur type :
|
)
|
||||||
if RADIUS_POLICY == 'MACHINE':
|
# Si on choisi de placer les machines sur le vlan
|
||||||
|
# correspondant à leur type :
|
||||||
|
if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE':
|
||||||
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
|
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
|
||||||
if not interface.ipv4:
|
if not interface.ipv4:
|
||||||
interface.assign_ipv4()
|
interface.assign_ipv4()
|
||||||
return (sw_name,
|
return (
|
||||||
|
sw_name,
|
||||||
room,
|
room,
|
||||||
u"Ok, Reassignation de l'ipv4" + extra_log,
|
u"Ok, Reassignation de l'ipv4" + extra_log,
|
||||||
DECISION_VLAN,
|
DECISION_VLAN,
|
||||||
True)
|
True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return (sw_name,
|
return (
|
||||||
|
sw_name,
|
||||||
room,
|
room,
|
||||||
u'Machine OK' + extra_log,
|
u'Machine OK' + extra_log,
|
||||||
DECISION_VLAN,
|
DECISION_VLAN,
|
||||||
True)
|
True
|
||||||
|
)
|
||||||
|
|
|
@ -316,6 +316,10 @@ update_django() {
|
||||||
echo "Collecting web frontend statics ..."
|
echo "Collecting web frontend statics ..."
|
||||||
python3 manage.py collectstatic --noinput
|
python3 manage.py collectstatic --noinput
|
||||||
echo "Collecting web frontend statics: Done"
|
echo "Collecting web frontend statics: Done"
|
||||||
|
|
||||||
|
echo "Generating locales ..."
|
||||||
|
python3 manage.py compilemessages
|
||||||
|
echo "Generating locales: Done"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -102,15 +102,18 @@ from re2o.utils import (
|
||||||
all_baned,
|
all_baned,
|
||||||
all_has_access,
|
all_has_access,
|
||||||
all_adherent,
|
all_adherent,
|
||||||
|
all_active_assigned_interfaces_count,
|
||||||
|
all_active_interfaces_count,
|
||||||
|
)
|
||||||
|
from re2o.base import (
|
||||||
re2o_paginator,
|
re2o_paginator,
|
||||||
|
SortTable
|
||||||
)
|
)
|
||||||
from re2o.acl import (
|
from re2o.acl import (
|
||||||
can_view_all,
|
can_view_all,
|
||||||
can_view_app,
|
can_view_app,
|
||||||
can_edit_history,
|
can_edit_history,
|
||||||
)
|
)
|
||||||
from re2o.utils import all_active_assigned_interfaces_count
|
|
||||||
from re2o.utils import all_active_interfaces_count, SortTable
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -273,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm):
|
||||||
self.fields['origin'].label = _("A record origin")
|
self.fields['origin'].label = _("A record origin")
|
||||||
self.fields['origin_v6'].label = _("AAAA record origin")
|
self.fields['origin_v6'].label = _("AAAA record origin")
|
||||||
self.fields['soa'].label = _("SOA record to use")
|
self.fields['soa'].label = _("SOA record to use")
|
||||||
|
self.fields['dnssec'].label = _("Sign with DNSSEC")
|
||||||
|
|
||||||
|
|
||||||
class DelExtensionForm(FormRevMixin, Form):
|
class DelExtensionForm(FormRevMixin, Form):
|
||||||
|
|
Binary file not shown.
20
machines/migrations/0097_extension_dnssec.py
Normal file
20
machines/migrations/0097_extension_dnssec.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-24 14:00
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0096_auto_20181013_1417'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='extension',
|
||||||
|
name='dnssec',
|
||||||
|
field=models.BooleanField(default=False, help_text='Should the zone be signed with DNSSEC'),
|
||||||
|
),
|
||||||
|
]
|
20
machines/migrations/0098_auto_20190102_1745.py
Normal file
20
machines/migrations/0098_auto_20190102_1745.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2019-01-02 23:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0097_extension_dnssec'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='specific_role',
|
||||||
|
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
|
||||||
|
),
|
||||||
|
]
|
26
machines/migrations/0099_role_recursive_dns.py
Normal file
26
machines/migrations/0099_role_recursive_dns.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2019-01-02 23:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(apps, schema_editor):
|
||||||
|
Role = apps.get_model('machines', 'Role')
|
||||||
|
|
||||||
|
for role in Role.objects.filter(specific_role='dns-recursif-server'):
|
||||||
|
role.specific_role = 'dns-recursive-server'
|
||||||
|
role.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0098_auto_20190102_1745'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
20
machines/migrations/0100_auto_20190102_1753.py
Normal file
20
machines/migrations/0100_auto_20190102_1753.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2019-01-02 23:53
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0099_role_recursive_dns'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='specific_role',
|
||||||
|
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -696,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model):
|
||||||
'SOA',
|
'SOA',
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
dnssec = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("Should the zone be signed with DNSSEC")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -741,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model):
|
||||||
.filter(cname__interface_parent__in=all_active_assigned_interfaces())
|
.filter(cname__interface_parent__in=all_active_assigned_interfaces())
|
||||||
.prefetch_related('cname'))
|
.prefetch_related('cname'))
|
||||||
|
|
||||||
|
def get_associated_dname_records(self):
|
||||||
|
return (DName.objects.filter(alias=self))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_use_all(user_request, *_args, **_kwargs):
|
def can_use_all(user_request, *_args, **_kwargs):
|
||||||
"""Superdroit qui permet d'utiliser toutes les extensions sans
|
"""Superdroit qui permet d'utiliser toutes les extensions sans
|
||||||
|
@ -1089,7 +1096,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||||
.get_cached_value('ipv6_mode') == 'DHCPV6'):
|
.get_cached_value('ipv6_mode') == 'DHCPV6'):
|
||||||
return self.ipv6list.filter(slaac_ip=False)
|
return self.ipv6list.filter(slaac_ip=False)
|
||||||
else:
|
else:
|
||||||
return None
|
return []
|
||||||
|
|
||||||
def mac_bare(self):
|
def mac_bare(self):
|
||||||
""" Formatage de la mac type mac_bare"""
|
""" Formatage de la mac type mac_bare"""
|
||||||
|
@ -1373,7 +1380,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||||
.filter(interface=self.interface, slaac_ip=True)
|
.filter(interface=self.interface, slaac_ip=True)
|
||||||
.exclude(id=self.id)):
|
.exclude(id=self.id)):
|
||||||
raise ValidationError(_("A SLAAC IP address is already registered."))
|
raise ValidationError(_("A SLAAC IP address is already registered."))
|
||||||
|
try:
|
||||||
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
|
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
|
||||||
|
except AttributeError: # Prevents from crashing when there is no defined prefix_v6
|
||||||
|
prefix_v6 = None
|
||||||
if prefix_v6:
|
if prefix_v6:
|
||||||
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
|
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
|
||||||
IPv6Address(prefix_v6).exploded[:20]):
|
IPv6Address(prefix_v6).exploded[:20]):
|
||||||
|
@ -1602,7 +1612,7 @@ class Role(RevMixin, AclMixin, models.Model):
|
||||||
ROLE = (
|
ROLE = (
|
||||||
('dhcp-server', _("DHCP server")),
|
('dhcp-server', _("DHCP server")),
|
||||||
('switch-conf-server', _("Switches configuration server")),
|
('switch-conf-server', _("Switches configuration server")),
|
||||||
('dns-recursif-server', _("Recursive DNS server")),
|
('dns-recursive-server', _("Recursive DNS server")),
|
||||||
('ntp-server', _("NTP server")),
|
('ntp-server', _("NTP server")),
|
||||||
('radius-server', _("RADIUS server")),
|
('radius-server', _("RADIUS server")),
|
||||||
('log-server', _("Log server")),
|
('log-server', _("Log server")),
|
||||||
|
@ -1631,18 +1641,6 @@ class Role(RevMixin, AclMixin, models.Model):
|
||||||
verbose_name = _("server role")
|
verbose_name = _("server role")
|
||||||
verbose_name_plural = _("server roles")
|
verbose_name_plural = _("server roles")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls, roleid, *_args, **_kwargs):
|
|
||||||
"""Get the Role instance with roleid.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
roleid: The id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The role.
|
|
||||||
"""
|
|
||||||
return cls.objects.get(pk=roleid)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def interface_for_roletype(cls, roletype):
|
def interface_for_roletype(cls, roletype):
|
||||||
"""Return interfaces for a roletype"""
|
"""Return interfaces for a roletype"""
|
||||||
|
@ -1657,14 +1655,6 @@ class Role(RevMixin, AclMixin, models.Model):
|
||||||
machine__interface__role=cls.objects.filter(specific_role=roletype)
|
machine__interface__role=cls.objects.filter(specific_role=roletype)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls, roleid, *_args, **_kwargs):
|
|
||||||
"""Get the Machine instance with machineid.
|
|
||||||
:param userid: The id
|
|
||||||
:return: The user
|
|
||||||
"""
|
|
||||||
return cls.objects.get(pk=roleid)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def interface_for_roletype(cls, roletype):
|
def interface_for_roletype(cls, roletype):
|
||||||
"""Return interfaces for a roletype"""
|
"""Return interfaces for a roletype"""
|
||||||
|
|
|
@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if ipv6_enabled %}
|
{% if ipv6_enabled %}
|
||||||
<th>{% trans "AAAA record origin" %}</th>
|
<th>{% trans "AAAA record origin" %}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<th>{% trans "DNSSEC" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -50,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if ipv6_enabled %}
|
{% if ipv6_enabled %}
|
||||||
<td>{{ extension.origin_v6 }}</td>
|
<td>{{ extension.origin_v6 }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<td>{{ extension.dnssec|tick }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% can_edit extension %}
|
{% can_edit extension %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
|
||||||
|
|
|
@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
{% if machines_list.paginator %}
|
{% if machines_list.paginator %}
|
||||||
{% include "pagination.html" with list=machines_list %}
|
{% include "pagination.html" with list=machines_list go_to_id="machines" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<table class="table" id="machines_table">
|
<table class="table" id="machines_table">
|
||||||
|
@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% if machines_list.paginator %}
|
{% if machines_list.paginator %}
|
||||||
{% include "pagination.html" with list=machines_list %}
|
{% include "pagination.html" with list=machines_list go_to_id="machines" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -95,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if interfaceform %}
|
{% if interfaceform %}
|
||||||
<h3>{% trans "Interface" %}</h3>
|
<h3>{% trans "Interface" %}</h3>
|
||||||
{% if i_mbf_param %}
|
{% if i_mbf_param %}
|
||||||
{% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %}
|
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% massive_bootstrap_form interfaceform 'ipv4,machine' %}
|
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if domainform %}
|
{% if domainform %}
|
||||||
|
@ -146,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if aliasform %}
|
{% if aliasform %}
|
||||||
<h3>{% trans "Alias" %}</h3>
|
<h3>{% trans "Alias" %}</h3>
|
||||||
{% bootstrap_form aliasform %}
|
{% massive_bootstrap_form aliasform 'extension' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if serviceform %}
|
{% if serviceform %}
|
||||||
<h3>{% trans "Service" %}</h3>
|
<h3>{% trans "Service" %}</h3>
|
||||||
|
|
|
@ -55,6 +55,8 @@ from re2o.acl import (
|
||||||
from re2o.utils import (
|
from re2o.utils import (
|
||||||
all_active_assigned_interfaces,
|
all_active_assigned_interfaces,
|
||||||
filter_active_interfaces,
|
filter_active_interfaces,
|
||||||
|
)
|
||||||
|
from re2o.base import (
|
||||||
SortTable,
|
SortTable,
|
||||||
re2o_paginator,
|
re2o_paginator,
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,6 +42,7 @@ from .models import (
|
||||||
Reminder,
|
Reminder,
|
||||||
RadiusKey,
|
RadiusKey,
|
||||||
SwitchManagementCred,
|
SwitchManagementCred,
|
||||||
|
RadiusOption,
|
||||||
)
|
)
|
||||||
from topologie.models import Switch
|
from topologie.models import Switch
|
||||||
|
|
||||||
|
@ -114,11 +115,6 @@ class EditOptionalTopologieForm(ModelForm):
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
self.fields['radius_general_policy'].label = _("RADIUS general policy")
|
|
||||||
self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted"
|
|
||||||
" by RADIUS")
|
|
||||||
self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected"
|
|
||||||
" by RADIUS")
|
|
||||||
|
|
||||||
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
|
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
|
||||||
|
|
||||||
|
@ -229,6 +225,13 @@ class EditHomeOptionForm(ModelForm):
|
||||||
self.fields['twitter_account_name'].label = _("Twitter account name")
|
self.fields['twitter_account_name'].label = _("Twitter account name")
|
||||||
|
|
||||||
|
|
||||||
|
class EditRadiusOptionForm(ModelForm):
|
||||||
|
"""Edition forms for Radius options"""
|
||||||
|
class Meta:
|
||||||
|
model = RadiusOption
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class ServiceForm(ModelForm):
|
class ServiceForm(ModelForm):
|
||||||
"""Edition, ajout de services sur la page d'accueil"""
|
"""Edition, ajout de services sur la page d'accueil"""
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
Binary file not shown.
20
preferences/migrations/0055_generaloption_main_site_url.py
Normal file
20
preferences/migrations/0055_generaloption_main_site_url.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-11-14 16:46
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('preferences', '0053_optionaluser_self_change_room'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='generaloption',
|
||||||
|
name='main_site_url',
|
||||||
|
field=models.URLField(default='http://re2o.example.org', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
84
preferences/migrations/0056_1_radiusoption.py
Normal file
84
preferences/migrations/0056_1_radiusoption.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-10-13 14:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import re2o.mixins
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0095_auto_20180919_2225'),
|
||||||
|
('preferences', '0055_generaloption_main_site_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RadiusOption',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('radius_general_policy', models.CharField(choices=[('MACHINE', "On the IP range's VLAN of the machine"), ('DEFINED', "Preset in 'VLAN for machines accepted by RADIUS'")], default='DEFINED', max_length=32)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'radius policies',
|
||||||
|
},
|
||||||
|
bases=(re2o.mixins.AclMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='banned_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Vlan for banned if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banned_vlan', to='machines.Vlan', verbose_name='Banned Vlan'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='non_member_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Vlan for non members if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='non_member_vlan', to='machines.Vlan', verbose_name='Non member Vlan'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_machine_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Vlan for unknown machines if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_vlan', to='machines.Vlan', verbose_name='Unknown machine Vlan'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_port_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Vlan for unknown ports if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_vlan', to='machines.Vlan', verbose_name='Unknown port Vlan'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_room_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Vlan for unknown room if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_vlan', to='machines.Vlan', verbose_name='Unknown room Vlan'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='banned',
|
||||||
|
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for banned users.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='non_member',
|
||||||
|
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy non member users.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_machine',
|
||||||
|
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_port',
|
||||||
|
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_room',
|
||||||
|
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='vlan_decision_ok',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'),
|
||||||
|
),
|
||||||
|
]
|
36
preferences/migrations/0056_2_radiusoption.py
Normal file
36
preferences/migrations/0056_2_radiusoption.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-10-13 14:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import re2o.mixins
|
||||||
|
|
||||||
|
|
||||||
|
def create_radius_policy(apps, schema_editor):
|
||||||
|
OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie')
|
||||||
|
RadiusOption = apps.get_model('preferences', 'RadiusOption')
|
||||||
|
|
||||||
|
option,_ = OptionalTopologie.objects.get_or_create()
|
||||||
|
|
||||||
|
radius_option = RadiusOption()
|
||||||
|
radius_option.radius_general_policy = option.radius_general_policy
|
||||||
|
radius_option.vlan_decision_ok = option.vlan_decision_ok
|
||||||
|
|
||||||
|
radius_option.save()
|
||||||
|
|
||||||
|
def revert_radius(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0095_auto_20180919_2225'),
|
||||||
|
('preferences', '0055_generaloption_main_site_url'),
|
||||||
|
('preferences', '0056_1_radiusoption'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_radius_policy, revert_radius),
|
||||||
|
]
|
31
preferences/migrations/0056_3_radiusoption.py
Normal file
31
preferences/migrations/0056_3_radiusoption.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-10-13 14:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import re2o.mixins
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0095_auto_20180919_2225'),
|
||||||
|
('preferences', '0055_generaloption_main_site_url'),
|
||||||
|
('preferences', '0056_2_radiusoption'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='optionaltopologie',
|
||||||
|
name='radius_general_policy',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='optionaltopologie',
|
||||||
|
name='vlan_decision_nok',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='optionaltopologie',
|
||||||
|
name='vlan_decision_ok',
|
||||||
|
),
|
||||||
|
]
|
20
preferences/migrations/0056_4_radiusoption.py
Normal file
20
preferences/migrations/0056_4_radiusoption.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-04 13:57
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('preferences', '0056_3_radiusoption'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='radiusoption',
|
||||||
|
name='unknown_port',
|
||||||
|
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown port'),
|
||||||
|
),
|
||||||
|
]
|
20
preferences/migrations/0057_optionaluser_all_users_active.py
Normal file
20
preferences/migrations/0057_optionaluser_all_users_active.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2019-01-05 17:15
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('preferences', '0056_4_radiusoption'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='optionaluser',
|
||||||
|
name='all_users_active',
|
||||||
|
field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -116,6 +116,11 @@ class OptionalUser(AclMixin, PreferencesModel):
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("A new user can create their account on Re2o")
|
help_text=_("A new user can create their account on Re2o")
|
||||||
)
|
)
|
||||||
|
all_users_active = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("If True, all new created and connected users are active.\
|
||||||
|
If False, only when a valid registration has been paid")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -199,25 +204,6 @@ class OptionalTopologie(AclMixin, PreferencesModel):
|
||||||
('tftp', 'tftp'),
|
('tftp', 'tftp'),
|
||||||
)
|
)
|
||||||
|
|
||||||
radius_general_policy = models.CharField(
|
|
||||||
max_length=32,
|
|
||||||
choices=CHOICE_RADIUS,
|
|
||||||
default='DEFINED'
|
|
||||||
)
|
|
||||||
vlan_decision_ok = models.OneToOneField(
|
|
||||||
'machines.Vlan',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='decision_ok',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
vlan_decision_nok = models.OneToOneField(
|
|
||||||
'machines.Vlan',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='decision_nok',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
switchs_web_management = models.BooleanField(
|
switchs_web_management = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text="Web management, activé si provision automatique"
|
help_text="Web management, activé si provision automatique"
|
||||||
|
@ -297,19 +283,19 @@ class OptionalTopologie(AclMixin, PreferencesModel):
|
||||||
log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type)
|
log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type)
|
||||||
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
|
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
|
||||||
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
|
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
|
||||||
|
dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type)
|
||||||
subnet = None
|
subnet = None
|
||||||
subnet6 = None
|
subnet6 = None
|
||||||
if self.switchs_ip_type:
|
if self.switchs_ip_type:
|
||||||
subnet = self.switchs_ip_type.ip_set_full_info
|
subnet = self.switchs_ip_type.ip_set_full_info
|
||||||
subnet6 = self.switchs_ip_type.ip6_set_full_info
|
subnet6 = self.switchs_ip_type.ip6_set_full_info
|
||||||
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6}
|
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def provision_switchs_enabled(self):
|
def provision_switchs_enabled(self):
|
||||||
"""Return true if all settings are ok : switchs on automatic provision,
|
"""Return true if all settings are ok : switchs on automatic provision,
|
||||||
ip_type"""
|
ip_type"""
|
||||||
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
|
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
("view_optionaltopologie", _("Can view the topology options")),
|
("view_optionaltopologie", _("Can view the topology options")),
|
||||||
|
@ -431,6 +417,7 @@ class GeneralOption(AclMixin, PreferencesModel):
|
||||||
req_expire_hrs = models.IntegerField(default=48)
|
req_expire_hrs = models.IntegerField(default=48)
|
||||||
site_name = models.CharField(max_length=32, default="Re2o")
|
site_name = models.CharField(max_length=32, default="Re2o")
|
||||||
email_from = models.EmailField(default="www-data@example.com")
|
email_from = models.EmailField(default="www-data@example.com")
|
||||||
|
main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
|
||||||
GTU_sum_up = models.TextField(
|
GTU_sum_up = models.TextField(
|
||||||
default="",
|
default="",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -587,3 +574,122 @@ class MailMessageOption(AclMixin, models.Model):
|
||||||
)
|
)
|
||||||
verbose_name = _("email message options")
|
verbose_name = _("email message options")
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusOption(AclMixin, PreferencesModel):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("radius policies")
|
||||||
|
|
||||||
|
MACHINE = 'MACHINE'
|
||||||
|
DEFINED = 'DEFINED'
|
||||||
|
CHOICE_RADIUS = (
|
||||||
|
(MACHINE, _("On the IP range's VLAN of the machine")),
|
||||||
|
(DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")),
|
||||||
|
)
|
||||||
|
REJECT = 'REJECT'
|
||||||
|
SET_VLAN = 'SET_VLAN'
|
||||||
|
CHOICE_POLICY = (
|
||||||
|
(REJECT, _('Reject the machine')),
|
||||||
|
(SET_VLAN, _('Place the machine on the VLAN'))
|
||||||
|
)
|
||||||
|
radius_general_policy = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=CHOICE_RADIUS,
|
||||||
|
default='DEFINED'
|
||||||
|
)
|
||||||
|
unknown_machine = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=CHOICE_POLICY,
|
||||||
|
default=REJECT,
|
||||||
|
verbose_name=_("Policy for unknown machines"),
|
||||||
|
)
|
||||||
|
unknown_machine_vlan = models.ForeignKey(
|
||||||
|
'machines.Vlan',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='unknown_machine_vlan',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Unknown machine Vlan'),
|
||||||
|
help_text=_(
|
||||||
|
'Vlan for unknown machines if not rejected.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
unknown_port = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=CHOICE_POLICY,
|
||||||
|
default=REJECT,
|
||||||
|
verbose_name=_("Policy for unknown port"),
|
||||||
|
)
|
||||||
|
unknown_port_vlan = models.ForeignKey(
|
||||||
|
'machines.Vlan',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='unknown_port_vlan',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Unknown port Vlan'),
|
||||||
|
help_text=_(
|
||||||
|
'Vlan for unknown ports if not rejected.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
unknown_room = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=CHOICE_POLICY,
|
||||||
|
default=REJECT,
|
||||||
|
verbose_name=_(
|
||||||
|
"Policy for machine connecting from "
|
||||||
|
"unregistered room (relevant on ports with STRICT "
|
||||||
|
"radius mode)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
unknown_room_vlan = models.ForeignKey(
|
||||||
|
'machines.Vlan',
|
||||||
|
related_name='unknown_room_vlan',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Unknown room Vlan'),
|
||||||
|
help_text=_(
|
||||||
|
'Vlan for unknown room if not rejected.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
non_member = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=CHOICE_POLICY,
|
||||||
|
default=REJECT,
|
||||||
|
verbose_name=_("Policy non member users."),
|
||||||
|
)
|
||||||
|
non_member_vlan = models.ForeignKey(
|
||||||
|
'machines.Vlan',
|
||||||
|
related_name='non_member_vlan',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Non member Vlan'),
|
||||||
|
help_text=_(
|
||||||
|
'Vlan for non members if not rejected.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
banned = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=CHOICE_POLICY,
|
||||||
|
default=REJECT,
|
||||||
|
verbose_name=_("Policy for banned users."),
|
||||||
|
)
|
||||||
|
banned_vlan = models.ForeignKey(
|
||||||
|
'machines.Vlan',
|
||||||
|
related_name='banned_vlan',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Banned Vlan'),
|
||||||
|
help_text=_(
|
||||||
|
'Vlan for banned if not rejected.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vlan_decision_ok = models.OneToOneField(
|
||||||
|
'machines.Vlan',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='vlan_ok_option',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
96
preferences/templates/preferences/aff_radiusoptions.html
Normal file
96
preferences/templates/preferences/aff_radiusoptions.html
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2018 Hugo Levy-Falk
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "General policy for VLAN setting" %}</th>
|
||||||
|
<td>{{ radiusoptions.radius_general_policy }}</td>
|
||||||
|
<td>{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "VLAN for machines accepted by RADIUS" %}</th>
|
||||||
|
<td><span class="label label-success">Vlan {{ radiusoptions.vlan_decision_ok }}</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<hr/>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Situation" %}</th>
|
||||||
|
<th>{% trans "Behavior" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Unknown machine" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if radiusoptions.unknown_machine == 'REJECT' %}
|
||||||
|
<span class="label label-danger">{% trans "Reject" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-success">Vlan {{ radiusoptions.unknown_machine_vlan }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Unknown port" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if radiusoptions.unknown_port == 'REJECT' %}
|
||||||
|
<span class="label label-danger">{% trans "Reject" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-success">Vlan {{ radiusoptions.unknown_port_vlan }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Unknown room" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if radiusoptions.unknown_room == 'REJECT' %}
|
||||||
|
<span class="label label-danger">{% trans "Reject" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-success">Vlan {{ radiusoptions.unknown_room_vlan }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Non member" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if radiusoptions.non_member == 'REJECT' %}
|
||||||
|
<span class="label label-danger">{% trans "Reject" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-success">Vlan {{ radiusoptions.non_member_vlan }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Banned user" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if radiusoptions.unknown_port == 'REJECT' %}
|
||||||
|
<span class="label label-danger">{% trans "Reject" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-success">Vlan {{ radiusoptions.banned_vlan }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
|
@ -31,14 +31,84 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% block title %}{% trans "Preferences" %}{% endblock %}
|
{% block title %}{% trans "Preferences" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h4>{% trans "User preferences" %}</h4>
|
<div id="accordion">
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="general">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_general">
|
||||||
|
<h4 class="panel-title" id="general">
|
||||||
|
<a><i class="fa fa-cog"></i> {% trans "General preferences" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="collapse_general" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
|
||||||
|
<i class="fa fa-edit"></i>{% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Website name" %}</th>
|
||||||
|
<td>{{ generaloptions.site_name }}</td>
|
||||||
|
<th>{% trans "Email address for automatic emailing" %}</th>
|
||||||
|
<td>{{ generaloptions.email_from }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Number of results displayed when searching" %}</th>
|
||||||
|
<td>{{ generaloptions.search_display_page }}</td>
|
||||||
|
<th>{% trans "Number of items per page (standard size)" %}</th>
|
||||||
|
<td>{{ generaloptions.pagination_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Number of items per page (large size)" %}</th>
|
||||||
|
<td>{{ generaloptions.pagination_large_number }}</td>
|
||||||
|
<th>{% trans "Time before expiration of the reset password link (in hours)" %}</th>
|
||||||
|
<td>{{ generaloptions.req_expire_hrs }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "General message displayed on the website" %}</th>
|
||||||
|
<td>{{ generaloptions.general_message }}</td>
|
||||||
|
<th>{% trans "Main site url" %}</th>
|
||||||
|
<td>{{ generaloptions.main_site_url }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Summary of the General Terms of Use" %}</th>
|
||||||
|
<td>{{ generaloptions.GTU_sum_up }}</td>
|
||||||
|
<th>{% trans "General Terms of Use" %}</th>
|
||||||
|
<td>{{ generaloptions.GTU }}</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Local email accounts enabled" %}</th>
|
||||||
|
<td>{{ useroptions.local_email_accounts_enabled|tick }}</td>
|
||||||
|
<th>{% trans "Local email domain" %}</th>
|
||||||
|
<td>{{ useroptions.local_email_domain }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Maximum number of email aliases allowed" %}</th>
|
||||||
|
<td>{{ useroptions.max_email_address }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="users">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_users">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-users fa-fw"></i> {% trans "User preferences" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_users" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
|
<p></p>
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans "Edit" %}
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<p>
|
<p></p>
|
||||||
</p>
|
|
||||||
<h5>{% trans "General preferences" %}</h5>
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Creation of members by everyone" %}</th>
|
<th>{% trans "Creation of members by everyone" %}</th>
|
||||||
|
@ -52,9 +122,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<th>{% trans "Delete not yet active users after" %}</th>
|
<th>{% trans "Delete not yet active users after" %}</th>
|
||||||
<td>{{ useroptions.delete_notyetactive }} days</td>
|
<td>{{ useroptions.delete_notyetactive }} days</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "All users are active by default" %}</th>
|
||||||
|
<td>{{ useroptions.all_users_active|tick }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h5>{% trans "Users general permissions" %}</h5>
|
<h4 id="users">{% trans "Users general permissions" %}</h4>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Default shell for users" %}</th>
|
<th>{% trans "Default shell for users" %}</th>
|
||||||
|
@ -73,27 +147,24 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ useroptions.gpg_fingerprint|tick }}</td>
|
<td>{{ useroptions.gpg_fingerprint|tick }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="machines">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_machines">
|
||||||
|
<h4 class ="panel-title">
|
||||||
|
<a><i class="fa fa-desktop"></i> {% trans "Machines preferences" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="collapse_machines" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
<h5>{% trans "Email accounts preferences" %}</h5>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Local email accounts enabled" %}</th>
|
|
||||||
<td>{{ useroptions.local_email_accounts_enabled|tick }}</td>
|
|
||||||
<th>{% trans "Local email domain" %}</th>
|
|
||||||
<td>{{ useroptions.local_email_domain }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Maximum number of email aliases allowed" %}</th>
|
|
||||||
<td>{{ useroptions.max_email_address }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<h4>{% trans "Machines preferences" %}</h4>
|
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans "Edit" %}
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<p>
|
<p></p>
|
||||||
</p>
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Password per machine" %}</th>
|
<th>{% trans "Password per machine" %}</th>
|
||||||
|
@ -112,13 +183,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ machineoptions.create_machine|tick }}</td>
|
<td>{{ machineoptions.create_machine|tick }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<h4>{% trans "Topology preferences" %}</h4>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="topo">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_topo">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-sitemap"></i> {% trans "Topology preferences" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_topo" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans "Edit" %}
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<p>
|
<p></p>
|
||||||
</p>
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "General policy for VLAN setting" %}</th>
|
<th>{% trans "General policy for VLAN setting" %}</th>
|
||||||
|
@ -133,18 +213,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ topologieoptions.vlan_decision_nok }}</td>
|
<td>{{ topologieoptions.vlan_decision_nok }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Placement sur ce vlan par default en cas de rejet</th>
|
<th>{% trans "VLAN for non members machines" %}</th>
|
||||||
<td>{{ topologieoptions.vlan_decision_nok }}</td>
|
<td>{{ topologieoptions.vlan_non_member }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h6>Clef radius</h6>
|
<h4>Clef radius</h4>
|
||||||
{% can_create RadiusKey%}
|
{% can_create RadiusKey%}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-radiuskey' %}"><i class="fa fa-plus"></i> Ajouter une clef radius</a>
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-radiuskey' %}"><i class="fa fa-plus"></i> Ajouter une clef radius</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
|
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
|
||||||
|
|
||||||
<h4>Configuration des switches</h4>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="switches">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_switches">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-server"></i> Configuration des Switches</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_switches" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
{% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
<p></p>
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Web management, activé si provision automatique</th>
|
<th>Web management, activé si provision automatique</th>
|
||||||
|
@ -192,53 +288,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</p>
|
</p>
|
||||||
{% if switchmanagementcred_list %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span>
|
{% if switchmanagementcred_list %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span>
|
||||||
{% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
|
{% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="radius">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_radius">
|
||||||
<h4>{% trans "General preferences" %}</h4>
|
<h4 class="panel-title"><a><i class="fa fa-circle"></i> {% trans "Radius preferences" %}</h4></a>
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
|
</div>
|
||||||
|
<div id="collapse_radius" class="panel-collapse panel-body collapse">
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'RadiusOption' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans "Edit" %}
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<p>
|
{% include "preferences/aff_radiusoptions.html" %}
|
||||||
</p>
|
</div>
|
||||||
<table class="table table-striped">
|
</div>
|
||||||
<tr>
|
|
||||||
<th>{% trans "Website name" %}</th>
|
<div class="panel panel-default" id="asso">
|
||||||
<td>{{ generaloptions.site_name }}</td>
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_asso">
|
||||||
<th>{% trans "Email address for automatic emailing" %}</th>
|
<h4 class="panel-title">
|
||||||
<td>{{ generaloptions.email_from }}</td>
|
<a><i class="fa fa-at"></i> {% trans "Information about the organisation" %}</a>
|
||||||
</tr>
|
</h4>
|
||||||
<tr>
|
</div>
|
||||||
<th>{% trans "Number of results displayed when searching" %}</th>
|
<div id="collapse_asso" class="panel-collapse panel-body collapse">
|
||||||
<td>{{ generaloptions.search_display_page }}</td>
|
|
||||||
<th>{% trans "Number of items per page (standard size)" %}</th>
|
|
||||||
<td>{{ generaloptions.pagination_number }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Number of items per page (large size)" %}</th>
|
|
||||||
<td>{{ generaloptions.pagination_large_number }}</td>
|
|
||||||
<th>{% trans "Time before expiration of the reset password link (in hours)" %}</th>
|
|
||||||
<td>{{ generaloptions.req_expire_hrs }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "General message displayed on the website" %}</th>
|
|
||||||
<td>{{ generaloptions.general_message }}</td>
|
|
||||||
<th>{% trans "Summary of the General Terms of Use" %}</th>
|
|
||||||
<td>{{ generaloptions.GTU_sum_up }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "General Terms of Use" %}</th>
|
|
||||||
<td>{{ generaloptions.GTU }}</th>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<h4>{% trans "Information about the organisation" %}</h4>
|
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans "Edit" %}
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<p>
|
<p></p>
|
||||||
</p>
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Name" %}</th>
|
<th>{% trans "Name" %}</th>
|
||||||
|
@ -267,13 +344,23 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ assooptions.description|safe }}</td>
|
<td>{{ assooptions.description|safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<h4>{% trans "Custom email message" %}</h4>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="mail">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_mail">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-comment"></i> Message pour les mails</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_mail" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans "Edit" %}
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<p>
|
<p></p>
|
||||||
</p>
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Welcome email (in French)" %}</th>
|
<th>{% trans "Welcome email (in French)" %}</th>
|
||||||
|
@ -284,29 +371,73 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
|
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<h4>Options pour le mail de fin d'adhésion</h2>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="rappels">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_rappels">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-bell"></i> Options pour le mail de fin d'adhésion</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_rappels" class="panel-collapse panel-body collapse">
|
||||||
{% can_create preferences.Reminder%}
|
{% can_create preferences.Reminder%}
|
||||||
|
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-reminder' %}"><i class="fa fa-plus"></i> Ajouter un rappel</a>
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-reminder' %}"><i class="fa fa-plus"></i> Ajouter un rappel</a>
|
||||||
|
<p></p>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
|
{% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>{% trans "List of services and homepage preferences" %}</h4>
|
|
||||||
|
<div class="panel panel-default" id="services">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_services">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-home"></i> {% trans "List of services and homepage preferences" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_services" class="panel-collapse panel-body collapse">
|
||||||
{% can_create preferences.Service%}
|
{% can_create preferences.Service%}
|
||||||
|
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i>{% trans " Add a service" %}</a>
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i>{% trans " Add a service" %}</a>
|
||||||
|
<p></p>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include "preferences/aff_service.html" with service_list=service_list %}
|
{% include "preferences/aff_service.html" with service_list=service_list %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
|
|
||||||
<i class="fa fa-edit"></i>
|
</div>
|
||||||
{% trans "Edit" %}
|
</div>
|
||||||
</a>
|
|
||||||
<h2>{% trans "List of contact email addresses" %}</h2>
|
<div class="panel panel-default" id="contact">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_contact">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-list-ul"></i> {% trans "List of contact email addresses" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_contact" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
{% can_create preferences.MailContact %}
|
{% can_create preferences.MailContact %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>{% trans "Add an address" %}</a>
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>{% trans "Add an address" %}</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>{% trans "Delete one or several addresses" %}</a>
|
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>{% trans "Delete one or several addresses" %}</a>
|
||||||
|
<p></p>
|
||||||
{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
|
{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
|
||||||
<p>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="social">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_social">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-facebook"></i><i class="fa fa-twitter"></i> Réseaux sociaux</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_social" class="panel-collapse panel-body collapse">
|
||||||
|
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
{% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
<p></p>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Twitter account URL" %}</th>
|
<th>{% trans "Twitter account URL" %}</th>
|
||||||
|
@ -319,5 +450,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ homeoptions.facebook_url }}</td>
|
<td>{{ homeoptions.facebook_url }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<form class="form" method="post" enctype="multipart/form-data">
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
|
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
|
||||||
|
{% if formset %}
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% for f in formset %}
|
||||||
|
{% bootstrap_form f %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% trans "Edit" as tr_edit %}
|
{% trans "Edit" as tr_edit %}
|
||||||
{% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
|
{% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -22,6 +22,8 @@ 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.,
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
|
|
@ -66,6 +66,11 @@ urlpatterns = [
|
||||||
views.edit_options,
|
views.edit_options,
|
||||||
name='edit-options'
|
name='edit-options'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^edit_options/(?P<section>RadiusOption)$',
|
||||||
|
views.edit_options,
|
||||||
|
name='edit-options'
|
||||||
|
),
|
||||||
url(r'^add_service/$', views.add_service, name='add-service'),
|
url(r'^add_service/$', views.add_service, name='add-service'),
|
||||||
url(
|
url(
|
||||||
r'^edit_service/(?P<serviceid>[0-9]+)$',
|
r'^edit_service/(?P<serviceid>[0-9]+)$',
|
||||||
|
|
|
@ -62,7 +62,8 @@ from .models import (
|
||||||
HomeOption,
|
HomeOption,
|
||||||
Reminder,
|
Reminder,
|
||||||
RadiusKey,
|
RadiusKey,
|
||||||
SwitchManagementCred
|
SwitchManagementCred,
|
||||||
|
RadiusOption,
|
||||||
)
|
)
|
||||||
from . import models
|
from . import models
|
||||||
from . import forms
|
from . import forms
|
||||||
|
@ -86,6 +87,7 @@ def display_options(request):
|
||||||
reminder_list = Reminder.objects.all()
|
reminder_list = Reminder.objects.all()
|
||||||
radiuskey_list = RadiusKey.objects.all()
|
radiuskey_list = RadiusKey.objects.all()
|
||||||
switchmanagementcred_list = SwitchManagementCred.objects.all()
|
switchmanagementcred_list = SwitchManagementCred.objects.all()
|
||||||
|
radiusoptions, _ = RadiusOption.objects.get_or_create()
|
||||||
return form({
|
return form({
|
||||||
'useroptions': useroptions,
|
'useroptions': useroptions,
|
||||||
'machineoptions': machineoptions,
|
'machineoptions': machineoptions,
|
||||||
|
@ -99,6 +101,7 @@ def display_options(request):
|
||||||
'reminder_list': reminder_list,
|
'reminder_list': reminder_list,
|
||||||
'radiuskey_list' : radiuskey_list,
|
'radiuskey_list' : radiuskey_list,
|
||||||
'switchmanagementcred_list': switchmanagementcred_list,
|
'switchmanagementcred_list': switchmanagementcred_list,
|
||||||
|
'radiusoptions' : radiusoptions,
|
||||||
}, 'preferences/display_preferences.html', request)
|
}, 'preferences/display_preferences.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,7 +137,9 @@ def edit_options(request, section):
|
||||||
messages.success(request, _("The preferences were edited."))
|
messages.success(request, _("The preferences were edited."))
|
||||||
return redirect(reverse('preferences:display-options'))
|
return redirect(reverse('preferences:display-options'))
|
||||||
return form(
|
return form(
|
||||||
{'options': options},
|
{
|
||||||
|
'options': options,
|
||||||
|
},
|
||||||
'preferences/edit_preferences.html',
|
'preferences/edit_preferences.html',
|
||||||
request
|
request
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,8 +37,8 @@ from django.db import models
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
EOD = '`%EofD%`' # This should be something that will not occur in strings
|
EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings
|
||||||
|
EOD = EOD_asbyte.decode('utf-8')
|
||||||
|
|
||||||
def genstring(length=16, chars=string.printable):
|
def genstring(length=16, chars=string.printable):
|
||||||
""" Generate a random string of length `length` and composed of
|
""" Generate a random string of length `length` and composed of
|
||||||
|
@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable):
|
||||||
return ''.join([choice(chars) for i in range(length)])
|
return ''.join([choice(chars) for i in range(length)])
|
||||||
|
|
||||||
|
|
||||||
def encrypt(key, s):
|
def encrypt(key, secret):
|
||||||
""" AES Encrypt a secret `s` with the key `key` """
|
""" AES Encrypt a secret with the key `key` """
|
||||||
obj = AES.new(key)
|
obj = AES.new(key)
|
||||||
datalength = len(s) + len(EOD)
|
datalength = len(secret) + len(EOD)
|
||||||
if datalength < 16:
|
if datalength < 16:
|
||||||
saltlength = 16 - datalength
|
saltlength = 16 - datalength
|
||||||
else:
|
else:
|
||||||
saltlength = 16 - datalength % 16
|
saltlength = 16 - datalength % 16
|
||||||
ss = ''.join([s, EOD, genstring(saltlength)])
|
encrypted_secret = ''.join([secret, EOD, genstring(saltlength)])
|
||||||
return obj.encrypt(ss)
|
return obj.encrypt(encrypted_secret)
|
||||||
|
|
||||||
|
|
||||||
def decrypt(key, s):
|
def decrypt(key, secret):
|
||||||
""" AES Decrypt a secret `s` with the key `key` """
|
""" AES Decrypt a secret with the key `key` """
|
||||||
obj = AES.new(key)
|
obj = AES.new(key)
|
||||||
ss = obj.decrypt(s)
|
uncrypted_secret = obj.decrypt(secret)
|
||||||
return ss.split(bytes(EOD, 'utf-8'))[0]
|
return uncrypted_secret.split(EOD_asbyte)[0]
|
||||||
|
|
||||||
|
|
||||||
class AESEncryptedFormField(forms.CharField):
|
class AESEncryptedFormField(forms.CharField):
|
||||||
|
@ -81,8 +81,7 @@ class AESEncryptedField(models.CharField):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return decrypt(settings.AES_KEY,
|
return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
|
||||||
binascii.a2b_base64(value)).decode('utf-8')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(value)
|
raise ValueError(value)
|
||||||
|
|
||||||
|
@ -90,18 +89,14 @@ class AESEncryptedField(models.CharField):
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
try:
|
try:
|
||||||
return decrypt(settings.AES_KEY,
|
return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
|
||||||
binascii.a2b_base64(value)).decode('utf-8')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(value)
|
raise ValueError(value)
|
||||||
|
|
||||||
def get_prep_value(self, value):
|
def get_prep_value(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
return binascii.b2a_base64(encrypt(
|
return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8')
|
||||||
settings.AES_KEY,
|
|
||||||
value
|
|
||||||
)).decode('utf-8')
|
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
defaults = {'form_class': AESEncryptedFormField}
|
defaults = {'form_class': AESEncryptedFormField}
|
||||||
|
|
267
re2o/base.py
Normal file
267
re2o/base.py
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
|
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
# quelques clics.
|
||||||
|
#
|
||||||
|
# Copyright © 2018 Gabriel Détraz
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Regroupe les fonctions transversales utiles
|
||||||
|
|
||||||
|
Et non corrélées/dépendantes des autres applications
|
||||||
|
"""
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
|
||||||
|
from re2o.settings import EMAIL_HOST
|
||||||
|
|
||||||
|
|
||||||
|
# Mapping of srtftime format for better understanding
|
||||||
|
# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior
|
||||||
|
datetime_mapping={
|
||||||
|
'%a': '%a',
|
||||||
|
'%A': '%A',
|
||||||
|
'%w': '%w',
|
||||||
|
'%d': 'dd',
|
||||||
|
'%b': '%b',
|
||||||
|
'%B': '%B',
|
||||||
|
'%m': 'mm',
|
||||||
|
'%y': 'yy',
|
||||||
|
'%Y': 'yyyy',
|
||||||
|
'%H': 'HH',
|
||||||
|
'%I': 'HH(12h)',
|
||||||
|
'%p': 'AMPM',
|
||||||
|
'%M': 'MM',
|
||||||
|
'%S': 'SS',
|
||||||
|
'%f': 'µµ',
|
||||||
|
'%z': 'UTC(+/-HHMM)',
|
||||||
|
'%Z': 'UTC(TZ)',
|
||||||
|
'%j': '%j',
|
||||||
|
'%U': 'ww',
|
||||||
|
'%W': 'ww',
|
||||||
|
'%c': '%c',
|
||||||
|
'%x': '%x',
|
||||||
|
'%X': '%X',
|
||||||
|
'%%': '%%',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def smtp_check(local_part):
|
||||||
|
"""Return True if the local_part is already taken
|
||||||
|
False if available"""
|
||||||
|
try:
|
||||||
|
srv = smtplib.SMTP(EMAIL_HOST)
|
||||||
|
srv.putcmd("vrfy", local_part)
|
||||||
|
reply_code = srv.getreply()[0]
|
||||||
|
srv.close()
|
||||||
|
if reply_code in [250, 252]:
|
||||||
|
return True, _("This domain is already taken")
|
||||||
|
except:
|
||||||
|
return True, _("Smtp unreachable")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def convert_datetime_format(format):
|
||||||
|
i=0
|
||||||
|
new_format = ""
|
||||||
|
while i < len(format):
|
||||||
|
if format[i] == '%':
|
||||||
|
char = format[i:i+2]
|
||||||
|
new_format += datetime_mapping.get(char, char)
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
new_format += format[i]
|
||||||
|
i += 1
|
||||||
|
return new_format
|
||||||
|
|
||||||
|
|
||||||
|
def get_input_formats_help_text(input_formats):
|
||||||
|
"""Returns a help text about the possible input formats"""
|
||||||
|
if len(input_formats) > 1:
|
||||||
|
help_text_template="Format: {main} {more}"
|
||||||
|
else:
|
||||||
|
help_text_template="Format: {main}"
|
||||||
|
more_text_template="<i class=\"fa fa-question-circle\" title=\"{}\"></i>"
|
||||||
|
help_text = help_text_template.format(
|
||||||
|
main=convert_datetime_format(input_formats[0]),
|
||||||
|
more=more_text_template.format(
|
||||||
|
'\n'.join(map(convert_datetime_format, input_formats))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return help_text
|
||||||
|
|
||||||
|
|
||||||
|
class SortTable:
|
||||||
|
""" Class gathering uselful stuff to sort the colums of a table, according
|
||||||
|
to the column and order requested. It's used with a dict of possible
|
||||||
|
values and associated model_fields """
|
||||||
|
|
||||||
|
# All the possible possible values
|
||||||
|
# The naming convention is based on the URL or the views function
|
||||||
|
# The syntax to describe the sort to apply is a dict where the keys are
|
||||||
|
# the url value and the values are a list of model field name to use to
|
||||||
|
# order the request. They are applied in the order they are given.
|
||||||
|
# A 'default' might be provided to specify what to do if the requested col
|
||||||
|
# doesn't match any keys.
|
||||||
|
|
||||||
|
USERS_INDEX = {
|
||||||
|
'user_name': ['name'],
|
||||||
|
'user_surname': ['surname'],
|
||||||
|
'user_pseudo': ['pseudo'],
|
||||||
|
'user_room': ['room'],
|
||||||
|
'default': ['state', 'pseudo']
|
||||||
|
}
|
||||||
|
USERS_INDEX_BAN = {
|
||||||
|
'ban_user': ['user__pseudo'],
|
||||||
|
'ban_start': ['date_start'],
|
||||||
|
'ban_end': ['date_end'],
|
||||||
|
'default': ['-date_end']
|
||||||
|
}
|
||||||
|
USERS_INDEX_WHITE = {
|
||||||
|
'white_user': ['user__pseudo'],
|
||||||
|
'white_start': ['date_start'],
|
||||||
|
'white_end': ['date_end'],
|
||||||
|
'default': ['-date_end']
|
||||||
|
}
|
||||||
|
USERS_INDEX_SCHOOL = {
|
||||||
|
'school_name': ['name'],
|
||||||
|
'default': ['name']
|
||||||
|
}
|
||||||
|
MACHINES_INDEX = {
|
||||||
|
'machine_name': ['name'],
|
||||||
|
'default': ['pk']
|
||||||
|
}
|
||||||
|
COTISATIONS_INDEX = {
|
||||||
|
'cotis_user': ['user__pseudo'],
|
||||||
|
'cotis_paiement': ['paiement__moyen'],
|
||||||
|
'cotis_date': ['date'],
|
||||||
|
'cotis_id': ['id'],
|
||||||
|
'default': ['-date']
|
||||||
|
}
|
||||||
|
COTISATIONS_CUSTOM = {
|
||||||
|
'invoice_date': ['date'],
|
||||||
|
'invoice_id': ['id'],
|
||||||
|
'invoice_recipient': ['recipient'],
|
||||||
|
'invoice_address': ['address'],
|
||||||
|
'invoice_payment': ['payment'],
|
||||||
|
'default': ['-date']
|
||||||
|
}
|
||||||
|
COTISATIONS_CONTROL = {
|
||||||
|
'control_name': ['user__adherent__name'],
|
||||||
|
'control_surname': ['user__surname'],
|
||||||
|
'control_paiement': ['paiement'],
|
||||||
|
'control_date': ['date'],
|
||||||
|
'control_valid': ['valid'],
|
||||||
|
'control_control': ['control'],
|
||||||
|
'control_id': ['id'],
|
||||||
|
'control_user-id': ['user__id'],
|
||||||
|
'default': ['-date']
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX = {
|
||||||
|
'switch_dns': ['interface__domain__name'],
|
||||||
|
'switch_ip': ['interface__ipv4__ipv4'],
|
||||||
|
'switch_loc': ['switchbay__name'],
|
||||||
|
'switch_ports': ['number'],
|
||||||
|
'switch_stack': ['stack__name'],
|
||||||
|
'default': ['switchbay', 'stack', 'stack_member_id']
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_PORT = {
|
||||||
|
'port_port': ['port'],
|
||||||
|
'port_room': ['room__name'],
|
||||||
|
'port_interface': ['machine_interface__domain__name'],
|
||||||
|
'port_related': ['related__switch__name'],
|
||||||
|
'port_radius': ['radius'],
|
||||||
|
'port_vlan': ['vlan_force__name'],
|
||||||
|
'default': ['port']
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_ROOM = {
|
||||||
|
'room_name': ['name'],
|
||||||
|
'default': ['name']
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_BUILDING = {
|
||||||
|
'building_name': ['name'],
|
||||||
|
'default': ['name']
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_BORNE = {
|
||||||
|
'ap_name': ['interface__domain__name'],
|
||||||
|
'ap_ip': ['interface__ipv4__ipv4'],
|
||||||
|
'ap_mac': ['interface__mac_address'],
|
||||||
|
'default': ['interface__domain__name']
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_STACK = {
|
||||||
|
'stack_name': ['name'],
|
||||||
|
'stack_id': ['stack_id'],
|
||||||
|
'default': ['stack_id'],
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_MODEL_SWITCH = {
|
||||||
|
'model-switch_name': ['reference'],
|
||||||
|
'model-switch_contructor': ['constructor__name'],
|
||||||
|
'default': ['reference'],
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_SWITCH_BAY = {
|
||||||
|
'switch-bay_name': ['name'],
|
||||||
|
'switch-bay_building': ['building__name'],
|
||||||
|
'default': ['name'],
|
||||||
|
}
|
||||||
|
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
|
||||||
|
'constructor-switch_name': ['name'],
|
||||||
|
'default': ['name'],
|
||||||
|
}
|
||||||
|
LOGS_INDEX = {
|
||||||
|
'sum_date': ['revision__date_created'],
|
||||||
|
'default': ['-revision__date_created'],
|
||||||
|
}
|
||||||
|
LOGS_STATS_LOGS = {
|
||||||
|
'logs_author': ['user__name'],
|
||||||
|
'logs_date': ['date_created'],
|
||||||
|
'default': ['-date_created']
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort(request, col, order, values):
|
||||||
|
""" Check if the given values are possible and add .order_by() and
|
||||||
|
a .reverse() as specified according to those values """
|
||||||
|
fields = values.get(col, None)
|
||||||
|
if not fields:
|
||||||
|
fields = values.get('default', [])
|
||||||
|
request = request.order_by(*fields)
|
||||||
|
if values.get(col, None) and order == 'desc':
|
||||||
|
return request.reverse()
|
||||||
|
else:
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
def re2o_paginator(request, query_set, pagination_number):
|
||||||
|
"""Paginator script for list display in re2o.
|
||||||
|
:request:
|
||||||
|
:query_set: Query_set to paginate
|
||||||
|
:pagination_number: Number of entries to display"""
|
||||||
|
paginator = Paginator(query_set, pagination_number)
|
||||||
|
page = request.GET.get('page')
|
||||||
|
try:
|
||||||
|
results = paginator.page(page)
|
||||||
|
except PageNotAnInteger:
|
||||||
|
# If page is not an integer, deliver first page.
|
||||||
|
results = paginator.page(1)
|
||||||
|
except EmptyPage:
|
||||||
|
# If page is out of range (e.g. 9999), deliver last page of results.
|
||||||
|
results = paginator.page(paginator.num_pages)
|
||||||
|
return results
|
Binary file not shown.
|
@ -114,9 +114,9 @@ class CryptPasswordHasher(hashers.BasePasswordHasher):
|
||||||
Check password against encoded using CRYPT algorithm
|
Check password against encoded using CRYPT algorithm
|
||||||
"""
|
"""
|
||||||
assert encoded.startswith(self.algorithm)
|
assert encoded.startswith(self.algorithm)
|
||||||
salt = hash_password_salt(challenge_password)
|
salt = hash_password_salt(encoded)
|
||||||
return constant_time_compare(crypt.crypt(password.encode(), salt),
|
return constant_time_compare(crypt.crypt(password, salt),
|
||||||
challenge.encode())
|
encoded)
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
def safe_summary(self, encoded):
|
||||||
"""
|
"""
|
||||||
|
|
246
re2o/utils.py
246
re2o/utils.py
|
@ -38,55 +38,11 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|
||||||
|
|
||||||
from cotisations.models import Cotisation, Facture, Vente
|
from cotisations.models import Cotisation, Facture, Vente
|
||||||
from machines.models import Interface, Machine
|
from machines.models import Interface, Machine
|
||||||
from users.models import Adherent, User, Ban, Whitelist
|
from users.models import Adherent, User, Ban, Whitelist
|
||||||
|
from preferences.models import AssoOption
|
||||||
# Mapping of srtftime format for better understanding
|
|
||||||
# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior
|
|
||||||
datetime_mapping={
|
|
||||||
'%a': '%a',
|
|
||||||
'%A': '%A',
|
|
||||||
'%w': '%w',
|
|
||||||
'%d': 'dd',
|
|
||||||
'%b': '%b',
|
|
||||||
'%B': '%B',
|
|
||||||
'%m': 'mm',
|
|
||||||
'%y': 'yy',
|
|
||||||
'%Y': 'yyyy',
|
|
||||||
'%H': 'HH',
|
|
||||||
'%I': 'HH(12h)',
|
|
||||||
'%p': 'AMPM',
|
|
||||||
'%M': 'MM',
|
|
||||||
'%S': 'SS',
|
|
||||||
'%f': 'µµ',
|
|
||||||
'%z': 'UTC(+/-HHMM)',
|
|
||||||
'%Z': 'UTC(TZ)',
|
|
||||||
'%j': '%j',
|
|
||||||
'%U': 'ww',
|
|
||||||
'%W': 'ww',
|
|
||||||
'%c': '%c',
|
|
||||||
'%x': '%x',
|
|
||||||
'%X': '%X',
|
|
||||||
'%%': '%%',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def convert_datetime_format(format):
|
|
||||||
i=0
|
|
||||||
new_format = ""
|
|
||||||
while i < len(format):
|
|
||||||
if format[i] == '%':
|
|
||||||
char = format[i:i+2]
|
|
||||||
new_format += datetime_mapping.get(char, char)
|
|
||||||
i += 2
|
|
||||||
else:
|
|
||||||
new_format += format[i]
|
|
||||||
i += 1
|
|
||||||
return new_format
|
|
||||||
|
|
||||||
|
|
||||||
def all_adherent(search_time=None):
|
def all_adherent(search_time=None):
|
||||||
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est
|
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est
|
||||||
|
@ -103,7 +59,7 @@ def all_adherent(search_time=None):
|
||||||
vente__in=Vente.objects.filter(
|
vente__in=Vente.objects.filter(
|
||||||
facture__in=Facture.objects.all().exclude(valid=False)
|
facture__in=Facture.objects.all().exclude(valid=False)
|
||||||
)
|
)
|
||||||
).filter(date_end__gt=search_time)
|
).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
@ -115,7 +71,7 @@ def all_baned(search_time=None):
|
||||||
search_time = timezone.now()
|
search_time = timezone.now()
|
||||||
return User.objects.filter(
|
return User.objects.filter(
|
||||||
ban__in=Ban.objects.filter(
|
ban__in=Ban.objects.filter(
|
||||||
date_end__gt=search_time
|
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
|
||||||
)
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
@ -126,20 +82,23 @@ def all_whitelisted(search_time=None):
|
||||||
search_time = timezone.now()
|
search_time = timezone.now()
|
||||||
return User.objects.filter(
|
return User.objects.filter(
|
||||||
whitelist__in=Whitelist.objects.filter(
|
whitelist__in=Whitelist.objects.filter(
|
||||||
date_end__gt=search_time
|
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
|
||||||
)
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
def all_has_access(search_time=None):
|
def all_has_access(search_time=None):
|
||||||
""" Renvoie tous les users beneficiant d'une connexion
|
""" Return all connected users : active users and whitelisted +
|
||||||
: user adherent ou whiteliste et non banni """
|
asso_user defined in AssoOption pannel
|
||||||
|
----
|
||||||
|
Renvoie tous les users beneficiant d'une connexion
|
||||||
|
: user adherent et whiteliste non banni plus l'utilisateur asso"""
|
||||||
if search_time is None:
|
if search_time is None:
|
||||||
search_time = timezone.now()
|
search_time = timezone.now()
|
||||||
return User.objects.filter(
|
filter_user = (
|
||||||
Q(state=User.STATE_ACTIVE) &
|
Q(state=User.STATE_ACTIVE) &
|
||||||
~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
|
~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) &
|
||||||
(Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
|
(Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) |
|
||||||
Q(facture__in=Facture.objects.filter(
|
Q(facture__in=Facture.objects.filter(
|
||||||
vente__in=Vente.objects.filter(
|
vente__in=Vente.objects.filter(
|
||||||
cotisation__in=Cotisation.objects.filter(
|
cotisation__in=Cotisation.objects.filter(
|
||||||
|
@ -148,10 +107,14 @@ def all_has_access(search_time=None):
|
||||||
facture__in=Facture.objects.all()
|
facture__in=Facture.objects.all()
|
||||||
.exclude(valid=False)
|
.exclude(valid=False)
|
||||||
)
|
)
|
||||||
).filter(date_end__gt=search_time)
|
).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
|
||||||
)
|
)
|
||||||
)))
|
)))
|
||||||
).distinct()
|
)
|
||||||
|
asso_user = AssoOption.get_cached_value('utilisateur_asso')
|
||||||
|
if asso_user:
|
||||||
|
filter_user |= Q(id=asso_user.id)
|
||||||
|
return User.objects.filter(filter_user).distinct()
|
||||||
|
|
||||||
|
|
||||||
def filter_active_interfaces(interface_set):
|
def filter_active_interfaces(interface_set):
|
||||||
|
@ -203,164 +166,6 @@ def all_active_assigned_interfaces_count():
|
||||||
return all_active_interfaces_count().filter(ipv4__isnull=False)
|
return all_active_interfaces_count().filter(ipv4__isnull=False)
|
||||||
|
|
||||||
|
|
||||||
class SortTable:
|
|
||||||
""" Class gathering uselful stuff to sort the colums of a table, according
|
|
||||||
to the column and order requested. It's used with a dict of possible
|
|
||||||
values and associated model_fields """
|
|
||||||
|
|
||||||
# All the possible possible values
|
|
||||||
# The naming convention is based on the URL or the views function
|
|
||||||
# The syntax to describe the sort to apply is a dict where the keys are
|
|
||||||
# the url value and the values are a list of model field name to use to
|
|
||||||
# order the request. They are applied in the order they are given.
|
|
||||||
# A 'default' might be provided to specify what to do if the requested col
|
|
||||||
# doesn't match any keys.
|
|
||||||
|
|
||||||
USERS_INDEX = {
|
|
||||||
'user_name': ['name'],
|
|
||||||
'user_surname': ['surname'],
|
|
||||||
'user_pseudo': ['pseudo'],
|
|
||||||
'user_room': ['room'],
|
|
||||||
'default': ['state', 'pseudo']
|
|
||||||
}
|
|
||||||
USERS_INDEX_BAN = {
|
|
||||||
'ban_user': ['user__pseudo'],
|
|
||||||
'ban_start': ['date_start'],
|
|
||||||
'ban_end': ['date_end'],
|
|
||||||
'default': ['-date_end']
|
|
||||||
}
|
|
||||||
USERS_INDEX_WHITE = {
|
|
||||||
'white_user': ['user__pseudo'],
|
|
||||||
'white_start': ['date_start'],
|
|
||||||
'white_end': ['date_end'],
|
|
||||||
'default': ['-date_end']
|
|
||||||
}
|
|
||||||
USERS_INDEX_SCHOOL = {
|
|
||||||
'school_name': ['name'],
|
|
||||||
'default': ['name']
|
|
||||||
}
|
|
||||||
MACHINES_INDEX = {
|
|
||||||
'machine_name': ['name'],
|
|
||||||
'default': ['pk']
|
|
||||||
}
|
|
||||||
COTISATIONS_INDEX = {
|
|
||||||
'cotis_user': ['user__pseudo'],
|
|
||||||
'cotis_paiement': ['paiement__moyen'],
|
|
||||||
'cotis_date': ['date'],
|
|
||||||
'cotis_id': ['id'],
|
|
||||||
'default': ['-date']
|
|
||||||
}
|
|
||||||
COTISATIONS_CUSTOM = {
|
|
||||||
'invoice_date': ['date'],
|
|
||||||
'invoice_id': ['id'],
|
|
||||||
'invoice_recipient': ['recipient'],
|
|
||||||
'invoice_address': ['address'],
|
|
||||||
'invoice_payment': ['payment'],
|
|
||||||
'default': ['-date']
|
|
||||||
}
|
|
||||||
COTISATIONS_CONTROL = {
|
|
||||||
'control_name': ['user__adherent__name'],
|
|
||||||
'control_surname': ['user__surname'],
|
|
||||||
'control_paiement': ['paiement'],
|
|
||||||
'control_date': ['date'],
|
|
||||||
'control_valid': ['valid'],
|
|
||||||
'control_control': ['control'],
|
|
||||||
'control_id': ['id'],
|
|
||||||
'control_user-id': ['user__id'],
|
|
||||||
'default': ['-date']
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX = {
|
|
||||||
'switch_dns': ['interface__domain__name'],
|
|
||||||
'switch_ip': ['interface__ipv4__ipv4'],
|
|
||||||
'switch_loc': ['switchbay__name'],
|
|
||||||
'switch_ports': ['number'],
|
|
||||||
'switch_stack': ['stack__name'],
|
|
||||||
'default': ['switchbay', 'stack', 'stack_member_id']
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_PORT = {
|
|
||||||
'port_port': ['port'],
|
|
||||||
'port_room': ['room__name'],
|
|
||||||
'port_interface': ['machine_interface__domain__name'],
|
|
||||||
'port_related': ['related__switch__name'],
|
|
||||||
'port_radius': ['radius'],
|
|
||||||
'port_vlan': ['vlan_force__name'],
|
|
||||||
'default': ['port']
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_ROOM = {
|
|
||||||
'room_name': ['name'],
|
|
||||||
'default': ['name']
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_BUILDING = {
|
|
||||||
'building_name': ['name'],
|
|
||||||
'default': ['name']
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_BORNE = {
|
|
||||||
'ap_name': ['interface__domain__name'],
|
|
||||||
'ap_ip': ['interface__ipv4__ipv4'],
|
|
||||||
'ap_mac': ['interface__mac_address'],
|
|
||||||
'default': ['interface__domain__name']
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_STACK = {
|
|
||||||
'stack_name': ['name'],
|
|
||||||
'stack_id': ['stack_id'],
|
|
||||||
'default': ['stack_id'],
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_MODEL_SWITCH = {
|
|
||||||
'model-switch_name': ['reference'],
|
|
||||||
'model-switch_contructor': ['constructor__name'],
|
|
||||||
'default': ['reference'],
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_SWITCH_BAY = {
|
|
||||||
'switch-bay_name': ['name'],
|
|
||||||
'switch-bay_building': ['building__name'],
|
|
||||||
'default': ['name'],
|
|
||||||
}
|
|
||||||
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
|
|
||||||
'constructor-switch_name': ['name'],
|
|
||||||
'default': ['name'],
|
|
||||||
}
|
|
||||||
LOGS_INDEX = {
|
|
||||||
'sum_date': ['revision__date_created'],
|
|
||||||
'default': ['-revision__date_created'],
|
|
||||||
}
|
|
||||||
LOGS_STATS_LOGS = {
|
|
||||||
'logs_author': ['user__name'],
|
|
||||||
'logs_date': ['date_created'],
|
|
||||||
'default': ['-date_created']
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sort(request, col, order, values):
|
|
||||||
""" Check if the given values are possible and add .order_by() and
|
|
||||||
a .reverse() as specified according to those values """
|
|
||||||
fields = values.get(col, None)
|
|
||||||
if not fields:
|
|
||||||
fields = values.get('default', [])
|
|
||||||
request = request.order_by(*fields)
|
|
||||||
if values.get(col, None) and order == 'desc':
|
|
||||||
return request.reverse()
|
|
||||||
else:
|
|
||||||
return request
|
|
||||||
|
|
||||||
|
|
||||||
def re2o_paginator(request, query_set, pagination_number):
|
|
||||||
"""Paginator script for list display in re2o.
|
|
||||||
:request:
|
|
||||||
:query_set: Query_set to paginate
|
|
||||||
:pagination_number: Number of entries to display"""
|
|
||||||
paginator = Paginator(query_set, pagination_number)
|
|
||||||
page = request.GET.get('page')
|
|
||||||
try:
|
|
||||||
results = paginator.page(page)
|
|
||||||
except PageNotAnInteger:
|
|
||||||
# If page is not an integer, deliver first page.
|
|
||||||
results = paginator.page(1)
|
|
||||||
except EmptyPage:
|
|
||||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
|
||||||
results = paginator.page(paginator.num_pages)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def remove_user_room(room):
|
def remove_user_room(room):
|
||||||
""" Déménage de force l'ancien locataire de la chambre """
|
""" Déménage de force l'ancien locataire de la chambre """
|
||||||
try:
|
try:
|
||||||
|
@ -370,18 +175,3 @@ def remove_user_room(room):
|
||||||
user.room = None
|
user.room = None
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
def get_input_formats_help_text(input_formats):
|
|
||||||
"""Returns a help text about the possible input formats"""
|
|
||||||
if len(input_formats) > 1:
|
|
||||||
help_text_template="Format: {main} {more}"
|
|
||||||
else:
|
|
||||||
help_text_template="Format: {main}"
|
|
||||||
more_text_template="<i class=\"fa fa-question-circle\" title=\"{}\"></i>"
|
|
||||||
help_text = help_text_template.format(
|
|
||||||
main=convert_datetime_format(input_formats[0]),
|
|
||||||
more=more_text_template.format(
|
|
||||||
'\n'.join(map(convert_datetime_format, input_formats))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return help_text
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ from __future__ import unicode_literals
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from re2o.utils import get_input_formats_help_text
|
from re2o.base import get_input_formats_help_text
|
||||||
|
|
||||||
CHOICES_USER = (
|
CHOICES_USER = (
|
||||||
('0', _("Active")),
|
('0', _("Active")),
|
||||||
|
|
Binary file not shown.
|
@ -46,7 +46,7 @@ from search.forms import (
|
||||||
CHOICES_AFF,
|
CHOICES_AFF,
|
||||||
initial_choices
|
initial_choices
|
||||||
)
|
)
|
||||||
from re2o.utils import SortTable
|
from re2o.base import SortTable
|
||||||
from re2o.acl import can_view_all
|
from re2o.acl import can_view_all
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -79,19 +79,6 @@ a > i.fa {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pull sidebars to the bottom */
|
|
||||||
@media (min-width: 767px) {
|
|
||||||
.row {
|
|
||||||
display: -webkit-box;
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.row > [class*='col-'] {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* On small screens, set height to 'auto' for sidenav and grid */
|
/* On small screens, set height to 'auto' for sidenav and grid */
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.sidenav {
|
.sidenav {
|
||||||
|
@ -102,7 +89,7 @@ a > i.fa {
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow-y: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make modal wider on wide screens */
|
/* Make modal wider on wide screens */
|
||||||
|
@ -145,3 +132,14 @@ th.long_text{
|
||||||
.dashboard{
|
.dashboard{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Detailed information on profile page */
|
||||||
|
dl.profile-info {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.profile-info > div {
|
||||||
|
padding: 8px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
33
static/js/collapse-from-url.js
Normal file
33
static/js/collapse-from-url.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
// se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
// quelques clics.
|
||||||
|
//
|
||||||
|
// Copyright © 2018 Alexandre Iooss
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
// This script makes URL hash controls Bootstrap collapse
|
||||||
|
// e.g. if there is #information in the URL
|
||||||
|
// then the collapse with id "information" will be open.
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
if(location.hash != null && location.hash !== ""){
|
||||||
|
// Open the collapse corresponding to URL hash
|
||||||
|
$(location.hash + '.collapse').collapse('show');
|
||||||
|
} else {
|
||||||
|
// Open default collapse
|
||||||
|
$('.collapse-default.collapse').collapse('show');
|
||||||
|
}
|
||||||
|
});
|
|
@ -45,6 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% bootstrap_javascript %}
|
{% bootstrap_javascript %}
|
||||||
<script src="/static/js/typeahead/typeahead.js"></script>
|
<script src="/static/js/typeahead/typeahead.js"></script>
|
||||||
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
|
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
|
||||||
|
<script src="{% static 'js/collapse-from-url.js' %}"></script>
|
||||||
|
|
||||||
{# Load CSS #}
|
{# Load CSS #}
|
||||||
{% bootstrap_css %}
|
{% bootstrap_css %}
|
||||||
|
|
Binary file not shown.
|
@ -23,23 +23,52 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load url_insert_param %}
|
{% load url_insert_param %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% if list.paginator.num_pages > 1 %}
|
{% if list.paginator.num_pages > 1 %}
|
||||||
<ul class="pagination nav navbar-nav">
|
<ul class="pagination text-center">
|
||||||
{% if list.has_previous %}
|
{% if list.has_previous %}
|
||||||
<li><a href="{% url_insert_param request.get_full_path page=1 %}"> << </a></li>
|
<li>
|
||||||
<li><a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}"> < </a></li>
|
<a href="{% url_insert_param request.get_full_path page=1 %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
<span class="sr-only">{% trans "First" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
|
||||||
|
<span aria-hidden="true">‹</span>
|
||||||
|
<span class="sr-only">{% trans "Previous" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><span aria-hidden="true">«</span></li>
|
||||||
|
<li class="disabled"><span aria-hidden="true">‹</span></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for page in list.paginator.page_range %}
|
{% for page in list.paginator.page_range %}
|
||||||
{% if list.number <= page|add:"3" and list.number >= page|add:"-3" %}
|
{% if list.number <= page|add:"3" and list.number >= page|add:"-3" %}
|
||||||
<li class="{% if list.number == page %}active{% endif %}"><a href="{% url_insert_param request.get_full_path page=page %}">{{ page }}</a></li>
|
<li class="{% if list.number == page %}active{% endif %}">
|
||||||
|
<a href="{% url_insert_param request.get_full_path page=page %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">{{ page }}</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if list.has_next %}
|
{% if list.has_next %}
|
||||||
<li><a href="{% url_insert_param request.get_full_path page=list.next_page_number %}"> > </a></li>
|
<li>
|
||||||
<li><a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}"> >> </a></li>
|
<a href="{% url_insert_param request.get_full_path page=list.next_page_number %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
|
<span class="sr-only">{% trans "Next" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
<span class="sr-only">{% trans "Last" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><span aria-hidden="true">›</span></li>
|
||||||
|
<li class="disabled"><span aria-hidden="true">»</span></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,8 @@ from .models import (
|
||||||
SwitchBay,
|
SwitchBay,
|
||||||
Building,
|
Building,
|
||||||
PortProfile,
|
PortProfile,
|
||||||
|
ModuleSwitch,
|
||||||
|
ModuleOnSwitch,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -269,3 +271,23 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
|
class EditModuleForm(FormRevMixin, ModelForm):
|
||||||
|
"""Add and edit module instance"""
|
||||||
|
class Meta:
|
||||||
|
model = ModuleSwitch
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||||
|
super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EditSwitchModuleForm(FormRevMixin, ModelForm):
|
||||||
|
"""Add/edit a switch to a module"""
|
||||||
|
class Meta:
|
||||||
|
model = ModuleOnSwitch
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||||
|
super(EditSwitchModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||||
|
|
Binary file not shown.
66
topologie/migrations/0067_auto_20181230_1819.py
Normal file
66
topologie/migrations/0067_auto_20181230_1819.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-30 17:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import re2o.mixins
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('topologie', '0066_modelswitch_commercial_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModuleOnSwitch',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('slot', models.CharField(help_text='Slot on switch', max_length=15, verbose_name='Slot')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'link between switchs and modules',
|
||||||
|
'permissions': (('view_moduleonswitch', 'Can view a moduleonswitch object'),),
|
||||||
|
},
|
||||||
|
bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModuleSwitch',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('reference', models.CharField(help_text='Reference of a module', max_length=255, verbose_name='Module reference')),
|
||||||
|
('comment', models.CharField(blank=True, help_text='Comment', max_length=255, null=True, verbose_name='Comment')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Module of a switch',
|
||||||
|
'permissions': (('view_moduleswitch', 'Can view a module object'),),
|
||||||
|
},
|
||||||
|
bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modelswitch',
|
||||||
|
name='is_itself_module',
|
||||||
|
field=models.BooleanField(default=False, help_text='Does the switch, itself, considered as a module'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modelswitch',
|
||||||
|
name='is_modular',
|
||||||
|
field=models.BooleanField(default=False, help_text='Is this switch model modular'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduleonswitch',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.ModuleSwitch'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduleonswitch',
|
||||||
|
name='switch',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.Switch'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='moduleonswitch',
|
||||||
|
unique_together=set([('slot', 'switch')]),
|
||||||
|
),
|
||||||
|
]
|
20
topologie/migrations/0068_auto_20190102_1758.py
Normal file
20
topologie/migrations/0068_auto_20190102_1758.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2019-01-02 23:58
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('topologie', '0067_auto_20181230_1819'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modelswitch',
|
||||||
|
name='is_itself_module',
|
||||||
|
field=models.BooleanField(default=False, help_text='Is the switch, itself, considered as a module'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -252,6 +252,7 @@ class Switch(AclMixin, Machine):
|
||||||
help_text='Provision automatique de ce switch',
|
help_text='Provision automatique de ce switch',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('stack', 'stack_member_id')
|
unique_together = ('stack', 'stack_member_id')
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -281,31 +282,18 @@ class Switch(AclMixin, Machine):
|
||||||
def create_ports(self, begin, end):
|
def create_ports(self, begin, end):
|
||||||
""" Crée les ports de begin à end si les valeurs données
|
""" Crée les ports de begin à end si les valeurs données
|
||||||
sont cohérentes. """
|
sont cohérentes. """
|
||||||
|
|
||||||
s_begin = s_end = 0
|
|
||||||
nb_ports = self.ports.count()
|
|
||||||
if nb_ports > 0:
|
|
||||||
ports = self.ports.order_by('port').values('port')
|
|
||||||
s_begin = ports.first().get('port')
|
|
||||||
s_end = ports.last().get('port')
|
|
||||||
|
|
||||||
if end < begin:
|
if end < begin:
|
||||||
raise ValidationError(_("The end port is less than the start"
|
raise ValidationError(_("The end port is less than the start"
|
||||||
" port."))
|
" port."))
|
||||||
if end - begin > self.number:
|
ports_to_create = range(begin, end + 1)
|
||||||
|
existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True)
|
||||||
|
non_existing_ports = list(set(ports_to_create) - set(existing_ports))
|
||||||
|
|
||||||
|
if len(non_existing_ports) + existing_ports.count() > self.number:
|
||||||
raise ValidationError(_("This switch can't have that many ports."))
|
raise ValidationError(_("This switch can't have that many ports."))
|
||||||
begin_range = range(begin, s_begin)
|
|
||||||
end_range = range(s_end+1, end+1)
|
|
||||||
for i in itertools.chain(begin_range, end_range):
|
|
||||||
port = Port()
|
|
||||||
port.switch = self
|
|
||||||
port.port = i
|
|
||||||
try:
|
|
||||||
with transaction.atomic(), reversion.create_revision():
|
with transaction.atomic(), reversion.create_revision():
|
||||||
port.save()
|
|
||||||
reversion.set_comment(_("Creation"))
|
reversion.set_comment(_("Creation"))
|
||||||
except IntegrityError:
|
Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports])
|
||||||
ValidationError(_("Creation of an existing port."))
|
|
||||||
|
|
||||||
def main_interface(self):
|
def main_interface(self):
|
||||||
""" Returns the 'main' interface of the switch
|
""" Returns the 'main' interface of the switch
|
||||||
|
@ -317,7 +305,7 @@ class Switch(AclMixin, Machine):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.name or self.main_interface().domain.name
|
return self.name or getattr(self.main_interface(), 'domain', 'Unknown')
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def get_radius_key(self):
|
def get_radius_key(self):
|
||||||
|
@ -380,6 +368,17 @@ class Switch(AclMixin, Machine):
|
||||||
"""Return dict ip6:subnet for all ipv6 of the switch"""
|
"""Return dict ip6:subnet for all ipv6 of the switch"""
|
||||||
return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
|
return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def list_modules(self):
|
||||||
|
"""Return modules of that switch, list of dict (rank, reference)"""
|
||||||
|
modules = []
|
||||||
|
if getattr(self.model, 'is_modular', None):
|
||||||
|
if self.model.is_itself_module:
|
||||||
|
modules.append((1, self.model.reference))
|
||||||
|
for module_of_self in self.moduleonswitch_set.all():
|
||||||
|
modules.append((module_of_self.slot, module_of_self.module.reference))
|
||||||
|
return modules
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.get_name)
|
return str(self.get_name)
|
||||||
|
|
||||||
|
@ -402,6 +401,14 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
is_modular = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("Is this switch model modular"),
|
||||||
|
)
|
||||||
|
is_itself_module = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("Is the switch, itself, considered as a module"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -417,6 +424,53 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
|
||||||
return str(self.constructor) + ' ' + self.reference
|
return str(self.constructor) + ' ' + self.reference
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleSwitch(AclMixin, RevMixin, models.Model):
|
||||||
|
"""A module of a switch"""
|
||||||
|
reference = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
help_text=_("Reference of a module"),
|
||||||
|
verbose_name=_("Module reference")
|
||||||
|
)
|
||||||
|
comment = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Comment"),
|
||||||
|
verbose_name=_("Comment")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
("view_moduleswitch", _("Can view a module object")),
|
||||||
|
)
|
||||||
|
verbose_name = _("Module of a switch")
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.reference)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleOnSwitch(AclMixin, RevMixin, models.Model):
|
||||||
|
"""Link beetween module and switch"""
|
||||||
|
module = models.ForeignKey('ModuleSwitch', on_delete=models.CASCADE)
|
||||||
|
switch = models.ForeignKey('Switch', on_delete=models.CASCADE)
|
||||||
|
slot = models.CharField(
|
||||||
|
max_length=15,
|
||||||
|
help_text=_("Slot on switch"),
|
||||||
|
verbose_name=_("Slot")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
("view_moduleonswitch", _("Can view a moduleonswitch object")),
|
||||||
|
)
|
||||||
|
verbose_name = _("link between switchs and modules")
|
||||||
|
unique_together = ['slot', 'switch']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'On slot ' + str(self.slot) + ' of ' + str(self.switch)
|
||||||
|
|
||||||
|
|
||||||
class ConstructorSwitch(AclMixin, RevMixin, models.Model):
|
class ConstructorSwitch(AclMixin, RevMixin, models.Model):
|
||||||
"""Un constructeur de switch"""
|
"""Un constructeur de switch"""
|
||||||
|
|
||||||
|
|
110
topologie/templates/topologie/aff_modules.html
Normal file
110
topologie/templates/topologie/aff_modules.html
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2017 Gabriel Détraz
|
||||||
|
Copyright © 2017 Goulven Kermarec
|
||||||
|
Copyright © 2017 Augustin Lemesle
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if module_list.paginator %}
|
||||||
|
{% include "pagination.html" with list=module_list %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Reference" %}</th>
|
||||||
|
<th>{% trans "Comment" %}</th>
|
||||||
|
<th>{% trans "Switchs" %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for module in module_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ module.reference }}</td>
|
||||||
|
<td>{{ module.comment }}</td>
|
||||||
|
<td>
|
||||||
|
{% for module_switch in module.moduleonswitch_set.all %}
|
||||||
|
<b>Slot</b> {{ module_switch.slot }} <b>of</b> {{ module_switch.switch }}
|
||||||
|
{% can_edit module_switch %}
|
||||||
|
<a class="btn btn-primary btn-xs" role="button" title={% trans "Edit" %} href="{% url 'topologie:edit-module-on' module_switch.id %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
|
{% can_delete module_switch %}
|
||||||
|
<a class="btn btn-danger btn-xs" role="button" title={% trans "Delete" %} href="{% url 'topologie:del-module-on' module_switch.id %}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% can_edit module %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" title={% trans "Add" %} href="{% url 'topologie:add-module-on' %}">
|
||||||
|
<i class="fa fa-plus"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" title={% trans "Edit" %} href="{% url 'topologie:edit-module' module.id %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
|
{% history_button module %}
|
||||||
|
{% can_delete module %}
|
||||||
|
<a class="btn btn-danger btn-sm" role="button" title={% trans "Delete" %} href="{% url 'topologie:del-module' module.id %}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if module_list.paginator %}
|
||||||
|
{% include "pagination.html" with list=module_list %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h4>{% trans "All modular switchs" %}</h4>
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>{% trans "Switch" %}</th>
|
||||||
|
<th>{% trans "Reference" %}</th>
|
||||||
|
<th>{% trans "Slot" %}</th>
|
||||||
|
<tbody>
|
||||||
|
{% for switch in modular_switchs %}
|
||||||
|
{% if switch.list_modules %}
|
||||||
|
<tr class="info">
|
||||||
|
<td colspan="4">
|
||||||
|
{{ switch }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% for module in switch.list_modules %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ module.1 }}</td>
|
||||||
|
<td>{{ module.0 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
43
topologie/templates/topologie/index_module.html
Normal file
43
topologie/templates/topologie/index_module.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "topologie/sidebar.html" %}
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2017 Gabriel Détraz
|
||||||
|
Copyright © 2017 Goulven Kermarec
|
||||||
|
Copyright © 2017 Augustin Lemesle
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% trans "Modules of switchs" %}</h2>
|
||||||
|
{% can_create ModuleSwitch %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:add-module' %}"><i class="fa fa-plus"></i>{% trans " Add a module" %}</a>
|
||||||
|
<hr>
|
||||||
|
{% acl_end %}
|
||||||
|
{% include "topologie/aff_modules.html" with module_list=module_list modular_switchs=modular_switchs %}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<a class="list-group-item list-group-item-info" href="{% url "topologie:index" %}">
|
<a class="list-group-item list-group-item-info" href="{% url "topologie:index" %}">
|
||||||
<i class="fa fa-microchip"></i>
|
<i class="fa fa-microchip"></i>
|
||||||
{% trans "Switches" %}
|
{% trans "Switches" %}
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-module" %}">
|
||||||
|
<i class="fa fa-microchip"></i>
|
||||||
|
{% trans "Switches modules" %}
|
||||||
</a>
|
</a>
|
||||||
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
|
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
|
||||||
<i class="fa fa-cogs"></i>
|
<i class="fa fa-cogs"></i>
|
||||||
|
|
|
@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged' %}
|
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %}
|
||||||
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
|
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
|
||||||
</form>
|
</form>
|
||||||
<br />
|
<br />
|
||||||
|
|
|
@ -123,4 +123,15 @@ urlpatterns = [
|
||||||
url(r'^edit_vlanoptions/(?P<vlanid>[0-9]+)$',
|
url(r'^edit_vlanoptions/(?P<vlanid>[0-9]+)$',
|
||||||
views.edit_vlanoptions,
|
views.edit_vlanoptions,
|
||||||
name='edit-vlanoptions'),
|
name='edit-vlanoptions'),
|
||||||
]
|
url(r'^add_module/$', views.add_module, name='add-module'),
|
||||||
|
url(r'^edit_module/(?P<moduleswitchid>[0-9]+)$',
|
||||||
|
views.edit_module,
|
||||||
|
name='edit-module'),
|
||||||
|
url(r'^del_module/(?P<moduleswitchid>[0-9]+)$', views.del_module, name='del-module'),
|
||||||
|
url(r'^index_module/$', views.index_module, name='index-module'),
|
||||||
|
url(r'^add_module_on/$', views.add_module_on, name='add-module-on'),
|
||||||
|
url(r'^edit_module_on/(?P<moduleonswitchid>[0-9]+)$',
|
||||||
|
views.edit_module_on,
|
||||||
|
name='edit-module-on'),
|
||||||
|
url(r'^del_module_on/(?P<moduleonswitchid>[0-9]+)$', views.del_module_on, name='del-module-on'),
|
||||||
|
]
|
||||||
|
|
|
@ -48,7 +48,10 @@ from django.utils.translation import ugettext as _
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from users.views import form
|
from users.views import form
|
||||||
from re2o.utils import re2o_paginator, SortTable
|
from re2o.base import (
|
||||||
|
re2o_paginator,
|
||||||
|
SortTable,
|
||||||
|
)
|
||||||
from re2o.acl import (
|
from re2o.acl import (
|
||||||
can_create,
|
can_create,
|
||||||
can_edit,
|
can_edit,
|
||||||
|
@ -83,6 +86,8 @@ from .models import (
|
||||||
Building,
|
Building,
|
||||||
Server,
|
Server,
|
||||||
PortProfile,
|
PortProfile,
|
||||||
|
ModuleSwitch,
|
||||||
|
ModuleOnSwitch,
|
||||||
)
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
EditPortForm,
|
EditPortForm,
|
||||||
|
@ -99,6 +104,8 @@ from .forms import (
|
||||||
EditSwitchBayForm,
|
EditSwitchBayForm,
|
||||||
EditBuildingForm,
|
EditBuildingForm,
|
||||||
EditPortProfileForm,
|
EditPortProfileForm,
|
||||||
|
EditModuleForm,
|
||||||
|
EditSwitchModuleForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
from subprocess import (
|
from subprocess import (
|
||||||
|
@ -313,6 +320,22 @@ def index_model_switch(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_view_all(ModuleSwitch)
|
||||||
|
def index_module(request):
|
||||||
|
"""Display all modules of switchs"""
|
||||||
|
module_list = ModuleSwitch.objects.all()
|
||||||
|
modular_switchs = Switch.objects.filter(model__is_modular=True)
|
||||||
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
|
module_list = re2o_paginator(request, module_list, pagination_number)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'topologie/index_module.html',
|
||||||
|
{'module_list': module_list,
|
||||||
|
'modular_switchs': modular_switchs}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_edit(Vlan)
|
@can_edit(Vlan)
|
||||||
def edit_vlanoptions(request, vlan_instance, **_kwargs):
|
def edit_vlanoptions(request, vlan_instance, **_kwargs):
|
||||||
|
@ -531,18 +554,12 @@ def create_ports(request, switchid):
|
||||||
messages.error(request, _("Nonexistent switch"))
|
messages.error(request, _("Nonexistent switch"))
|
||||||
return redirect(reverse('topologie:index'))
|
return redirect(reverse('topologie:index'))
|
||||||
|
|
||||||
s_begin = s_end = 0
|
first_port = getattr(switch.ports.order_by('port').first(), 'port', 1)
|
||||||
nb_ports = switch.ports.count()
|
last_port = switch.number + first_port - 1
|
||||||
if nb_ports > 0:
|
|
||||||
ports = switch.ports.order_by('port').values('port')
|
|
||||||
s_begin = ports.first().get('port')
|
|
||||||
s_end = ports.last().get('port')
|
|
||||||
|
|
||||||
port_form = CreatePortsForm(
|
port_form = CreatePortsForm(
|
||||||
request.POST or None,
|
request.POST or None,
|
||||||
initial={'begin': s_begin, 'end': s_end}
|
initial={'begin': first_port, 'end': last_port}
|
||||||
)
|
)
|
||||||
|
|
||||||
if port_form.is_valid():
|
if port_form.is_valid():
|
||||||
begin = port_form.cleaned_data['begin']
|
begin = port_form.cleaned_data['begin']
|
||||||
end = port_form.cleaned_data['end']
|
end = port_form.cleaned_data['end']
|
||||||
|
@ -1051,6 +1068,115 @@ def del_port_profile(request, port_profile, **_kwargs):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_create(ModuleSwitch)
|
||||||
|
def add_module(request):
|
||||||
|
""" View used to add a Module object """
|
||||||
|
module = EditModuleForm(request.POST or None)
|
||||||
|
if module.is_valid():
|
||||||
|
module.save()
|
||||||
|
messages.success(request, _("The module was created."))
|
||||||
|
return redirect(reverse('topologie:index-module'))
|
||||||
|
return form(
|
||||||
|
{'topoform': module, 'action_name': _("Create a module")},
|
||||||
|
'topologie/topo.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(ModuleSwitch)
|
||||||
|
def edit_module(request, module_instance, **_kwargs):
|
||||||
|
""" View used to edit a Module object """
|
||||||
|
module = EditModuleForm(request.POST or None, instance=module_instance)
|
||||||
|
if module.is_valid():
|
||||||
|
if module.changed_data:
|
||||||
|
module.save()
|
||||||
|
messages.success(request, _("The module was edited."))
|
||||||
|
return redirect(reverse('topologie:index-module'))
|
||||||
|
return form(
|
||||||
|
{'topoform': module, 'action_name': _("Edit")},
|
||||||
|
'topologie/topo.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_delete(ModuleSwitch)
|
||||||
|
def del_module(request, module, **_kwargs):
|
||||||
|
"""Compleete delete a module"""
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
module.delete()
|
||||||
|
messages.success(request, _("The module was deleted."))
|
||||||
|
except ProtectedError:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
(_("The module %s is used by another object, impossible to"
|
||||||
|
" deleted it.") % module)
|
||||||
|
)
|
||||||
|
return redirect(reverse('topologie:index-module'))
|
||||||
|
return form(
|
||||||
|
{'objet': module, 'objet_name': _("Module")},
|
||||||
|
'topologie/delete.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_create(ModuleOnSwitch)
|
||||||
|
def add_module_on(request):
|
||||||
|
"""Add a module to a switch"""
|
||||||
|
module_switch = EditSwitchModuleForm(request.POST or None)
|
||||||
|
if module_switch.is_valid():
|
||||||
|
module_switch.save()
|
||||||
|
messages.success(request, _("The module added to that switch"))
|
||||||
|
return redirect(reverse('topologie:index-module'))
|
||||||
|
return form(
|
||||||
|
{'topoform': module_switch, 'action_name': _("Create")},
|
||||||
|
'topologie/topo.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(ModuleOnSwitch)
|
||||||
|
def edit_module_on(request, module_instance, **_kwargs):
|
||||||
|
""" View used to edit a Module object """
|
||||||
|
module = EditSwitchModuleForm(request.POST or None, instance=module_instance)
|
||||||
|
if module.is_valid():
|
||||||
|
if module.changed_data:
|
||||||
|
module.save()
|
||||||
|
messages.success(request, _("The module was edited."))
|
||||||
|
return redirect(reverse('topologie:index-module'))
|
||||||
|
return form(
|
||||||
|
{'topoform': module, 'action_name': _("Edit")},
|
||||||
|
'topologie/topo.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_delete(ModuleOnSwitch)
|
||||||
|
def del_module_on(request, module, **_kwargs):
|
||||||
|
"""Compleete delete a module"""
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
module.delete()
|
||||||
|
messages.success(request, _("The module was deleted."))
|
||||||
|
except ProtectedError:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
(_("The module %s is used by another object, impossible to"
|
||||||
|
" deleted it.") % module)
|
||||||
|
)
|
||||||
|
return redirect(reverse('topologie:index-module'))
|
||||||
|
return form(
|
||||||
|
{'objet': module, 'objet_name': _("Module")},
|
||||||
|
'topologie/delete.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_machine_graph():
|
def make_machine_graph():
|
||||||
"""
|
"""
|
||||||
Create the graph of switchs, machines and access points.
|
Create the graph of switchs, machines and access points.
|
||||||
|
|
|
@ -45,7 +45,8 @@ from django.utils.safestring import mark_safe
|
||||||
from machines.models import Interface, Machine, Nas
|
from machines.models import Interface, Machine, Nas
|
||||||
from topologie.models import Port
|
from topologie.models import Port
|
||||||
from preferences.models import OptionalUser
|
from preferences.models import OptionalUser
|
||||||
from re2o.utils import remove_user_room, get_input_formats_help_text
|
from re2o.utils import remove_user_room
|
||||||
|
from re2o.base import get_input_formats_help_text
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
from re2o.field_permissions import FieldPermissionFormMixin
|
from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
|
|
||||||
|
@ -116,6 +117,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
|
||||||
"""Changement du mot de passe"""
|
"""Changement du mot de passe"""
|
||||||
user = super(PassForm, self).save(commit=False)
|
user = super(PassForm, self).save(commit=False)
|
||||||
user.set_password(self.cleaned_data.get("passwd1"))
|
user.set_password(self.cleaned_data.get("passwd1"))
|
||||||
|
user.set_active()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -323,14 +325,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
self.fields['room'].empty_label = _("No room")
|
self.fields['room'].empty_label = _("No room")
|
||||||
self.fields['school'].empty_label = _("Select a school")
|
self.fields['school'].empty_label = _("Select a school")
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
|
|
||||||
return self.cleaned_data.get('email').lower()
|
|
||||||
else:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("You can't use a {} address.").format(
|
|
||||||
OptionalUser.objects.first().local_email_domain))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Adherent
|
model = Adherent
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -344,6 +338,19 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
'room',
|
'room',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
force = forms.BooleanField(
|
||||||
|
label=_("Force the move?"),
|
||||||
|
initial=False,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
|
||||||
|
return self.cleaned_data.get('email').lower()
|
||||||
|
else:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("You can't use a {} address.").format(
|
||||||
|
OptionalUser.objects.first().local_email_domain))
|
||||||
|
|
||||||
def clean_telephone(self):
|
def clean_telephone(self):
|
||||||
"""Verifie que le tel est présent si 'option est validée
|
"""Verifie que le tel est présent si 'option est validée
|
||||||
|
@ -355,12 +362,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
)
|
)
|
||||||
return telephone
|
return telephone
|
||||||
|
|
||||||
force = forms.BooleanField(
|
|
||||||
label=_("Force the move?"),
|
|
||||||
initial=False,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_force(self):
|
def clean_force(self):
|
||||||
"""On supprime l'ancien user de la chambre si et seulement si la
|
"""On supprime l'ancien user de la chambre si et seulement si la
|
||||||
case est cochée"""
|
case est cochée"""
|
||||||
|
@ -368,6 +369,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
remove_user_room(self.cleaned_data.get('room'))
|
remove_user_room(self.cleaned_data.get('room'))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class AdherentCreationForm(AdherentForm):
|
class AdherentCreationForm(AdherentForm):
|
||||||
"""Formulaire de création d'un user.
|
"""Formulaire de création d'un user.
|
||||||
AdherentForm auquel on ajoute une checkbox afin d'éviter les
|
AdherentForm auquel on ajoute une checkbox afin d'éviter les
|
||||||
|
@ -383,8 +385,22 @@ class AdherentCreationForm(AdherentForm):
|
||||||
|
|
||||||
# Checkbox for GTU
|
# Checkbox for GTU
|
||||||
gtu_check = forms.BooleanField(required=True)
|
gtu_check = forms.BooleanField(required=True)
|
||||||
gtu_check.label = mark_safe("{} <a href='/media/{}' download='CGU'>{}</a>{}".format(
|
#gtu_check.label = mark_safe("{} <a href='/media/{}' download='CGU'>{}</a>{}".format(
|
||||||
_("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
|
# _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Adherent
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
'surname',
|
||||||
|
'pseudo',
|
||||||
|
'email',
|
||||||
|
'school',
|
||||||
|
'comment',
|
||||||
|
'telephone',
|
||||||
|
'room',
|
||||||
|
'state',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(AdherentCreationForm, self).__init__(*args, **kwargs)
|
super(AdherentCreationForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -398,12 +414,6 @@ class AdherentEditForm(AdherentForm):
|
||||||
if 'shell' in self.fields:
|
if 'shell' in self.fields:
|
||||||
self.fields['shell'].empty_label = _("Default shell")
|
self.fields['shell'].empty_label = _("Default shell")
|
||||||
|
|
||||||
def clean_gpg_fingerprint(self):
|
|
||||||
"""Format the GPG fingerprint"""
|
|
||||||
gpg_fingerprint = self.cleaned_data.get('gpg_fingerprint', None)
|
|
||||||
if gpg_fingerprint:
|
|
||||||
return gpg_fingerprint.replace(' ', '').upper()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Adherent
|
model = Adherent
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -429,6 +439,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
self.fields['surname'].label = _("Name")
|
self.fields['surname'].label = _("Name")
|
||||||
self.fields['school'].label = _("School")
|
self.fields['school'].label = _("School")
|
||||||
self.fields['comment'].label = _("Comment")
|
self.fields['comment'].label = _("Comment")
|
||||||
|
self.fields['email'].label = _("Email Address")
|
||||||
if 'room' in self.fields:
|
if 'room' in self.fields:
|
||||||
self.fields['room'].label = _("Room")
|
self.fields['room'].label = _("Room")
|
||||||
self.fields['room'].empty_label = _("No room")
|
self.fields['room'].empty_label = _("No room")
|
||||||
|
@ -443,7 +454,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
'school',
|
'school',
|
||||||
'comment',
|
'comment',
|
||||||
'room',
|
'room',
|
||||||
|
'email',
|
||||||
'telephone',
|
'telephone',
|
||||||
|
'email',
|
||||||
'shell',
|
'shell',
|
||||||
'mailing'
|
'mailing'
|
||||||
]
|
]
|
||||||
|
@ -488,13 +501,14 @@ class PasswordForm(FormRevMixin, ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class ServiceUserForm(FormRevMixin, ModelForm):
|
class ServiceUserForm(FormRevMixin, ModelForm):
|
||||||
""" Modification d'un service user"""
|
"""Service user creation
|
||||||
|
force initial password set"""
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
label=_("New password"),
|
label=_("New password"),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
validators=[MinLengthValidator(8)],
|
validators=[MinLengthValidator(8)],
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput,
|
||||||
required=False
|
required=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -506,7 +520,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
|
||||||
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
|
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
"""Changement du mot de passe"""
|
"""Password change"""
|
||||||
user = super(ServiceUserForm, self).save(commit=False)
|
user = super(ServiceUserForm, self).save(commit=False)
|
||||||
if self.cleaned_data['password']:
|
if self.cleaned_data['password']:
|
||||||
user.set_password(self.cleaned_data.get("password"))
|
user.set_password(self.cleaned_data.get("password"))
|
||||||
|
@ -516,6 +530,14 @@ class ServiceUserForm(FormRevMixin, ModelForm):
|
||||||
class EditServiceUserForm(ServiceUserForm):
|
class EditServiceUserForm(ServiceUserForm):
|
||||||
"""Formulaire d'edition de base d'un service user. Ne permet
|
"""Formulaire d'edition de base d'un service user. Ne permet
|
||||||
d'editer que son group d'acl et son commentaire"""
|
d'editer que son group d'acl et son commentaire"""
|
||||||
|
password = forms.CharField(
|
||||||
|
label=_("New password"),
|
||||||
|
max_length=255,
|
||||||
|
validators=[MinLengthValidator(8)],
|
||||||
|
widget=forms.PasswordInput,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(ServiceUserForm.Meta):
|
class Meta(ServiceUserForm.Meta):
|
||||||
fields = ['access_group', 'comment']
|
fields = ['access_group', 'comment']
|
||||||
|
|
||||||
|
|
Binary file not shown.
20
users/migrations/0079_auto_20181228_2039.py
Normal file
20
users/migrations/0079_auto_20181228_2039.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-28 19:39
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0078_auto_20181011_1405'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='adherent',
|
||||||
|
name='gpg_fingerprint',
|
||||||
|
field=models.CharField(blank=True, max_length=49, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -81,6 +81,7 @@ from re2o.settings import LDAP, GID_RANGES, UID_RANGES
|
||||||
from re2o.login import hashNT
|
from re2o.login import hashNT
|
||||||
from re2o.field_permissions import FieldPermissionModelMixin
|
from re2o.field_permissions import FieldPermissionModelMixin
|
||||||
from re2o.mixins import AclMixin, RevMixin
|
from re2o.mixins import AclMixin, RevMixin
|
||||||
|
from re2o.base import smtp_check
|
||||||
|
|
||||||
from cotisations.models import Cotisation, Facture, Paiement, Vente
|
from cotisations.models import Cotisation, Facture, Paiement, Vente
|
||||||
from machines.models import Domain, Interface, Machine, regen
|
from machines.models import Domain, Interface, Machine, regen
|
||||||
|
@ -93,7 +94,7 @@ from preferences.models import OptionalMachine, MailMessageOption
|
||||||
|
|
||||||
def linux_user_check(login):
|
def linux_user_check(login):
|
||||||
""" Validation du pseudo pour respecter les contraintes unix"""
|
""" Validation du pseudo pour respecter les contraintes unix"""
|
||||||
UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9-]*[$]?$")
|
UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$")
|
||||||
return UNIX_LOGIN_PATTERN.match(login)
|
return UNIX_LOGIN_PATTERN.match(login)
|
||||||
|
|
||||||
|
|
||||||
|
@ -336,7 +337,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
||||||
def set_active(self):
|
def set_active(self):
|
||||||
"""Enable this user if he subscribed successfully one time before"""
|
"""Enable this user if he subscribed successfully one time before"""
|
||||||
if self.state == self.STATE_NOT_YET_ACTIVE:
|
if self.state == self.STATE_NOT_YET_ACTIVE:
|
||||||
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists():
|
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists() or OptionalUser.get_cached_value('all_users_active'):
|
||||||
self.state = self.STATE_ACTIVE
|
self.state = self.STATE_ACTIVE
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@ -474,7 +475,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
||||||
""" Renvoie si un utilisateur a accès à internet """
|
""" Renvoie si un utilisateur a accès à internet """
|
||||||
return (self.state == User.STATE_ACTIVE and
|
return (self.state == User.STATE_ACTIVE and
|
||||||
not self.is_ban() and
|
not self.is_ban() and
|
||||||
(self.is_connected() or self.is_whitelisted()))
|
(self.is_connected() or self.is_whitelisted())) \
|
||||||
|
or self == AssoOption.get_cached_value('utilisateur_asso')
|
||||||
|
|
||||||
def end_access(self):
|
def end_access(self):
|
||||||
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
|
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
|
||||||
|
@ -576,7 +578,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
||||||
mac_refresh : synchronise les machines de l'user
|
mac_refresh : synchronise les machines de l'user
|
||||||
group_refresh : synchronise les group de l'user
|
group_refresh : synchronise les group de l'user
|
||||||
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
|
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\
|
||||||
|
self.state != self.STATE_DISABLED:
|
||||||
self.refresh_from_db()
|
self.refresh_from_db()
|
||||||
try:
|
try:
|
||||||
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
|
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
|
||||||
|
@ -693,10 +696,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
||||||
def autoregister_machine(self, mac_address, nas_type):
|
def autoregister_machine(self, mac_address, nas_type):
|
||||||
""" Fonction appellée par freeradius. Enregistre la mac pour
|
""" Fonction appellée par freeradius. Enregistre la mac pour
|
||||||
une machine inconnue sur le compte de l'user"""
|
une machine inconnue sur le compte de l'user"""
|
||||||
all_interfaces = self.user_interfaces(active=False)
|
allowed, _message = Machine.can_create(self, self.id)
|
||||||
if all_interfaces.count() > OptionalMachine.get_cached_value(
|
if not allowed:
|
||||||
'max_lambdauser_interfaces'
|
|
||||||
):
|
|
||||||
return False, _("Maximum number of registered machines reached.")
|
return False, _("Maximum number of registered machines reached.")
|
||||||
if not nas_type:
|
if not nas_type:
|
||||||
return False, _("Re2o doesn't know wich machine type to assign.")
|
return False, _("Re2o doesn't know wich machine type to assign.")
|
||||||
|
@ -1025,17 +1026,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
||||||
):
|
):
|
||||||
raise ValidationError("This pseudo is already in use.")
|
raise ValidationError("This pseudo is already in use.")
|
||||||
if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE):
|
if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE):
|
||||||
raise ValidationError(
|
raise ValidationError(_("There is neither a local email address nor an external"
|
||||||
{'email': (
|
|
||||||
_("There is neither a local email address nor an external"
|
|
||||||
" email address for this user.")
|
" email address for this user.")
|
||||||
), }
|
|
||||||
)
|
)
|
||||||
if self.local_email_redirect and not self.email:
|
if self.local_email_redirect and not self.email:
|
||||||
raise ValidationError(
|
raise ValidationError(_("You can't redirect your local emails if no external email"
|
||||||
{'local_email_redirect': (
|
" address has been set.")
|
||||||
_("You can't redirect your local emails if no external email"
|
|
||||||
" address has been set.")), }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -1054,20 +1050,27 @@ class Adherent(User):
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
gpg_fingerprint = models.CharField(
|
gpg_fingerprint = models.CharField(
|
||||||
max_length=40,
|
max_length=49,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[RegexValidator(
|
|
||||||
'^[0-9A-F]{40}$',
|
|
||||||
message=_("A GPG fingerprint must contain 40 hexadecimal"
|
|
||||||
" characters.")
|
|
||||||
)]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(User.Meta):
|
class Meta(User.Meta):
|
||||||
verbose_name = _("member")
|
verbose_name = _("member")
|
||||||
verbose_name_plural = _("members")
|
verbose_name_plural = _("members")
|
||||||
|
|
||||||
|
def format_gpgfp(self):
|
||||||
|
"""Format gpg finger print as AAAA BBBB... from a string AAAABBBB...."""
|
||||||
|
self.gpg_fingerprint = ' '.join([self.gpg_fingerprint[i:i + 4] for i in range(0, len(self.gpg_fingerprint), 4)])
|
||||||
|
|
||||||
|
def validate_gpgfp(self):
|
||||||
|
"""Validate from raw entry if is it a valid gpg fp"""
|
||||||
|
if self.gpg_fingerprint:
|
||||||
|
gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper()
|
||||||
|
if not re.match("^[0-9A-F]{40}$", gpg_fingerprint):
|
||||||
|
raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters"))
|
||||||
|
self.gpg_fingerprint = gpg_fingerprint
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_instance(cls, adherentid, *_args, **_kwargs):
|
def get_instance(cls, adherentid, *_args, **_kwargs):
|
||||||
"""Try to find an instance of `Adherent` with the given id.
|
"""Try to find an instance of `Adherent` with the given id.
|
||||||
|
@ -1098,6 +1101,13 @@ class Adherent(User):
|
||||||
_("You don't have the right to create a user.")
|
_("You don't have the right to create a user.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean(self, *args, **kwargs):
|
||||||
|
"""Format the GPG fingerprint"""
|
||||||
|
super(Adherent, self).clean(*args, **kwargs)
|
||||||
|
if self.gpg_fingerprint:
|
||||||
|
self.validate_gpgfp()
|
||||||
|
self.format_gpgfp()
|
||||||
|
|
||||||
|
|
||||||
class Club(User):
|
class Club(User):
|
||||||
""" A class representing a club (it is considered as a user
|
""" A class representing a club (it is considered as a user
|
||||||
|
@ -1889,6 +1899,9 @@ class EMailAddress(RevMixin, AclMixin, models.Model):
|
||||||
|
|
||||||
def clean(self, *args, **kwargs):
|
def clean(self, *args, **kwargs):
|
||||||
self.local_part = self.local_part.lower()
|
self.local_part = self.local_part.lower()
|
||||||
if "@" in self.local_part:
|
if "@" in self.local_part or "+" in self.local_part:
|
||||||
raise ValidationError(_("The local part must not contain @."))
|
raise ValidationError(_("The local part must not contain @ or +."))
|
||||||
|
result, reason = smtp_check(self.local_part)
|
||||||
|
if result:
|
||||||
|
raise ValidationError(reason)
|
||||||
super(EMailAddress, self).clean(*args, **kwargs)
|
super(EMailAddress, self).clean(*args, **kwargs)
|
||||||
|
|
|
@ -23,7 +23,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
{% load logs_extra %}
|
{% load logs_extra %}
|
||||||
{% load design %}
|
{% load design %}
|
||||||
|
@ -78,7 +77,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if solde_activated %}
|
{% if solde_activated %}
|
||||||
<div class="col-sm-6 col-md-4">
|
<div class="col-sm-6 col-md-4">
|
||||||
<div class="panel panel-info">
|
<div class="panel panel-info">
|
||||||
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
|
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
|
||||||
|
data-target="#subscriptions">
|
||||||
{{ users.solde }} <i class="fa fa-eur"></i>
|
{{ users.solde }} <i class="fa fa-eur"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body dashboard">
|
<div class="panel-body dashboard">
|
||||||
|
@ -92,7 +92,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<div class="col-sm-6 {% if solde_activated %}col-md-4{% else %}col-md-6{% endif %}">
|
<div class="col-sm-6 {% if solde_activated %}col-md-4{% else %}col-md-6{% endif %}">
|
||||||
{% if nb_machines %}
|
{% if nb_machines %}
|
||||||
<div class="panel panel-info">
|
<div class="panel panel-info">
|
||||||
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
|
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
|
||||||
|
data-target="#machines">
|
||||||
<i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span>
|
<i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body dashboard">
|
<div class="panel-body dashboard">
|
||||||
|
@ -103,7 +104,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="panel panel-warning">
|
<div class="panel panel-warning">
|
||||||
<div class="panel-heading dashboard">{% trans "No machine" %}</div>
|
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
|
||||||
|
data-target="#machines">
|
||||||
|
{% trans "No machine" %}
|
||||||
|
</div>
|
||||||
<div class="panel-body dashboard">
|
<div class="panel-body dashboard">
|
||||||
<a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
|
<a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
|
||||||
<i class="fa fa-desktop"></i>{% trans " Add a machine" %}
|
<i class="fa fa-desktop"></i>{% trans " Add a machine" %}
|
||||||
|
@ -117,12 +121,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
<div class="panel-group" id="accordion">
|
<div class="panel-group" id="accordion">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse1">
|
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
|
||||||
<h3 class="panel-title pull-left" >
|
data-target="#information">
|
||||||
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-user"></i>{% trans " Detailed information" %}
|
<i class="fa fa-user"></i>{% trans " Detailed information" %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-collapse collapse in" id="collapse1">
|
<div class="panel-collapse collapse collapse-default" id="information">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
|
@ -148,127 +153,168 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="table-responsive">
|
<dl class="dl-horizontal row profile-info">
|
||||||
<table class="table table-striped">
|
<div class="col-md-6">
|
||||||
<tr>
|
|
||||||
{% if users.is_class_club %}
|
{% if users.is_class_club %}
|
||||||
<th>{% trans "Mailing" %}</th>
|
<dt>{% trans "Mailing" %}</dt>
|
||||||
{% if users.club.mailing %}
|
{% if users.club.mailing %}
|
||||||
<td>{{ users.pseudo }}(-admin)</td>
|
<dd>{{ users.pseudo }}(-admin)</dd>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{% trans "Mailing disabled" %}</td>
|
<dd>{% trans "Mailing disabled" %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<th>{% trans "Firt name" %}</th>
|
<dt>{% trans "Firt name" %}</dt>
|
||||||
<td>{{ users.name }}</td>
|
<dd>{{ users.name }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>{% trans "Surname" %}</th>
|
</div>
|
||||||
<td>{{ users.surname }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Username" %}</th>
|
|
||||||
<td>{{ users.pseudo }}</td>
|
|
||||||
<th>{% trans "Email address" %}</th>
|
|
||||||
<td><a href="mailto:{{ users.email }}">{{users.email}}</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Room" %}</th>
|
|
||||||
<td>{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} / {{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}</td>
|
|
||||||
<th>{% trans "Telephone number" %}</th>
|
|
||||||
<td>{{ users.telephone }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "School" %}</th>
|
|
||||||
<td>{{ users.school }}</td>
|
|
||||||
<th>{% trans "Comment" %}</th>
|
|
||||||
<td>{{ users.comment }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Registration date" %}</th>
|
|
||||||
<td>{{ users.registered }}</td>
|
|
||||||
<th>{% trans "Last login" %}</th>
|
|
||||||
<td>{{ users.last_login }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "End of membership" %}</th>
|
|
||||||
{% if users.end_adhesion != None %}
|
|
||||||
<td><i class="text-success">{{ users.end_adhesion }}</i></td>
|
|
||||||
{% else %}
|
|
||||||
<td><i class="text-danger">{% trans "Not a member" %}</i></td>
|
|
||||||
{% endif %}
|
|
||||||
<th>{% trans "Whitelist" %}</th>
|
|
||||||
{% if users.end_whitelist != None %}
|
|
||||||
<td><i class="text-success">{{ users.end_whitelist }}</i></td>
|
|
||||||
{% else %}
|
|
||||||
<td><i class="text-warning">{% trans "None" %}</i></td>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Ban" %}</th>
|
|
||||||
{% if users.end_ban != None %}
|
|
||||||
<td><i class="text-danger">{{ users.end_ban }}</i></td>
|
|
||||||
{% else %}
|
|
||||||
<td><i class="text-success">{% trans "Not banned" %}</i></td>
|
|
||||||
{% endif %}
|
|
||||||
<th>{% trans "State" %}</th>
|
|
||||||
{% if users.state == 0 %}
|
|
||||||
<td><i class="text-success">{% trans "Active" %}</i></td>
|
|
||||||
{% elif users.state == 1 %}
|
|
||||||
<td><i class="text-warning">{% trans "Disabled" %}</i></td>
|
|
||||||
{% elif users.state == 2 %}
|
|
||||||
<td><i class="text-danger">{% trans "Archived" %}</i></td>
|
|
||||||
{% elif users.state == 3 %}
|
|
||||||
<td><i class="text-danger">{% trans "Not yet Member" %}</i></td>
|
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Surname" %}</dt>
|
||||||
|
<dd>{{ users.surname }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Username" %}</dt>
|
||||||
|
<dd>{{ users.pseudo }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Email address" %}</dt>
|
||||||
|
<dd><a href="mailto:{{ users.email }}">{{ users.email }}</a></dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Room" %}</dt>
|
||||||
|
<dd>
|
||||||
|
{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} /
|
||||||
|
{{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Telephone number" %}</dt>
|
||||||
|
<dd>{{ users.telephone }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "School" %}</dt>
|
||||||
|
<dd>{{ users.school }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Comment" %}</dt>
|
||||||
|
<dd>{{ users.comment }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Registration date" %}</dt>
|
||||||
|
<dd>{{ users.registered }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Last login" %}</dt>
|
||||||
|
<dd>{{ users.last_login }}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "End of membership" %}</dt>
|
||||||
|
{% if users.end_adhesion != None %}
|
||||||
|
<dd><i class="text-success">{{ users.end_adhesion }}</i></dd>
|
||||||
|
{% else %}
|
||||||
|
<dd><i class="text-danger">{% trans "Not a member" %}</i></dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
|
||||||
<th>{% trans "Internet access" %}</th>
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Whitelist" %}</dt>
|
||||||
|
{% if users.end_whitelist != None %}
|
||||||
|
<dd><i class="text-success">{{ users.end_whitelist }}</i></dd>
|
||||||
|
{% else %}
|
||||||
|
<dd><i class="text-warning">{% trans "None" %}</i></dd>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Ban" %}</dt>
|
||||||
|
{% if users.end_ban != None %}
|
||||||
|
<dd><i class="text-danger">{{ users.end_ban }}</i></dd>
|
||||||
|
{% else %}
|
||||||
|
<dd><i class="text-success">{% trans "Not banned" %}</i></dd>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "State" %}</dt>
|
||||||
|
{% if users.state == 0 %}
|
||||||
|
<dd><i class="text-success">{% trans "Active" %}</i></dd>
|
||||||
|
{% elif users.state == 1 %}
|
||||||
|
<dd><i class="text-warning">{% trans "Disabled" %}</i></dd>
|
||||||
|
{% elif users.state == 2 %}
|
||||||
|
<dd><i class="text-danger">{% trans "Archived" %}</i></dd>
|
||||||
|
{% elif users.state == 3 %}
|
||||||
|
<dd><i class="text-danger">{% trans "Not yet Member" %}</i></dd>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Internet access" %}</dt>
|
||||||
{% if users.has_access == True %}
|
{% if users.has_access == True %}
|
||||||
<td><i class="text-success">{% blocktrans with end_access=users.end_access %}Active (until {{ end_access }}){% endblocktrans %}</i></td>
|
<dd><i class="text-success">
|
||||||
|
{% blocktrans with end_access=users.end_access %}Active
|
||||||
|
(until {{ end_access }}){% endblocktrans %}</i></dd>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><i class="text-danger">{% trans "Disabled" %}</i></td>
|
<dd><i class="text-danger">{% trans "Disabled" %}</i></dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>{% trans "Groups of rights" %}</th>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<dt>{% trans "Groups of rights" %}</dt>
|
||||||
{% if users.groups.all %}
|
{% if users.groups.all %}
|
||||||
<td>{{ users.groups.all|join:", "}}</td>
|
<dd>{{ users.groups.all|join:", " }}</dd>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{% trans "None" %}</td>
|
<dd>{% trans "None" %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
|
||||||
<th>{% trans "Balance" %}</th>
|
<div class="col-md-6">
|
||||||
<td>{{ users.solde }} €
|
<dt>{% trans "Balance" %}</dt>
|
||||||
|
<dd>
|
||||||
|
{{ users.solde }} €
|
||||||
{% if user_solde %}
|
{% if user_solde %}
|
||||||
<a class="btn btn-primary btn-sm" style='float:right' role="button" href="{% url 'cotisations:credit-solde' users.pk%}">
|
<a class="btn btn-primary btn-sm" style='float:right' role="button"
|
||||||
|
href="{% url 'cotisations:credit-solde' users.pk %}">
|
||||||
<i class="fa fa-eur"></i>
|
<i class="fa fa-eur"></i>
|
||||||
{% trans "Refill" %}
|
{% trans "Refill" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</dd>
|
||||||
{% if users.adherent.gpg_fingerprint %}
|
|
||||||
<th>{% trans "GPG fingerprint" %}</th>
|
|
||||||
<td>{{ users.adherent.gpg_fingerprint }}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
{% if users.shell %}
|
|
||||||
<th>{% trans "Shell" %}</th>
|
|
||||||
<td>{{ users.shell }}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if users.adherent.gpg_fingerprint %}
|
||||||
|
<div class="col-md-6 col-xs-12">
|
||||||
|
<dt>{% trans "GPG fingerprint" %}</dt>
|
||||||
|
<dd>{{ users.adherent.gpg_fingerprint }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if users.shell %}
|
||||||
|
<div class="col-md-6 col-xs-12">
|
||||||
|
<dt>{% trans "Shell" %}</dt>
|
||||||
|
<dd>{{ users.shell }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if users.is_class_club %}
|
{% if users.is_class_club %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse2">
|
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#club">
|
||||||
<h3 class="panel-title pull-left">
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-users"></i>{% trans " Manage the club" %}
|
<i class="fa fa-users"></i>{% trans " Manage the club" %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-collapse collapse" id="collapse2">
|
<div class="panel-collapse collapse" id="club">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}">
|
||||||
<i class="fa fa-lock"></i>
|
<i class="fa fa-lock"></i>
|
||||||
|
@ -319,14 +365,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
|
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
|
||||||
|
data-target="#machines">
|
||||||
<h3 class="panel-title pull-left">
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-desktop"></i>
|
<i class="fa fa-desktop"></i>
|
||||||
{% trans "Machines" %}
|
{% trans "Machines" %}
|
||||||
<span class="badge">{{nb_machines}}</span>
|
<span class="badge">{{nb_machines}}</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse3" class="panel-collapse collapse">
|
<div id="machines" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
|
||||||
<i class="fa fa-desktop"></i>
|
<i class="fa fa-desktop"></i>
|
||||||
|
@ -343,13 +390,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
|
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
|
||||||
|
data-target="#subscriptions">
|
||||||
<h3 class="panel-title pull-left">
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-eur"></i>
|
<i class="fa fa-eur"></i>
|
||||||
{% trans "Subscriptions" %}
|
{% trans "Subscriptions" %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse4" class="panel-collapse collapse">
|
<div id="subscriptions" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% can_create Facture %}
|
{% can_create Facture %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
|
||||||
|
@ -374,13 +422,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse5">
|
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#bans">
|
||||||
<h3 class="panel-title pull-left">
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-ban"></i>
|
<i class="fa fa-ban"></i>
|
||||||
{% trans "Bans" %}
|
{% trans "Bans" %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse5" class="panel-collapse collapse">
|
<div id="bans" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% can_create Ban %}
|
{% can_create Ban %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-ban' users.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-ban' users.id %}">
|
||||||
|
@ -399,13 +447,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse6">
|
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#whitelists">
|
||||||
<h3 class="panel-title pull-left">
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-check-circle"></i>
|
<i class="fa fa-check-circle"></i>
|
||||||
{% trans "Whitelists" %}
|
{% trans "Whitelists" %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse6" class="panel-collapse collapse">
|
<div id="whitelists" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% can_create Whitelist %}
|
{% can_create Whitelist %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-whitelist' users.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-whitelist' users.id %}">
|
||||||
|
@ -424,12 +472,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse7">
|
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#email">
|
||||||
<h3 class="panel-title pull-left">
|
<h3 class="panel-title pull-left">
|
||||||
<i class="fa fa-envelope"></i>{% trans " Email settings" %}
|
<i class="fa fa-envelope"></i>{% trans " Email settings" %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse7" class="panel-collapse collapse">
|
<div id="email" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% can_edit users %}
|
{% can_edit users %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-email-settings' users.id %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-email-settings' users.id %}">
|
||||||
|
|
|
@ -57,8 +57,10 @@ from preferences.models import OptionalUser, GeneralOption, AssoOption
|
||||||
from re2o.views import form
|
from re2o.views import form
|
||||||
from re2o.utils import (
|
from re2o.utils import (
|
||||||
all_has_access,
|
all_has_access,
|
||||||
SortTable,
|
)
|
||||||
re2o_paginator
|
from re2o.base import (
|
||||||
|
re2o_paginator,
|
||||||
|
SortTable
|
||||||
)
|
)
|
||||||
from re2o.acl import (
|
from re2o.acl import (
|
||||||
can_create,
|
can_create,
|
||||||
|
|
Loading…
Reference in a new issue