mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-24 12:23:11 +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
|
||||
|
||||
# Translations
|
||||
#*.mo TODO
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff
|
||||
|
|
|
@ -391,13 +391,25 @@ class OptionalTopologieSerializer(NamespacedHMSerializer):
|
|||
|
||||
class Meta:
|
||||
model = preferences.OptionalTopologie
|
||||
fields = ('radius_general_policy', 'vlan_decision_ok',
|
||||
'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management',
|
||||
fields = ('switchs_ip_type', 'switchs_web_management',
|
||||
'switchs_web_management_ssl', 'switchs_rest_management',
|
||||
'switchs_management_utils', 'switchs_management_interface_ip',
|
||||
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
|
||||
|
||||
|
||||
class RadiusOptionSerializer(NamespacedHMSerializer):
|
||||
"""Serialize `preferences.models.RadiusOption` objects
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = preferences.RadiusOption
|
||||
fields = ('radius_general_policy', 'unknown_machine',
|
||||
'unknown_machine_vlan', 'unknown_port',
|
||||
'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan',
|
||||
'non_member', 'non_member_vlan', 'banned', 'banned_vlan',
|
||||
'vlan_decision_ok')
|
||||
|
||||
|
||||
class GeneralOptionSerializer(NamespacedHMSerializer):
|
||||
"""Serialize `preferences.models.GeneralOption` objects.
|
||||
"""
|
||||
|
@ -407,9 +419,8 @@ class GeneralOptionSerializer(NamespacedHMSerializer):
|
|||
fields = ('general_message_fr', 'general_message_en',
|
||||
'search_display_page', 'pagination_number',
|
||||
'pagination_large_number', 'req_expire_hrs',
|
||||
'site_name', 'email_from', 'GTU_sum_up',
|
||||
'GTU')
|
||||
|
||||
'site_name', 'main_site_url', 'email_from',
|
||||
'GTU_sum_up', 'GTU')
|
||||
|
||||
class HomeServiceSerializer(NamespacedHMSerializer):
|
||||
"""Serialize `preferences.models.Service` objects.
|
||||
|
@ -633,9 +644,8 @@ class AdherentSerializer(NamespacedHMSerializer):
|
|||
}
|
||||
|
||||
|
||||
class HomeCreationSerializer(NamespacedHMSerializer):
|
||||
"""Serialize 'users.models.User' minimal infos to create home
|
||||
"""
|
||||
class BasicUserSerializer(NamespacedHMSerializer):
|
||||
"""Serialize 'users.models.User' minimal infos"""
|
||||
uid = serializers.IntegerField(source='uid_number')
|
||||
gid = serializers.IntegerField(source='gid_number')
|
||||
|
||||
|
@ -813,7 +823,8 @@ class SwitchPortSerializer(serializers.ModelSerializer):
|
|||
model = topologie.Switch
|
||||
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
|
||||
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
|
||||
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value')
|
||||
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value',
|
||||
'list_modules')
|
||||
|
||||
# LOCAL EMAILS
|
||||
|
||||
|
@ -1001,6 +1012,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer):
|
|||
model = machines.Domain
|
||||
fields = ('alias', 'hostname')
|
||||
|
||||
class DNAMERecordSerializer(serializers.ModelSerializer):
|
||||
"""Serialize `machines.models.Domain` objects with the data needed to
|
||||
generate a DNAME DNS record.
|
||||
"""
|
||||
alias = serializers.CharField(read_only=True)
|
||||
zone = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = machines.DName
|
||||
fields = ('alias', 'zone')
|
||||
|
||||
|
||||
class DNSZonesSerializer(serializers.ModelSerializer):
|
||||
"""Serialize the data about DNS Zones.
|
||||
|
@ -1015,14 +1037,14 @@ class DNSZonesSerializer(serializers.ModelSerializer):
|
|||
a_records = ARecordSerializer(many=True, source='get_associated_a_records')
|
||||
aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records')
|
||||
cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records')
|
||||
dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records')
|
||||
sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records')
|
||||
|
||||
class Meta:
|
||||
model = machines.Extension
|
||||
fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6',
|
||||
'mx_records', 'txt_records', 'srv_records', 'a_records',
|
||||
'aaaa_records', 'cname_records', 'sshfp_records')
|
||||
|
||||
'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records')
|
||||
#REMINDER
|
||||
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ router.register_viewset(r'machines/role', views.RoleViewSet)
|
|||
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
|
||||
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
|
||||
router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView),
|
||||
router.register_view(r'preferences/radiusoption', views.RadiusOptionView),
|
||||
router.register_view(r'preferences/generaloption', views.GeneralOptionView),
|
||||
router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
|
||||
router.register_view(r'preferences/assooption', views.AssoOptionView),
|
||||
|
@ -88,6 +89,8 @@ router.register(r'topologie/portprofile', views.PortProfileViewSet)
|
|||
# USERS
|
||||
router.register_viewset(r'users/user', views.UserViewSet, base_name='user')
|
||||
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation')
|
||||
router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser')
|
||||
router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser')
|
||||
router.register_viewset(r'users/club', views.ClubViewSet)
|
||||
router.register_viewset(r'users/adherent', views.AdherentViewSet)
|
||||
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)
|
||||
|
|
35
api/views.py
35
api/views.py
|
@ -292,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView):
|
|||
return preferences.OptionalTopologie.objects.first()
|
||||
|
||||
|
||||
class RadiusOptionView(generics.RetrieveAPIView):
|
||||
"""Exposes details of `preferences.models.OptionalTopologie` settings.
|
||||
"""
|
||||
permission_classes = (ACLPermission,)
|
||||
perms_map = {'GET': [preferences.RadiusOption.can_view_all]}
|
||||
serializer_class = serializers.RadiusOptionSerializer
|
||||
|
||||
def get_object(self):
|
||||
return preferences.RadiusOption.objects.first()
|
||||
|
||||
|
||||
class GeneralOptionView(generics.RetrieveAPIView):
|
||||
"""Exposes details of `preferences.models.GeneralOption` settings.
|
||||
"""
|
||||
|
@ -445,7 +456,19 @@ class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
"""Exposes infos of `users.models.Users` objects to create homes.
|
||||
"""
|
||||
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
|
||||
serializer_class = serializers.HomeCreationSerializer
|
||||
serializer_class = serializers.BasicUserSerializer
|
||||
|
||||
|
||||
class NormalUserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""Exposes infos of `users.models.Users`without specific rights objects."""
|
||||
queryset = users.User.objects.exclude(groups__listright__critical=True).distinct()
|
||||
serializer_class = serializers.BasicUserSerializer
|
||||
|
||||
|
||||
class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""Exposes infos of `users.models.Users`without specific rights objects."""
|
||||
queryset = users.User.objects.filter(groups__listright__critical=True).distinct()
|
||||
serializer_class = serializers.BasicUserSerializer
|
||||
|
||||
|
||||
class ClubViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
@ -541,8 +564,8 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
|
|||
# Config des switches
|
||||
|
||||
class SwitchPortView(generics.ListAPIView):
|
||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
||||
order to build the DHCP lease files.
|
||||
"""Output each port of a switch, to be serialized with
|
||||
additionnal informations (profiles etc)
|
||||
"""
|
||||
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
|
||||
|
||||
|
@ -551,16 +574,14 @@ class SwitchPortView(generics.ListAPIView):
|
|||
# Rappel fin adhésion
|
||||
|
||||
class ReminderView(generics.ListAPIView):
|
||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
||||
order to build the DHCP lease files.
|
||||
"""Output for users to remind an end of their subscription.
|
||||
"""
|
||||
queryset = preferences.Reminder.objects.all()
|
||||
serializer_class = serializers.ReminderSerializer
|
||||
|
||||
|
||||
class RoleView(generics.ListAPIView):
|
||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
||||
order to build the DHCP lease files.
|
||||
"""Output of roles for each server
|
||||
"""
|
||||
queryset = machines.Role.objects.all().prefetch_related('servers')
|
||||
serializer_class = serializers.RoleSerializer
|
||||
|
|
|
@ -17,3 +17,4 @@ libjs-bootstrap
|
|||
fonts-font-awesome
|
||||
graphviz
|
||||
git
|
||||
gettext
|
||||
|
|
|
@ -30,7 +30,7 @@ from django.contrib import admin
|
|||
from reversion.admin import VersionAdmin
|
||||
|
||||
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
|
||||
from .models import CustomInvoice
|
||||
from .models import CustomInvoice, CostEstimate
|
||||
|
||||
|
||||
class FactureAdmin(VersionAdmin):
|
||||
|
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
|
|||
pass
|
||||
|
||||
|
||||
class CostEstimateAdmin(VersionAdmin):
|
||||
"""Admin class for cost estimates."""
|
||||
pass
|
||||
|
||||
|
||||
class CustomInvoiceAdmin(VersionAdmin):
|
||||
"""Admin class for custom invoices."""
|
||||
pass
|
||||
|
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
|
|||
admin.site.register(Vente, VenteAdmin)
|
||||
admin.site.register(Cotisation, CotisationAdmin)
|
||||
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
|
||||
admin.site.register(CostEstimate, CostEstimateAdmin)
|
||||
|
|
|
@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
|
|||
|
||||
from re2o.field_permissions import FieldPermissionFormMixin
|
||||
from re2o.mixins import FormRevMixin
|
||||
from .models import Article, Paiement, Facture, Banque, CustomInvoice
|
||||
from .models import (
|
||||
Article, Paiement, Facture, Banque,
|
||||
CustomInvoice, Vente, CostEstimate
|
||||
)
|
||||
from .payment_methods import balance
|
||||
|
||||
|
||||
|
@ -104,7 +107,44 @@ class SelectArticleForm(FormRevMixin, Form):
|
|||
user = kwargs.pop('user')
|
||||
target_user = kwargs.pop('target_user', None)
|
||||
super(SelectArticleForm, self).__init__(*args, **kwargs)
|
||||
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user)
|
||||
self.fields['article'].queryset = Article.find_allowed_articles(
|
||||
user, target_user)
|
||||
|
||||
|
||||
class DiscountForm(Form):
|
||||
"""
|
||||
Form used in oder to create a discount on an invoice.
|
||||
"""
|
||||
is_relative = forms.BooleanField(
|
||||
label=_("Discount is on percentage"),
|
||||
required=False,
|
||||
)
|
||||
discount = forms.DecimalField(
|
||||
label=_("Discount"),
|
||||
max_value=100,
|
||||
min_value=0,
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def apply_to_invoice(self, invoice):
|
||||
invoice_price = invoice.prix_total()
|
||||
discount = self.cleaned_data['discount']
|
||||
is_relative = self.cleaned_data['is_relative']
|
||||
if is_relative:
|
||||
amount = discount/100 * invoice_price
|
||||
else:
|
||||
amount = discount
|
||||
if amount:
|
||||
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):
|
||||
|
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
|
|||
fields = '__all__'
|
||||
|
||||
|
||||
class CostEstimateForm(FormRevMixin, ModelForm):
|
||||
"""
|
||||
Form used to create a cost estimate.
|
||||
"""
|
||||
class Meta:
|
||||
model = CostEstimate
|
||||
exclude = ['paid', 'final_invoice']
|
||||
|
||||
|
||||
class ArticleForm(FormRevMixin, ModelForm):
|
||||
"""
|
||||
Form used to create an article.
|
||||
|
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
|
|||
super(RechargeForm, self).__init__(*args, **kwargs)
|
||||
self.fields['payment'].empty_label = \
|
||||
_("Select a payment method")
|
||||
self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True)
|
||||
self.fields['payment'].queryset = Paiement.find_allowed_payments(
|
||||
user_source).exclude(is_balance=True)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
|
@ -266,4 +316,3 @@ class RechargeForm(FormRevMixin, Form):
|
|||
}
|
||||
)
|
||||
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")
|
||||
)
|
||||
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
|
||||
|
@ -624,7 +681,7 @@ class Article(RevMixin, AclMixin, models.Model):
|
|||
objects_pool = cls.objects.filter(
|
||||
Q(type_user='All') | Q(type_user='Adherent')
|
||||
)
|
||||
if not target_user.is_adherent():
|
||||
if target_user is not None and not target_user.is_adherent():
|
||||
objects_pool = objects_pool.filter(
|
||||
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
|
||||
)
|
||||
|
|
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">
|
||||
{% csrf_token %}
|
||||
{% if title %}
|
||||
<h3>{{title}}</h3>
|
||||
{% else %}
|
||||
<h3>{% trans "Edit the invoice" %}</h3>
|
||||
{% endif %}
|
||||
{% massive_bootstrap_form factureform 'user' %}
|
||||
{{ venteform.management_form }}
|
||||
<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 %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if factureform %}
|
||||
{% bootstrap_form_errors factureform %}
|
||||
{% endif %}
|
||||
{% if discount_form %}
|
||||
{% bootstrap_form_errors discount_form %}
|
||||
{% endif %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -68,8 +74,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endfor %}
|
||||
</div>
|
||||
<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>
|
||||
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
|
||||
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
|
||||
|
@ -78,105 +88,117 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% if articlesformset or payment_method%}
|
||||
<script type="text/javascript">
|
||||
{% if articlesformset %}
|
||||
var prices = {};
|
||||
{% for article in articlelist %}
|
||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||
{% endfor %}
|
||||
var prices = {};
|
||||
{% for article in articlelist %}
|
||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||
{% endfor %}
|
||||
|
||||
var template = `Article :
|
||||
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm"
|
||||
id="id_form-__prefix__-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>`
|
||||
var template = `Article :
|
||||
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm"
|
||||
id="id_form-__prefix__-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>`
|
||||
|
||||
function add_article(){
|
||||
// Index start at 0 => new_index = number of items
|
||||
var new_index =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
||||
var new_article = document.createElement('div');
|
||||
new_article.className = 'product_to_sell form-inline';
|
||||
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
||||
document.getElementById('form_set').appendChild(new_article);
|
||||
add_listenner_for_id(new_index);
|
||||
}
|
||||
function add_article(){
|
||||
// Index start at 0 => new_index = number of items
|
||||
var new_index =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
||||
var new_article = document.createElement('div');
|
||||
new_article.className = 'product_to_sell form-inline';
|
||||
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
||||
document.getElementById('form_set').appendChild(new_article);
|
||||
add_listenner_for_id(new_index);
|
||||
}
|
||||
|
||||
function update_price(){
|
||||
var price = 0;
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
var article, article_price, quantity;
|
||||
for (i = 0; i < product_count; ++i){
|
||||
article = document.getElementById(
|
||||
function update_price(){
|
||||
var price = 0;
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
var article, article_price, quantity;
|
||||
for (i = 0; i < product_count; ++i){
|
||||
article = document.getElementById(
|
||||
'id_form-' + i.toString() + '-article').value;
|
||||
if (article == '') {
|
||||
continue;
|
||||
}
|
||||
article_price = prices[article];
|
||||
quantity = document.getElementById(
|
||||
if (article == '') {
|
||||
continue;
|
||||
}
|
||||
article_price = prices[article];
|
||||
quantity = document.getElementById(
|
||||
'id_form-' + i.toString() + '-quantity').value;
|
||||
price += article_price * quantity;
|
||||
}
|
||||
document.getElementById('total_price').innerHTML =
|
||||
price.toFixed(2).toString().replace('.', ',');
|
||||
price += article_price * quantity;
|
||||
}
|
||||
|
||||
function add_listenner_for_id(i){
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("onkeypress", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-quantity')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article-remove')
|
||||
.addEventListener("click", function(event) {
|
||||
var article = event.target.parentNode;
|
||||
article.parentNode.removeChild(article);
|
||||
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||
update_price();
|
||||
})
|
||||
{% 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 =
|
||||
price.toFixed(2).toString().replace('.', ',');
|
||||
}
|
||||
|
||||
// Add events manager when DOM is fully loaded
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.getElementById("add_one")
|
||||
.addEventListener("click", add_article, true);
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
for (i = 0; i < product_count; ++i){
|
||||
add_listenner_for_id(i);
|
||||
}
|
||||
update_price();
|
||||
});
|
||||
function add_listenner_for_id(i){
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("onkeypress", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-quantity')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article-remove')
|
||||
.addEventListener("click", function(event) {
|
||||
var article = event.target.parentNode;
|
||||
article.parentNode.removeChild(article);
|
||||
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||
update_price();
|
||||
})
|
||||
}
|
||||
|
||||
// Add events manager when DOM is fully loaded
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.getElementById("add_one")
|
||||
.addEventListener("click", add_article, true);
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
for (i = 0; i < product_count; ++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();
|
||||
});
|
||||
{% endif %}
|
||||
{% if payment_method.templates %}
|
||||
var TEMPLATES = [
|
||||
"",
|
||||
{% for t in payment_method.templates %}
|
||||
{% if t %}
|
||||
`{% bootstrap_form t %}`,
|
||||
{% else %}
|
||||
"",
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
function update_payment_method_form(){
|
||||
var method = document.getElementById('paymentMethodSelect').value;
|
||||
if(method==""){
|
||||
method=0;
|
||||
}
|
||||
else{
|
||||
method = Number(method);
|
||||
method += 1;
|
||||
}
|
||||
console.log(method);
|
||||
var html = TEMPLATES[method];
|
||||
|
||||
document.getElementById('paymentMethod').innerHTML = html;
|
||||
var TEMPLATES = [
|
||||
"",
|
||||
{% for t in payment_method.templates %}
|
||||
{% if t %}
|
||||
`{% bootstrap_form t %}`,
|
||||
{% else %}
|
||||
"",
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
function update_payment_method_form(){
|
||||
var method = document.getElementById('paymentMethodSelect').value;
|
||||
if(method==""){
|
||||
method=0;
|
||||
}
|
||||
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
||||
else{
|
||||
method = Number(method);
|
||||
method += 1;
|
||||
}
|
||||
console.log(method);
|
||||
var html = TEMPLATES[method];
|
||||
|
||||
document.getElementById('paymentMethod').innerHTML = html;
|
||||
}
|
||||
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
@ -75,8 +75,12 @@
|
|||
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
|
||||
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
|
||||
{% if fid is not None %}
|
||||
{% if is_estimate %}
|
||||
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
|
||||
{% else %}
|
||||
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
\end{tabular*}
|
||||
\\
|
||||
|
||||
|
@ -104,12 +108,30 @@
|
|||
\begin{tabular}{|l|r|}
|
||||
\hline
|
||||
\textbf{Total} & {{total|floatformat:2}} \euro \\
|
||||
{% if not is_estimate %}
|
||||
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
|
||||
\doublehline
|
||||
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
|
||||
{% endif %}
|
||||
\hline
|
||||
\end{tabular}
|
||||
|
||||
\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
|
||||
|
||||
|
||||
|
|
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" %}
|
||||
</a>
|
||||
{% 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 %}
|
||||
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
|
||||
<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.conf import settings
|
||||
from django.utils.text import slugify
|
||||
import logging
|
||||
|
||||
|
||||
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
|
||||
date, the user, the articles, the prices, ...
|
||||
"""
|
||||
is_estimate = ctx.get('is_estimate', False)
|
||||
filename = '_'.join([
|
||||
'invoice',
|
||||
'cost_estimate' if is_estimate else 'invoice',
|
||||
slugify(ctx.get('asso_name', "")),
|
||||
slugify(ctx.get('recipient_name', "")),
|
||||
str(ctx.get('DATE', datetime.now()).year),
|
||||
|
@ -93,6 +95,20 @@ def create_pdf(template, ctx={}):
|
|||
return pdf
|
||||
|
||||
|
||||
def escape_chars(string):
|
||||
"""Escape the '%' and the '€' signs to avoid messing with LaTeX"""
|
||||
if not isinstance(string, str):
|
||||
return string
|
||||
mapping = (
|
||||
('€', r'\euro'),
|
||||
('%', r'\%'),
|
||||
)
|
||||
r = str(string)
|
||||
for k, v in mapping:
|
||||
r = r.replace(k, v)
|
||||
return r
|
||||
|
||||
|
||||
def render_tex(_request, template, ctx={}):
|
||||
"""Creates a PDF from a LaTex templates using pdflatex.
|
||||
|
||||
|
|
|
@ -51,11 +51,41 @@ urlpatterns = [
|
|||
views.facture_pdf,
|
||||
name='facture-pdf'
|
||||
),
|
||||
url(
|
||||
r'^new_cost_estimate/$',
|
||||
views.new_cost_estimate,
|
||||
name='new-cost-estimate'
|
||||
),
|
||||
url(
|
||||
r'^index_cost_estimate/$',
|
||||
views.index_cost_estimate,
|
||||
name='index-cost-estimate'
|
||||
),
|
||||
url(
|
||||
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
|
||||
views.cost_estimate_pdf,
|
||||
name='cost-estimate-pdf',
|
||||
),
|
||||
url(
|
||||
r'^index_custom_invoice/$',
|
||||
views.index_custom_invoice,
|
||||
name='index-custom-invoice'
|
||||
),
|
||||
url(
|
||||
r'^edit_cost_estimate/(?P<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(
|
||||
r'^new_custom_invoice/$',
|
||||
views.new_custom_invoice,
|
||||
|
|
|
@ -47,7 +47,10 @@ from users.models import User
|
|||
from re2o.settings import LOGO_PATH
|
||||
from re2o import settings
|
||||
from re2o.views import form
|
||||
from re2o.utils import SortTable, re2o_paginator
|
||||
from re2o.base import (
|
||||
SortTable,
|
||||
re2o_paginator,
|
||||
)
|
||||
from re2o.acl import (
|
||||
can_create,
|
||||
can_edit,
|
||||
|
@ -65,7 +68,8 @@ from .models import (
|
|||
Paiement,
|
||||
Banque,
|
||||
CustomInvoice,
|
||||
BaseInvoice
|
||||
BaseInvoice,
|
||||
CostEstimate
|
||||
)
|
||||
from .forms import (
|
||||
FactureForm,
|
||||
|
@ -77,9 +81,11 @@ from .forms import (
|
|||
DelBanqueForm,
|
||||
SelectArticleForm,
|
||||
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 .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
|
||||
@can_create(CustomInvoice)
|
||||
def new_custom_invoice(request):
|
||||
|
@ -195,8 +252,9 @@ def new_custom_invoice(request):
|
|||
request.POST or None,
|
||||
form_kwargs={'user': request.user}
|
||||
)
|
||||
discount_form = DiscountForm(request.POST or None)
|
||||
|
||||
if invoice_form.is_valid() and articles_formset.is_valid():
|
||||
if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
|
||||
new_invoice_instance = invoice_form.save()
|
||||
for art_item in articles_formset:
|
||||
if art_item.cleaned_data:
|
||||
|
@ -210,6 +268,7 @@ def new_custom_invoice(request):
|
|||
duration=article.duration,
|
||||
number=quantity
|
||||
)
|
||||
discount_form.apply_to_invoice(new_invoice_instance)
|
||||
messages.success(
|
||||
request,
|
||||
_("The custom invoice was created.")
|
||||
|
@ -220,7 +279,8 @@ def new_custom_invoice(request):
|
|||
'factureform': invoice_form,
|
||||
'action_name': _("Confirm"),
|
||||
'articlesformset': articles_formset,
|
||||
'articlelist': articles
|
||||
'articlelist': articles,
|
||||
'discount_form': discount_form
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
|
@ -263,7 +323,8 @@ def facture_pdf(request, facture, **_kwargs):
|
|||
'siret': AssoOption.get_cached_value('siret'),
|
||||
'email': AssoOption.get_cached_value('contact'),
|
||||
'phone': AssoOption.get_cached_value('telephone'),
|
||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||
'payment_method': facture.paiement.moyen,
|
||||
})
|
||||
|
||||
|
||||
|
@ -328,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
|
|||
}, 'cotisations/delete.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
@can_edit(CostEstimate)
|
||||
def edit_cost_estimate(request, invoice, **kwargs):
|
||||
# Building the invocie form and the article formset
|
||||
invoice_form = CostEstimateForm(
|
||||
request.POST or None,
|
||||
instance=invoice
|
||||
)
|
||||
purchases_objects = Vente.objects.filter(facture=invoice)
|
||||
purchase_form_set = modelformset_factory(
|
||||
Vente,
|
||||
fields=('name', 'number'),
|
||||
extra=0,
|
||||
max_num=len(purchases_objects)
|
||||
)
|
||||
purchase_form = purchase_form_set(
|
||||
request.POST or None,
|
||||
queryset=purchases_objects
|
||||
)
|
||||
if invoice_form.is_valid() and purchase_form.is_valid():
|
||||
if invoice_form.changed_data:
|
||||
invoice_form.save()
|
||||
purchase_form.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("The cost estimate was edited.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||
|
||||
return form({
|
||||
'factureform': invoice_form,
|
||||
'venteform': purchase_form,
|
||||
'title': "Edit the cost estimate"
|
||||
}, 'cotisations/edit_facture.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
@can_edit(CostEstimate)
|
||||
@can_create(CustomInvoice)
|
||||
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
|
||||
"""Create a custom invoice from a cos estimate"""
|
||||
cost_estimate.create_invoice()
|
||||
messages.success(
|
||||
request,
|
||||
_("An invoice was successfully created from your cost estimate.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-custom-invoice'))
|
||||
|
||||
|
||||
@login_required
|
||||
@can_edit(CustomInvoice)
|
||||
def edit_custom_invoice(request, invoice, **kwargs):
|
||||
|
@ -364,10 +474,10 @@ def edit_custom_invoice(request, invoice, **kwargs):
|
|||
|
||||
|
||||
@login_required
|
||||
@can_view(CustomInvoice)
|
||||
def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||
@can_view(CostEstimate)
|
||||
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
|
||||
invoice with the total price, the payment method, the address and the
|
||||
legal information for the user.
|
||||
|
@ -379,7 +489,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
|
|||
purchases_info = []
|
||||
for purchase in purchases_objects:
|
||||
purchases_info.append({
|
||||
'name': purchase.name,
|
||||
'name': escape_chars(purchase.name),
|
||||
'price': purchase.prix,
|
||||
'quantity': purchase.number,
|
||||
'total_price': purchase.prix_total
|
||||
|
@ -398,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
|
|||
'siret': AssoOption.get_cached_value('siret'),
|
||||
'email': AssoOption.get_cached_value('contact'),
|
||||
'phone': AssoOption.get_cached_value('telephone'),
|
||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||
'payment_method': invoice.payment,
|
||||
'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
|
||||
@can_delete(CustomInvoice)
|
||||
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
|
||||
@can_view_all(CustomInvoice)
|
||||
def index_custom_invoice(request):
|
||||
"""View used to display every custom invoice."""
|
||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
|
||||
cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
|
||||
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
|
||||
custom_invoice_list = SortTable.sort(
|
||||
custom_invoice_list,
|
||||
request.GET.get('col'),
|
||||
|
|
|
@ -57,14 +57,9 @@ application = get_wsgi_application()
|
|||
from machines.models import Interface, IpList, Nas, Domain
|
||||
from topologie.models import Port, Switch
|
||||
from users.models import User
|
||||
from preferences.models import OptionalTopologie
|
||||
from preferences.models import RadiusOption
|
||||
|
||||
|
||||
options, created = OptionalTopologie.objects.get_or_create()
|
||||
VLAN_NOK = options.vlan_decision_nok.vlan_id
|
||||
VLAN_OK = options.vlan_decision_ok.vlan_id
|
||||
RADIUS_POLICY = options.radius_general_policy
|
||||
|
||||
#: Serveur radius de test (pas la prod)
|
||||
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
|
||||
|
||||
|
@ -287,6 +282,7 @@ def find_nas_from_request(nas_id):
|
|||
.select_related('machine__switch__stack'))
|
||||
return nas.first()
|
||||
|
||||
|
||||
def check_user_machine_and_register(nas_type, username, mac_address):
|
||||
"""Verifie le username et la mac renseignee. L'enregistre si elle est
|
||||
inconnue.
|
||||
|
@ -327,32 +323,50 @@ def check_user_machine_and_register(nas_type, username, mac_address):
|
|||
|
||||
|
||||
def decide_vlan_switch(nas_machine, nas_type, port_number,
|
||||
mac_address):
|
||||
mac_address):
|
||||
"""Fonction de placement vlan pour un switch en radius filaire auth par
|
||||
mac.
|
||||
Plusieurs modes :
|
||||
- nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK
|
||||
- pas de radius sur le port : VLAN_OK
|
||||
- bloq : VLAN_NOK
|
||||
- force : placement sur le vlan indiqué dans la bdd
|
||||
- mode strict :
|
||||
- pas de chambre associée : VLAN_NOK
|
||||
- pas d'utilisateur dans la chambre : VLAN_NOK
|
||||
- cotisation non à jour : VLAN_NOK
|
||||
- sinon passe à common (ci-dessous)
|
||||
- mode common :
|
||||
- interface connue (macaddress):
|
||||
- utilisateur proprio non cotisant ou banni : VLAN_NOK
|
||||
- user à jour : VLAN_OK
|
||||
- interface inconnue :
|
||||
- register mac désactivé : VLAN_NOK
|
||||
- register mac activé -> redirection vers webauth
|
||||
- tous les modes:
|
||||
- nas inconnu: VLAN_OK
|
||||
- port inconnu: Politique définie dans RadiusOption
|
||||
- pas de radius sur le port: VLAN_OK
|
||||
- force: placement sur le vlan indiqué dans la bdd
|
||||
- mode strict:
|
||||
- pas de chambre associée: Politique définie
|
||||
dans RadiusOption
|
||||
- pas d'utilisateur dans la chambre : Rejet
|
||||
(redirection web si disponible)
|
||||
- utilisateur de la chambre banni ou désactivé : Rejet
|
||||
(redirection web si disponible)
|
||||
- utilisateur de la chambre non cotisant et non whiteslist:
|
||||
Politique définie dans RadiusOption
|
||||
|
||||
- sinon passe à common (ci-dessous)
|
||||
- mode common :
|
||||
- interface connue (macaddress):
|
||||
- utilisateur proprio non cotisant / machine désactivée:
|
||||
Politique définie dans RadiusOption
|
||||
- utilisateur proprio banni :
|
||||
Politique définie dans RadiusOption
|
||||
- user à jour : VLAN_OK (réassignation de l'ipv4 au besoin)
|
||||
- interface inconnue :
|
||||
- register mac désactivé : Politique définie
|
||||
dans RadiusOption
|
||||
- register mac activé: redirection vers webauth
|
||||
Returns:
|
||||
tuple avec :
|
||||
- Nom du switch (str)
|
||||
- chambre (str)
|
||||
- raison de la décision (str)
|
||||
- vlan_id (int)
|
||||
- decision (bool)
|
||||
"""
|
||||
# Get port from switch and port number
|
||||
extra_log = ""
|
||||
# Si le NAS est inconnu, on place sur le vlan defaut
|
||||
if not nas_machine:
|
||||
return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK, True)
|
||||
return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True)
|
||||
|
||||
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
|
||||
|
||||
|
@ -367,7 +381,13 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
|||
# Aucune information particulière ne permet de déterminer quelle
|
||||
# politique à appliquer sur ce port
|
||||
if not port:
|
||||
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK, True)
|
||||
return (
|
||||
sw_name,
|
||||
"Chambre inconnue",
|
||||
u'Port inconnu',
|
||||
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
|
||||
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)
|
||||
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
|
||||
else:
|
||||
DECISION_VLAN = VLAN_OK
|
||||
DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok')
|
||||
|
||||
# Si le port est désactivé, on rejette sur le vlan de déconnexion
|
||||
# Si le port est désactivé, on rejette la connexion
|
||||
if not port.state:
|
||||
return (sw_name, port.room, u'Port desactivé', VLAN_NOK, True)
|
||||
return (sw_name, port.room, u'Port desactivé', None, False)
|
||||
|
||||
# Si radius est désactivé, on laisse passer
|
||||
if port_profile.radius_type == 'NO':
|
||||
|
@ -392,33 +412,68 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
|||
DECISION_VLAN,
|
||||
True)
|
||||
|
||||
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
|
||||
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a
|
||||
# été accept précédemment
|
||||
# Par conséquent, on laisse passer sur le bon vlan
|
||||
if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
|
||||
if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'):
|
||||
room = port.room or "Chambre/local inconnu"
|
||||
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN, True)
|
||||
return (
|
||||
sw_name,
|
||||
room,
|
||||
u'Acceptation authentification 802.1X',
|
||||
DECISION_VLAN,
|
||||
True
|
||||
)
|
||||
|
||||
# Sinon, cela veut dire qu'on fait de l'auth radius par mac
|
||||
# Si le port est en mode strict, on vérifie que tous les users
|
||||
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
|
||||
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
|
||||
# dedans
|
||||
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette
|
||||
# (anti squattage)
|
||||
# Il n'est pas possible de se connecter sur une prise strict sans adhérent
|
||||
# à jour de cotis dedans
|
||||
if port_profile.radius_mode == 'STRICT':
|
||||
room = port.room
|
||||
if not room:
|
||||
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK, True)
|
||||
return (
|
||||
sw_name,
|
||||
"Inconnue",
|
||||
u'Chambre inconnue',
|
||||
getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None),
|
||||
RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT
|
||||
)
|
||||
|
||||
room_user = User.objects.filter(
|
||||
Q(club__room=port.room) | Q(adherent__room=port.room)
|
||||
)
|
||||
if not room_user:
|
||||
return (sw_name, room, u'Chambre non cotisante -> Web redirect', None, False)
|
||||
return (
|
||||
sw_name,
|
||||
room,
|
||||
u'Chambre non cotisante -> Web redirect',
|
||||
None,
|
||||
False
|
||||
)
|
||||
for user in room_user:
|
||||
if not user.has_access():
|
||||
return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False)
|
||||
if user.is_ban() or user.state != User.STATE_ACTIVE:
|
||||
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
|
||||
|
||||
# Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
|
||||
# Si on fait de l'auth par mac, on cherche l'interface
|
||||
# via sa mac dans la bdd
|
||||
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
|
||||
# Authentification par mac
|
||||
interface = (Interface.objects
|
||||
|
@ -428,38 +483,67 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
|
|||
.first())
|
||||
if not interface:
|
||||
room = port.room
|
||||
# On essaye de register la mac, si l'autocapture a été activée
|
||||
# Sinon on rejette sur vlan_nok
|
||||
if not nas_type.autocapture_mac:
|
||||
return (sw_name, "", u'Machine inconnue', VLAN_NOK, True)
|
||||
# On rejette pour basculer sur du webauth
|
||||
# On essaye de register la mac, si l'autocapture a été activée,
|
||||
# on rejette pour faire une redirection web si possible.
|
||||
if nas_type.autocapture_mac:
|
||||
return (
|
||||
sw_name,
|
||||
room,
|
||||
u'Machine Inconnue -> Web redirect',
|
||||
None,
|
||||
False
|
||||
)
|
||||
# Sinon on bascule sur la politique définie dans les options
|
||||
# radius.
|
||||
else:
|
||||
return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False)
|
||||
return (
|
||||
sw_name,
|
||||
"",
|
||||
u'Machine inconnue',
|
||||
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
|
||||
# Enfin on laisse passer sur le vlan pertinent
|
||||
else:
|
||||
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:
|
||||
return (sw_name,
|
||||
room,
|
||||
u'Machine non active / adherent non cotisant',
|
||||
VLAN_NOK,
|
||||
True)
|
||||
## Si on choisi de placer les machines sur le vlan correspondant à leur type :
|
||||
if RADIUS_POLICY == 'MACHINE':
|
||||
return (
|
||||
sw_name,
|
||||
room,
|
||||
u'Machine non active / adherent non cotisant',
|
||||
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
|
||||
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
|
||||
)
|
||||
# Si on choisi de placer les machines sur le vlan
|
||||
# correspondant à leur type :
|
||||
if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE':
|
||||
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
|
||||
if not interface.ipv4:
|
||||
interface.assign_ipv4()
|
||||
return (sw_name,
|
||||
room,
|
||||
u"Ok, Reassignation de l'ipv4" + extra_log,
|
||||
DECISION_VLAN,
|
||||
True)
|
||||
return (
|
||||
sw_name,
|
||||
room,
|
||||
u"Ok, Reassignation de l'ipv4" + extra_log,
|
||||
DECISION_VLAN,
|
||||
True
|
||||
)
|
||||
else:
|
||||
return (sw_name,
|
||||
room,
|
||||
u'Machine OK' + extra_log,
|
||||
DECISION_VLAN,
|
||||
True)
|
||||
return (
|
||||
sw_name,
|
||||
room,
|
||||
u'Machine OK' + extra_log,
|
||||
DECISION_VLAN,
|
||||
True
|
||||
)
|
||||
|
|
|
@ -316,6 +316,10 @@ update_django() {
|
|||
echo "Collecting web frontend statics ..."
|
||||
python3 manage.py collectstatic --noinput
|
||||
echo "Collecting web frontend statics: Done"
|
||||
|
||||
echo "Generating locales ..."
|
||||
python3 manage.py compilemessages
|
||||
echo "Generating locales: Done"
|
||||
}
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -102,15 +102,18 @@ from re2o.utils import (
|
|||
all_baned,
|
||||
all_has_access,
|
||||
all_adherent,
|
||||
all_active_assigned_interfaces_count,
|
||||
all_active_interfaces_count,
|
||||
)
|
||||
from re2o.base import (
|
||||
re2o_paginator,
|
||||
SortTable
|
||||
)
|
||||
from re2o.acl import (
|
||||
can_view_all,
|
||||
can_view_app,
|
||||
can_edit_history,
|
||||
)
|
||||
from re2o.utils import all_active_assigned_interfaces_count
|
||||
from re2o.utils import all_active_interfaces_count, SortTable
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
|
@ -273,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm):
|
|||
self.fields['origin'].label = _("A record origin")
|
||||
self.fields['origin_v6'].label = _("AAAA record origin")
|
||||
self.fields['soa'].label = _("SOA record to use")
|
||||
self.fields['dnssec'].label = _("Sign with DNSSEC")
|
||||
|
||||
|
||||
class DelExtensionForm(FormRevMixin, Form):
|
||||
|
|
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',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
dnssec = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("Should the zone be signed with DNSSEC")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
|
@ -741,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model):
|
|||
.filter(cname__interface_parent__in=all_active_assigned_interfaces())
|
||||
.prefetch_related('cname'))
|
||||
|
||||
def get_associated_dname_records(self):
|
||||
return (DName.objects.filter(alias=self))
|
||||
|
||||
@staticmethod
|
||||
def can_use_all(user_request, *_args, **_kwargs):
|
||||
"""Superdroit qui permet d'utiliser toutes les extensions sans
|
||||
|
@ -1089,7 +1096,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|||
.get_cached_value('ipv6_mode') == 'DHCPV6'):
|
||||
return self.ipv6list.filter(slaac_ip=False)
|
||||
else:
|
||||
return None
|
||||
return []
|
||||
|
||||
def mac_bare(self):
|
||||
""" Formatage de la mac type mac_bare"""
|
||||
|
@ -1373,7 +1380,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|||
.filter(interface=self.interface, slaac_ip=True)
|
||||
.exclude(id=self.id)):
|
||||
raise ValidationError(_("A SLAAC IP address is already registered."))
|
||||
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
|
||||
try:
|
||||
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
|
||||
except AttributeError: # Prevents from crashing when there is no defined prefix_v6
|
||||
prefix_v6 = None
|
||||
if prefix_v6:
|
||||
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
|
||||
IPv6Address(prefix_v6).exploded[:20]):
|
||||
|
@ -1602,7 +1612,7 @@ class Role(RevMixin, AclMixin, models.Model):
|
|||
ROLE = (
|
||||
('dhcp-server', _("DHCP server")),
|
||||
('switch-conf-server', _("Switches configuration server")),
|
||||
('dns-recursif-server', _("Recursive DNS server")),
|
||||
('dns-recursive-server', _("Recursive DNS server")),
|
||||
('ntp-server', _("NTP server")),
|
||||
('radius-server', _("RADIUS server")),
|
||||
('log-server', _("Log server")),
|
||||
|
@ -1631,18 +1641,6 @@ class Role(RevMixin, AclMixin, models.Model):
|
|||
verbose_name = _("server role")
|
||||
verbose_name_plural = _("server roles")
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, roleid, *_args, **_kwargs):
|
||||
"""Get the Role instance with roleid.
|
||||
|
||||
Args:
|
||||
roleid: The id
|
||||
|
||||
Returns:
|
||||
The role.
|
||||
"""
|
||||
return cls.objects.get(pk=roleid)
|
||||
|
||||
@classmethod
|
||||
def interface_for_roletype(cls, roletype):
|
||||
"""Return interfaces for a roletype"""
|
||||
|
@ -1657,14 +1655,6 @@ class Role(RevMixin, AclMixin, models.Model):
|
|||
machine__interface__role=cls.objects.filter(specific_role=roletype)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, roleid, *_args, **_kwargs):
|
||||
"""Get the Machine instance with machineid.
|
||||
:param userid: The id
|
||||
:return: The user
|
||||
"""
|
||||
return cls.objects.get(pk=roleid)
|
||||
|
||||
@classmethod
|
||||
def interface_for_roletype(cls, roletype):
|
||||
"""Return interfaces for a roletype"""
|
||||
|
|
|
@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% if ipv6_enabled %}
|
||||
<th>{% trans "AAAA record origin" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "DNSSEC" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -50,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% if ipv6_enabled %}
|
||||
<td>{{ extension.origin_v6 }}</td>
|
||||
{% endif %}
|
||||
<td>{{ extension.dnssec|tick }}</td>
|
||||
<td class="text-right">
|
||||
{% can_edit extension %}
|
||||
{% 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">
|
||||
{% if machines_list.paginator %}
|
||||
{% include "pagination.html" with list=machines_list %}
|
||||
{% include "pagination.html" with list=machines_list go_to_id="machines" %}
|
||||
{% endif %}
|
||||
|
||||
<table class="table" id="machines_table">
|
||||
|
@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</script>
|
||||
|
||||
{% if machines_list.paginator %}
|
||||
{% include "pagination.html" with list=machines_list %}
|
||||
{% include "pagination.html" with list=machines_list go_to_id="machines" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -95,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% if interfaceform %}
|
||||
<h3>{% trans "Interface" %}</h3>
|
||||
{% if i_mbf_param %}
|
||||
{% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %}
|
||||
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
|
||||
{% else %}
|
||||
{% massive_bootstrap_form interfaceform 'ipv4,machine' %}
|
||||
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if domainform %}
|
||||
|
@ -146,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
{% if aliasform %}
|
||||
<h3>{% trans "Alias" %}</h3>
|
||||
{% bootstrap_form aliasform %}
|
||||
{% massive_bootstrap_form aliasform 'extension' %}
|
||||
{% endif %}
|
||||
{% if serviceform %}
|
||||
<h3>{% trans "Service" %}</h3>
|
||||
|
|
|
@ -55,6 +55,8 @@ from re2o.acl import (
|
|||
from re2o.utils import (
|
||||
all_active_assigned_interfaces,
|
||||
filter_active_interfaces,
|
||||
)
|
||||
from re2o.base import (
|
||||
SortTable,
|
||||
re2o_paginator,
|
||||
)
|
||||
|
|
|
@ -42,6 +42,7 @@ from .models import (
|
|||
Reminder,
|
||||
RadiusKey,
|
||||
SwitchManagementCred,
|
||||
RadiusOption,
|
||||
)
|
||||
from topologie.models import Switch
|
||||
|
||||
|
@ -114,11 +115,6 @@ class EditOptionalTopologieForm(ModelForm):
|
|||
prefix=prefix,
|
||||
**kwargs
|
||||
)
|
||||
self.fields['radius_general_policy'].label = _("RADIUS general policy")
|
||||
self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted"
|
||||
" by RADIUS")
|
||||
self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected"
|
||||
" by RADIUS")
|
||||
|
||||
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
|
||||
|
||||
|
@ -229,6 +225,13 @@ class EditHomeOptionForm(ModelForm):
|
|||
self.fields['twitter_account_name'].label = _("Twitter account name")
|
||||
|
||||
|
||||
class EditRadiusOptionForm(ModelForm):
|
||||
"""Edition forms for Radius options"""
|
||||
class Meta:
|
||||
model = RadiusOption
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ServiceForm(ModelForm):
|
||||
"""Edition, ajout de services sur la page d'accueil"""
|
||||
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,
|
||||
help_text=_("A new user can create their account on Re2o")
|
||||
)
|
||||
all_users_active = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("If True, all new created and connected users are active.\
|
||||
If False, only when a valid registration has been paid")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
|
@ -199,25 +204,6 @@ class OptionalTopologie(AclMixin, PreferencesModel):
|
|||
('tftp', 'tftp'),
|
||||
)
|
||||
|
||||
radius_general_policy = models.CharField(
|
||||
max_length=32,
|
||||
choices=CHOICE_RADIUS,
|
||||
default='DEFINED'
|
||||
)
|
||||
vlan_decision_ok = models.OneToOneField(
|
||||
'machines.Vlan',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='decision_ok',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
vlan_decision_nok = models.OneToOneField(
|
||||
'machines.Vlan',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='decision_nok',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
switchs_web_management = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Web management, activé si provision automatique"
|
||||
|
@ -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)
|
||||
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
|
||||
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
|
||||
dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type)
|
||||
subnet = None
|
||||
subnet6 = None
|
||||
if self.switchs_ip_type:
|
||||
subnet = self.switchs_ip_type.ip_set_full_info
|
||||
subnet6 = self.switchs_ip_type.ip6_set_full_info
|
||||
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6}
|
||||
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6}
|
||||
|
||||
@cached_property
|
||||
def provision_switchs_enabled(self):
|
||||
"""Return true if all settings are ok : switchs on automatic provision,
|
||||
ip_type"""
|
||||
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_optionaltopologie", _("Can view the topology options")),
|
||||
|
@ -431,6 +417,7 @@ class GeneralOption(AclMixin, PreferencesModel):
|
|||
req_expire_hrs = models.IntegerField(default=48)
|
||||
site_name = models.CharField(max_length=32, default="Re2o")
|
||||
email_from = models.EmailField(default="www-data@example.com")
|
||||
main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
|
||||
GTU_sum_up = models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
|
@ -587,3 +574,122 @@ class MailMessageOption(AclMixin, models.Model):
|
|||
)
|
||||
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,293 +31,427 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% block title %}{% trans "Preferences" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4>{% trans "User preferences" %}</h4>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<p>
|
||||
</p>
|
||||
<h5>{% trans "General preferences" %}</h5>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Creation of members by everyone" %}</th>
|
||||
<td>{{ useroptions.all_can_create_adherent|tick }}</td>
|
||||
<th>{% trans "Creation of clubs by everyone" %}</th>
|
||||
<td>{{ useroptions.all_can_create_club|tick }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Self registration" %}</th>
|
||||
<td>{{ useroptions.self_adhesion|tick }}</td>
|
||||
<th>{% trans "Delete not yet active users after" %}</th>
|
||||
<td>{{ useroptions.delete_notyetactive }} days</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div id="accordion">
|
||||
|
||||
<h5>{% trans "Users general permissions" %}</h5>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Default shell for users" %}</th>
|
||||
<td>{{ useroptions.shell_default }}</td>
|
||||
<th>{% trans "Users can edit their shell" %}</th>
|
||||
<td>{{ useroptions.self_change_shell|tick }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Users can edit their room" %}</th>
|
||||
<td>{{ useroptions.self_change_room|tick }}</td>
|
||||
<th>{% trans "Telephone number required" %}</th>
|
||||
<td>{{ useroptions.is_tel_mandatory|tick }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "GPG fingerprint field" %}</th>
|
||||
<td>{{ useroptions.gpg_fingerprint|tick }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<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' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<p></p>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Creation of members by everyone" %}</th>
|
||||
<td>{{ useroptions.all_can_create_adherent|tick }}</td>
|
||||
<th>{% trans "Creation of clubs by everyone" %}</th>
|
||||
<td>{{ useroptions.all_can_create_club|tick }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Self registration" %}</th>
|
||||
<td>{{ useroptions.self_adhesion|tick }}</td>
|
||||
<th>{% trans "Delete not yet active users after" %}</th>
|
||||
<td>{{ useroptions.delete_notyetactive }} days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "All users are active by default" %}</th>
|
||||
<td>{{ useroptions.all_users_active|tick }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4 id="users">{% trans "Users general permissions" %}</h4>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Default shell for users" %}</th>
|
||||
<td>{{ useroptions.shell_default }}</td>
|
||||
<th>{% trans "Users can edit their shell" %}</th>
|
||||
<td>{{ useroptions.self_change_shell|tick }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Users can edit their room" %}</th>
|
||||
<td>{{ useroptions.self_change_room|tick }}</td>
|
||||
<th>{% trans "Telephone number required" %}</th>
|
||||
<td>{{ useroptions.is_tel_mandatory|tick }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "GPG fingerprint field" %}</th>
|
||||
<td>{{ useroptions.gpg_fingerprint|tick }}</td>
|
||||
</tr>
|
||||
</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">
|
||||
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<p></p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Password per machine" %}</th>
|
||||
<td>{{ machineoptions.password_machine|tick }}</td>
|
||||
<th>{% trans "Maximum number of interfaces allowed for a standard user" %}</th>
|
||||
<td>{{ machineoptions.max_lambdauser_interfaces }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Maximum number of DNS aliases allowed for a standard user" %}</th>
|
||||
<td>{{ machineoptions.max_lambdauser_aliases }}</td>
|
||||
<th>{% trans "IPv6 support" %}</th>
|
||||
<td>{{ machineoptions.ipv6_mode }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Creation of machines" %}</th>
|
||||
<td>{{ machineoptions.create_machine|tick }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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">
|
||||
|
||||
<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' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<p>
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Password per machine" %}</th>
|
||||
<td>{{ machineoptions.password_machine|tick }}</td>
|
||||
<th>{% trans "Maximum number of interfaces allowed for a standard user" %}</th>
|
||||
<td>{{ machineoptions.max_lambdauser_interfaces }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Maximum number of DNS aliases allowed for a standard user" %}</th>
|
||||
<td>{{ machineoptions.max_lambdauser_aliases }}</td>
|
||||
<th>{% trans "IPv6 support" %}</th>
|
||||
<td>{{ machineoptions.ipv6_mode }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Creation of machines" %}</th>
|
||||
<td>{{ machineoptions.create_machine|tick }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>{% trans "Topology preferences" %}</h4>
|
||||
<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">
|
||||
<tr>
|
||||
<th>{% trans "General policy for VLAN setting" %}</th>
|
||||
<td>{{ topologieoptions.radius_general_policy }}</td>
|
||||
<th>{% 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'" %}</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "VLAN for machines accepted by RADIUS" %}</th>
|
||||
<td>{{ topologieoptions.vlan_decision_ok }}</td>
|
||||
<th>{% trans "VLAN for machines rejected by RADIUS" %}</th>
|
||||
<td>{{ topologieoptions.vlan_decision_nok }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Placement sur ce vlan par default en cas de rejet</th>
|
||||
<td>{{ topologieoptions.vlan_decision_nok }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p></p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "General policy for VLAN setting" %}</th>
|
||||
<td>{{ topologieoptions.radius_general_policy }}</td>
|
||||
<th>{% 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'" %}</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "VLAN for machines accepted by RADIUS" %}</th>
|
||||
<td>{{ topologieoptions.vlan_decision_ok }}</td>
|
||||
<th>{% trans "VLAN for machines rejected by RADIUS" %}</th>
|
||||
<td>{{ topologieoptions.vlan_decision_nok }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "VLAN for non members machines" %}</th>
|
||||
<td>{{ topologieoptions.vlan_non_member }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h6>Clef radius</h6>
|
||||
{% 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>
|
||||
{% acl_end %}
|
||||
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
|
||||
<h4>Clef radius</h4>
|
||||
{% 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>
|
||||
{% acl_end %}
|
||||
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
|
||||
|
||||
<h4>Configuration des switches</h4>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Web management, activé si provision automatique</th>
|
||||
<td>{{ topologieoptions.switchs_web_management }}</td>
|
||||
<th>Rest management, activé si provision auto</th>
|
||||
<td>{{ topologieoptions.switchs_rest_management }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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">
|
||||
|
||||
|
||||
<h5>{% if topologieoptions.provision_switchs_enabled %}<span class="label label-success">Provision de la config des switchs{% else %}<span class="label label-danger">Provision de la config des switchs{% endif%}</span></h5>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Switchs configurés automatiquement</th>
|
||||
<td>{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Plage d'ip de management des switchs</th>
|
||||
<td>{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Serveur des config des switchs</th>
|
||||
<td>{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} <span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mode de provision des switchs</th>
|
||||
<td>{{ topologieoptions.switchs_provision }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mode TFTP</th>
|
||||
<td><span class="label label-success"> OK</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mode SFTP</th>
|
||||
<td>{% if topologieoptions.switchs_management_sftp_creds %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Creds manquants{% endif %}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h6>Creds de management des switchs</h6>
|
||||
{% can_create SwitchManagementCred%}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-switchmanagementcred' %}"><i class="fa fa-plus"></i> Ajouter un id/mdp de management switch</a>
|
||||
{% acl_end %}
|
||||
<p>
|
||||
</p>
|
||||
{% 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 %}
|
||||
|
||||
|
||||
|
||||
<h4>{% trans "General preferences" %}</h4>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
|
||||
<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">
|
||||
<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 "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>
|
||||
<p></p>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Web management, activé si provision automatique</th>
|
||||
<td>{{ topologieoptions.switchs_web_management }}</td>
|
||||
<th>Rest management, activé si provision auto</th>
|
||||
<td>{{ topologieoptions.switchs_rest_management }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<h5>{% if topologieoptions.provision_switchs_enabled %}<span class="label label-success">Provision de la config des switchs{% else %}<span class="label label-danger">Provision de la config des switchs{% endif%}</span></h5>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Switchs configurés automatiquement</th>
|
||||
<td>{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Plage d'ip de management des switchs</th>
|
||||
<td>{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Serveur des config des switchs</th>
|
||||
<td>{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} <span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mode de provision des switchs</th>
|
||||
<td>{{ topologieoptions.switchs_provision }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mode TFTP</th>
|
||||
<td><span class="label label-success"> OK</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mode SFTP</th>
|
||||
<td>{% if topologieoptions.switchs_management_sftp_creds %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Creds manquants{% endif %}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h6>Creds de management des switchs</h6>
|
||||
{% can_create SwitchManagementCred%}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-switchmanagementcred' %}"><i class="fa fa-plus"></i> Ajouter un id/mdp de management switch</a>
|
||||
{% acl_end %}
|
||||
<p>
|
||||
</p>
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default" id="radius">
|
||||
<div class="panel-heading" data-toggle="collapse" href="#collapse_radius">
|
||||
<h4 class="panel-title"><a><i class="fa fa-circle"></i> {% trans "Radius preferences" %}</h4></a>
|
||||
</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>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
{% include "preferences/aff_radiusoptions.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default" id="asso">
|
||||
<div class="panel-heading" data-toggle="collapse" href="#collapse_asso">
|
||||
<h4 class="panel-title">
|
||||
<a><i class="fa fa-at"></i> {% trans "Information about the organisation" %}</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse_asso" class="panel-collapse panel-body collapse">
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<p>
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<td>{{ assooptions.name }}</td>
|
||||
<th>{% trans "SIRET number" %}</th>
|
||||
<td>{{ assooptions.siret }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<td>{{ assooptions.adresse1 }}<br>
|
||||
{{ assooptions.adresse2 }}
|
||||
</td>
|
||||
<th>{% trans "Contact email address" %}</th>
|
||||
<td>{{ assooptions.contact }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Telephone number" %}</th>
|
||||
<td>{{ assooptions.telephone }}</td>
|
||||
<th>{% trans "Usual name" %}</th>
|
||||
<td>{{ assooptions.pseudo }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "User object of the organisation" %}</th>
|
||||
<td>{{ assooptions.utilisateur_asso }}</td>
|
||||
<th>{% trans "Description of the organisation" %}</th>
|
||||
<td>{{ assooptions.description|safe }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>{% trans "Custom email message" %}</h4>
|
||||
<p></p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<td>{{ assooptions.name }}</td>
|
||||
<th>{% trans "SIRET number" %}</th>
|
||||
<td>{{ assooptions.siret }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<td>{{ assooptions.adresse1 }}<br>
|
||||
{{ assooptions.adresse2 }}
|
||||
</td>
|
||||
<th>{% trans "Contact email address" %}</th>
|
||||
<td>{{ assooptions.contact }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Telephone number" %}</th>
|
||||
<td>{{ assooptions.telephone }}</td>
|
||||
<th>{% trans "Usual name" %}</th>
|
||||
<td>{{ assooptions.pseudo }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "User object of the organisation" %}</th>
|
||||
<td>{{ assooptions.utilisateur_asso }}</td>
|
||||
<th>{% trans "Description of the organisation" %}</th>
|
||||
<td>{{ assooptions.description|safe }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<p>
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Welcome email (in French)" %}</th>
|
||||
<td>{{ mailmessageoptions.welcome_mail_fr|safe }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Welcome email (in English)" %}</th>
|
||||
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>Options pour le mail de fin d'adhésion</h2>
|
||||
<p></p>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Welcome email (in French)" %}</th>
|
||||
<td>{{ mailmessageoptions.welcome_mail_fr|safe }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Welcome email (in English)" %}</th>
|
||||
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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%}
|
||||
|
||||
<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 %}
|
||||
{% 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%}
|
||||
|
||||
<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 %}
|
||||
{% 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>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<h2>{% trans "List of contact email addresses" %}</h2>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
<p>
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Twitter account URL" %}</th>
|
||||
<td>{{ homeoptions.twitter_url }}</td>
|
||||
<th>{% trans "Twitter account name" %}</th>
|
||||
<td>{{ homeoptions.twitter_account_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Facebook account URL" %}</th>
|
||||
<td>{{ homeoptions.facebook_url }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</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">
|
||||
<tr>
|
||||
<th>{% trans "Twitter account URL" %}</th>
|
||||
<td>{{ homeoptions.twitter_url }}</td>
|
||||
<th>{% trans "Twitter account name" %}</th>
|
||||
<td>{{ homeoptions.twitter_account_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Facebook account URL" %}</th>
|
||||
<td>{{ homeoptions.facebook_url }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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">
|
||||
{% csrf_token %}
|
||||
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
|
||||
{% if formset %}
|
||||
{{ formset.management_form }}
|
||||
{% for f in formset %}
|
||||
{% bootstrap_form f %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% trans "Edit" as tr_edit %}
|
||||
{% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
|
||||
</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.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
{% endcomment %}
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block sidebar %}
|
||||
|
|
|
@ -66,6 +66,11 @@ urlpatterns = [
|
|||
views.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'^edit_service/(?P<serviceid>[0-9]+)$',
|
||||
|
|
|
@ -62,7 +62,8 @@ from .models import (
|
|||
HomeOption,
|
||||
Reminder,
|
||||
RadiusKey,
|
||||
SwitchManagementCred
|
||||
SwitchManagementCred,
|
||||
RadiusOption,
|
||||
)
|
||||
from . import models
|
||||
from . import forms
|
||||
|
@ -86,6 +87,7 @@ def display_options(request):
|
|||
reminder_list = Reminder.objects.all()
|
||||
radiuskey_list = RadiusKey.objects.all()
|
||||
switchmanagementcred_list = SwitchManagementCred.objects.all()
|
||||
radiusoptions, _ = RadiusOption.objects.get_or_create()
|
||||
return form({
|
||||
'useroptions': useroptions,
|
||||
'machineoptions': machineoptions,
|
||||
|
@ -99,6 +101,7 @@ def display_options(request):
|
|||
'reminder_list': reminder_list,
|
||||
'radiuskey_list' : radiuskey_list,
|
||||
'switchmanagementcred_list': switchmanagementcred_list,
|
||||
'radiusoptions' : radiusoptions,
|
||||
}, 'preferences/display_preferences.html', request)
|
||||
|
||||
|
||||
|
@ -134,7 +137,9 @@ def edit_options(request, section):
|
|||
messages.success(request, _("The preferences were edited."))
|
||||
return redirect(reverse('preferences:display-options'))
|
||||
return form(
|
||||
{'options': options},
|
||||
{
|
||||
'options': options,
|
||||
},
|
||||
'preferences/edit_preferences.html',
|
||||
request
|
||||
)
|
||||
|
|
|
@ -37,8 +37,8 @@ from django.db import models
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
EOD = '`%EofD%`' # This should be something that will not occur in strings
|
||||
|
||||
EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings
|
||||
EOD = EOD_asbyte.decode('utf-8')
|
||||
|
||||
def genstring(length=16, chars=string.printable):
|
||||
""" Generate a random string of length `length` and composed of
|
||||
|
@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable):
|
|||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
|
||||
def encrypt(key, s):
|
||||
""" AES Encrypt a secret `s` with the key `key` """
|
||||
def encrypt(key, secret):
|
||||
""" AES Encrypt a secret with the key `key` """
|
||||
obj = AES.new(key)
|
||||
datalength = len(s) + len(EOD)
|
||||
datalength = len(secret) + len(EOD)
|
||||
if datalength < 16:
|
||||
saltlength = 16 - datalength
|
||||
else:
|
||||
saltlength = 16 - datalength % 16
|
||||
ss = ''.join([s, EOD, genstring(saltlength)])
|
||||
return obj.encrypt(ss)
|
||||
encrypted_secret = ''.join([secret, EOD, genstring(saltlength)])
|
||||
return obj.encrypt(encrypted_secret)
|
||||
|
||||
|
||||
def decrypt(key, s):
|
||||
""" AES Decrypt a secret `s` with the key `key` """
|
||||
def decrypt(key, secret):
|
||||
""" AES Decrypt a secret with the key `key` """
|
||||
obj = AES.new(key)
|
||||
ss = obj.decrypt(s)
|
||||
return ss.split(bytes(EOD, 'utf-8'))[0]
|
||||
uncrypted_secret = obj.decrypt(secret)
|
||||
return uncrypted_secret.split(EOD_asbyte)[0]
|
||||
|
||||
|
||||
class AESEncryptedFormField(forms.CharField):
|
||||
|
@ -81,8 +81,7 @@ class AESEncryptedField(models.CharField):
|
|||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return decrypt(settings.AES_KEY,
|
||||
binascii.a2b_base64(value)).decode('utf-8')
|
||||
return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
|
||||
except Exception as e:
|
||||
raise ValueError(value)
|
||||
|
||||
|
@ -90,18 +89,14 @@ class AESEncryptedField(models.CharField):
|
|||
if value is None:
|
||||
return value
|
||||
try:
|
||||
return decrypt(settings.AES_KEY,
|
||||
binascii.a2b_base64(value)).decode('utf-8')
|
||||
return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
|
||||
except Exception as e:
|
||||
raise ValueError(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
return binascii.b2a_base64(encrypt(
|
||||
settings.AES_KEY,
|
||||
value
|
||||
)).decode('utf-8')
|
||||
return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8')
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': AESEncryptedFormField}
|
||||
|
|
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
|
||||
"""
|
||||
assert encoded.startswith(self.algorithm)
|
||||
salt = hash_password_salt(challenge_password)
|
||||
return constant_time_compare(crypt.crypt(password.encode(), salt),
|
||||
challenge.encode())
|
||||
salt = hash_password_salt(encoded)
|
||||
return constant_time_compare(crypt.crypt(password, salt),
|
||||
encoded)
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
"""
|
||||
|
|
246
re2o/utils.py
246
re2o/utils.py
|
@ -38,55 +38,11 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
|
||||
from cotisations.models import Cotisation, Facture, Vente
|
||||
from machines.models import Interface, Machine
|
||||
from users.models import Adherent, User, Ban, Whitelist
|
||||
|
||||
# Mapping of srtftime format for better understanding
|
||||
# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior
|
||||
datetime_mapping={
|
||||
'%a': '%a',
|
||||
'%A': '%A',
|
||||
'%w': '%w',
|
||||
'%d': 'dd',
|
||||
'%b': '%b',
|
||||
'%B': '%B',
|
||||
'%m': 'mm',
|
||||
'%y': 'yy',
|
||||
'%Y': 'yyyy',
|
||||
'%H': 'HH',
|
||||
'%I': 'HH(12h)',
|
||||
'%p': 'AMPM',
|
||||
'%M': 'MM',
|
||||
'%S': 'SS',
|
||||
'%f': 'µµ',
|
||||
'%z': 'UTC(+/-HHMM)',
|
||||
'%Z': 'UTC(TZ)',
|
||||
'%j': '%j',
|
||||
'%U': 'ww',
|
||||
'%W': 'ww',
|
||||
'%c': '%c',
|
||||
'%x': '%x',
|
||||
'%X': '%X',
|
||||
'%%': '%%',
|
||||
}
|
||||
|
||||
|
||||
def convert_datetime_format(format):
|
||||
i=0
|
||||
new_format = ""
|
||||
while i < len(format):
|
||||
if format[i] == '%':
|
||||
char = format[i:i+2]
|
||||
new_format += datetime_mapping.get(char, char)
|
||||
i += 2
|
||||
else:
|
||||
new_format += format[i]
|
||||
i += 1
|
||||
return new_format
|
||||
|
||||
from preferences.models import AssoOption
|
||||
|
||||
def all_adherent(search_time=None):
|
||||
""" 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(
|
||||
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()
|
||||
|
@ -115,7 +71,7 @@ def all_baned(search_time=None):
|
|||
search_time = timezone.now()
|
||||
return User.objects.filter(
|
||||
ban__in=Ban.objects.filter(
|
||||
date_end__gt=search_time
|
||||
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
@ -126,20 +82,23 @@ def all_whitelisted(search_time=None):
|
|||
search_time = timezone.now()
|
||||
return User.objects.filter(
|
||||
whitelist__in=Whitelist.objects.filter(
|
||||
date_end__gt=search_time
|
||||
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
def all_has_access(search_time=None):
|
||||
""" Renvoie tous les users beneficiant d'une connexion
|
||||
: user adherent ou whiteliste et non banni """
|
||||
""" Return all connected users : active users and whitelisted +
|
||||
asso_user defined in AssoOption pannel
|
||||
----
|
||||
Renvoie tous les users beneficiant d'une connexion
|
||||
: user adherent et whiteliste non banni plus l'utilisateur asso"""
|
||||
if search_time is None:
|
||||
search_time = timezone.now()
|
||||
return User.objects.filter(
|
||||
filter_user = (
|
||||
Q(state=User.STATE_ACTIVE) &
|
||||
~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
|
||||
(Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
|
||||
~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) &
|
||||
(Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) |
|
||||
Q(facture__in=Facture.objects.filter(
|
||||
vente__in=Vente.objects.filter(
|
||||
cotisation__in=Cotisation.objects.filter(
|
||||
|
@ -148,10 +107,14 @@ def all_has_access(search_time=None):
|
|||
facture__in=Facture.objects.all()
|
||||
.exclude(valid=False)
|
||||
)
|
||||
).filter(date_end__gt=search_time)
|
||||
).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
|
||||
)
|
||||
)))
|
||||
).distinct()
|
||||
)
|
||||
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):
|
||||
|
@ -203,164 +166,6 @@ def all_active_assigned_interfaces_count():
|
|||
return all_active_interfaces_count().filter(ipv4__isnull=False)
|
||||
|
||||
|
||||
class SortTable:
|
||||
""" Class gathering uselful stuff to sort the colums of a table, according
|
||||
to the column and order requested. It's used with a dict of possible
|
||||
values and associated model_fields """
|
||||
|
||||
# All the possible possible values
|
||||
# The naming convention is based on the URL or the views function
|
||||
# The syntax to describe the sort to apply is a dict where the keys are
|
||||
# the url value and the values are a list of model field name to use to
|
||||
# order the request. They are applied in the order they are given.
|
||||
# A 'default' might be provided to specify what to do if the requested col
|
||||
# doesn't match any keys.
|
||||
|
||||
USERS_INDEX = {
|
||||
'user_name': ['name'],
|
||||
'user_surname': ['surname'],
|
||||
'user_pseudo': ['pseudo'],
|
||||
'user_room': ['room'],
|
||||
'default': ['state', 'pseudo']
|
||||
}
|
||||
USERS_INDEX_BAN = {
|
||||
'ban_user': ['user__pseudo'],
|
||||
'ban_start': ['date_start'],
|
||||
'ban_end': ['date_end'],
|
||||
'default': ['-date_end']
|
||||
}
|
||||
USERS_INDEX_WHITE = {
|
||||
'white_user': ['user__pseudo'],
|
||||
'white_start': ['date_start'],
|
||||
'white_end': ['date_end'],
|
||||
'default': ['-date_end']
|
||||
}
|
||||
USERS_INDEX_SCHOOL = {
|
||||
'school_name': ['name'],
|
||||
'default': ['name']
|
||||
}
|
||||
MACHINES_INDEX = {
|
||||
'machine_name': ['name'],
|
||||
'default': ['pk']
|
||||
}
|
||||
COTISATIONS_INDEX = {
|
||||
'cotis_user': ['user__pseudo'],
|
||||
'cotis_paiement': ['paiement__moyen'],
|
||||
'cotis_date': ['date'],
|
||||
'cotis_id': ['id'],
|
||||
'default': ['-date']
|
||||
}
|
||||
COTISATIONS_CUSTOM = {
|
||||
'invoice_date': ['date'],
|
||||
'invoice_id': ['id'],
|
||||
'invoice_recipient': ['recipient'],
|
||||
'invoice_address': ['address'],
|
||||
'invoice_payment': ['payment'],
|
||||
'default': ['-date']
|
||||
}
|
||||
COTISATIONS_CONTROL = {
|
||||
'control_name': ['user__adherent__name'],
|
||||
'control_surname': ['user__surname'],
|
||||
'control_paiement': ['paiement'],
|
||||
'control_date': ['date'],
|
||||
'control_valid': ['valid'],
|
||||
'control_control': ['control'],
|
||||
'control_id': ['id'],
|
||||
'control_user-id': ['user__id'],
|
||||
'default': ['-date']
|
||||
}
|
||||
TOPOLOGIE_INDEX = {
|
||||
'switch_dns': ['interface__domain__name'],
|
||||
'switch_ip': ['interface__ipv4__ipv4'],
|
||||
'switch_loc': ['switchbay__name'],
|
||||
'switch_ports': ['number'],
|
||||
'switch_stack': ['stack__name'],
|
||||
'default': ['switchbay', 'stack', 'stack_member_id']
|
||||
}
|
||||
TOPOLOGIE_INDEX_PORT = {
|
||||
'port_port': ['port'],
|
||||
'port_room': ['room__name'],
|
||||
'port_interface': ['machine_interface__domain__name'],
|
||||
'port_related': ['related__switch__name'],
|
||||
'port_radius': ['radius'],
|
||||
'port_vlan': ['vlan_force__name'],
|
||||
'default': ['port']
|
||||
}
|
||||
TOPOLOGIE_INDEX_ROOM = {
|
||||
'room_name': ['name'],
|
||||
'default': ['name']
|
||||
}
|
||||
TOPOLOGIE_INDEX_BUILDING = {
|
||||
'building_name': ['name'],
|
||||
'default': ['name']
|
||||
}
|
||||
TOPOLOGIE_INDEX_BORNE = {
|
||||
'ap_name': ['interface__domain__name'],
|
||||
'ap_ip': ['interface__ipv4__ipv4'],
|
||||
'ap_mac': ['interface__mac_address'],
|
||||
'default': ['interface__domain__name']
|
||||
}
|
||||
TOPOLOGIE_INDEX_STACK = {
|
||||
'stack_name': ['name'],
|
||||
'stack_id': ['stack_id'],
|
||||
'default': ['stack_id'],
|
||||
}
|
||||
TOPOLOGIE_INDEX_MODEL_SWITCH = {
|
||||
'model-switch_name': ['reference'],
|
||||
'model-switch_contructor': ['constructor__name'],
|
||||
'default': ['reference'],
|
||||
}
|
||||
TOPOLOGIE_INDEX_SWITCH_BAY = {
|
||||
'switch-bay_name': ['name'],
|
||||
'switch-bay_building': ['building__name'],
|
||||
'default': ['name'],
|
||||
}
|
||||
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
|
||||
'constructor-switch_name': ['name'],
|
||||
'default': ['name'],
|
||||
}
|
||||
LOGS_INDEX = {
|
||||
'sum_date': ['revision__date_created'],
|
||||
'default': ['-revision__date_created'],
|
||||
}
|
||||
LOGS_STATS_LOGS = {
|
||||
'logs_author': ['user__name'],
|
||||
'logs_date': ['date_created'],
|
||||
'default': ['-date_created']
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def sort(request, col, order, values):
|
||||
""" Check if the given values are possible and add .order_by() and
|
||||
a .reverse() as specified according to those values """
|
||||
fields = values.get(col, None)
|
||||
if not fields:
|
||||
fields = values.get('default', [])
|
||||
request = request.order_by(*fields)
|
||||
if values.get(col, None) and order == 'desc':
|
||||
return request.reverse()
|
||||
else:
|
||||
return request
|
||||
|
||||
|
||||
def re2o_paginator(request, query_set, pagination_number):
|
||||
"""Paginator script for list display in re2o.
|
||||
:request:
|
||||
:query_set: Query_set to paginate
|
||||
:pagination_number: Number of entries to display"""
|
||||
paginator = Paginator(query_set, pagination_number)
|
||||
page = request.GET.get('page')
|
||||
try:
|
||||
results = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
# If page is not an integer, deliver first page.
|
||||
results = paginator.page(1)
|
||||
except EmptyPage:
|
||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
||||
results = paginator.page(paginator.num_pages)
|
||||
return results
|
||||
|
||||
|
||||
def remove_user_room(room):
|
||||
""" Déménage de force l'ancien locataire de la chambre """
|
||||
try:
|
||||
|
@ -370,18 +175,3 @@ def remove_user_room(room):
|
|||
user.room = None
|
||||
user.save()
|
||||
|
||||
|
||||
def get_input_formats_help_text(input_formats):
|
||||
"""Returns a help text about the possible input formats"""
|
||||
if len(input_formats) > 1:
|
||||
help_text_template="Format: {main} {more}"
|
||||
else:
|
||||
help_text_template="Format: {main}"
|
||||
more_text_template="<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.forms import Form
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from re2o.utils import get_input_formats_help_text
|
||||
from re2o.base import get_input_formats_help_text
|
||||
|
||||
CHOICES_USER = (
|
||||
('0', _("Active")),
|
||||
|
|
Binary file not shown.
|
@ -46,7 +46,7 @@ from search.forms import (
|
|||
CHOICES_AFF,
|
||||
initial_choices
|
||||
)
|
||||
from re2o.utils import SortTable
|
||||
from re2o.base import SortTable
|
||||
from re2o.acl import can_view_all
|
||||
|
||||
|
||||
|
|
|
@ -79,19 +79,6 @@ a > i.fa {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Pull sidebars to the bottom */
|
||||
@media (min-width: 767px) {
|
||||
.row {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.row > [class*='col-'] {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* On small screens, set height to 'auto' for sidenav and grid */
|
||||
@media screen and (max-width: 767px) {
|
||||
.sidenav {
|
||||
|
@ -102,7 +89,7 @@ a > i.fa {
|
|||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-y: visible;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Make modal wider on wide screens */
|
||||
|
@ -145,3 +132,14 @@ th.long_text{
|
|||
.dashboard{
|
||||
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 %}
|
||||
<script src="/static/js/typeahead/typeahead.js"></script>
|
||||
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
|
||||
<script src="{% static 'js/collapse-from-url.js' %}"></script>
|
||||
|
||||
{# Load CSS #}
|
||||
{% bootstrap_css %}
|
||||
|
|
Binary file not shown.
|
@ -23,23 +23,52 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load url_insert_param %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if list.paginator.num_pages > 1 %}
|
||||
<ul class="pagination nav navbar-nav">
|
||||
{% if list.has_previous %}
|
||||
<li><a href="{% url_insert_param request.get_full_path page=1 %}"> << </a></li>
|
||||
<li><a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}"> < </a></li>
|
||||
{% endif %}
|
||||
{% for page in list.paginator.page_range %}
|
||||
{% 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>
|
||||
<ul class="pagination text-center">
|
||||
{% if list.has_previous %}
|
||||
<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 %}
|
||||
{% endfor %}
|
||||
|
||||
{% if list.has_next %}
|
||||
<li><a href="{% url_insert_param request.get_full_path page=list.next_page_number %}"> > </a></li>
|
||||
<li><a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}"> >> </a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% for page in list.paginator.page_range %}
|
||||
{% 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 %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">{{ page }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if list.has_next %}
|
||||
<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 %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -55,6 +55,8 @@ from .models import (
|
|||
SwitchBay,
|
||||
Building,
|
||||
PortProfile,
|
||||
ModuleSwitch,
|
||||
ModuleOnSwitch,
|
||||
)
|
||||
|
||||
|
||||
|
@ -269,3 +271,23 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
|
|||
prefix=prefix,
|
||||
**kwargs)
|
||||
|
||||
class EditModuleForm(FormRevMixin, ModelForm):
|
||||
"""Add and edit module instance"""
|
||||
class Meta:
|
||||
model = ModuleSwitch
|
||||
fields = '__all__'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
|
||||
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ('stack', 'stack_member_id')
|
||||
permissions = (
|
||||
|
@ -281,31 +282,18 @@ class Switch(AclMixin, Machine):
|
|||
def create_ports(self, begin, end):
|
||||
""" Crée les ports de begin à end si les valeurs données
|
||||
sont cohérentes. """
|
||||
|
||||
s_begin = s_end = 0
|
||||
nb_ports = self.ports.count()
|
||||
if nb_ports > 0:
|
||||
ports = self.ports.order_by('port').values('port')
|
||||
s_begin = ports.first().get('port')
|
||||
s_end = ports.last().get('port')
|
||||
|
||||
if end < begin:
|
||||
raise ValidationError(_("The end port is less than the start"
|
||||
" port."))
|
||||
if end - begin > self.number:
|
||||
ports_to_create = range(begin, end + 1)
|
||||
existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True)
|
||||
non_existing_ports = list(set(ports_to_create) - set(existing_ports))
|
||||
|
||||
if len(non_existing_ports) + existing_ports.count() > self.number:
|
||||
raise ValidationError(_("This switch can't have that many ports."))
|
||||
begin_range = range(begin, s_begin)
|
||||
end_range = range(s_end+1, end+1)
|
||||
for i in itertools.chain(begin_range, end_range):
|
||||
port = Port()
|
||||
port.switch = self
|
||||
port.port = i
|
||||
try:
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
port.save()
|
||||
reversion.set_comment(_("Creation"))
|
||||
except IntegrityError:
|
||||
ValidationError(_("Creation of an existing port."))
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
reversion.set_comment(_("Creation"))
|
||||
Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports])
|
||||
|
||||
def main_interface(self):
|
||||
""" Returns the 'main' interface of the switch
|
||||
|
@ -317,7 +305,7 @@ class Switch(AclMixin, Machine):
|
|||
|
||||
@cached_property
|
||||
def get_name(self):
|
||||
return self.name or self.main_interface().domain.name
|
||||
return self.name or getattr(self.main_interface(), 'domain', 'Unknown')
|
||||
|
||||
@cached_property
|
||||
def get_radius_key(self):
|
||||
|
@ -380,6 +368,17 @@ class Switch(AclMixin, Machine):
|
|||
"""Return dict ip6:subnet for all ipv6 of the switch"""
|
||||
return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
|
||||
|
||||
@cached_property
|
||||
def list_modules(self):
|
||||
"""Return modules of that switch, list of dict (rank, reference)"""
|
||||
modules = []
|
||||
if 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):
|
||||
return str(self.get_name)
|
||||
|
||||
|
@ -402,6 +401,14 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
|
|||
null=True,
|
||||
blank=True
|
||||
)
|
||||
is_modular = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("Is this switch model modular"),
|
||||
)
|
||||
is_itself_module = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("Is the switch, itself, considered as a module"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
|
@ -417,6 +424,53 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
|
|||
return str(self.constructor) + ' ' + self.reference
|
||||
|
||||
|
||||
class ModuleSwitch(AclMixin, RevMixin, models.Model):
|
||||
"""A module of a switch"""
|
||||
reference = models.CharField(
|
||||
max_length=255,
|
||||
help_text=_("Reference of a module"),
|
||||
verbose_name=_("Module reference")
|
||||
)
|
||||
comment = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Comment"),
|
||||
verbose_name=_("Comment")
|
||||
)
|
||||
|
||||
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):
|
||||
"""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" %}">
|
||||
<i class="fa fa-microchip"></i>
|
||||
{% 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 class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
|
||||
<i class="fa fa-cogs"></i>
|
||||
|
|
|
@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
<form class="form" method="post">
|
||||
{% 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' %}
|
||||
</form>
|
||||
<br />
|
||||
|
|
|
@ -123,4 +123,15 @@ urlpatterns = [
|
|||
url(r'^edit_vlanoptions/(?P<vlanid>[0-9]+)$',
|
||||
views.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
|
||||
|
||||
from users.views import form
|
||||
from re2o.utils import re2o_paginator, SortTable
|
||||
from re2o.base import (
|
||||
re2o_paginator,
|
||||
SortTable,
|
||||
)
|
||||
from re2o.acl import (
|
||||
can_create,
|
||||
can_edit,
|
||||
|
@ -83,6 +86,8 @@ from .models import (
|
|||
Building,
|
||||
Server,
|
||||
PortProfile,
|
||||
ModuleSwitch,
|
||||
ModuleOnSwitch,
|
||||
)
|
||||
from .forms import (
|
||||
EditPortForm,
|
||||
|
@ -99,6 +104,8 @@ from .forms import (
|
|||
EditSwitchBayForm,
|
||||
EditBuildingForm,
|
||||
EditPortProfileForm,
|
||||
EditModuleForm,
|
||||
EditSwitchModuleForm,
|
||||
)
|
||||
|
||||
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
|
||||
@can_edit(Vlan)
|
||||
def edit_vlanoptions(request, vlan_instance, **_kwargs):
|
||||
|
@ -531,18 +554,12 @@ def create_ports(request, switchid):
|
|||
messages.error(request, _("Nonexistent switch"))
|
||||
return redirect(reverse('topologie:index'))
|
||||
|
||||
s_begin = s_end = 0
|
||||
nb_ports = switch.ports.count()
|
||||
if nb_ports > 0:
|
||||
ports = switch.ports.order_by('port').values('port')
|
||||
s_begin = ports.first().get('port')
|
||||
s_end = ports.last().get('port')
|
||||
|
||||
first_port = getattr(switch.ports.order_by('port').first(), 'port', 1)
|
||||
last_port = switch.number + first_port - 1
|
||||
port_form = CreatePortsForm(
|
||||
request.POST or None,
|
||||
initial={'begin': s_begin, 'end': s_end}
|
||||
initial={'begin': first_port, 'end': last_port}
|
||||
)
|
||||
|
||||
if port_form.is_valid():
|
||||
begin = port_form.cleaned_data['begin']
|
||||
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():
|
||||
"""
|
||||
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 topologie.models import Port
|
||||
from preferences.models import OptionalUser
|
||||
from re2o.utils import remove_user_room, get_input_formats_help_text
|
||||
from re2o.utils import remove_user_room
|
||||
from re2o.base import get_input_formats_help_text
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.field_permissions import FieldPermissionFormMixin
|
||||
|
||||
|
@ -116,6 +117,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
|
|||
"""Changement du mot de passe"""
|
||||
user = super(PassForm, self).save(commit=False)
|
||||
user.set_password(self.cleaned_data.get("passwd1"))
|
||||
user.set_active()
|
||||
user.save()
|
||||
|
||||
|
||||
|
@ -323,14 +325,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
self.fields['room'].empty_label = _("No room")
|
||||
self.fields['school'].empty_label = _("Select a school")
|
||||
|
||||
def clean_email(self):
|
||||
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
|
||||
return self.cleaned_data.get('email').lower()
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
_("You can't use a {} address.").format(
|
||||
OptionalUser.objects.first().local_email_domain))
|
||||
|
||||
class Meta:
|
||||
model = Adherent
|
||||
fields = [
|
||||
|
@ -344,6 +338,19 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
'room',
|
||||
]
|
||||
|
||||
force = forms.BooleanField(
|
||||
label=_("Force the move?"),
|
||||
initial=False,
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean_email(self):
|
||||
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
|
||||
return self.cleaned_data.get('email').lower()
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
_("You can't use a {} address.").format(
|
||||
OptionalUser.objects.first().local_email_domain))
|
||||
|
||||
def clean_telephone(self):
|
||||
"""Verifie que le tel est présent si 'option est validée
|
||||
|
@ -355,12 +362,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
)
|
||||
return telephone
|
||||
|
||||
force = forms.BooleanField(
|
||||
label=_("Force the move?"),
|
||||
initial=False,
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean_force(self):
|
||||
"""On supprime l'ancien user de la chambre si et seulement si la
|
||||
case est cochée"""
|
||||
|
@ -368,6 +369,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
remove_user_room(self.cleaned_data.get('room'))
|
||||
return
|
||||
|
||||
|
||||
class AdherentCreationForm(AdherentForm):
|
||||
"""Formulaire de création d'un user.
|
||||
AdherentForm auquel on ajoute une checkbox afin d'éviter les
|
||||
|
@ -383,8 +385,22 @@ class AdherentCreationForm(AdherentForm):
|
|||
|
||||
# Checkbox for GTU
|
||||
gtu_check = forms.BooleanField(required=True)
|
||||
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"), _(".")))
|
||||
#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"), _(".")))
|
||||
|
||||
class Meta:
|
||||
model = Adherent
|
||||
fields = [
|
||||
'name',
|
||||
'surname',
|
||||
'pseudo',
|
||||
'email',
|
||||
'school',
|
||||
'comment',
|
||||
'telephone',
|
||||
'room',
|
||||
'state',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdherentCreationForm, self).__init__(*args, **kwargs)
|
||||
|
@ -398,12 +414,6 @@ class AdherentEditForm(AdherentForm):
|
|||
if 'shell' in self.fields:
|
||||
self.fields['shell'].empty_label = _("Default shell")
|
||||
|
||||
def clean_gpg_fingerprint(self):
|
||||
"""Format the GPG fingerprint"""
|
||||
gpg_fingerprint = self.cleaned_data.get('gpg_fingerprint', None)
|
||||
if gpg_fingerprint:
|
||||
return gpg_fingerprint.replace(' ', '').upper()
|
||||
|
||||
class Meta:
|
||||
model = Adherent
|
||||
fields = [
|
||||
|
@ -429,6 +439,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
self.fields['surname'].label = _("Name")
|
||||
self.fields['school'].label = _("School")
|
||||
self.fields['comment'].label = _("Comment")
|
||||
self.fields['email'].label = _("Email Address")
|
||||
if 'room' in self.fields:
|
||||
self.fields['room'].label = _("Room")
|
||||
self.fields['room'].empty_label = _("No room")
|
||||
|
@ -443,7 +454,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
'school',
|
||||
'comment',
|
||||
'room',
|
||||
'email',
|
||||
'telephone',
|
||||
'email',
|
||||
'shell',
|
||||
'mailing'
|
||||
]
|
||||
|
@ -488,13 +501,14 @@ class PasswordForm(FormRevMixin, ModelForm):
|
|||
|
||||
|
||||
class ServiceUserForm(FormRevMixin, ModelForm):
|
||||
""" Modification d'un service user"""
|
||||
"""Service user creation
|
||||
force initial password set"""
|
||||
password = forms.CharField(
|
||||
label=_("New password"),
|
||||
max_length=255,
|
||||
validators=[MinLengthValidator(8)],
|
||||
widget=forms.PasswordInput,
|
||||
required=False
|
||||
required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -506,7 +520,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
|
|||
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
|
||||
def save(self, commit=True):
|
||||
"""Changement du mot de passe"""
|
||||
"""Password change"""
|
||||
user = super(ServiceUserForm, self).save(commit=False)
|
||||
if self.cleaned_data['password']:
|
||||
user.set_password(self.cleaned_data.get("password"))
|
||||
|
@ -516,6 +530,14 @@ class ServiceUserForm(FormRevMixin, ModelForm):
|
|||
class EditServiceUserForm(ServiceUserForm):
|
||||
"""Formulaire d'edition de base d'un service user. Ne permet
|
||||
d'editer que son group d'acl et son commentaire"""
|
||||
password = forms.CharField(
|
||||
label=_("New password"),
|
||||
max_length=255,
|
||||
validators=[MinLengthValidator(8)],
|
||||
widget=forms.PasswordInput,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ServiceUserForm.Meta):
|
||||
fields = ['access_group', 'comment']
|
||||
|
||||
|
|
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.field_permissions import FieldPermissionModelMixin
|
||||
from re2o.mixins import AclMixin, RevMixin
|
||||
from re2o.base import smtp_check
|
||||
|
||||
from cotisations.models import Cotisation, Facture, Paiement, Vente
|
||||
from machines.models import Domain, Interface, Machine, regen
|
||||
|
@ -93,7 +94,7 @@ from preferences.models import OptionalMachine, MailMessageOption
|
|||
|
||||
def linux_user_check(login):
|
||||
""" Validation du pseudo pour respecter les contraintes unix"""
|
||||
UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9-]*[$]?$")
|
||||
UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$")
|
||||
return UNIX_LOGIN_PATTERN.match(login)
|
||||
|
||||
|
||||
|
@ -336,7 +337,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
|||
def set_active(self):
|
||||
"""Enable this user if he subscribed successfully one time before"""
|
||||
if self.state == self.STATE_NOT_YET_ACTIVE:
|
||||
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists():
|
||||
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists() or OptionalUser.get_cached_value('all_users_active'):
|
||||
self.state = self.STATE_ACTIVE
|
||||
self.save()
|
||||
|
||||
|
@ -474,7 +475,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
|||
""" Renvoie si un utilisateur a accès à internet """
|
||||
return (self.state == User.STATE_ACTIVE and
|
||||
not self.is_ban() and
|
||||
(self.is_connected() or self.is_whitelisted()))
|
||||
(self.is_connected() or self.is_whitelisted())) \
|
||||
or self == AssoOption.get_cached_value('utilisateur_asso')
|
||||
|
||||
def end_access(self):
|
||||
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
|
||||
|
@ -576,7 +578,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
|||
mac_refresh : synchronise les machines de l'user
|
||||
group_refresh : synchronise les group de l'user
|
||||
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
|
||||
if sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\
|
||||
self.state != self.STATE_DISABLED:
|
||||
self.refresh_from_db()
|
||||
try:
|
||||
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
|
||||
|
@ -693,10 +696,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
|||
def autoregister_machine(self, mac_address, nas_type):
|
||||
""" Fonction appellée par freeradius. Enregistre la mac pour
|
||||
une machine inconnue sur le compte de l'user"""
|
||||
all_interfaces = self.user_interfaces(active=False)
|
||||
if all_interfaces.count() > OptionalMachine.get_cached_value(
|
||||
'max_lambdauser_interfaces'
|
||||
):
|
||||
allowed, _message = Machine.can_create(self, self.id)
|
||||
if not allowed:
|
||||
return False, _("Maximum number of registered machines reached.")
|
||||
if not nas_type:
|
||||
return False, _("Re2o doesn't know wich machine type to assign.")
|
||||
|
@ -1025,17 +1026,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
|
|||
):
|
||||
raise ValidationError("This pseudo is already in use.")
|
||||
if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE):
|
||||
raise ValidationError(
|
||||
{'email': (
|
||||
_("There is neither a local email address nor an external"
|
||||
raise ValidationError(_("There is neither a local email address nor an external"
|
||||
" email address for this user.")
|
||||
), }
|
||||
)
|
||||
if self.local_email_redirect and not self.email:
|
||||
raise ValidationError(
|
||||
{'local_email_redirect': (
|
||||
_("You can't redirect your local emails if no external email"
|
||||
" address has been set.")), }
|
||||
raise ValidationError(_("You can't redirect your local emails if no external email"
|
||||
" address has been set.")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -1054,20 +1050,27 @@ class Adherent(User):
|
|||
null=True
|
||||
)
|
||||
gpg_fingerprint = models.CharField(
|
||||
max_length=40,
|
||||
max_length=49,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[RegexValidator(
|
||||
'^[0-9A-F]{40}$',
|
||||
message=_("A GPG fingerprint must contain 40 hexadecimal"
|
||||
" characters.")
|
||||
)]
|
||||
)
|
||||
|
||||
class Meta(User.Meta):
|
||||
verbose_name = _("member")
|
||||
verbose_name_plural = _("members")
|
||||
|
||||
def format_gpgfp(self):
|
||||
"""Format gpg finger print as AAAA BBBB... from a string AAAABBBB...."""
|
||||
self.gpg_fingerprint = ' '.join([self.gpg_fingerprint[i:i + 4] for i in range(0, len(self.gpg_fingerprint), 4)])
|
||||
|
||||
def validate_gpgfp(self):
|
||||
"""Validate from raw entry if is it a valid gpg fp"""
|
||||
if self.gpg_fingerprint:
|
||||
gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper()
|
||||
if not re.match("^[0-9A-F]{40}$", gpg_fingerprint):
|
||||
raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters"))
|
||||
self.gpg_fingerprint = gpg_fingerprint
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, adherentid, *_args, **_kwargs):
|
||||
"""Try to find an instance of `Adherent` with the given id.
|
||||
|
@ -1098,6 +1101,13 @@ class Adherent(User):
|
|||
_("You don't have the right to create a user.")
|
||||
)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
"""Format the GPG fingerprint"""
|
||||
super(Adherent, self).clean(*args, **kwargs)
|
||||
if self.gpg_fingerprint:
|
||||
self.validate_gpgfp()
|
||||
self.format_gpgfp()
|
||||
|
||||
|
||||
class Club(User):
|
||||
""" A class representing a club (it is considered as a user
|
||||
|
@ -1889,6 +1899,9 @@ class EMailAddress(RevMixin, AclMixin, models.Model):
|
|||
|
||||
def clean(self, *args, **kwargs):
|
||||
self.local_part = self.local_part.lower()
|
||||
if "@" in self.local_part:
|
||||
raise ValidationError(_("The local part must not contain @."))
|
||||
if "@" in self.local_part or "+" in self.local_part:
|
||||
raise ValidationError(_("The local part must not contain @ or +."))
|
||||
result, reason = smtp_check(self.local_part)
|
||||
if result:
|
||||
raise ValidationError(reason)
|
||||
super(EMailAddress, self).clean(*args, **kwargs)
|
||||
|
|
|
@ -23,7 +23,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load acl %}
|
||||
{% load logs_extra %}
|
||||
{% load design %}
|
||||
|
@ -78,8 +77,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% if solde_activated %}
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
|
||||
{{ users.solde }} <i class="fa fa-eur"></i>
|
||||
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
|
||||
data-target="#subscriptions">
|
||||
{{ users.solde }} <i class="fa fa-eur"></i>
|
||||
</div>
|
||||
<div class="panel-body dashboard">
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}">
|
||||
|
@ -92,8 +92,9 @@ 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 %}">
|
||||
{% if nb_machines %}
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
|
||||
<i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="panel-body dashboard">
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
|
||||
|
@ -103,7 +104,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</div>
|
||||
{% else %}
|
||||
<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">
|
||||
<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" %}
|
||||
|
@ -117,158 +121,200 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
<div class="panel-group" id="accordion">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse1">
|
||||
<h3 class="panel-title pull-left" >
|
||||
<i class="fa fa-user"></i>{% trans " Detailed information" %}
|
||||
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
|
||||
data-target="#information">
|
||||
<h3 class="panel-title pull-left">
|
||||
<i class="fa fa-user"></i>{% trans " Detailed information" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-collapse collapse in" id="collapse1">
|
||||
<div class="panel-body">
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:password' users.id %}">
|
||||
<i class="fa fa-lock"></i>
|
||||
{% trans "Change the password" %}
|
||||
</a>
|
||||
{% can_change User state %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:state' users.id %}">
|
||||
<i class="fa fa-id-badge"></i>
|
||||
{% trans "Change the state" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% can_change User groups %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:groups' users.id %}">
|
||||
<i class="fa fa-check"></i>
|
||||
{% trans "Edit the groups" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% history_button users text=True %}
|
||||
<div class="panel-collapse collapse collapse-default" id="information">
|
||||
<div class="panel-body">
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:password' users.id %}">
|
||||
<i class="fa fa-lock"></i>
|
||||
{% trans "Change the password" %}
|
||||
</a>
|
||||
{% can_change User state %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:state' users.id %}">
|
||||
<i class="fa fa-id-badge"></i>
|
||||
{% trans "Change the state" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% can_change User groups %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:groups' users.id %}">
|
||||
<i class="fa fa-check"></i>
|
||||
{% trans "Edit the groups" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% history_button users text=True %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<dl class="dl-horizontal row profile-info">
|
||||
<div class="col-md-6">
|
||||
{% if users.is_class_club %}
|
||||
<th>{% trans "Mailing" %}</th>
|
||||
{% if users.club.mailing %}
|
||||
<td>{{ users.pseudo }}(-admin)</td>
|
||||
{% else %}
|
||||
<td>{% trans "Mailing disabled" %}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<th>{% trans "Firt name" %}</th>
|
||||
<td>{{ users.name }}</td>
|
||||
{% endif %}
|
||||
<th>{% trans "Surname" %}</th>
|
||||
<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>
|
||||
<dt>{% trans "Mailing" %}</dt>
|
||||
{% if users.club.mailing %}
|
||||
<dd>{{ users.pseudo }}(-admin)</dd>
|
||||
{% else %}
|
||||
<dd>{% trans "Mailing disabled" %}</dd>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td><i class="text-danger">{% trans "Not a member" %}</i></td>
|
||||
<dt>{% trans "Firt name" %}</dt>
|
||||
<dd>{{ users.name }}</dd>
|
||||
{% 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>
|
||||
|
||||
<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 %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Internet access" %}</th>
|
||||
</div>
|
||||
|
||||
<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 %}
|
||||
<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 %}
|
||||
<td><i class="text-danger">{% trans "Disabled" %}</i></td>
|
||||
<dd><i class="text-danger">{% trans "Disabled" %}</i></dd>
|
||||
{% endif %}
|
||||
<th>{% trans "Groups of rights" %}</th>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<dt>{% trans "Groups of rights" %}</dt>
|
||||
{% if users.groups.all %}
|
||||
<td>{{ users.groups.all|join:", "}}</td>
|
||||
<dd>{{ users.groups.all|join:", " }}</dd>
|
||||
{% else %}
|
||||
<td>{% trans "None" %}</td>
|
||||
<dd>{% trans "None" %}</dd>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Balance" %}</th>
|
||||
<td>{{ users.solde }} €
|
||||
{% if user_solde %}
|
||||
<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>
|
||||
{% trans "Refill" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% 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>
|
||||
|
||||
<div class="col-md-6">
|
||||
<dt>{% trans "Balance" %}</dt>
|
||||
<dd>
|
||||
{{ users.solde }} €
|
||||
{% if user_solde %}
|
||||
<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>
|
||||
{% trans "Refill" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</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>
|
||||
{% if users.is_class_club %}
|
||||
<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">
|
||||
<i class="fa fa-users"></i>{% trans " Manage the club" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-collapse collapse" id="collapse2">
|
||||
<div class="panel-collapse collapse" id="club">
|
||||
<div class="panel-body">
|
||||
<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>
|
||||
|
@ -319,14 +365,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</div>
|
||||
{% endif %}
|
||||
<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">
|
||||
<i class="fa fa-desktop"></i>
|
||||
{% trans "Machines" %}
|
||||
<span class="badge">{{nb_machines}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="collapse3" class="panel-collapse collapse">
|
||||
<div id="machines" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
|
||||
<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 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">
|
||||
<i class="fa fa-eur"></i>
|
||||
{% trans "Subscriptions" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div id="collapse4" class="panel-collapse collapse">
|
||||
<div id="subscriptions" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
{% can_create Facture %}
|
||||
<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 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">
|
||||
<i class="fa fa-ban"></i>
|
||||
{% trans "Bans" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div id="collapse5" class="panel-collapse collapse">
|
||||
<div id="bans" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
{% can_create Ban %}
|
||||
<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 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">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
{% trans "Whitelists" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div id="collapse6" class="panel-collapse collapse">
|
||||
<div id="whitelists" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
{% can_create Whitelist %}
|
||||
<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 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">
|
||||
<i class="fa fa-envelope"></i>{% trans " Email settings" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div id="collapse7" class="panel-collapse collapse">
|
||||
<div id="email" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
{% can_edit users %}
|
||||
<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.utils import (
|
||||
all_has_access,
|
||||
SortTable,
|
||||
re2o_paginator
|
||||
)
|
||||
from re2o.base import (
|
||||
re2o_paginator,
|
||||
SortTable
|
||||
)
|
||||
from re2o.acl import (
|
||||
can_create,
|
||||
|
|
Loading…
Reference in a new issue