8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-28 07:32:25 +00:00

Merge branch 'dev' of https://gitlab.federez.net/federez/re2o into anonymisation

This commit is contained in:
Grizzly 2019-01-09 17:40:50 +00:00
commit 8035f14e1b
81 changed files with 3109 additions and 1025 deletions

2
.gitignore vendored
View file

@ -5,7 +5,7 @@ __pycache__/
*.swp *.swp
# Translations # Translations
#*.mo TODO *.mo
*.pot *.pot
# Django stuff # Django stuff

View file

@ -391,13 +391,25 @@ class OptionalTopologieSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = preferences.OptionalTopologie model = preferences.OptionalTopologie
fields = ('radius_general_policy', 'vlan_decision_ok', fields = ('switchs_ip_type', 'switchs_web_management',
'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management',
'switchs_web_management_ssl', 'switchs_rest_management', 'switchs_web_management_ssl', 'switchs_rest_management',
'switchs_management_utils', 'switchs_management_interface_ip', 'switchs_management_utils', 'switchs_management_interface_ip',
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds') 'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
class RadiusOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.RadiusOption` objects
"""
class Meta:
model = preferences.RadiusOption
fields = ('radius_general_policy', 'unknown_machine',
'unknown_machine_vlan', 'unknown_port',
'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan',
'non_member', 'non_member_vlan', 'banned', 'banned_vlan',
'vlan_decision_ok')
class GeneralOptionSerializer(NamespacedHMSerializer): class GeneralOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.GeneralOption` objects. """Serialize `preferences.models.GeneralOption` objects.
""" """
@ -407,9 +419,8 @@ class GeneralOptionSerializer(NamespacedHMSerializer):
fields = ('general_message_fr', 'general_message_en', fields = ('general_message_fr', 'general_message_en',
'search_display_page', 'pagination_number', 'search_display_page', 'pagination_number',
'pagination_large_number', 'req_expire_hrs', 'pagination_large_number', 'req_expire_hrs',
'site_name', 'email_from', 'GTU_sum_up', 'site_name', 'main_site_url', 'email_from',
'GTU') 'GTU_sum_up', 'GTU')
class HomeServiceSerializer(NamespacedHMSerializer): class HomeServiceSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.Service` objects. """Serialize `preferences.models.Service` objects.
@ -633,9 +644,8 @@ class AdherentSerializer(NamespacedHMSerializer):
} }
class HomeCreationSerializer(NamespacedHMSerializer): class BasicUserSerializer(NamespacedHMSerializer):
"""Serialize 'users.models.User' minimal infos to create home """Serialize 'users.models.User' minimal infos"""
"""
uid = serializers.IntegerField(source='uid_number') uid = serializers.IntegerField(source='uid_number')
gid = serializers.IntegerField(source='gid_number') gid = serializers.IntegerField(source='gid_number')
@ -813,7 +823,8 @@ class SwitchPortSerializer(serializers.ModelSerializer):
model = topologie.Switch model = topologie.Switch
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6', fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled', 'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value') 'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value',
'list_modules')
# LOCAL EMAILS # LOCAL EMAILS
@ -1001,6 +1012,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer):
model = machines.Domain model = machines.Domain
fields = ('alias', 'hostname') fields = ('alias', 'hostname')
class DNAMERecordSerializer(serializers.ModelSerializer):
"""Serialize `machines.models.Domain` objects with the data needed to
generate a DNAME DNS record.
"""
alias = serializers.CharField(read_only=True)
zone = serializers.CharField(read_only=True)
class Meta:
model = machines.DName
fields = ('alias', 'zone')
class DNSZonesSerializer(serializers.ModelSerializer): class DNSZonesSerializer(serializers.ModelSerializer):
"""Serialize the data about DNS Zones. """Serialize the data about DNS Zones.
@ -1015,14 +1037,14 @@ class DNSZonesSerializer(serializers.ModelSerializer):
a_records = ARecordSerializer(many=True, source='get_associated_a_records') a_records = ARecordSerializer(many=True, source='get_associated_a_records')
aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records') aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records')
cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records') cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records')
dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records')
sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records') sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records')
class Meta: class Meta:
model = machines.Extension model = machines.Extension
fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6', fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6',
'mx_records', 'txt_records', 'srv_records', 'a_records', 'mx_records', 'txt_records', 'srv_records', 'a_records',
'aaaa_records', 'cname_records', 'sshfp_records') 'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records')
#REMINDER #REMINDER

View file

@ -67,6 +67,7 @@ router.register_viewset(r'machines/role', views.RoleViewSet)
router.register_view(r'preferences/optionaluser', views.OptionalUserView), router.register_view(r'preferences/optionaluser', views.OptionalUserView),
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView), router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView),
router.register_view(r'preferences/radiusoption', views.RadiusOptionView),
router.register_view(r'preferences/generaloption', views.GeneralOptionView), router.register_view(r'preferences/generaloption', views.GeneralOptionView),
router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'), router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
router.register_view(r'preferences/assooption', views.AssoOptionView), router.register_view(r'preferences/assooption', views.AssoOptionView),
@ -88,6 +89,8 @@ router.register(r'topologie/portprofile', views.PortProfileViewSet)
# USERS # USERS
router.register_viewset(r'users/user', views.UserViewSet, base_name='user') router.register_viewset(r'users/user', views.UserViewSet, base_name='user')
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation') router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation')
router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser')
router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser')
router.register_viewset(r'users/club', views.ClubViewSet) router.register_viewset(r'users/club', views.ClubViewSet)
router.register_viewset(r'users/adherent', views.AdherentViewSet) router.register_viewset(r'users/adherent', views.AdherentViewSet)
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)

View file

@ -292,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView):
return preferences.OptionalTopologie.objects.first() return preferences.OptionalTopologie.objects.first()
class RadiusOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.OptionalTopologie` settings.
"""
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.RadiusOption.can_view_all]}
serializer_class = serializers.RadiusOptionSerializer
def get_object(self):
return preferences.RadiusOption.objects.first()
class GeneralOptionView(generics.RetrieveAPIView): class GeneralOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.GeneralOption` settings. """Exposes details of `preferences.models.GeneralOption` settings.
""" """
@ -445,7 +456,19 @@ class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes. """Exposes infos of `users.models.Users` objects to create homes.
""" """
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE)) queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
serializer_class = serializers.HomeCreationSerializer serializer_class = serializers.BasicUserSerializer
class NormalUserViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users`without specific rights objects."""
queryset = users.User.objects.exclude(groups__listright__critical=True).distinct()
serializer_class = serializers.BasicUserSerializer
class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users`without specific rights objects."""
queryset = users.User.objects.filter(groups__listright__critical=True).distinct()
serializer_class = serializers.BasicUserSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet): class ClubViewSet(viewsets.ReadOnlyModelViewSet):
@ -541,8 +564,8 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
# Config des switches # Config des switches
class SwitchPortView(generics.ListAPIView): class SwitchPortView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in """Output each port of a switch, to be serialized with
order to build the DHCP lease files. additionnal informations (profiles etc)
""" """
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room") queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
@ -551,16 +574,14 @@ class SwitchPortView(generics.ListAPIView):
# Rappel fin adhésion # Rappel fin adhésion
class ReminderView(generics.ListAPIView): class ReminderView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in """Output for users to remind an end of their subscription.
order to build the DHCP lease files.
""" """
queryset = preferences.Reminder.objects.all() queryset = preferences.Reminder.objects.all()
serializer_class = serializers.ReminderSerializer serializer_class = serializers.ReminderSerializer
class RoleView(generics.ListAPIView): class RoleView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in """Output of roles for each server
order to build the DHCP lease files.
""" """
queryset = machines.Role.objects.all().prefetch_related('servers') queryset = machines.Role.objects.all().prefetch_related('servers')
serializer_class = serializers.RoleSerializer serializer_class = serializers.RoleSerializer

View file

@ -17,3 +17,4 @@ libjs-bootstrap
fonts-font-awesome fonts-font-awesome
graphviz graphviz
git git
gettext

View file

@ -30,7 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice from .models import CustomInvoice, CostEstimate
class FactureAdmin(VersionAdmin): class FactureAdmin(VersionAdmin):
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass pass
class CostEstimateAdmin(VersionAdmin):
"""Admin class for cost estimates."""
pass
class CustomInvoiceAdmin(VersionAdmin): class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices.""" """Admin class for custom invoices."""
pass pass
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin) admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin) admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(CustomInvoice, CustomInvoiceAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin)
admin.site.register(CostEstimate, CostEstimateAdmin)

View file

@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
from re2o.field_permissions import FieldPermissionFormMixin from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
from .models import Article, Paiement, Facture, Banque, CustomInvoice from .models import (
Article, Paiement, Facture, Banque,
CustomInvoice, Vente, CostEstimate
)
from .payment_methods import balance from .payment_methods import balance
@ -104,7 +107,44 @@ class SelectArticleForm(FormRevMixin, Form):
user = kwargs.pop('user') user = kwargs.pop('user')
target_user = kwargs.pop('target_user', None) target_user = kwargs.pop('target_user', None)
super(SelectArticleForm, self).__init__(*args, **kwargs) super(SelectArticleForm, self).__init__(*args, **kwargs)
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user) self.fields['article'].queryset = Article.find_allowed_articles(
user, target_user)
class DiscountForm(Form):
"""
Form used in oder to create a discount on an invoice.
"""
is_relative = forms.BooleanField(
label=_("Discount is on percentage"),
required=False,
)
discount = forms.DecimalField(
label=_("Discount"),
max_value=100,
min_value=0,
max_digits=5,
decimal_places=2,
required=False,
)
def apply_to_invoice(self, invoice):
invoice_price = invoice.prix_total()
discount = self.cleaned_data['discount']
is_relative = self.cleaned_data['is_relative']
if is_relative:
amount = discount/100 * invoice_price
else:
amount = discount
if amount:
name = _("{}% discount") if is_relative else _("{}€ discount")
name = name.format(discount)
Vente.objects.create(
facture=invoice,
name=name,
prix=-amount,
number=1
)
class CustomInvoiceForm(FormRevMixin, ModelForm): class CustomInvoiceForm(FormRevMixin, ModelForm):
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
fields = '__all__' fields = '__all__'
class CostEstimateForm(FormRevMixin, ModelForm):
"""
Form used to create a cost estimate.
"""
class Meta:
model = CostEstimate
exclude = ['paid', 'final_invoice']
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):
""" """
Form used to create an article. Form used to create an article.
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
super(RechargeForm, self).__init__(*args, **kwargs) super(RechargeForm, self).__init__(*args, **kwargs)
self.fields['payment'].empty_label = \ self.fields['payment'].empty_label = \
_("Select a payment method") _("Select a payment method")
self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True) self.fields['payment'].queryset = Paiement.find_allowed_payments(
user_source).exclude(is_balance=True)
def clean(self): def clean(self):
""" """
@ -266,4 +316,3 @@ class RechargeForm(FormRevMixin, Form):
} }
) )
return self.cleaned_data return self.cleaned_data

View 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'),
),
]

View 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',),
),
]

View 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'),
),
]

View file

@ -284,8 +284,65 @@ class CustomInvoice(BaseInvoice):
verbose_name=_("Address") verbose_name=_("Address")
) )
paid = models.BooleanField( paid = models.BooleanField(
verbose_name=_("Paid") verbose_name=_("Paid"),
default=False
) )
remark = models.TextField(
verbose_name=_("Remark"),
blank=True,
null=True
)
class CostEstimate(CustomInvoice):
class Meta:
permissions = (
('view_costestimate', _("Can view a cost estimate object")),
)
validity = models.DurationField(
verbose_name=_("Period of validity"),
help_text="DD HH:MM:SS"
)
final_invoice = models.ForeignKey(
CustomInvoice,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="origin_cost_estimate",
primary_key=False
)
def create_invoice(self):
"""Create a CustomInvoice from the CostEstimate."""
if self.final_invoice is not None:
return self.final_invoice
invoice = CustomInvoice()
invoice.recipient = self.recipient
invoice.payment = self.payment
invoice.address = self.address
invoice.paid = False
invoice.remark = self.remark
invoice.date = timezone.now()
invoice.save()
self.final_invoice = invoice
self.save()
for sale in self.vente_set.all():
Vente.objects.create(
facture=invoice,
name=sale.name,
prix=sale.prix,
number=sale.number,
)
return invoice
def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_costestimate'):
return False, _("You don't have the right "
"to delete a cost estimate.")
if self.final_invoice is not None:
return False, _("The cost estimate has an "
"invoice and cannot be deleted.")
return True, None
# TODO : change Vente to Purchase # TODO : change Vente to Purchase
@ -624,7 +681,7 @@ class Article(RevMixin, AclMixin, models.Model):
objects_pool = cls.objects.filter( objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Adherent') Q(type_user='All') | Q(type_user='Adherent')
) )
if not target_user.is_adherent(): if target_user is not None and not target_user.is_adherent():
objects_pool = objects_pool.filter( objects_pool = objects_pool.filter(
Q(type_cotisation='All') | Q(type_cotisation='Adhesion') Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
) )

View 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>

View file

@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% if title %}
<h3>{{title}}</h3>
{% else %}
<h3>{% trans "Edit the invoice" %}</h3> <h3>{% trans "Edit the invoice" %}</h3>
{% endif %}
{% massive_bootstrap_form factureform 'user' %} {% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }} {{ venteform.management_form }}
<h3>{% trans "Articles" %}</h3> <h3>{% trans "Articles" %}</h3>

View file

@ -44,6 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %} {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{% if factureform %}
{% bootstrap_form_errors factureform %}
{% endif %}
{% if discount_form %}
{% bootstrap_form_errors discount_form %}
{% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
@ -68,6 +74,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</div> </div>
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one"> <input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
<h3>{% trans "Discount" %}</h3>
{% if discount_form %}
{% bootstrap_form discount_form %}
{% endif %}
<p> <p>
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %} {% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
</p> </p>
@ -78,20 +88,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if articlesformset or payment_method%} {% if articlesformset or payment_method%}
<script type="text/javascript"> <script type="text/javascript">
{% if articlesformset %} {% if articlesformset %}
var prices = {}; var prices = {};
{% for article in articlelist %} {% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }}; prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %} {% endfor %}
var template = `Article : &nbsp; var template = `Article : &nbsp;
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %} {% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
&nbsp; &nbsp;
<button class="btn btn-danger btn-sm" <button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button"> id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span> <span class="fa fa-times"></span>
</button>` </button>`
function add_article(){ function add_article(){
// Index start at 0 => new_index = number of items // Index start at 0 => new_index = number of items
var new_index = var new_index =
document.getElementsByClassName('product_to_sell').length; document.getElementsByClassName('product_to_sell').length;
@ -101,9 +111,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
new_article.innerHTML = template.replace(/__prefix__/g, new_index); new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article); document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index); add_listenner_for_id(new_index);
} }
function update_price(){ function update_price(){
var price = 0; var price = 0;
var product_count = var product_count =
document.getElementsByClassName('product_to_sell').length; document.getElementsByClassName('product_to_sell').length;
@ -119,11 +129,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
'id_form-' + i.toString() + '-quantity').value; 'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity; price += article_price * quantity;
} }
{% if discount_form %}
var relative_discount = document.getElementById('id_is_relative').checked;
var discount = document.getElementById('id_discount').value;
if(relative_discount) {
discount = discount/100 * price;
}
price -= discount;
{% endif %}
document.getElementById('total_price').innerHTML = document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ','); price.toFixed(2).toString().replace('.', ',');
} }
function add_listenner_for_id(i){ function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article') document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true); .addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article') document.getElementById('id_form-' + i.toString() + '-article')
@ -137,10 +155,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
document.getElementById('id_form-TOTAL_FORMS').value --; document.getElementById('id_form-TOTAL_FORMS').value --;
update_price(); update_price();
}) })
} }
// Add events manager when DOM is fully loaded // Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one") document.getElementById("add_one")
.addEventListener("click", add_article, true); .addEventListener("click", add_article, true);
var product_count = var product_count =
@ -148,21 +166,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
for (i = 0; i < product_count; ++i){ for (i = 0; i < product_count; ++i){
add_listenner_for_id(i); add_listenner_for_id(i);
} }
document.getElementById('id_discount')
.addEventListener('change', update_price, true);
document.getElementById('id_is_relative')
.addEventListener('click', update_price, true);
update_price(); update_price();
}); });
{% endif %} {% endif %}
{% if payment_method.templates %} {% if payment_method.templates %}
var TEMPLATES = [ var TEMPLATES = [
"", "",
{% for t in payment_method.templates %} {% for t in payment_method.templates %}
{% if t %} {% if t %}
`{% bootstrap_form t %}`, `{% bootstrap_form t %}`,
{% else %} {% else %}
"", "",
{% endif %} {% endif %}
{% endfor %} {% endfor %}
]; ];
function update_payment_method_form(){ function update_payment_method_form(){
var method = document.getElementById('paymentMethodSelect').value; var method = document.getElementById('paymentMethodSelect').value;
if(method==""){ if(method==""){
method=0; method=0;
@ -175,8 +197,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
var html = TEMPLATES[method]; var html = TEMPLATES[method];
document.getElementById('paymentMethod').innerHTML = html; document.getElementById('paymentMethod').innerHTML = html;
} }
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form); document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
{% endif %} {% endif %}
</script> </script>
{% endif %} {% endif %}

View file

@ -75,8 +75,12 @@
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
{% if fid is not None %} {% if fid is not None %}
{% if is_estimate %}
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
{% else %}
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\ {\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
{% endif %} {% endif %}
{% endif %}
\end{tabular*} \end{tabular*}
\\ \\
@ -104,12 +108,30 @@
\begin{tabular}{|l|r|} \begin{tabular}{|l|r|}
\hline \hline
\textbf{Total} & {{total|floatformat:2}} \euro \\ \textbf{Total} & {{total|floatformat:2}} \euro \\
{% if not is_estimate %}
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\ \textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
\doublehline \doublehline
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
{% endif %}
\hline \hline
\end{tabular} \end{tabular}
\vspace{1cm}
\begin{tabularx}{\textwidth}{r X}
\hline
\textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\
\hline
{% if remark %}
\textbf{Remarque} & {{remark|safe}} \\
\hline
{% endif %}
{% if end_validity %}
\textbf{Validité} & Jusqu'au {{end_validity}} \\
\hline
{% endif %}
\end{tabularx}
\vfill \vfill

View 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 %}

View file

@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %} <i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all CostEstimate %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-cost-estimate" %}">
<i class="fa fa-list-ul"></i> {% trans "Cost estimate" %}
</a>
{% acl_end %}
{% can_view_all Article %} {% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %} <i class="fa fa-list-ul"></i> {% trans "Available articles" %}

View file

@ -36,6 +36,7 @@ from django.template import Context
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from django.utils.text import slugify from django.utils.text import slugify
import logging
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
@ -48,8 +49,9 @@ def render_invoice(_request, ctx={}):
Render an invoice using some available information such as the current Render an invoice using some available information such as the current
date, the user, the articles, the prices, ... date, the user, the articles, the prices, ...
""" """
is_estimate = ctx.get('is_estimate', False)
filename = '_'.join([ filename = '_'.join([
'invoice', 'cost_estimate' if is_estimate else 'invoice',
slugify(ctx.get('asso_name', "")), slugify(ctx.get('asso_name', "")),
slugify(ctx.get('recipient_name', "")), slugify(ctx.get('recipient_name', "")),
str(ctx.get('DATE', datetime.now()).year), str(ctx.get('DATE', datetime.now()).year),
@ -93,6 +95,20 @@ def create_pdf(template, ctx={}):
return pdf return pdf
def escape_chars(string):
"""Escape the '%' and the '' signs to avoid messing with LaTeX"""
if not isinstance(string, str):
return string
mapping = (
('', r'\euro'),
('%', r'\%'),
)
r = str(string)
for k, v in mapping:
r = r.replace(k, v)
return r
def render_tex(_request, template, ctx={}): def render_tex(_request, template, ctx={}):
"""Creates a PDF from a LaTex templates using pdflatex. """Creates a PDF from a LaTex templates using pdflatex.

View file

@ -51,11 +51,41 @@ urlpatterns = [
views.facture_pdf, views.facture_pdf,
name='facture-pdf' name='facture-pdf'
), ),
url(
r'^new_cost_estimate/$',
views.new_cost_estimate,
name='new-cost-estimate'
),
url(
r'^index_cost_estimate/$',
views.index_cost_estimate,
name='index-cost-estimate'
),
url(
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_pdf,
name='cost-estimate-pdf',
),
url( url(
r'^index_custom_invoice/$', r'^index_custom_invoice/$',
views.index_custom_invoice, views.index_custom_invoice,
name='index-custom-invoice' name='index-custom-invoice'
), ),
url(
r'^edit_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.edit_cost_estimate,
name='edit-cost-estimate'
),
url(
r'^cost_estimate_to_invoice/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_to_invoice,
name='cost-estimate-to-invoice'
),
url(
r'^del_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.del_cost_estimate,
name='del-cost-estimate'
),
url( url(
r'^new_custom_invoice/$', r'^new_custom_invoice/$',
views.new_custom_invoice, views.new_custom_invoice,

View file

@ -47,7 +47,10 @@ from users.models import User
from re2o.settings import LOGO_PATH from re2o.settings import LOGO_PATH
from re2o import settings from re2o import settings
from re2o.views import form from re2o.views import form
from re2o.utils import SortTable, re2o_paginator from re2o.base import (
SortTable,
re2o_paginator,
)
from re2o.acl import ( from re2o.acl import (
can_create, can_create,
can_edit, can_edit,
@ -65,7 +68,8 @@ from .models import (
Paiement, Paiement,
Banque, Banque,
CustomInvoice, CustomInvoice,
BaseInvoice BaseInvoice,
CostEstimate
) )
from .forms import ( from .forms import (
FactureForm, FactureForm,
@ -77,9 +81,11 @@ from .forms import (
DelBanqueForm, DelBanqueForm,
SelectArticleForm, SelectArticleForm,
RechargeForm, RechargeForm,
CustomInvoiceForm CustomInvoiceForm,
DiscountForm,
CostEstimateForm,
) )
from .tex import render_invoice from .tex import render_invoice, escape_chars
from .payment_methods.forms import payment_method_factory from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method from .utils import find_payment_method
@ -175,7 +181,58 @@ def new_facture(request, user, userid):
) )
# TODO : change facture to invoice @login_required
@can_create(CostEstimate)
def new_cost_estimate(request):
"""
View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative
point of view.
"""
# The template needs the list of articles (for the JS part)
articles = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
# Building the invocie form and the article formset
cost_estimate_form = CostEstimateForm(request.POST or None)
articles_formset = formset_factory(SelectArticleForm)(
request.POST or None,
form_kwargs={'user': request.user}
)
discount_form = DiscountForm(request.POST or None)
if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
cost_estimate_instance = cost_estimate_form.save()
for art_item in articles_formset:
if art_item.cleaned_data:
article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity']
Vente.objects.create(
facture=cost_estimate_instance,
name=article.name,
prix=article.prix,
type_cotisation=article.type_cotisation,
duration=article.duration,
number=quantity
)
discount_form.apply_to_invoice(cost_estimate_instance)
messages.success(
request,
_("The cost estimate was created.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': cost_estimate_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles,
'discount_form': discount_form,
'title': _("Cost estimate"),
}, 'cotisations/facture.html', request)
@login_required @login_required
@can_create(CustomInvoice) @can_create(CustomInvoice)
def new_custom_invoice(request): def new_custom_invoice(request):
@ -195,8 +252,9 @@ def new_custom_invoice(request):
request.POST or None, request.POST or None,
form_kwargs={'user': request.user} form_kwargs={'user': request.user}
) )
discount_form = DiscountForm(request.POST or None)
if invoice_form.is_valid() and articles_formset.is_valid(): if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
new_invoice_instance = invoice_form.save() new_invoice_instance = invoice_form.save()
for art_item in articles_formset: for art_item in articles_formset:
if art_item.cleaned_data: if art_item.cleaned_data:
@ -210,6 +268,7 @@ def new_custom_invoice(request):
duration=article.duration, duration=article.duration,
number=quantity number=quantity
) )
discount_form.apply_to_invoice(new_invoice_instance)
messages.success( messages.success(
request, request,
_("The custom invoice was created.") _("The custom invoice was created.")
@ -220,7 +279,8 @@ def new_custom_invoice(request):
'factureform': invoice_form, 'factureform': invoice_form,
'action_name': _("Confirm"), 'action_name': _("Confirm"),
'articlesformset': articles_formset, 'articlesformset': articles_formset,
'articlelist': articles 'articlelist': articles,
'discount_form': discount_form
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -263,7 +323,8 @@ def facture_pdf(request, facture, **_kwargs):
'siret': AssoOption.get_cached_value('siret'), 'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'), 'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'), 'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': facture.paiement.moyen,
}) })
@ -328,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request) }, 'cotisations/delete.html', request)
@login_required
@can_edit(CostEstimate)
def edit_cost_estimate(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CostEstimateForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(purchases_objects)
)
purchase_form = purchase_form_set(
request.POST or None,
queryset=purchases_objects
)
if invoice_form.is_valid() and purchase_form.is_valid():
if invoice_form.changed_data:
invoice_form.save()
purchase_form.save()
messages.success(
request,
_("The cost estimate was edited.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': invoice_form,
'venteform': purchase_form,
'title': "Edit the cost estimate"
}, 'cotisations/edit_facture.html', request)
@login_required
@can_edit(CostEstimate)
@can_create(CustomInvoice)
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
"""Create a custom invoice from a cos estimate"""
cost_estimate.create_invoice()
messages.success(
request,
_("An invoice was successfully created from your cost estimate.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
@login_required @login_required
@can_edit(CustomInvoice) @can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs): def edit_custom_invoice(request, invoice, **kwargs):
@ -364,10 +474,10 @@ def edit_custom_invoice(request, invoice, **kwargs):
@login_required @login_required
@can_view(CustomInvoice) @can_view(CostEstimate)
def custom_invoice_pdf(request, invoice, **_kwargs): def cost_estimate_pdf(request, invoice, **_kwargs):
""" """
View used to generate a PDF file from an existing invoice in database View used to generate a PDF file from an existing cost estimate in database
Creates a line for each Purchase (thus article sold) and generate the Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the invoice with the total price, the payment method, the address and the
legal information for the user. legal information for the user.
@ -379,7 +489,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
purchases_info = [] purchases_info = []
for purchase in purchases_objects: for purchase in purchases_objects:
purchases_info.append({ purchases_info.append({
'name': purchase.name, 'name': escape_chars(purchase.name),
'price': purchase.prix, 'price': purchase.prix,
'quantity': purchase.number, 'quantity': purchase.number,
'total_price': purchase.prix_total 'total_price': purchase.prix_total
@ -398,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
'siret': AssoOption.get_cached_value('siret'), 'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'), 'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'), 'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
'end_validity': invoice.date + invoice.validity,
'is_estimate': True,
})
@login_required
@can_delete(CostEstimate)
def del_cost_estimate(request, estimate, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
estimate.delete()
messages.success(
request,
_("The cost estimate was deleted.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'objet': estimate,
'objet_name': _("Cost Estimate")
}, 'cotisations/delete.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing invoice in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
}) })
# TODO : change facture to invoice
@login_required @login_required
@can_delete(CustomInvoice) @can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs): def del_custom_invoice(request, invoice, **_kwargs):
@ -753,12 +926,35 @@ def index_banque(request):
}) })
@login_required
@can_view_all(CustomInvoice)
def index_cost_estimate(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
cost_estimate_list = SortTable.sort(
cost_estimate_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
cost_estimate_list = re2o_paginator(
request,
cost_estimate_list,
pagination_number,
)
return render(request, 'cotisations/index_cost_estimate.html', {
'cost_estimate_list': cost_estimate_list
})
@login_required @login_required
@can_view_all(CustomInvoice) @can_view_all(CustomInvoice)
def index_custom_invoice(request): def index_custom_invoice(request):
"""View used to display every custom invoice.""" """View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number') pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set') cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
custom_invoice_list = SortTable.sort( custom_invoice_list = SortTable.sort(
custom_invoice_list, custom_invoice_list,
request.GET.get('col'), request.GET.get('col'),

View file

@ -57,14 +57,9 @@ application = get_wsgi_application()
from machines.models import Interface, IpList, Nas, Domain from machines.models import Interface, IpList, Nas, Domain
from topologie.models import Port, Switch from topologie.models import Port, Switch
from users.models import User from users.models import User
from preferences.models import OptionalTopologie from preferences.models import RadiusOption
options, created = OptionalTopologie.objects.get_or_create()
VLAN_NOK = options.vlan_decision_nok.vlan_id
VLAN_OK = options.vlan_decision_ok.vlan_id
RADIUS_POLICY = options.radius_general_policy
#: Serveur radius de test (pas la prod) #: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
@ -287,6 +282,7 @@ def find_nas_from_request(nas_id):
.select_related('machine__switch__stack')) .select_related('machine__switch__stack'))
return nas.first() return nas.first()
def check_user_machine_and_register(nas_type, username, mac_address): def check_user_machine_and_register(nas_type, username, mac_address):
"""Verifie le username et la mac renseignee. L'enregistre si elle est """Verifie le username et la mac renseignee. L'enregistre si elle est
inconnue. inconnue.
@ -331,28 +327,46 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
"""Fonction de placement vlan pour un switch en radius filaire auth par """Fonction de placement vlan pour un switch en radius filaire auth par
mac. mac.
Plusieurs modes : Plusieurs modes :
- nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK - tous les modes:
- pas de radius sur le port : VLAN_OK - nas inconnu: VLAN_OK
- bloq : VLAN_NOK - port inconnu: Politique définie dans RadiusOption
- force : placement sur le vlan indiqué dans la bdd - pas de radius sur le port: VLAN_OK
- mode strict : - force: placement sur le vlan indiqué dans la bdd
- pas de chambre associée : VLAN_NOK - mode strict:
- pas d'utilisateur dans la chambre : VLAN_NOK - pas de chambre associée: Politique définie
- cotisation non à jour : VLAN_NOK dans RadiusOption
- pas d'utilisateur dans la chambre : Rejet
(redirection web si disponible)
- utilisateur de la chambre banni ou désactivé : Rejet
(redirection web si disponible)
- utilisateur de la chambre non cotisant et non whiteslist:
Politique définie dans RadiusOption
- sinon passe à common (ci-dessous) - sinon passe à common (ci-dessous)
- mode common : - mode common :
- interface connue (macaddress): - interface connue (macaddress):
- utilisateur proprio non cotisant ou banni : VLAN_NOK - utilisateur proprio non cotisant / machine désactivée:
- user à jour : VLAN_OK Politique définie dans RadiusOption
- utilisateur proprio banni :
Politique définie dans RadiusOption
- user à jour : VLAN_OK (réassignation de l'ipv4 au besoin)
- interface inconnue : - interface inconnue :
- register mac désactivé : VLAN_NOK - register mac désactivé : Politique définie
- register mac activé -> redirection vers webauth dans RadiusOption
- register mac activé: redirection vers webauth
Returns:
tuple avec :
- Nom du switch (str)
- chambre (str)
- raison de la décision (str)
- vlan_id (int)
- decision (bool)
""" """
# Get port from switch and port number # Get port from switch and port number
extra_log = "" extra_log = ""
# Si le NAS est inconnu, on place sur le vlan defaut # Si le NAS est inconnu, on place sur le vlan defaut
if not nas_machine: if not nas_machine:
return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK, True) return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True)
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine))) sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
@ -367,7 +381,13 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
# Aucune information particulière ne permet de déterminer quelle # Aucune information particulière ne permet de déterminer quelle
# politique à appliquer sur ce port # politique à appliquer sur ce port
if not port: if not port:
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK, True) return (
sw_name,
"Chambre inconnue",
u'Port inconnu',
getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT
)
# On récupère le profil du port # On récupère le profil du port
port_profile = port.get_port_profile port_profile = port.get_port_profile
@ -378,11 +398,11 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
extra_log = u"Force sur vlan " + str(DECISION_VLAN) extra_log = u"Force sur vlan " + str(DECISION_VLAN)
else: else:
DECISION_VLAN = VLAN_OK DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok')
# Si le port est désactivé, on rejette sur le vlan de déconnexion # Si le port est désactivé, on rejette la connexion
if not port.state: if not port.state:
return (sw_name, port.room, u'Port desactivé', VLAN_NOK, True) return (sw_name, port.room, u'Port desactivé', None, False)
# Si radius est désactivé, on laisse passer # Si radius est désactivé, on laisse passer
if port_profile.radius_type == 'NO': if port_profile.radius_type == 'NO':
@ -392,33 +412,68 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
DECISION_VLAN, DECISION_VLAN,
True) True)
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment # Si le 802.1X est activé sur ce port, cela veut dire que la personne a
# été accept précédemment
# Par conséquent, on laisse passer sur le bon vlan # Par conséquent, on laisse passer sur le bon vlan
if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X': if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'):
room = port.room or "Chambre/local inconnu" room = port.room or "Chambre/local inconnu"
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN, True) return (
sw_name,
room,
u'Acceptation authentification 802.1X',
DECISION_VLAN,
True
)
# Sinon, cela veut dire qu'on fait de l'auth radius par mac # Sinon, cela veut dire qu'on fait de l'auth radius par mac
# Si le port est en mode strict, on vérifie que tous les users # Si le port est en mode strict, on vérifie que tous les users
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage) # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis # (anti squattage)
# dedans # Il n'est pas possible de se connecter sur une prise strict sans adhérent
# à jour de cotis dedans
if port_profile.radius_mode == 'STRICT': if port_profile.radius_mode == 'STRICT':
room = port.room room = port.room
if not room: if not room:
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK, True) return (
sw_name,
"Inconnue",
u'Chambre inconnue',
getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT
)
room_user = User.objects.filter( room_user = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room) Q(club__room=port.room) | Q(adherent__room=port.room)
) )
if not room_user: if not room_user:
return (sw_name, room, u'Chambre non cotisante -> Web redirect', None, False) return (
sw_name,
room,
u'Chambre non cotisante -> Web redirect',
None,
False
)
for user in room_user: for user in room_user:
if not user.has_access(): if user.is_ban() or user.state != User.STATE_ACTIVE:
return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False) return (
sw_name,
room,
u'Utilisateur banni ou désactivé -> Web redirect',
None,
False
)
elif not (user.is_connected() or user.is_whitelisted()):
return (
sw_name,
room,
u'Utilisateur non cotisant',
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
)
# else: user OK, on passe à la verif MAC # else: user OK, on passe à la verif MAC
# Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd # Si on fait de l'auth par mac, on cherche l'interface
# via sa mac dans la bdd
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT': if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
# Authentification par mac # Authentification par mac
interface = (Interface.objects interface = (Interface.objects
@ -428,38 +483,67 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
.first()) .first())
if not interface: if not interface:
room = port.room room = port.room
# On essaye de register la mac, si l'autocapture a été activée # On essaye de register la mac, si l'autocapture a été activée,
# Sinon on rejette sur vlan_nok # on rejette pour faire une redirection web si possible.
if not nas_type.autocapture_mac: if nas_type.autocapture_mac:
return (sw_name, "", u'Machine inconnue', VLAN_NOK, True) return (
# On rejette pour basculer sur du webauth sw_name,
room,
u'Machine Inconnue -> Web redirect',
None,
False
)
# Sinon on bascule sur la politique définie dans les options
# radius.
else: else:
return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False) return (
sw_name,
"",
u'Machine inconnue',
getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT
)
# L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject # L'interface a été trouvée, on vérifie qu'elle est active,
# sinon on reject
# Si elle n'a pas d'ipv4, on lui en met une # Si elle n'a pas d'ipv4, on lui en met une
# Enfin on laisse passer sur le vlan pertinent # Enfin on laisse passer sur le vlan pertinent
else: else:
room = port.room room = port.room
if interface.machine.user.is_ban():
return (
sw_name,
room,
u'Adherent banni',
getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT
)
if not interface.is_active: if not interface.is_active:
return (sw_name, return (
sw_name,
room, room,
u'Machine non active / adherent non cotisant', u'Machine non active / adherent non cotisant',
VLAN_NOK, getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
True) RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
## Si on choisi de placer les machines sur le vlan correspondant à leur type : )
if RADIUS_POLICY == 'MACHINE': # Si on choisi de placer les machines sur le vlan
# correspondant à leur type :
if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE':
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
if not interface.ipv4: if not interface.ipv4:
interface.assign_ipv4() interface.assign_ipv4()
return (sw_name, return (
sw_name,
room, room,
u"Ok, Reassignation de l'ipv4" + extra_log, u"Ok, Reassignation de l'ipv4" + extra_log,
DECISION_VLAN, DECISION_VLAN,
True) True
)
else: else:
return (sw_name, return (
sw_name,
room, room,
u'Machine OK' + extra_log, u'Machine OK' + extra_log,
DECISION_VLAN, DECISION_VLAN,
True) True
)

View file

@ -316,6 +316,10 @@ update_django() {
echo "Collecting web frontend statics ..." echo "Collecting web frontend statics ..."
python3 manage.py collectstatic --noinput python3 manage.py collectstatic --noinput
echo "Collecting web frontend statics: Done" echo "Collecting web frontend statics: Done"
echo "Generating locales ..."
python3 manage.py compilemessages
echo "Generating locales: Done"
} }

Binary file not shown.

View file

@ -102,15 +102,18 @@ from re2o.utils import (
all_baned, all_baned,
all_has_access, all_has_access,
all_adherent, all_adherent,
all_active_assigned_interfaces_count,
all_active_interfaces_count,
)
from re2o.base import (
re2o_paginator, re2o_paginator,
SortTable
) )
from re2o.acl import ( from re2o.acl import (
can_view_all, can_view_all,
can_view_app, can_view_app,
can_edit_history, can_edit_history,
) )
from re2o.utils import all_active_assigned_interfaces_count
from re2o.utils import all_active_interfaces_count, SortTable
@login_required @login_required

View file

@ -273,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm):
self.fields['origin'].label = _("A record origin") self.fields['origin'].label = _("A record origin")
self.fields['origin_v6'].label = _("AAAA record origin") self.fields['origin_v6'].label = _("AAAA record origin")
self.fields['soa'].label = _("SOA record to use") self.fields['soa'].label = _("SOA record to use")
self.fields['dnssec'].label = _("Sign with DNSSEC")
class DelExtensionForm(FormRevMixin, Form): class DelExtensionForm(FormRevMixin, Form):

View 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'),
),
]

View 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),
),
]

View 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),
]

View 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),
),
]

View file

@ -696,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model):
'SOA', 'SOA',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
dnssec = models.BooleanField(
default=False,
help_text=_("Should the zone be signed with DNSSEC")
)
class Meta: class Meta:
permissions = ( permissions = (
@ -741,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model):
.filter(cname__interface_parent__in=all_active_assigned_interfaces()) .filter(cname__interface_parent__in=all_active_assigned_interfaces())
.prefetch_related('cname')) .prefetch_related('cname'))
def get_associated_dname_records(self):
return (DName.objects.filter(alias=self))
@staticmethod @staticmethod
def can_use_all(user_request, *_args, **_kwargs): def can_use_all(user_request, *_args, **_kwargs):
"""Superdroit qui permet d'utiliser toutes les extensions sans """Superdroit qui permet d'utiliser toutes les extensions sans
@ -1089,7 +1096,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.get_cached_value('ipv6_mode') == 'DHCPV6'): .get_cached_value('ipv6_mode') == 'DHCPV6'):
return self.ipv6list.filter(slaac_ip=False) return self.ipv6list.filter(slaac_ip=False)
else: else:
return None return []
def mac_bare(self): def mac_bare(self):
""" Formatage de la mac type mac_bare""" """ Formatage de la mac type mac_bare"""
@ -1373,7 +1380,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.filter(interface=self.interface, slaac_ip=True) .filter(interface=self.interface, slaac_ip=True)
.exclude(id=self.id)): .exclude(id=self.id)):
raise ValidationError(_("A SLAAC IP address is already registered.")) raise ValidationError(_("A SLAAC IP address is already registered."))
try:
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
except AttributeError: # Prevents from crashing when there is no defined prefix_v6
prefix_v6 = None
if prefix_v6: if prefix_v6:
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] != if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
IPv6Address(prefix_v6).exploded[:20]): IPv6Address(prefix_v6).exploded[:20]):
@ -1602,7 +1612,7 @@ class Role(RevMixin, AclMixin, models.Model):
ROLE = ( ROLE = (
('dhcp-server', _("DHCP server")), ('dhcp-server', _("DHCP server")),
('switch-conf-server', _("Switches configuration server")), ('switch-conf-server', _("Switches configuration server")),
('dns-recursif-server', _("Recursive DNS server")), ('dns-recursive-server', _("Recursive DNS server")),
('ntp-server', _("NTP server")), ('ntp-server', _("NTP server")),
('radius-server', _("RADIUS server")), ('radius-server', _("RADIUS server")),
('log-server', _("Log server")), ('log-server', _("Log server")),
@ -1631,18 +1641,6 @@ class Role(RevMixin, AclMixin, models.Model):
verbose_name = _("server role") verbose_name = _("server role")
verbose_name_plural = _("server roles") verbose_name_plural = _("server roles")
@classmethod
def get_instance(cls, roleid, *_args, **_kwargs):
"""Get the Role instance with roleid.
Args:
roleid: The id
Returns:
The role.
"""
return cls.objects.get(pk=roleid)
@classmethod @classmethod
def interface_for_roletype(cls, roletype): def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype""" """Return interfaces for a roletype"""
@ -1657,14 +1655,6 @@ class Role(RevMixin, AclMixin, models.Model):
machine__interface__role=cls.objects.filter(specific_role=roletype) machine__interface__role=cls.objects.filter(specific_role=roletype)
) )
@classmethod
def get_instance(cls, roleid, *_args, **_kwargs):
"""Get the Machine instance with machineid.
:param userid: The id
:return: The user
"""
return cls.objects.get(pk=roleid)
@classmethod @classmethod
def interface_for_roletype(cls, roletype): def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype""" """Return interfaces for a roletype"""

View file

@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %} {% if ipv6_enabled %}
<th>{% trans "AAAA record origin" %}</th> <th>{% trans "AAAA record origin" %}</th>
{% endif %} {% endif %}
<th>{% trans "DNSSEC" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -50,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %} {% if ipv6_enabled %}
<td>{{ extension.origin_v6 }}</td> <td>{{ extension.origin_v6 }}</td>
{% endif %} {% endif %}
<td>{{ extension.dnssec|tick }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit extension %} {% can_edit extension %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="table-responsive"> <div class="table-responsive">
{% if machines_list.paginator %} {% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %} {% include "pagination.html" with list=machines_list go_to_id="machines" %}
{% endif %} {% endif %}
<table class="table" id="machines_table"> <table class="table" id="machines_table">
@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</script> </script>
{% if machines_list.paginator %} {% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %} {% include "pagination.html" with list=machines_list go_to_id="machines" %}
{% endif %} {% endif %}
</div> </div>

View file

@ -95,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if interfaceform %} {% if interfaceform %}
<h3>{% trans "Interface" %}</h3> <h3>{% trans "Interface" %}</h3>
{% if i_mbf_param %} {% if i_mbf_param %}
{% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
{% else %} {% else %}
{% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if domainform %} {% if domainform %}
@ -146,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
{% if aliasform %} {% if aliasform %}
<h3>{% trans "Alias" %}</h3> <h3>{% trans "Alias" %}</h3>
{% bootstrap_form aliasform %} {% massive_bootstrap_form aliasform 'extension' %}
{% endif %} {% endif %}
{% if serviceform %} {% if serviceform %}
<h3>{% trans "Service" %}</h3> <h3>{% trans "Service" %}</h3>

View file

@ -55,6 +55,8 @@ from re2o.acl import (
from re2o.utils import ( from re2o.utils import (
all_active_assigned_interfaces, all_active_assigned_interfaces,
filter_active_interfaces, filter_active_interfaces,
)
from re2o.base import (
SortTable, SortTable,
re2o_paginator, re2o_paginator,
) )

View file

@ -42,6 +42,7 @@ from .models import (
Reminder, Reminder,
RadiusKey, RadiusKey,
SwitchManagementCred, SwitchManagementCred,
RadiusOption,
) )
from topologie.models import Switch from topologie.models import Switch
@ -114,11 +115,6 @@ class EditOptionalTopologieForm(ModelForm):
prefix=prefix, prefix=prefix,
**kwargs **kwargs
) )
self.fields['radius_general_policy'].label = _("RADIUS general policy")
self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted"
" by RADIUS")
self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected"
" by RADIUS")
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name') self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
@ -229,6 +225,13 @@ class EditHomeOptionForm(ModelForm):
self.fields['twitter_account_name'].label = _("Twitter account name") self.fields['twitter_account_name'].label = _("Twitter account name")
class EditRadiusOptionForm(ModelForm):
"""Edition forms for Radius options"""
class Meta:
model = RadiusOption
fields = '__all__'
class ServiceForm(ModelForm): class ServiceForm(ModelForm):
"""Edition, ajout de services sur la page d'accueil""" """Edition, ajout de services sur la page d'accueil"""
class Meta: class Meta:

View 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),
),
]

View 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'),
),
]

View 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),
]

View 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',
),
]

View 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'),
),
]

View 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'),
),
]

View file

@ -116,6 +116,11 @@ class OptionalUser(AclMixin, PreferencesModel):
default=False, default=False,
help_text=_("A new user can create their account on Re2o") help_text=_("A new user can create their account on Re2o")
) )
all_users_active = models.BooleanField(
default=False,
help_text=_("If True, all new created and connected users are active.\
If False, only when a valid registration has been paid")
)
class Meta: class Meta:
permissions = ( permissions = (
@ -199,25 +204,6 @@ class OptionalTopologie(AclMixin, PreferencesModel):
('tftp', 'tftp'), ('tftp', 'tftp'),
) )
radius_general_policy = models.CharField(
max_length=32,
choices=CHOICE_RADIUS,
default='DEFINED'
)
vlan_decision_ok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='decision_ok',
blank=True,
null=True
)
vlan_decision_nok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='decision_nok',
blank=True,
null=True
)
switchs_web_management = models.BooleanField( switchs_web_management = models.BooleanField(
default=False, default=False,
help_text="Web management, activé si provision automatique" help_text="Web management, activé si provision automatique"
@ -297,19 +283,19 @@ class OptionalTopologie(AclMixin, PreferencesModel):
log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type) log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type)
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type) radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server") dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type)
subnet = None subnet = None
subnet6 = None subnet6 = None
if self.switchs_ip_type: if self.switchs_ip_type:
subnet = self.switchs_ip_type.ip_set_full_info subnet = self.switchs_ip_type.ip_set_full_info
subnet6 = self.switchs_ip_type.ip6_set_full_info subnet6 = self.switchs_ip_type.ip6_set_full_info
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6} return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6}
@cached_property @cached_property
def provision_switchs_enabled(self): def provision_switchs_enabled(self):
"""Return true if all settings are ok : switchs on automatic provision, """Return true if all settings are ok : switchs on automatic provision,
ip_type""" ip_type"""
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds)) return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
class Meta: class Meta:
permissions = ( permissions = (
("view_optionaltopologie", _("Can view the topology options")), ("view_optionaltopologie", _("Can view the topology options")),
@ -431,6 +417,7 @@ class GeneralOption(AclMixin, PreferencesModel):
req_expire_hrs = models.IntegerField(default=48) req_expire_hrs = models.IntegerField(default=48)
site_name = models.CharField(max_length=32, default="Re2o") site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@example.com") email_from = models.EmailField(default="www-data@example.com")
main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
GTU_sum_up = models.TextField( GTU_sum_up = models.TextField(
default="", default="",
blank=True, blank=True,
@ -587,3 +574,122 @@ class MailMessageOption(AclMixin, models.Model):
) )
verbose_name = _("email message options") verbose_name = _("email message options")
class RadiusOption(AclMixin, PreferencesModel):
class Meta:
verbose_name = _("radius policies")
MACHINE = 'MACHINE'
DEFINED = 'DEFINED'
CHOICE_RADIUS = (
(MACHINE, _("On the IP range's VLAN of the machine")),
(DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")),
)
REJECT = 'REJECT'
SET_VLAN = 'SET_VLAN'
CHOICE_POLICY = (
(REJECT, _('Reject the machine')),
(SET_VLAN, _('Place the machine on the VLAN'))
)
radius_general_policy = models.CharField(
max_length=32,
choices=CHOICE_RADIUS,
default='DEFINED'
)
unknown_machine = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for unknown machines"),
)
unknown_machine_vlan = models.ForeignKey(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='unknown_machine_vlan',
blank=True,
null=True,
verbose_name=_('Unknown machine Vlan'),
help_text=_(
'Vlan for unknown machines if not rejected.'
)
)
unknown_port = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for unknown port"),
)
unknown_port_vlan = models.ForeignKey(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='unknown_port_vlan',
blank=True,
null=True,
verbose_name=_('Unknown port Vlan'),
help_text=_(
'Vlan for unknown ports if not rejected.'
)
)
unknown_room = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_(
"Policy for machine connecting from "
"unregistered room (relevant on ports with STRICT "
"radius mode)"
),
)
unknown_room_vlan = models.ForeignKey(
'machines.Vlan',
related_name='unknown_room_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Unknown room Vlan'),
help_text=_(
'Vlan for unknown room if not rejected.'
)
)
non_member = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy non member users."),
)
non_member_vlan = models.ForeignKey(
'machines.Vlan',
related_name='non_member_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Non member Vlan'),
help_text=_(
'Vlan for non members if not rejected.'
)
)
banned = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for banned users."),
)
banned_vlan = models.ForeignKey(
'machines.Vlan',
related_name='banned_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Banned Vlan'),
help_text=_(
'Vlan for banned if not rejected.'
)
)
vlan_decision_ok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='vlan_ok_option',
blank=True,
null=True
)

View 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>

View file

@ -31,14 +31,84 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Preferences" %}{% endblock %} {% block title %}{% trans "Preferences" %}{% endblock %}
{% block content %} {% block content %}
<h4>{% trans "User preferences" %}</h4> <div id="accordion">
<div class="panel panel-default" id="general">
<div class="panel-heading" data-toggle="collapse" href="#collapse_general">
<h4 class="panel-title" id="general">
<a><i class="fa fa-cog"></i> {% trans "General preferences" %}</a>
</h4>
</div>
<div id="collapse_general" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
<i class="fa fa-edit"></i>{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Website name" %}</th>
<td>{{ generaloptions.site_name }}</td>
<th>{% trans "Email address for automatic emailing" %}</th>
<td>{{ generaloptions.email_from }}</td>
</tr>
<tr>
<th>{% trans "Number of results displayed when searching" %}</th>
<td>{{ generaloptions.search_display_page }}</td>
<th>{% trans "Number of items per page (standard size)" %}</th>
<td>{{ generaloptions.pagination_number }}</td>
</tr>
<tr>
<th>{% trans "Number of items per page (large size)" %}</th>
<td>{{ generaloptions.pagination_large_number }}</td>
<th>{% trans "Time before expiration of the reset password link (in hours)" %}</th>
<td>{{ generaloptions.req_expire_hrs }}</td>
</tr>
<tr>
<th>{% trans "General message displayed on the website" %}</th>
<td>{{ generaloptions.general_message }}</td>
<th>{% trans "Main site url" %}</th>
<td>{{ generaloptions.main_site_url }}</td>
</tr>
<tr>
<th>{% trans "Summary of the General Terms of Use" %}</th>
<td>{{ generaloptions.GTU_sum_up }}</td>
<th>{% trans "General Terms of Use" %}</th>
<td>{{ generaloptions.GTU }}</th>
</tr>
</table>
<table class="table table-striped">
<tr>
<th>{% trans "Local email accounts enabled" %}</th>
<td>{{ useroptions.local_email_accounts_enabled|tick }}</td>
<th>{% trans "Local email domain" %}</th>
<td>{{ useroptions.local_email_domain }}</td>
</tr>
<tr>
<th>{% trans "Maximum number of email aliases allowed" %}</th>
<td>{{ useroptions.max_email_address }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="users">
<div class="panel-heading" data-toggle="collapse" href="#collapse_users">
<h4 class="panel-title">
<a><i class="fa fa-users fa-fw"></i> {% trans "User preferences" %}</a>
</h4>
</div>
<div id="collapse_users" class="panel-collapse panel-body collapse">
<p></p>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
<p> <p></p>
</p>
<h5>{% trans "General preferences" %}</h5>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "Creation of members by everyone" %}</th> <th>{% trans "Creation of members by everyone" %}</th>
@ -52,9 +122,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% trans "Delete not yet active users after" %}</th> <th>{% trans "Delete not yet active users after" %}</th>
<td>{{ useroptions.delete_notyetactive }} days</td> <td>{{ useroptions.delete_notyetactive }} days</td>
</tr> </tr>
<tr>
<th>{% trans "All users are active by default" %}</th>
<td>{{ useroptions.all_users_active|tick }}</td>
</tr>
</table> </table>
<h5>{% trans "Users general permissions" %}</h5> <h4 id="users">{% trans "Users general permissions" %}</h4>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "Default shell for users" %}</th> <th>{% trans "Default shell for users" %}</th>
@ -73,27 +147,24 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ useroptions.gpg_fingerprint|tick }}</td> <td>{{ useroptions.gpg_fingerprint|tick }}</td>
</tr> </tr>
</table> </table>
</div>
</div>
</div>
<div class="panel panel-default" id="machines">
<div class="panel-heading" data-toggle="collapse" href="#collapse_machines">
<h4 class ="panel-title">
<a><i class="fa fa-desktop"></i> {% trans "Machines preferences" %}</a>
</h4>
</div>
<div id="collapse_machines" class="panel-collapse panel-body collapse">
<h5>{% trans "Email accounts preferences" %}</h5>
<table class="table table-striped">
<tr>
<th>{% trans "Local email accounts enabled" %}</th>
<td>{{ useroptions.local_email_accounts_enabled|tick }}</td>
<th>{% trans "Local email domain" %}</th>
<td>{{ useroptions.local_email_domain }}</td>
</tr>
<tr>
<th>{% trans "Maximum number of email aliases allowed" %}</th>
<td>{{ useroptions.max_email_address }}</td>
</tr>
</table>
<h4>{% trans "Machines preferences" %}</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
<p> <p></p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "Password per machine" %}</th> <th>{% trans "Password per machine" %}</th>
@ -112,13 +183,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ machineoptions.create_machine|tick }}</td> <td>{{ machineoptions.create_machine|tick }}</td>
</tr> </tr>
</table> </table>
<h4>{% trans "Topology preferences" %}</h4> </div>
</div>
<div class="panel panel-default" id="topo">
<div class="panel-heading" data-toggle="collapse" href="#collapse_topo">
<h4 class="panel-title">
<a><i class="fa fa-sitemap"></i> {% trans "Topology preferences" %}</a>
</h4>
</div>
<div id="collapse_topo" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
<p> <p></p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "General policy for VLAN setting" %}</th> <th>{% trans "General policy for VLAN setting" %}</th>
@ -133,18 +213,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ topologieoptions.vlan_decision_nok }}</td> <td>{{ topologieoptions.vlan_decision_nok }}</td>
</tr> </tr>
<tr> <tr>
<th>Placement sur ce vlan par default en cas de rejet</th> <th>{% trans "VLAN for non members machines" %}</th>
<td>{{ topologieoptions.vlan_decision_nok }}</td> <td>{{ topologieoptions.vlan_non_member }}</td>
</tr> </tr>
</table> </table>
<h6>Clef radius</h6> <h4>Clef radius</h4>
{% can_create RadiusKey%} {% can_create RadiusKey%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-radiuskey' %}"><i class="fa fa-plus"></i> Ajouter une clef radius</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-radiuskey' %}"><i class="fa fa-plus"></i> Ajouter une clef radius</a>
{% acl_end %} {% acl_end %}
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %} {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
<h4>Configuration des switches</h4> </div>
</div>
<div class="panel panel-default" id="switches">
<div class="panel-heading" data-toggle="collapse" href="#collapse_switches">
<h4 class="panel-title">
<a><i class="fa fa-server"></i> Configuration des Switches</a>
</h4>
</div>
<div id="collapse_switches" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Web management, activé si provision automatique</th> <th>Web management, activé si provision automatique</th>
@ -192,53 +288,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</p> </p>
{% if switchmanagementcred_list %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span> {% if switchmanagementcred_list %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span>
{% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %} {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
</div>
</div>
<div class="panel panel-default" id="radius">
<div class="panel-heading" data-toggle="collapse" href="#collapse_radius">
<h4>{% trans "General preferences" %}</h4> <h4 class="panel-title"><a><i class="fa fa-circle"></i> {% trans "Radius preferences" %}</h4></a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}"> </div>
<div id="collapse_radius" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'RadiusOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
<p> {% include "preferences/aff_radiusoptions.html" %}
</p> </div>
<table class="table table-striped"> </div>
<tr>
<th>{% trans "Website name" %}</th> <div class="panel panel-default" id="asso">
<td>{{ generaloptions.site_name }}</td> <div class="panel-heading" data-toggle="collapse" href="#collapse_asso">
<th>{% trans "Email address for automatic emailing" %}</th> <h4 class="panel-title">
<td>{{ generaloptions.email_from }}</td> <a><i class="fa fa-at"></i> {% trans "Information about the organisation" %}</a>
</tr> </h4>
<tr> </div>
<th>{% trans "Number of results displayed when searching" %}</th> <div id="collapse_asso" class="panel-collapse panel-body collapse">
<td>{{ generaloptions.search_display_page }}</td>
<th>{% trans "Number of items per page (standard size)" %}</th>
<td>{{ generaloptions.pagination_number }}</td>
</tr>
<tr>
<th>{% trans "Number of items per page (large size)" %}</th>
<td>{{ generaloptions.pagination_large_number }}</td>
<th>{% trans "Time before expiration of the reset password link (in hours)" %}</th>
<td>{{ generaloptions.req_expire_hrs }}</td>
</tr>
<tr>
<th>{% trans "General message displayed on the website" %}</th>
<td>{{ generaloptions.general_message }}</td>
<th>{% trans "Summary of the General Terms of Use" %}</th>
<td>{{ generaloptions.GTU_sum_up }}</td>
</tr>
<tr>
<th>{% trans "General Terms of Use" %}</th>
<td>{{ generaloptions.GTU }}</th>
</tr>
</table>
<h4>{% trans "Information about the organisation" %}</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
<p> <p></p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "Name" %}</th> <th>{% trans "Name" %}</th>
@ -267,13 +344,23 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ assooptions.description|safe }}</td> <td>{{ assooptions.description|safe }}</td>
</tr> </tr>
</table> </table>
<h4>{% trans "Custom email message" %}</h4> </div>
</div>
<div class="panel panel-default" id="mail">
<div class="panel-heading" data-toggle="collapse" href="#collapse_mail">
<h4 class="panel-title">
<a><i class="fa fa-comment"></i> Message pour les mails</a>
</h4>
</div>
<div id="collapse_mail" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
<p> <p></p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "Welcome email (in French)" %}</th> <th>{% trans "Welcome email (in French)" %}</th>
@ -284,29 +371,73 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td> <td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
</tr> </tr>
</table> </table>
<h4>Options pour le mail de fin d'adhésion</h2> </div>
</div>
<div class="panel panel-default" id="rappels">
<div class="panel-heading" data-toggle="collapse" href="#collapse_rappels">
<h4 class="panel-title">
<a><i class="fa fa-bell"></i> Options pour le mail de fin d'adhésion</a>
</h4>
</div>
<div id="collapse_rappels" class="panel-collapse panel-body collapse">
{% can_create preferences.Reminder%} {% can_create preferences.Reminder%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-reminder' %}"><i class="fa fa-plus"></i> Ajouter un rappel</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-reminder' %}"><i class="fa fa-plus"></i> Ajouter un rappel</a>
<p></p>
{% acl_end %} {% acl_end %}
{% include "preferences/aff_reminder.html" with reminder_list=reminder_list %} {% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
</div>
</div>
<h4>{% trans "List of services and homepage preferences" %}</h4>
<div class="panel panel-default" id="services">
<div class="panel-heading" data-toggle="collapse" href="#collapse_services">
<h4 class="panel-title">
<a><i class="fa fa-home"></i> {% trans "List of services and homepage preferences" %}</a>
</h4>
</div>
<div id="collapse_services" class="panel-collapse panel-body collapse">
{% can_create preferences.Service%} {% can_create preferences.Service%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i>{% trans " Add a service" %}</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i>{% trans " Add a service" %}</a>
<p></p>
{% acl_end %} {% acl_end %}
{% include "preferences/aff_service.html" with service_list=service_list %} {% include "preferences/aff_service.html" with service_list=service_list %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i> </div>
{% trans "Edit" %} </div>
</a>
<h2>{% trans "List of contact email addresses" %}</h2> <div class="panel panel-default" id="contact">
<div class="panel-heading" data-toggle="collapse" href="#collapse_contact">
<h4 class="panel-title">
<a><i class="fa fa-list-ul"></i> {% trans "List of contact email addresses" %}</a>
</h4>
</div>
<div id="collapse_contact" class="panel-collapse panel-body collapse">
{% can_create preferences.MailContact %} {% can_create preferences.MailContact %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>{% trans "Add an address" %}</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>{% trans "Add an address" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>{% trans "Delete one or several addresses" %}</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>{% trans "Delete one or several addresses" %}</a>
<p></p>
{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %} {% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
<p> </div>
</p> </div>
<div class="panel panel-default" id="social">
<div class="panel-heading" data-toggle="collapse" href="#collapse_social">
<h4 class="panel-title">
<a><i class="fa fa-facebook"></i><i class="fa fa-twitter"></i> Réseaux sociaux</a>
</h4>
</div>
<div id="collapse_social" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>{% trans "Twitter account URL" %}</th> <th>{% trans "Twitter account URL" %}</th>
@ -319,5 +450,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ homeoptions.facebook_url }}</td> <td>{{ homeoptions.facebook_url }}</td>
</tr> </tr>
</table> </table>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -37,6 +37,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post" enctype="multipart/form-data"> <form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %} {% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
{% if formset %}
{{ formset.management_form }}
{% for f in formset %}
{% bootstrap_form f %}
{% endfor %}
{% endif %}
{% trans "Edit" as tr_edit %} {% trans "Edit" as tr_edit %}
{% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %} {% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
</form> </form>

View file

@ -22,6 +22,8 @@ You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc., with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load acl %}
{% load i18n %}
{% block sidebar %} {% block sidebar %}

View file

@ -66,6 +66,11 @@ urlpatterns = [
views.edit_options, views.edit_options,
name='edit-options' name='edit-options'
), ),
url(
r'^edit_options/(?P<section>RadiusOption)$',
views.edit_options,
name='edit-options'
),
url(r'^add_service/$', views.add_service, name='add-service'), url(r'^add_service/$', views.add_service, name='add-service'),
url( url(
r'^edit_service/(?P<serviceid>[0-9]+)$', r'^edit_service/(?P<serviceid>[0-9]+)$',

View file

@ -62,7 +62,8 @@ from .models import (
HomeOption, HomeOption,
Reminder, Reminder,
RadiusKey, RadiusKey,
SwitchManagementCred SwitchManagementCred,
RadiusOption,
) )
from . import models from . import models
from . import forms from . import forms
@ -86,6 +87,7 @@ def display_options(request):
reminder_list = Reminder.objects.all() reminder_list = Reminder.objects.all()
radiuskey_list = RadiusKey.objects.all() radiuskey_list = RadiusKey.objects.all()
switchmanagementcred_list = SwitchManagementCred.objects.all() switchmanagementcred_list = SwitchManagementCred.objects.all()
radiusoptions, _ = RadiusOption.objects.get_or_create()
return form({ return form({
'useroptions': useroptions, 'useroptions': useroptions,
'machineoptions': machineoptions, 'machineoptions': machineoptions,
@ -99,6 +101,7 @@ def display_options(request):
'reminder_list': reminder_list, 'reminder_list': reminder_list,
'radiuskey_list' : radiuskey_list, 'radiuskey_list' : radiuskey_list,
'switchmanagementcred_list': switchmanagementcred_list, 'switchmanagementcred_list': switchmanagementcred_list,
'radiusoptions' : radiusoptions,
}, 'preferences/display_preferences.html', request) }, 'preferences/display_preferences.html', request)
@ -134,7 +137,9 @@ def edit_options(request, section):
messages.success(request, _("The preferences were edited.")) messages.success(request, _("The preferences were edited."))
return redirect(reverse('preferences:display-options')) return redirect(reverse('preferences:display-options'))
return form( return form(
{'options': options}, {
'options': options,
},
'preferences/edit_preferences.html', 'preferences/edit_preferences.html',
request request
) )

View file

@ -37,8 +37,8 @@ from django.db import models
from django import forms from django import forms
from django.conf import settings from django.conf import settings
EOD = '`%EofD%`' # This should be something that will not occur in strings EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings
EOD = EOD_asbyte.decode('utf-8')
def genstring(length=16, chars=string.printable): def genstring(length=16, chars=string.printable):
""" Generate a random string of length `length` and composed of """ Generate a random string of length `length` and composed of
@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable):
return ''.join([choice(chars) for i in range(length)]) return ''.join([choice(chars) for i in range(length)])
def encrypt(key, s): def encrypt(key, secret):
""" AES Encrypt a secret `s` with the key `key` """ """ AES Encrypt a secret with the key `key` """
obj = AES.new(key) obj = AES.new(key)
datalength = len(s) + len(EOD) datalength = len(secret) + len(EOD)
if datalength < 16: if datalength < 16:
saltlength = 16 - datalength saltlength = 16 - datalength
else: else:
saltlength = 16 - datalength % 16 saltlength = 16 - datalength % 16
ss = ''.join([s, EOD, genstring(saltlength)]) encrypted_secret = ''.join([secret, EOD, genstring(saltlength)])
return obj.encrypt(ss) return obj.encrypt(encrypted_secret)
def decrypt(key, s): def decrypt(key, secret):
""" AES Decrypt a secret `s` with the key `key` """ """ AES Decrypt a secret with the key `key` """
obj = AES.new(key) obj = AES.new(key)
ss = obj.decrypt(s) uncrypted_secret = obj.decrypt(secret)
return ss.split(bytes(EOD, 'utf-8'))[0] return uncrypted_secret.split(EOD_asbyte)[0]
class AESEncryptedFormField(forms.CharField): class AESEncryptedFormField(forms.CharField):
@ -81,8 +81,7 @@ class AESEncryptedField(models.CharField):
if value is None: if value is None:
return None return None
try: try:
return decrypt(settings.AES_KEY, return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
binascii.a2b_base64(value)).decode('utf-8')
except Exception as e: except Exception as e:
raise ValueError(value) raise ValueError(value)
@ -90,18 +89,14 @@ class AESEncryptedField(models.CharField):
if value is None: if value is None:
return value return value
try: try:
return decrypt(settings.AES_KEY, return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
binascii.a2b_base64(value)).decode('utf-8')
except Exception as e: except Exception as e:
raise ValueError(value) raise ValueError(value)
def get_prep_value(self, value): def get_prep_value(self, value):
if value is None: if value is None:
return value return value
return binascii.b2a_base64(encrypt( return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8')
settings.AES_KEY,
value
)).decode('utf-8')
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': AESEncryptedFormField} defaults = {'form_class': AESEncryptedFormField}

267
re2o/base.py Normal file
View 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.

View file

@ -114,9 +114,9 @@ class CryptPasswordHasher(hashers.BasePasswordHasher):
Check password against encoded using CRYPT algorithm Check password against encoded using CRYPT algorithm
""" """
assert encoded.startswith(self.algorithm) assert encoded.startswith(self.algorithm)
salt = hash_password_salt(challenge_password) salt = hash_password_salt(encoded)
return constant_time_compare(crypt.crypt(password.encode(), salt), return constant_time_compare(crypt.crypt(password, salt),
challenge.encode()) encoded)
def safe_summary(self, encoded): def safe_summary(self, encoded):
""" """

View file

@ -38,55 +38,11 @@ from __future__ import unicode_literals
from django.utils import timezone from django.utils import timezone
from django.db.models import Q from django.db.models import Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from cotisations.models import Cotisation, Facture, Vente from cotisations.models import Cotisation, Facture, Vente
from machines.models import Interface, Machine from machines.models import Interface, Machine
from users.models import Adherent, User, Ban, Whitelist from users.models import Adherent, User, Ban, Whitelist
from preferences.models import AssoOption
# Mapping of srtftime format for better understanding
# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior
datetime_mapping={
'%a': '%a',
'%A': '%A',
'%w': '%w',
'%d': 'dd',
'%b': '%b',
'%B': '%B',
'%m': 'mm',
'%y': 'yy',
'%Y': 'yyyy',
'%H': 'HH',
'%I': 'HH(12h)',
'%p': 'AMPM',
'%M': 'MM',
'%S': 'SS',
'%f': 'µµ',
'%z': 'UTC(+/-HHMM)',
'%Z': 'UTC(TZ)',
'%j': '%j',
'%U': 'ww',
'%W': 'ww',
'%c': '%c',
'%x': '%x',
'%X': '%X',
'%%': '%%',
}
def convert_datetime_format(format):
i=0
new_format = ""
while i < len(format):
if format[i] == '%':
char = format[i:i+2]
new_format += datetime_mapping.get(char, char)
i += 2
else:
new_format += format[i]
i += 1
return new_format
def all_adherent(search_time=None): def all_adherent(search_time=None):
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est """ Fonction renvoyant tous les users adherents. Optimisee pour n'est
@ -103,7 +59,7 @@ def all_adherent(search_time=None):
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.all().exclude(valid=False) facture__in=Facture.objects.all().exclude(valid=False)
) )
).filter(date_end__gt=search_time) ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
) )
) )
).distinct() ).distinct()
@ -115,7 +71,7 @@ def all_baned(search_time=None):
search_time = timezone.now() search_time = timezone.now()
return User.objects.filter( return User.objects.filter(
ban__in=Ban.objects.filter( ban__in=Ban.objects.filter(
date_end__gt=search_time Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
) )
).distinct() ).distinct()
@ -126,20 +82,23 @@ def all_whitelisted(search_time=None):
search_time = timezone.now() search_time = timezone.now()
return User.objects.filter( return User.objects.filter(
whitelist__in=Whitelist.objects.filter( whitelist__in=Whitelist.objects.filter(
date_end__gt=search_time Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
) )
).distinct() ).distinct()
def all_has_access(search_time=None): def all_has_access(search_time=None):
""" Renvoie tous les users beneficiant d'une connexion """ Return all connected users : active users and whitelisted +
: user adherent ou whiteliste et non banni """ asso_user defined in AssoOption pannel
----
Renvoie tous les users beneficiant d'une connexion
: user adherent et whiteliste non banni plus l'utilisateur asso"""
if search_time is None: if search_time is None:
search_time = timezone.now() search_time = timezone.now()
return User.objects.filter( filter_user = (
Q(state=User.STATE_ACTIVE) & Q(state=User.STATE_ACTIVE) &
~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) & ~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) &
(Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) | (Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) |
Q(facture__in=Facture.objects.filter( Q(facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter( cotisation__in=Cotisation.objects.filter(
@ -148,10 +107,14 @@ def all_has_access(search_time=None):
facture__in=Facture.objects.all() facture__in=Facture.objects.all()
.exclude(valid=False) .exclude(valid=False)
) )
).filter(date_end__gt=search_time) ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
) )
))) )))
).distinct() )
asso_user = AssoOption.get_cached_value('utilisateur_asso')
if asso_user:
filter_user |= Q(id=asso_user.id)
return User.objects.filter(filter_user).distinct()
def filter_active_interfaces(interface_set): def filter_active_interfaces(interface_set):
@ -203,164 +166,6 @@ def all_active_assigned_interfaces_count():
return all_active_interfaces_count().filter(ipv4__isnull=False) return all_active_interfaces_count().filter(ipv4__isnull=False)
class SortTable:
""" Class gathering uselful stuff to sort the colums of a table, according
to the column and order requested. It's used with a dict of possible
values and associated model_fields """
# All the possible possible values
# The naming convention is based on the URL or the views function
# The syntax to describe the sort to apply is a dict where the keys are
# the url value and the values are a list of model field name to use to
# order the request. They are applied in the order they are given.
# A 'default' might be provided to specify what to do if the requested col
# doesn't match any keys.
USERS_INDEX = {
'user_name': ['name'],
'user_surname': ['surname'],
'user_pseudo': ['pseudo'],
'user_room': ['room'],
'default': ['state', 'pseudo']
}
USERS_INDEX_BAN = {
'ban_user': ['user__pseudo'],
'ban_start': ['date_start'],
'ban_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_WHITE = {
'white_user': ['user__pseudo'],
'white_start': ['date_start'],
'white_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_SCHOOL = {
'school_name': ['name'],
'default': ['name']
}
MACHINES_INDEX = {
'machine_name': ['name'],
'default': ['pk']
}
COTISATIONS_INDEX = {
'cotis_user': ['user__pseudo'],
'cotis_paiement': ['paiement__moyen'],
'cotis_date': ['date'],
'cotis_id': ['id'],
'default': ['-date']
}
COTISATIONS_CUSTOM = {
'invoice_date': ['date'],
'invoice_id': ['id'],
'invoice_recipient': ['recipient'],
'invoice_address': ['address'],
'invoice_payment': ['payment'],
'default': ['-date']
}
COTISATIONS_CONTROL = {
'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'],
'control_paiement': ['paiement'],
'control_date': ['date'],
'control_valid': ['valid'],
'control_control': ['control'],
'control_id': ['id'],
'control_user-id': ['user__id'],
'default': ['-date']
}
TOPOLOGIE_INDEX = {
'switch_dns': ['interface__domain__name'],
'switch_ip': ['interface__ipv4__ipv4'],
'switch_loc': ['switchbay__name'],
'switch_ports': ['number'],
'switch_stack': ['stack__name'],
'default': ['switchbay', 'stack', 'stack_member_id']
}
TOPOLOGIE_INDEX_PORT = {
'port_port': ['port'],
'port_room': ['room__name'],
'port_interface': ['machine_interface__domain__name'],
'port_related': ['related__switch__name'],
'port_radius': ['radius'],
'port_vlan': ['vlan_force__name'],
'default': ['port']
}
TOPOLOGIE_INDEX_ROOM = {
'room_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_BUILDING = {
'building_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_BORNE = {
'ap_name': ['interface__domain__name'],
'ap_ip': ['interface__ipv4__ipv4'],
'ap_mac': ['interface__mac_address'],
'default': ['interface__domain__name']
}
TOPOLOGIE_INDEX_STACK = {
'stack_name': ['name'],
'stack_id': ['stack_id'],
'default': ['stack_id'],
}
TOPOLOGIE_INDEX_MODEL_SWITCH = {
'model-switch_name': ['reference'],
'model-switch_contructor': ['constructor__name'],
'default': ['reference'],
}
TOPOLOGIE_INDEX_SWITCH_BAY = {
'switch-bay_name': ['name'],
'switch-bay_building': ['building__name'],
'default': ['name'],
}
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
'constructor-switch_name': ['name'],
'default': ['name'],
}
LOGS_INDEX = {
'sum_date': ['revision__date_created'],
'default': ['-revision__date_created'],
}
LOGS_STATS_LOGS = {
'logs_author': ['user__name'],
'logs_date': ['date_created'],
'default': ['-date_created']
}
@staticmethod
def sort(request, col, order, values):
""" Check if the given values are possible and add .order_by() and
a .reverse() as specified according to those values """
fields = values.get(col, None)
if not fields:
fields = values.get('default', [])
request = request.order_by(*fields)
if values.get(col, None) and order == 'desc':
return request.reverse()
else:
return request
def re2o_paginator(request, query_set, pagination_number):
"""Paginator script for list display in re2o.
:request:
:query_set: Query_set to paginate
:pagination_number: Number of entries to display"""
paginator = Paginator(query_set, pagination_number)
page = request.GET.get('page')
try:
results = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
results = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
results = paginator.page(paginator.num_pages)
return results
def remove_user_room(room): def remove_user_room(room):
""" Déménage de force l'ancien locataire de la chambre """ """ Déménage de force l'ancien locataire de la chambre """
try: try:
@ -370,18 +175,3 @@ def remove_user_room(room):
user.room = None user.room = None
user.save() user.save()
def get_input_formats_help_text(input_formats):
"""Returns a help text about the possible input formats"""
if len(input_formats) > 1:
help_text_template="Format: {main} {more}"
else:
help_text_template="Format: {main}"
more_text_template="<i class=\"fa fa-question-circle\" title=\"{}\"></i>"
help_text = help_text_template.format(
main=convert_datetime_format(input_formats[0]),
more=more_text_template.format(
'\n'.join(map(convert_datetime_format, input_formats))
)
)
return help_text

View file

@ -27,7 +27,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.forms import Form from django.forms import Form
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from re2o.utils import get_input_formats_help_text from re2o.base import get_input_formats_help_text
CHOICES_USER = ( CHOICES_USER = (
('0', _("Active")), ('0', _("Active")),

View file

@ -46,7 +46,7 @@ from search.forms import (
CHOICES_AFF, CHOICES_AFF,
initial_choices initial_choices
) )
from re2o.utils import SortTable from re2o.base import SortTable
from re2o.acl import can_view_all from re2o.acl import can_view_all

View file

@ -79,19 +79,6 @@ a > i.fa {
vertical-align: middle; vertical-align: middle;
} }
/* Pull sidebars to the bottom */
@media (min-width: 767px) {
.row {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.row > [class*='col-'] {
flex-direction: column;
}
}
/* On small screens, set height to 'auto' for sidenav and grid */ /* On small screens, set height to 'auto' for sidenav and grid */
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
.sidenav { .sidenav {
@ -102,7 +89,7 @@ a > i.fa {
} }
.table-responsive { .table-responsive {
overflow-y: visible; overflow: visible;
} }
/* Make modal wider on wide screens */ /* Make modal wider on wide screens */
@ -145,3 +132,14 @@ th.long_text{
.dashboard{ .dashboard{
text-align: center; text-align: center;
} }
/* Detailed information on profile page */
dl.profile-info {
margin-top: -16px;
margin-bottom: 0;
}
dl.profile-info > div {
padding: 8px;
border-top: 1px solid #ddd;
}

View 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');
}
});

View file

@ -45,6 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_javascript %} {% bootstrap_javascript %}
<script src="/static/js/typeahead/typeahead.js"></script> <script src="/static/js/typeahead/typeahead.js"></script>
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script> <script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
<script src="{% static 'js/collapse-from-url.js' %}"></script>
{# Load CSS #} {# Load CSS #}
{% bootstrap_css %} {% bootstrap_css %}

View file

@ -23,23 +23,52 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load url_insert_param %} {% load url_insert_param %}
{% load i18n %}
{% if list.paginator.num_pages > 1 %} {% if list.paginator.num_pages > 1 %}
<ul class="pagination nav navbar-nav"> <ul class="pagination text-center">
{% if list.has_previous %} {% if list.has_previous %}
<li><a href="{% url_insert_param request.get_full_path page=1 %}"> << </a></li> <li>
<li><a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}"> < </a></li> <a href="{% url_insert_param request.get_full_path page=1 %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
<span aria-hidden="true">&laquo;</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">&lsaquo;</span>
<span class="sr-only">{% trans "Previous" %}</span>
</a>
</li>
{% else %}
<li class="disabled"><span aria-hidden="true">&laquo;</span></li>
<li class="disabled"><span aria-hidden="true">&lsaquo;</span></li>
{% endif %} {% endif %}
{% for page in list.paginator.page_range %} {% for page in list.paginator.page_range %}
{% if list.number <= page|add:"3" and list.number >= page|add:"-3" %} {% if list.number <= page|add:"3" and list.number >= page|add:"-3" %}
<li class="{% if list.number == page %}active{% endif %}"><a href="{% url_insert_param request.get_full_path page=page %}">{{ page }}</a></li> <li class="{% if list.number == page %}active{% endif %}">
<a href="{% url_insert_param request.get_full_path page=page %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">{{ page }}</a>
</li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if list.has_next %} {% if list.has_next %}
<li><a href="{% url_insert_param request.get_full_path page=list.next_page_number %}"> > </a></li> <li>
<li><a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}"> >> </a></li> <a href="{% url_insert_param request.get_full_path page=list.next_page_number %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
<span aria-hidden="true">&rsaquo;</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">&raquo;</span>
<span class="sr-only">{% trans "Last" %}</span>
</a>
</li>
{% else %}
<li class="disabled"><span aria-hidden="true">&rsaquo;</span></li>
<li class="disabled"><span aria-hidden="true">&raquo;</span></li>
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}

View file

@ -55,6 +55,8 @@ from .models import (
SwitchBay, SwitchBay,
Building, Building,
PortProfile, PortProfile,
ModuleSwitch,
ModuleOnSwitch,
) )
@ -269,3 +271,23 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
prefix=prefix, prefix=prefix,
**kwargs) **kwargs)
class EditModuleForm(FormRevMixin, ModelForm):
"""Add and edit module instance"""
class Meta:
model = ModuleSwitch
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditSwitchModuleForm(FormRevMixin, ModelForm):
"""Add/edit a switch to a module"""
class Meta:
model = ModuleOnSwitch
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditSwitchModuleForm, self).__init__(*args, prefix=prefix, **kwargs)

View 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')]),
),
]

View 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'),
),
]

View file

@ -252,6 +252,7 @@ class Switch(AclMixin, Machine):
help_text='Provision automatique de ce switch', help_text='Provision automatique de ce switch',
) )
class Meta: class Meta:
unique_together = ('stack', 'stack_member_id') unique_together = ('stack', 'stack_member_id')
permissions = ( permissions = (
@ -281,31 +282,18 @@ class Switch(AclMixin, Machine):
def create_ports(self, begin, end): def create_ports(self, begin, end):
""" Crée les ports de begin à end si les valeurs données """ Crée les ports de begin à end si les valeurs données
sont cohérentes. """ sont cohérentes. """
s_begin = s_end = 0
nb_ports = self.ports.count()
if nb_ports > 0:
ports = self.ports.order_by('port').values('port')
s_begin = ports.first().get('port')
s_end = ports.last().get('port')
if end < begin: if end < begin:
raise ValidationError(_("The end port is less than the start" raise ValidationError(_("The end port is less than the start"
" port.")) " port."))
if end - begin > self.number: ports_to_create = range(begin, end + 1)
existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True)
non_existing_ports = list(set(ports_to_create) - set(existing_ports))
if len(non_existing_ports) + existing_ports.count() > self.number:
raise ValidationError(_("This switch can't have that many ports.")) raise ValidationError(_("This switch can't have that many ports."))
begin_range = range(begin, s_begin)
end_range = range(s_end+1, end+1)
for i in itertools.chain(begin_range, end_range):
port = Port()
port.switch = self
port.port = i
try:
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
port.save()
reversion.set_comment(_("Creation")) reversion.set_comment(_("Creation"))
except IntegrityError: Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports])
ValidationError(_("Creation of an existing port."))
def main_interface(self): def main_interface(self):
""" Returns the 'main' interface of the switch """ Returns the 'main' interface of the switch
@ -317,7 +305,7 @@ class Switch(AclMixin, Machine):
@cached_property @cached_property
def get_name(self): def get_name(self):
return self.name or self.main_interface().domain.name return self.name or getattr(self.main_interface(), 'domain', 'Unknown')
@cached_property @cached_property
def get_radius_key(self): def get_radius_key(self):
@ -380,6 +368,17 @@ class Switch(AclMixin, Machine):
"""Return dict ip6:subnet for all ipv6 of the switch""" """Return dict ip6:subnet for all ipv6 of the switch"""
return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all()) return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
@cached_property
def list_modules(self):
"""Return modules of that switch, list of dict (rank, reference)"""
modules = []
if getattr(self.model, 'is_modular', None):
if self.model.is_itself_module:
modules.append((1, self.model.reference))
for module_of_self in self.moduleonswitch_set.all():
modules.append((module_of_self.slot, module_of_self.module.reference))
return modules
def __str__(self): def __str__(self):
return str(self.get_name) return str(self.get_name)
@ -402,6 +401,14 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
null=True, null=True,
blank=True blank=True
) )
is_modular = models.BooleanField(
default=False,
help_text=_("Is this switch model modular"),
)
is_itself_module = models.BooleanField(
default=False,
help_text=_("Is the switch, itself, considered as a module"),
)
class Meta: class Meta:
permissions = ( permissions = (
@ -417,6 +424,53 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
return str(self.constructor) + ' ' + self.reference return str(self.constructor) + ' ' + self.reference
class ModuleSwitch(AclMixin, RevMixin, models.Model):
"""A module of a switch"""
reference = models.CharField(
max_length=255,
help_text=_("Reference of a module"),
verbose_name=_("Module reference")
)
comment = models.CharField(
max_length=255,
null=True,
blank=True,
help_text=_("Comment"),
verbose_name=_("Comment")
)
class Meta:
permissions = (
("view_moduleswitch", _("Can view a module object")),
)
verbose_name = _("Module of a switch")
def __str__(self):
return str(self.reference)
class ModuleOnSwitch(AclMixin, RevMixin, models.Model):
"""Link beetween module and switch"""
module = models.ForeignKey('ModuleSwitch', on_delete=models.CASCADE)
switch = models.ForeignKey('Switch', on_delete=models.CASCADE)
slot = models.CharField(
max_length=15,
help_text=_("Slot on switch"),
verbose_name=_("Slot")
)
class Meta:
permissions = (
("view_moduleonswitch", _("Can view a moduleonswitch object")),
)
verbose_name = _("link between switchs and modules")
unique_together = ['slot', 'switch']
def __str__(self):
return 'On slot ' + str(self.slot) + ' of ' + str(self.switch)
class ConstructorSwitch(AclMixin, RevMixin, models.Model): class ConstructorSwitch(AclMixin, RevMixin, models.Model):
"""Un constructeur de switch""" """Un constructeur de switch"""

View 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>

View 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 %}

View file

@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="list-group-item list-group-item-info" href="{% url "topologie:index" %}"> <a class="list-group-item list-group-item-info" href="{% url "topologie:index" %}">
<i class="fa fa-microchip"></i> <i class="fa fa-microchip"></i>
{% trans "Switches" %} {% trans "Switches" %}
</a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-module" %}">
<i class="fa fa-microchip"></i>
{% trans "Switches modules" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}"> <a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
<i class="fa fa-cogs"></i> <i class="fa fa-cogs"></i>

View file

@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged' %} {% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %}
{% bootstrap_button action_name icon='ok' button_class='btn-success' %} {% bootstrap_button action_name icon='ok' button_class='btn-success' %}
</form> </form>
<br /> <br />

View file

@ -123,4 +123,15 @@ urlpatterns = [
url(r'^edit_vlanoptions/(?P<vlanid>[0-9]+)$', url(r'^edit_vlanoptions/(?P<vlanid>[0-9]+)$',
views.edit_vlanoptions, views.edit_vlanoptions,
name='edit-vlanoptions'), name='edit-vlanoptions'),
] url(r'^add_module/$', views.add_module, name='add-module'),
url(r'^edit_module/(?P<moduleswitchid>[0-9]+)$',
views.edit_module,
name='edit-module'),
url(r'^del_module/(?P<moduleswitchid>[0-9]+)$', views.del_module, name='del-module'),
url(r'^index_module/$', views.index_module, name='index-module'),
url(r'^add_module_on/$', views.add_module_on, name='add-module-on'),
url(r'^edit_module_on/(?P<moduleonswitchid>[0-9]+)$',
views.edit_module_on,
name='edit-module-on'),
url(r'^del_module_on/(?P<moduleonswitchid>[0-9]+)$', views.del_module_on, name='del-module-on'),
]

View file

@ -48,7 +48,10 @@ from django.utils.translation import ugettext as _
import tempfile import tempfile
from users.views import form from users.views import form
from re2o.utils import re2o_paginator, SortTable from re2o.base import (
re2o_paginator,
SortTable,
)
from re2o.acl import ( from re2o.acl import (
can_create, can_create,
can_edit, can_edit,
@ -83,6 +86,8 @@ from .models import (
Building, Building,
Server, Server,
PortProfile, PortProfile,
ModuleSwitch,
ModuleOnSwitch,
) )
from .forms import ( from .forms import (
EditPortForm, EditPortForm,
@ -99,6 +104,8 @@ from .forms import (
EditSwitchBayForm, EditSwitchBayForm,
EditBuildingForm, EditBuildingForm,
EditPortProfileForm, EditPortProfileForm,
EditModuleForm,
EditSwitchModuleForm,
) )
from subprocess import ( from subprocess import (
@ -313,6 +320,22 @@ def index_model_switch(request):
) )
@login_required
@can_view_all(ModuleSwitch)
def index_module(request):
"""Display all modules of switchs"""
module_list = ModuleSwitch.objects.all()
modular_switchs = Switch.objects.filter(model__is_modular=True)
pagination_number = GeneralOption.get_cached_value('pagination_number')
module_list = re2o_paginator(request, module_list, pagination_number)
return render(
request,
'topologie/index_module.html',
{'module_list': module_list,
'modular_switchs': modular_switchs}
)
@login_required @login_required
@can_edit(Vlan) @can_edit(Vlan)
def edit_vlanoptions(request, vlan_instance, **_kwargs): def edit_vlanoptions(request, vlan_instance, **_kwargs):
@ -531,18 +554,12 @@ def create_ports(request, switchid):
messages.error(request, _("Nonexistent switch")) messages.error(request, _("Nonexistent switch"))
return redirect(reverse('topologie:index')) return redirect(reverse('topologie:index'))
s_begin = s_end = 0 first_port = getattr(switch.ports.order_by('port').first(), 'port', 1)
nb_ports = switch.ports.count() last_port = switch.number + first_port - 1
if nb_ports > 0:
ports = switch.ports.order_by('port').values('port')
s_begin = ports.first().get('port')
s_end = ports.last().get('port')
port_form = CreatePortsForm( port_form = CreatePortsForm(
request.POST or None, request.POST or None,
initial={'begin': s_begin, 'end': s_end} initial={'begin': first_port, 'end': last_port}
) )
if port_form.is_valid(): if port_form.is_valid():
begin = port_form.cleaned_data['begin'] begin = port_form.cleaned_data['begin']
end = port_form.cleaned_data['end'] end = port_form.cleaned_data['end']
@ -1051,6 +1068,115 @@ def del_port_profile(request, port_profile, **_kwargs):
) )
@login_required
@can_create(ModuleSwitch)
def add_module(request):
""" View used to add a Module object """
module = EditModuleForm(request.POST or None)
if module.is_valid():
module.save()
messages.success(request, _("The module was created."))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module, 'action_name': _("Create a module")},
'topologie/topo.html',
request
)
@login_required
@can_edit(ModuleSwitch)
def edit_module(request, module_instance, **_kwargs):
""" View used to edit a Module object """
module = EditModuleForm(request.POST or None, instance=module_instance)
if module.is_valid():
if module.changed_data:
module.save()
messages.success(request, _("The module was edited."))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module, 'action_name': _("Edit")},
'topologie/topo.html',
request
)
@login_required
@can_delete(ModuleSwitch)
def del_module(request, module, **_kwargs):
"""Compleete delete a module"""
if request.method == "POST":
try:
module.delete()
messages.success(request, _("The module was deleted."))
except ProtectedError:
messages.error(
request,
(_("The module %s is used by another object, impossible to"
" deleted it.") % module)
)
return redirect(reverse('topologie:index-module'))
return form(
{'objet': module, 'objet_name': _("Module")},
'topologie/delete.html',
request
)
@login_required
@can_create(ModuleOnSwitch)
def add_module_on(request):
"""Add a module to a switch"""
module_switch = EditSwitchModuleForm(request.POST or None)
if module_switch.is_valid():
module_switch.save()
messages.success(request, _("The module added to that switch"))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module_switch, 'action_name': _("Create")},
'topologie/topo.html',
request
)
@login_required
@can_edit(ModuleOnSwitch)
def edit_module_on(request, module_instance, **_kwargs):
""" View used to edit a Module object """
module = EditSwitchModuleForm(request.POST or None, instance=module_instance)
if module.is_valid():
if module.changed_data:
module.save()
messages.success(request, _("The module was edited."))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module, 'action_name': _("Edit")},
'topologie/topo.html',
request
)
@login_required
@can_delete(ModuleOnSwitch)
def del_module_on(request, module, **_kwargs):
"""Compleete delete a module"""
if request.method == "POST":
try:
module.delete()
messages.success(request, _("The module was deleted."))
except ProtectedError:
messages.error(
request,
(_("The module %s is used by another object, impossible to"
" deleted it.") % module)
)
return redirect(reverse('topologie:index-module'))
return form(
{'objet': module, 'objet_name': _("Module")},
'topologie/delete.html',
request
)
def make_machine_graph(): def make_machine_graph():
""" """
Create the graph of switchs, machines and access points. Create the graph of switchs, machines and access points.

View file

@ -45,7 +45,8 @@ from django.utils.safestring import mark_safe
from machines.models import Interface, Machine, Nas from machines.models import Interface, Machine, Nas
from topologie.models import Port from topologie.models import Port
from preferences.models import OptionalUser from preferences.models import OptionalUser
from re2o.utils import remove_user_room, get_input_formats_help_text from re2o.utils import remove_user_room
from re2o.base import get_input_formats_help_text
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
from re2o.field_permissions import FieldPermissionFormMixin from re2o.field_permissions import FieldPermissionFormMixin
@ -116,6 +117,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
"""Changement du mot de passe""" """Changement du mot de passe"""
user = super(PassForm, self).save(commit=False) user = super(PassForm, self).save(commit=False)
user.set_password(self.cleaned_data.get("passwd1")) user.set_password(self.cleaned_data.get("passwd1"))
user.set_active()
user.save() user.save()
@ -323,14 +325,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['room'].empty_label = _("No room") self.fields['room'].empty_label = _("No room")
self.fields['school'].empty_label = _("Select a school") self.fields['school'].empty_label = _("Select a school")
def clean_email(self):
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
return self.cleaned_data.get('email').lower()
else:
raise forms.ValidationError(
_("You can't use a {} address.").format(
OptionalUser.objects.first().local_email_domain))
class Meta: class Meta:
model = Adherent model = Adherent
fields = [ fields = [
@ -344,6 +338,19 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'room', 'room',
] ]
force = forms.BooleanField(
label=_("Force the move?"),
initial=False,
required=False
)
def clean_email(self):
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
return self.cleaned_data.get('email').lower()
else:
raise forms.ValidationError(
_("You can't use a {} address.").format(
OptionalUser.objects.first().local_email_domain))
def clean_telephone(self): def clean_telephone(self):
"""Verifie que le tel est présent si 'option est validée """Verifie que le tel est présent si 'option est validée
@ -355,12 +362,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
) )
return telephone return telephone
force = forms.BooleanField(
label=_("Force the move?"),
initial=False,
required=False
)
def clean_force(self): def clean_force(self):
"""On supprime l'ancien user de la chambre si et seulement si la """On supprime l'ancien user de la chambre si et seulement si la
case est cochée""" case est cochée"""
@ -368,6 +369,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
remove_user_room(self.cleaned_data.get('room')) remove_user_room(self.cleaned_data.get('room'))
return return
class AdherentCreationForm(AdherentForm): class AdherentCreationForm(AdherentForm):
"""Formulaire de création d'un user. """Formulaire de création d'un user.
AdherentForm auquel on ajoute une checkbox afin d'éviter les AdherentForm auquel on ajoute une checkbox afin d'éviter les
@ -383,8 +385,22 @@ class AdherentCreationForm(AdherentForm):
# Checkbox for GTU # Checkbox for GTU
gtu_check = forms.BooleanField(required=True) gtu_check = forms.BooleanField(required=True)
gtu_check.label = mark_safe("{} <a href='/media/{}' download='CGU'>{}</a>{}".format( #gtu_check.label = mark_safe("{} <a href='/media/{}' download='CGU'>{}</a>{}".format(
_("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _("."))) # _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
class Meta:
model = Adherent
fields = [
'name',
'surname',
'pseudo',
'email',
'school',
'comment',
'telephone',
'room',
'state',
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AdherentCreationForm, self).__init__(*args, **kwargs) super(AdherentCreationForm, self).__init__(*args, **kwargs)
@ -398,12 +414,6 @@ class AdherentEditForm(AdherentForm):
if 'shell' in self.fields: if 'shell' in self.fields:
self.fields['shell'].empty_label = _("Default shell") self.fields['shell'].empty_label = _("Default shell")
def clean_gpg_fingerprint(self):
"""Format the GPG fingerprint"""
gpg_fingerprint = self.cleaned_data.get('gpg_fingerprint', None)
if gpg_fingerprint:
return gpg_fingerprint.replace(' ', '').upper()
class Meta: class Meta:
model = Adherent model = Adherent
fields = [ fields = [
@ -429,6 +439,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['surname'].label = _("Name") self.fields['surname'].label = _("Name")
self.fields['school'].label = _("School") self.fields['school'].label = _("School")
self.fields['comment'].label = _("Comment") self.fields['comment'].label = _("Comment")
self.fields['email'].label = _("Email Address")
if 'room' in self.fields: if 'room' in self.fields:
self.fields['room'].label = _("Room") self.fields['room'].label = _("Room")
self.fields['room'].empty_label = _("No room") self.fields['room'].empty_label = _("No room")
@ -443,7 +454,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'school', 'school',
'comment', 'comment',
'room', 'room',
'email',
'telephone', 'telephone',
'email',
'shell', 'shell',
'mailing' 'mailing'
] ]
@ -488,13 +501,14 @@ class PasswordForm(FormRevMixin, ModelForm):
class ServiceUserForm(FormRevMixin, ModelForm): class ServiceUserForm(FormRevMixin, ModelForm):
""" Modification d'un service user""" """Service user creation
force initial password set"""
password = forms.CharField( password = forms.CharField(
label=_("New password"), label=_("New password"),
max_length=255, max_length=255,
validators=[MinLengthValidator(8)], validators=[MinLengthValidator(8)],
widget=forms.PasswordInput, widget=forms.PasswordInput,
required=False required=True
) )
class Meta: class Meta:
@ -506,7 +520,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
def save(self, commit=True): def save(self, commit=True):
"""Changement du mot de passe""" """Password change"""
user = super(ServiceUserForm, self).save(commit=False) user = super(ServiceUserForm, self).save(commit=False)
if self.cleaned_data['password']: if self.cleaned_data['password']:
user.set_password(self.cleaned_data.get("password")) user.set_password(self.cleaned_data.get("password"))
@ -516,6 +530,14 @@ class ServiceUserForm(FormRevMixin, ModelForm):
class EditServiceUserForm(ServiceUserForm): class EditServiceUserForm(ServiceUserForm):
"""Formulaire d'edition de base d'un service user. Ne permet """Formulaire d'edition de base d'un service user. Ne permet
d'editer que son group d'acl et son commentaire""" d'editer que son group d'acl et son commentaire"""
password = forms.CharField(
label=_("New password"),
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
required=False
)
class Meta(ServiceUserForm.Meta): class Meta(ServiceUserForm.Meta):
fields = ['access_group', 'comment'] fields = ['access_group', 'comment']

View 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),
),
]

View file

@ -81,6 +81,7 @@ from re2o.settings import LDAP, GID_RANGES, UID_RANGES
from re2o.login import hashNT from re2o.login import hashNT
from re2o.field_permissions import FieldPermissionModelMixin from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin from re2o.mixins import AclMixin, RevMixin
from re2o.base import smtp_check
from cotisations.models import Cotisation, Facture, Paiement, Vente from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine, regen from machines.models import Domain, Interface, Machine, regen
@ -93,7 +94,7 @@ from preferences.models import OptionalMachine, MailMessageOption
def linux_user_check(login): def linux_user_check(login):
""" Validation du pseudo pour respecter les contraintes unix""" """ Validation du pseudo pour respecter les contraintes unix"""
UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9-]*[$]?$") UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$")
return UNIX_LOGIN_PATTERN.match(login) return UNIX_LOGIN_PATTERN.match(login)
@ -336,7 +337,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
def set_active(self): def set_active(self):
"""Enable this user if he subscribed successfully one time before""" """Enable this user if he subscribed successfully one time before"""
if self.state == self.STATE_NOT_YET_ACTIVE: if self.state == self.STATE_NOT_YET_ACTIVE:
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists(): if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists() or OptionalUser.get_cached_value('all_users_active'):
self.state = self.STATE_ACTIVE self.state = self.STATE_ACTIVE
self.save() self.save()
@ -474,7 +475,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
""" Renvoie si un utilisateur a accès à internet """ """ Renvoie si un utilisateur a accès à internet """
return (self.state == User.STATE_ACTIVE and return (self.state == User.STATE_ACTIVE and
not self.is_ban() and not self.is_ban() and
(self.is_connected() or self.is_whitelisted())) (self.is_connected() or self.is_whitelisted())) \
or self == AssoOption.get_cached_value('utilisateur_asso')
def end_access(self): def end_access(self):
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)""" """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
@ -576,7 +578,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
mac_refresh : synchronise les machines de l'user mac_refresh : synchronise les machines de l'user
group_refresh : synchronise les group de l'user group_refresh : synchronise les group de l'user
Si l'instance n'existe pas, on crée le ldapuser correspondant""" Si l'instance n'existe pas, on crée le ldapuser correspondant"""
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\
self.state != self.STATE_DISABLED:
self.refresh_from_db() self.refresh_from_db()
try: try:
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
@ -693,10 +696,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
def autoregister_machine(self, mac_address, nas_type): def autoregister_machine(self, mac_address, nas_type):
""" Fonction appellée par freeradius. Enregistre la mac pour """ Fonction appellée par freeradius. Enregistre la mac pour
une machine inconnue sur le compte de l'user""" une machine inconnue sur le compte de l'user"""
all_interfaces = self.user_interfaces(active=False) allowed, _message = Machine.can_create(self, self.id)
if all_interfaces.count() > OptionalMachine.get_cached_value( if not allowed:
'max_lambdauser_interfaces'
):
return False, _("Maximum number of registered machines reached.") return False, _("Maximum number of registered machines reached.")
if not nas_type: if not nas_type:
return False, _("Re2o doesn't know wich machine type to assign.") return False, _("Re2o doesn't know wich machine type to assign.")
@ -1025,17 +1026,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
): ):
raise ValidationError("This pseudo is already in use.") raise ValidationError("This pseudo is already in use.")
if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE): if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE):
raise ValidationError( raise ValidationError(_("There is neither a local email address nor an external"
{'email': (
_("There is neither a local email address nor an external"
" email address for this user.") " email address for this user.")
), }
) )
if self.local_email_redirect and not self.email: if self.local_email_redirect and not self.email:
raise ValidationError( raise ValidationError(_("You can't redirect your local emails if no external email"
{'local_email_redirect': ( " address has been set.")
_("You can't redirect your local emails if no external email"
" address has been set.")), }
) )
def __str__(self): def __str__(self):
@ -1054,20 +1050,27 @@ class Adherent(User):
null=True null=True
) )
gpg_fingerprint = models.CharField( gpg_fingerprint = models.CharField(
max_length=40, max_length=49,
blank=True, blank=True,
null=True, null=True,
validators=[RegexValidator(
'^[0-9A-F]{40}$',
message=_("A GPG fingerprint must contain 40 hexadecimal"
" characters.")
)]
) )
class Meta(User.Meta): class Meta(User.Meta):
verbose_name = _("member") verbose_name = _("member")
verbose_name_plural = _("members") verbose_name_plural = _("members")
def format_gpgfp(self):
"""Format gpg finger print as AAAA BBBB... from a string AAAABBBB...."""
self.gpg_fingerprint = ' '.join([self.gpg_fingerprint[i:i + 4] for i in range(0, len(self.gpg_fingerprint), 4)])
def validate_gpgfp(self):
"""Validate from raw entry if is it a valid gpg fp"""
if self.gpg_fingerprint:
gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper()
if not re.match("^[0-9A-F]{40}$", gpg_fingerprint):
raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters"))
self.gpg_fingerprint = gpg_fingerprint
@classmethod @classmethod
def get_instance(cls, adherentid, *_args, **_kwargs): def get_instance(cls, adherentid, *_args, **_kwargs):
"""Try to find an instance of `Adherent` with the given id. """Try to find an instance of `Adherent` with the given id.
@ -1098,6 +1101,13 @@ class Adherent(User):
_("You don't have the right to create a user.") _("You don't have the right to create a user.")
) )
def clean(self, *args, **kwargs):
"""Format the GPG fingerprint"""
super(Adherent, self).clean(*args, **kwargs)
if self.gpg_fingerprint:
self.validate_gpgfp()
self.format_gpgfp()
class Club(User): class Club(User):
""" A class representing a club (it is considered as a user """ A class representing a club (it is considered as a user
@ -1889,6 +1899,9 @@ class EMailAddress(RevMixin, AclMixin, models.Model):
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
self.local_part = self.local_part.lower() self.local_part = self.local_part.lower()
if "@" in self.local_part: if "@" in self.local_part or "+" in self.local_part:
raise ValidationError(_("The local part must not contain @.")) raise ValidationError(_("The local part must not contain @ or +."))
result, reason = smtp_check(self.local_part)
if result:
raise ValidationError(reason)
super(EMailAddress, self).clean(*args, **kwargs) super(EMailAddress, self).clean(*args, **kwargs)

View file

@ -23,7 +23,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load design %} {% load design %}
@ -78,7 +77,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if solde_activated %} {% if solde_activated %}
<div class="col-sm-6 col-md-4"> <div class="col-sm-6 col-md-4">
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4"> <div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
data-target="#subscriptions">
{{ users.solde }} <i class="fa fa-eur"></i> {{ users.solde }} <i class="fa fa-eur"></i>
</div> </div>
<div class="panel-body dashboard"> <div class="panel-body dashboard">
@ -92,7 +92,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="col-sm-6 {% if solde_activated %}col-md-4{% else %}col-md-6{% endif %}"> <div class="col-sm-6 {% if solde_activated %}col-md-4{% else %}col-md-6{% endif %}">
{% if nb_machines %} {% if nb_machines %}
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3"> <div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
data-target="#machines">
<i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span> <i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span>
</div> </div>
<div class="panel-body dashboard"> <div class="panel-body dashboard">
@ -103,7 +104,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
{% else %} {% else %}
<div class="panel panel-warning"> <div class="panel panel-warning">
<div class="panel-heading dashboard">{% trans "No machine" %}</div> <div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
data-target="#machines">
{% trans "No machine" %}
</div>
<div class="panel-body dashboard"> <div class="panel-body dashboard">
<a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}"> <a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>{% trans " Add a machine" %} <i class="fa fa-desktop"></i>{% trans " Add a machine" %}
@ -117,12 +121,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="panel-group" id="accordion"> <div class="panel-group" id="accordion">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse1"> <div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
<h3 class="panel-title pull-left" > data-target="#information">
<h3 class="panel-title pull-left">
<i class="fa fa-user"></i>{% trans " Detailed information" %} <i class="fa fa-user"></i>{% trans " Detailed information" %}
</h3> </h3>
</div> </div>
<div class="panel-collapse collapse in" id="collapse1"> <div class="panel-collapse collapse collapse-default" id="information">
<div class="panel-body"> <div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
@ -148,127 +153,168 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</ul> </ul>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="table-responsive"> <dl class="dl-horizontal row profile-info">
<table class="table table-striped"> <div class="col-md-6">
<tr>
{% if users.is_class_club %} {% if users.is_class_club %}
<th>{% trans "Mailing" %}</th> <dt>{% trans "Mailing" %}</dt>
{% if users.club.mailing %} {% if users.club.mailing %}
<td>{{ users.pseudo }}(-admin)</td> <dd>{{ users.pseudo }}(-admin)</dd>
{% else %} {% else %}
<td>{% trans "Mailing disabled" %}</td> <dd>{% trans "Mailing disabled" %}</dd>
{% endif %} {% endif %}
{% else %} {% else %}
<th>{% trans "Firt name" %}</th> <dt>{% trans "Firt name" %}</dt>
<td>{{ users.name }}</td> <dd>{{ users.name }}</dd>
{% endif %} {% endif %}
<th>{% trans "Surname" %}</th> </div>
<td>{{ users.surname }}</td>
</tr>
<tr>
<th>{% trans "Username" %}</th>
<td>{{ users.pseudo }}</td>
<th>{% trans "Email address" %}</th>
<td><a href="mailto:{{ users.email }}">{{users.email}}</a></td>
</tr>
<tr>
<th>{% trans "Room" %}</th>
<td>{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} / {{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}</td>
<th>{% trans "Telephone number" %}</th>
<td>{{ users.telephone }}</td>
</tr>
<tr>
<th>{% trans "School" %}</th>
<td>{{ users.school }}</td>
<th>{% trans "Comment" %}</th>
<td>{{ users.comment }}</td>
</tr>
<tr>
<th>{% trans "Registration date" %}</th>
<td>{{ users.registered }}</td>
<th>{% trans "Last login" %}</th>
<td>{{ users.last_login }}</td>
</tr>
<tr>
<th>{% trans "End of membership" %}</th>
{% if users.end_adhesion != None %}
<td><i class="text-success">{{ users.end_adhesion }}</i></td>
{% else %}
<td><i class="text-danger">{% trans "Not a member" %}</i></td>
{% endif %}
<th>{% trans "Whitelist" %}</th>
{% if users.end_whitelist != None %}
<td><i class="text-success">{{ users.end_whitelist }}</i></td>
{% else %}
<td><i class="text-warning">{% trans "None" %}</i></td>
{% endif %}
<tr>
<th>{% trans "Ban" %}</th>
{% if users.end_ban != None %}
<td><i class="text-danger">{{ users.end_ban }}</i></td>
{% else %}
<td><i class="text-success">{% trans "Not banned" %}</i></td>
{% endif %}
<th>{% trans "State" %}</th>
{% if users.state == 0 %}
<td><i class="text-success">{% trans "Active" %}</i></td>
{% elif users.state == 1 %}
<td><i class="text-warning">{% trans "Disabled" %}</i></td>
{% elif users.state == 2 %}
<td><i class="text-danger">{% trans "Archived" %}</i></td>
{% elif users.state == 3 %}
<td><i class="text-danger">{% trans "Not yet Member" %}</i></td>
<div class="col-md-6">
<dt>{% trans "Surname" %}</dt>
<dd>{{ users.surname }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Username" %}</dt>
<dd>{{ users.pseudo }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Email address" %}</dt>
<dd><a href="mailto:{{ users.email }}">{{ users.email }}</a></dd>
</div>
<div class="col-md-6">
<dt>{% trans "Room" %}</dt>
<dd>
{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} /
{{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}
</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Telephone number" %}</dt>
<dd>{{ users.telephone }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "School" %}</dt>
<dd>{{ users.school }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Comment" %}</dt>
<dd>{{ users.comment }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Registration date" %}</dt>
<dd>{{ users.registered }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Last login" %}</dt>
<dd>{{ users.last_login }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "End of membership" %}</dt>
{% if users.end_adhesion != None %}
<dd><i class="text-success">{{ users.end_adhesion }}</i></dd>
{% else %}
<dd><i class="text-danger">{% trans "Not a member" %}</i></dd>
{% endif %} {% endif %}
</tr> </div>
<tr>
<th>{% trans "Internet access" %}</th> <div class="col-md-6">
<dt>{% trans "Whitelist" %}</dt>
{% if users.end_whitelist != None %}
<dd><i class="text-success">{{ users.end_whitelist }}</i></dd>
{% else %}
<dd><i class="text-warning">{% trans "None" %}</i></dd>
{% endif %}
</div>
<div class="col-md-6">
<dt>{% trans "Ban" %}</dt>
{% if users.end_ban != None %}
<dd><i class="text-danger">{{ users.end_ban }}</i></dd>
{% else %}
<dd><i class="text-success">{% trans "Not banned" %}</i></dd>
{% endif %}
</div>
<div class="col-md-6">
<dt>{% trans "State" %}</dt>
{% if users.state == 0 %}
<dd><i class="text-success">{% trans "Active" %}</i></dd>
{% elif users.state == 1 %}
<dd><i class="text-warning">{% trans "Disabled" %}</i></dd>
{% elif users.state == 2 %}
<dd><i class="text-danger">{% trans "Archived" %}</i></dd>
{% elif users.state == 3 %}
<dd><i class="text-danger">{% trans "Not yet Member" %}</i></dd>
{% endif %}
</div>
<div class="col-md-6">
<dt>{% trans "Internet access" %}</dt>
{% if users.has_access == True %} {% if users.has_access == True %}
<td><i class="text-success">{% blocktrans with end_access=users.end_access %}Active (until {{ end_access }}){% endblocktrans %}</i></td> <dd><i class="text-success">
{% blocktrans with end_access=users.end_access %}Active
(until {{ end_access }}){% endblocktrans %}</i></dd>
{% else %} {% else %}
<td><i class="text-danger">{% trans "Disabled" %}</i></td> <dd><i class="text-danger">{% trans "Disabled" %}</i></dd>
{% endif %} {% endif %}
<th>{% trans "Groups of rights" %}</th> </div>
<div class="col-md-6">
<dt>{% trans "Groups of rights" %}</dt>
{% if users.groups.all %} {% if users.groups.all %}
<td>{{ users.groups.all|join:", "}}</td> <dd>{{ users.groups.all|join:", " }}</dd>
{% else %} {% else %}
<td>{% trans "None" %}</td> <dd>{% trans "None" %}</dd>
{% endif %} {% endif %}
</tr> </div>
<tr>
<th>{% trans "Balance" %}</th> <div class="col-md-6">
<td>{{ users.solde }} € <dt>{% trans "Balance" %}</dt>
<dd>
{{ users.solde }} €
{% if user_solde %} {% if user_solde %}
<a class="btn btn-primary btn-sm" style='float:right' role="button" href="{% url 'cotisations:credit-solde' users.pk%}"> <a class="btn btn-primary btn-sm" style='float:right' role="button"
href="{% url 'cotisations:credit-solde' users.pk %}">
<i class="fa fa-eur"></i> <i class="fa fa-eur"></i>
{% trans "Refill" %} {% trans "Refill" %}
</a> </a>
{% endif %} {% endif %}
</td> </dd>
{% if users.adherent.gpg_fingerprint %}
<th>{% trans "GPG fingerprint" %}</th>
<td>{{ users.adherent.gpg_fingerprint }}</td>
{% endif %}
</tr>
<tr>
{% if users.shell %}
<th>{% trans "Shell" %}</th>
<td>{{ users.shell }}</td>
{% endif %}
</tr>
</table>
</div> </div>
{% if users.adherent.gpg_fingerprint %}
<div class="col-md-6 col-xs-12">
<dt>{% trans "GPG fingerprint" %}</dt>
<dd>{{ users.adherent.gpg_fingerprint }}</dd>
</div>
{% endif %}
{% if users.shell %}
<div class="col-md-6 col-xs-12">
<dt>{% trans "Shell" %}</dt>
<dd>{{ users.shell }}</dd>
</div>
{% endif %}
</dl>
</div> </div>
</div> </div>
</div> </div>
{% if users.is_class_club %} {% if users.is_class_club %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse2"> <div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#club">
<h3 class="panel-title pull-left"> <h3 class="panel-title pull-left">
<i class="fa fa-users"></i>{% trans " Manage the club" %} <i class="fa fa-users"></i>{% trans " Manage the club" %}
</h3> </h3>
</div> </div>
<div class="panel-collapse collapse" id="collapse2"> <div class="panel-collapse collapse" id="club">
<div class="panel-body"> <div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}">
<i class="fa fa-lock"></i> <i class="fa fa-lock"></i>
@ -319,14 +365,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3"> <div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
data-target="#machines">
<h3 class="panel-title pull-left"> <h3 class="panel-title pull-left">
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Machines" %} {% trans "Machines" %}
<span class="badge">{{nb_machines}}</span> <span class="badge">{{nb_machines}}</span>
</h3> </h3>
</div> </div>
<div id="collapse3" class="panel-collapse collapse"> <div id="machines" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
@ -343,13 +390,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4"> <div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
data-target="#subscriptions">
<h3 class="panel-title pull-left"> <h3 class="panel-title pull-left">
<i class="fa fa-eur"></i> <i class="fa fa-eur"></i>
{% trans "Subscriptions" %} {% trans "Subscriptions" %}
</h3> </h3>
</div> </div>
<div id="collapse4" class="panel-collapse collapse"> <div id="subscriptions" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
{% can_create Facture %} {% can_create Facture %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
@ -374,13 +422,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse5"> <div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#bans">
<h3 class="panel-title pull-left"> <h3 class="panel-title pull-left">
<i class="fa fa-ban"></i> <i class="fa fa-ban"></i>
{% trans "Bans" %} {% trans "Bans" %}
</h3> </h3>
</div> </div>
<div id="collapse5" class="panel-collapse collapse"> <div id="bans" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
{% can_create Ban %} {% can_create Ban %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-ban' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-ban' users.id %}">
@ -399,13 +447,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse6"> <div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#whitelists">
<h3 class="panel-title pull-left"> <h3 class="panel-title pull-left">
<i class="fa fa-check-circle"></i> <i class="fa fa-check-circle"></i>
{% trans "Whitelists" %} {% trans "Whitelists" %}
</h3> </h3>
</div> </div>
<div id="collapse6" class="panel-collapse collapse"> <div id="whitelists" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
{% can_create Whitelist %} {% can_create Whitelist %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-whitelist' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-whitelist' users.id %}">
@ -424,12 +472,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse7"> <div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#email">
<h3 class="panel-title pull-left"> <h3 class="panel-title pull-left">
<i class="fa fa-envelope"></i>{% trans " Email settings" %} <i class="fa fa-envelope"></i>{% trans " Email settings" %}
</h3> </h3>
</div> </div>
<div id="collapse7" class="panel-collapse collapse"> <div id="email" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
{% can_edit users %} {% can_edit users %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-email-settings' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-email-settings' users.id %}">

View file

@ -57,8 +57,10 @@ from preferences.models import OptionalUser, GeneralOption, AssoOption
from re2o.views import form from re2o.views import form
from re2o.utils import ( from re2o.utils import (
all_has_access, all_has_access,
SortTable, )
re2o_paginator from re2o.base import (
re2o_paginator,
SortTable
) )
from re2o.acl import ( from re2o.acl import (
can_create, can_create,