[0-9]+)$',
+ views.del_cost_estimate,
+ name='del-cost-estimate'
+ ),
url(
r'^new_custom_invoice/$',
views.new_custom_invoice,
diff --git a/cotisations/views.py b/cotisations/views.py
index 4cd76f93..d4805dc2 100644
--- a/cotisations/views.py
+++ b/cotisations/views.py
@@ -47,7 +47,10 @@ from users.models import User
from re2o.settings import LOGO_PATH
from re2o import settings
from re2o.views import form
-from re2o.utils import SortTable, re2o_paginator
+from re2o.base import (
+ SortTable,
+ re2o_paginator,
+)
from re2o.acl import (
can_create,
can_edit,
@@ -65,7 +68,8 @@ from .models import (
Paiement,
Banque,
CustomInvoice,
- BaseInvoice
+ BaseInvoice,
+ CostEstimate
)
from .forms import (
FactureForm,
@@ -77,9 +81,11 @@ from .forms import (
DelBanqueForm,
SelectArticleForm,
RechargeForm,
- CustomInvoiceForm
+ CustomInvoiceForm,
+ DiscountForm,
+ CostEstimateForm,
)
-from .tex import render_invoice
+from .tex import render_invoice, escape_chars
from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method
@@ -175,7 +181,58 @@ def new_facture(request, user, userid):
)
-# TODO : change facture to invoice
+@login_required
+@can_create(CostEstimate)
+def new_cost_estimate(request):
+ """
+ View used to generate a custom invoice. It's mainly used to
+ get invoices that are not taken into account, for the administrative
+ point of view.
+ """
+ # The template needs the list of articles (for the JS part)
+ articles = Article.objects.filter(
+ Q(type_user='All') | Q(type_user=request.user.class_name)
+ )
+ # Building the invocie form and the article formset
+ cost_estimate_form = CostEstimateForm(request.POST or None)
+
+ articles_formset = formset_factory(SelectArticleForm)(
+ request.POST or None,
+ form_kwargs={'user': request.user}
+ )
+ discount_form = DiscountForm(request.POST or None)
+
+ if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
+ cost_estimate_instance = cost_estimate_form.save()
+ for art_item in articles_formset:
+ if art_item.cleaned_data:
+ article = art_item.cleaned_data['article']
+ quantity = art_item.cleaned_data['quantity']
+ Vente.objects.create(
+ facture=cost_estimate_instance,
+ name=article.name,
+ prix=article.prix,
+ type_cotisation=article.type_cotisation,
+ duration=article.duration,
+ number=quantity
+ )
+ discount_form.apply_to_invoice(cost_estimate_instance)
+ messages.success(
+ request,
+ _("The cost estimate was created.")
+ )
+ return redirect(reverse('cotisations:index-cost-estimate'))
+
+ return form({
+ 'factureform': cost_estimate_form,
+ 'action_name': _("Confirm"),
+ 'articlesformset': articles_formset,
+ 'articlelist': articles,
+ 'discount_form': discount_form,
+ 'title': _("Cost estimate"),
+ }, 'cotisations/facture.html', request)
+
+
@login_required
@can_create(CustomInvoice)
def new_custom_invoice(request):
@@ -195,8 +252,9 @@ def new_custom_invoice(request):
request.POST or None,
form_kwargs={'user': request.user}
)
+ discount_form = DiscountForm(request.POST or None)
- if invoice_form.is_valid() and articles_formset.is_valid():
+ if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
new_invoice_instance = invoice_form.save()
for art_item in articles_formset:
if art_item.cleaned_data:
@@ -210,6 +268,7 @@ def new_custom_invoice(request):
duration=article.duration,
number=quantity
)
+ discount_form.apply_to_invoice(new_invoice_instance)
messages.success(
request,
_("The custom invoice was created.")
@@ -220,7 +279,8 @@ def new_custom_invoice(request):
'factureform': invoice_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
- 'articlelist': articles
+ 'articlelist': articles,
+ 'discount_form': discount_form
}, 'cotisations/facture.html', request)
@@ -263,7 +323,8 @@ def facture_pdf(request, facture, **_kwargs):
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
- 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
+ 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
+ 'payment_method': facture.paiement.moyen,
})
@@ -328,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request)
+@login_required
+@can_edit(CostEstimate)
+def edit_cost_estimate(request, invoice, **kwargs):
+ # Building the invocie form and the article formset
+ invoice_form = CostEstimateForm(
+ request.POST or None,
+ instance=invoice
+ )
+ purchases_objects = Vente.objects.filter(facture=invoice)
+ purchase_form_set = modelformset_factory(
+ Vente,
+ fields=('name', 'number'),
+ extra=0,
+ max_num=len(purchases_objects)
+ )
+ purchase_form = purchase_form_set(
+ request.POST or None,
+ queryset=purchases_objects
+ )
+ if invoice_form.is_valid() and purchase_form.is_valid():
+ if invoice_form.changed_data:
+ invoice_form.save()
+ purchase_form.save()
+ messages.success(
+ request,
+ _("The cost estimate was edited.")
+ )
+ return redirect(reverse('cotisations:index-cost-estimate'))
+
+ return form({
+ 'factureform': invoice_form,
+ 'venteform': purchase_form,
+ 'title': "Edit the cost estimate"
+ }, 'cotisations/edit_facture.html', request)
+
+
+@login_required
+@can_edit(CostEstimate)
+@can_create(CustomInvoice)
+def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
+ """Create a custom invoice from a cos estimate"""
+ cost_estimate.create_invoice()
+ messages.success(
+ request,
+ _("An invoice was successfully created from your cost estimate.")
+ )
+ return redirect(reverse('cotisations:index-custom-invoice'))
+
+
@login_required
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
@@ -364,10 +474,10 @@ def edit_custom_invoice(request, invoice, **kwargs):
@login_required
-@can_view(CustomInvoice)
-def custom_invoice_pdf(request, invoice, **_kwargs):
+@can_view(CostEstimate)
+def cost_estimate_pdf(request, invoice, **_kwargs):
"""
- View used to generate a PDF file from an existing invoice in database
+ View used to generate a PDF file from an existing cost estimate in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
@@ -379,7 +489,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
- 'name': purchase.name,
+ 'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
@@ -398,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
- 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
+ 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
+ 'payment_method': invoice.payment,
+ 'remark': invoice.remark,
+ 'end_validity': invoice.date + invoice.validity,
+ 'is_estimate': True,
+ })
+
+
+@login_required
+@can_delete(CostEstimate)
+def del_cost_estimate(request, estimate, **_kwargs):
+ """
+ View used to delete an existing invocie.
+ """
+ if request.method == "POST":
+ estimate.delete()
+ messages.success(
+ request,
+ _("The cost estimate was deleted.")
+ )
+ return redirect(reverse('cotisations:index-cost-estimate'))
+ return form({
+ 'objet': estimate,
+ 'objet_name': _("Cost Estimate")
+ }, 'cotisations/delete.html', request)
+
+
+@login_required
+@can_view(CustomInvoice)
+def custom_invoice_pdf(request, invoice, **_kwargs):
+ """
+ View used to generate a PDF file from an existing invoice in database
+ Creates a line for each Purchase (thus article sold) and generate the
+ invoice with the total price, the payment method, the address and the
+ legal information for the user.
+ """
+ # TODO : change vente to purchase
+ purchases_objects = Vente.objects.all().filter(facture=invoice)
+ # Get the article list and build an list out of it
+ # contiaining (article_name, article_price, quantity, total_price)
+ purchases_info = []
+ for purchase in purchases_objects:
+ purchases_info.append({
+ 'name': escape_chars(purchase.name),
+ 'price': purchase.prix,
+ 'quantity': purchase.number,
+ 'total_price': purchase.prix_total
+ })
+ return render_invoice(request, {
+ 'paid': invoice.paid,
+ 'fid': invoice.id,
+ 'DATE': invoice.date,
+ 'recipient_name': invoice.recipient,
+ 'address': invoice.address,
+ 'article': purchases_info,
+ 'total': invoice.prix_total(),
+ 'asso_name': AssoOption.get_cached_value('name'),
+ 'line1': AssoOption.get_cached_value('adresse1'),
+ 'line2': AssoOption.get_cached_value('adresse2'),
+ 'siret': AssoOption.get_cached_value('siret'),
+ 'email': AssoOption.get_cached_value('contact'),
+ 'phone': AssoOption.get_cached_value('telephone'),
+ 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
+ 'payment_method': invoice.payment,
+ 'remark': invoice.remark,
})
-# TODO : change facture to invoice
@login_required
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
@@ -753,12 +926,35 @@ def index_banque(request):
})
+@login_required
+@can_view_all(CustomInvoice)
+def index_cost_estimate(request):
+ """View used to display every custom invoice."""
+ pagination_number = GeneralOption.get_cached_value('pagination_number')
+ cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
+ cost_estimate_list = SortTable.sort(
+ cost_estimate_list,
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.COTISATIONS_CUSTOM
+ )
+ cost_estimate_list = re2o_paginator(
+ request,
+ cost_estimate_list,
+ pagination_number,
+ )
+ return render(request, 'cotisations/index_cost_estimate.html', {
+ 'cost_estimate_list': cost_estimate_list
+ })
+
+
@login_required
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
- custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
+ cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
+ custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
custom_invoice_list = SortTable.sort(
custom_invoice_list,
request.GET.get('col'),
diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py
index ab10d457..e605aea5 100644
--- a/freeradius_utils/auth.py
+++ b/freeradius_utils/auth.py
@@ -57,14 +57,9 @@ application = get_wsgi_application()
from machines.models import Interface, IpList, Nas, Domain
from topologie.models import Port, Switch
from users.models import User
-from preferences.models import OptionalTopologie
+from preferences.models import RadiusOption
-options, created = OptionalTopologie.objects.get_or_create()
-VLAN_NOK = options.vlan_decision_nok.vlan_id
-VLAN_OK = options.vlan_decision_ok.vlan_id
-RADIUS_POLICY = options.radius_general_policy
-
#: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
@@ -287,6 +282,7 @@ def find_nas_from_request(nas_id):
.select_related('machine__switch__stack'))
return nas.first()
+
def check_user_machine_and_register(nas_type, username, mac_address):
"""Verifie le username et la mac renseignee. L'enregistre si elle est
inconnue.
@@ -327,32 +323,50 @@ def check_user_machine_and_register(nas_type, username, mac_address):
def decide_vlan_switch(nas_machine, nas_type, port_number,
- mac_address):
+ mac_address):
"""Fonction de placement vlan pour un switch en radius filaire auth par
mac.
Plusieurs modes :
- - nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK
- - pas de radius sur le port : VLAN_OK
- - bloq : VLAN_NOK
- - force : placement sur le vlan indiqué dans la bdd
- - mode strict :
- - pas de chambre associée : VLAN_NOK
- - pas d'utilisateur dans la chambre : VLAN_NOK
- - cotisation non à jour : VLAN_NOK
- - sinon passe à common (ci-dessous)
- - mode common :
- - interface connue (macaddress):
- - utilisateur proprio non cotisant ou banni : VLAN_NOK
- - user à jour : VLAN_OK
- - interface inconnue :
- - register mac désactivé : VLAN_NOK
- - register mac activé -> redirection vers webauth
+ - tous les modes:
+ - nas inconnu: VLAN_OK
+ - port inconnu: Politique définie dans RadiusOption
+ - pas de radius sur le port: VLAN_OK
+ - force: placement sur le vlan indiqué dans la bdd
+ - mode strict:
+ - pas de chambre associée: Politique définie
+ dans RadiusOption
+ - pas d'utilisateur dans la chambre : Rejet
+ (redirection web si disponible)
+ - utilisateur de la chambre banni ou désactivé : Rejet
+ (redirection web si disponible)
+ - utilisateur de la chambre non cotisant et non whiteslist:
+ Politique définie dans RadiusOption
+
+ - sinon passe à common (ci-dessous)
+ - mode common :
+ - interface connue (macaddress):
+ - utilisateur proprio non cotisant / machine désactivée:
+ Politique définie dans RadiusOption
+ - utilisateur proprio banni :
+ Politique définie dans RadiusOption
+ - user à jour : VLAN_OK (réassignation de l'ipv4 au besoin)
+ - interface inconnue :
+ - register mac désactivé : Politique définie
+ dans RadiusOption
+ - register mac activé: redirection vers webauth
+ Returns:
+ tuple avec :
+ - Nom du switch (str)
+ - chambre (str)
+ - raison de la décision (str)
+ - vlan_id (int)
+ - decision (bool)
"""
# Get port from switch and port number
extra_log = ""
# Si le NAS est inconnu, on place sur le vlan defaut
if not nas_machine:
- return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK, True)
+ return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True)
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
@@ -367,7 +381,13 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
# Aucune information particulière ne permet de déterminer quelle
# politique à appliquer sur ce port
if not port:
- return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK, True)
+ return (
+ sw_name,
+ "Chambre inconnue",
+ u'Port inconnu',
+ getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None),
+ RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT
+ )
# On récupère le profil du port
port_profile = port.get_port_profile
@@ -378,11 +398,11 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
else:
- DECISION_VLAN = VLAN_OK
+ DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok')
- # Si le port est désactivé, on rejette sur le vlan de déconnexion
+ # Si le port est désactivé, on rejette la connexion
if not port.state:
- return (sw_name, port.room, u'Port desactivé', VLAN_NOK, True)
+ return (sw_name, port.room, u'Port desactivé', None, False)
# Si radius est désactivé, on laisse passer
if port_profile.radius_type == 'NO':
@@ -392,33 +412,68 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
DECISION_VLAN,
True)
- # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
+ # Si le 802.1X est activé sur ce port, cela veut dire que la personne a
+ # été accept précédemment
# Par conséquent, on laisse passer sur le bon vlan
- if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
+ if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'):
room = port.room or "Chambre/local inconnu"
- return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN, True)
+ return (
+ sw_name,
+ room,
+ u'Acceptation authentification 802.1X',
+ DECISION_VLAN,
+ True
+ )
# Sinon, cela veut dire qu'on fait de l'auth radius par mac
# Si le port est en mode strict, on vérifie que tous les users
- # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
- # Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
- # dedans
+ # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette
+ # (anti squattage)
+ # Il n'est pas possible de se connecter sur une prise strict sans adhérent
+ # à jour de cotis dedans
if port_profile.radius_mode == 'STRICT':
room = port.room
if not room:
- return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK, True)
+ return (
+ sw_name,
+ "Inconnue",
+ u'Chambre inconnue',
+ getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None),
+ RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT
+ )
room_user = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room)
)
if not room_user:
- return (sw_name, room, u'Chambre non cotisante -> Web redirect', None, False)
+ return (
+ sw_name,
+ room,
+ u'Chambre non cotisante -> Web redirect',
+ None,
+ False
+ )
for user in room_user:
- if not user.has_access():
- return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False)
+ if user.is_ban() or user.state != User.STATE_ACTIVE:
+ return (
+ sw_name,
+ room,
+ u'Utilisateur banni ou désactivé -> Web redirect',
+ None,
+ False
+ )
+ elif not (user.is_connected() or user.is_whitelisted()):
+ return (
+ sw_name,
+ room,
+ u'Utilisateur non cotisant',
+ getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
+ RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
+ )
# else: user OK, on passe à la verif MAC
- # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
+ # Si on fait de l'auth par mac, on cherche l'interface
+ # via sa mac dans la bdd
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
# Authentification par mac
interface = (Interface.objects
@@ -428,38 +483,67 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
.first())
if not interface:
room = port.room
- # On essaye de register la mac, si l'autocapture a été activée
- # Sinon on rejette sur vlan_nok
- if not nas_type.autocapture_mac:
- return (sw_name, "", u'Machine inconnue', VLAN_NOK, True)
- # On rejette pour basculer sur du webauth
+ # On essaye de register la mac, si l'autocapture a été activée,
+ # on rejette pour faire une redirection web si possible.
+ if nas_type.autocapture_mac:
+ return (
+ sw_name,
+ room,
+ u'Machine Inconnue -> Web redirect',
+ None,
+ False
+ )
+ # Sinon on bascule sur la politique définie dans les options
+ # radius.
else:
- return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False)
+ return (
+ sw_name,
+ "",
+ u'Machine inconnue',
+ getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None),
+ RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT
+ )
- # L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
+ # L'interface a été trouvée, on vérifie qu'elle est active,
+ # sinon on reject
# Si elle n'a pas d'ipv4, on lui en met une
# Enfin on laisse passer sur le vlan pertinent
else:
room = port.room
+ if interface.machine.user.is_ban():
+ return (
+ sw_name,
+ room,
+ u'Adherent banni',
+ getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None),
+ RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT
+ )
if not interface.is_active:
- return (sw_name,
- room,
- u'Machine non active / adherent non cotisant',
- VLAN_NOK,
- True)
- ## Si on choisi de placer les machines sur le vlan correspondant à leur type :
- if RADIUS_POLICY == 'MACHINE':
+ return (
+ sw_name,
+ room,
+ u'Machine non active / adherent non cotisant',
+ getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
+ RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
+ )
+ # Si on choisi de placer les machines sur le vlan
+ # correspondant à leur type :
+ if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE':
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
if not interface.ipv4:
interface.assign_ipv4()
- return (sw_name,
- room,
- u"Ok, Reassignation de l'ipv4" + extra_log,
- DECISION_VLAN,
- True)
+ return (
+ sw_name,
+ room,
+ u"Ok, Reassignation de l'ipv4" + extra_log,
+ DECISION_VLAN,
+ True
+ )
else:
- return (sw_name,
- room,
- u'Machine OK' + extra_log,
- DECISION_VLAN,
- True)
+ return (
+ sw_name,
+ room,
+ u'Machine OK' + extra_log,
+ DECISION_VLAN,
+ True
+ )
diff --git a/install_re2o.sh b/install_re2o.sh
index 6168ec08..b6d8b2aa 100755
--- a/install_re2o.sh
+++ b/install_re2o.sh
@@ -316,6 +316,10 @@ update_django() {
echo "Collecting web frontend statics ..."
python3 manage.py collectstatic --noinput
echo "Collecting web frontend statics: Done"
+
+ echo "Generating locales ..."
+ python3 manage.py compilemessages
+ echo "Generating locales: Done"
}
diff --git a/logs/locale/fr/LC_MESSAGES/django.mo b/logs/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 030b0cac..00000000
Binary files a/logs/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/logs/views.py b/logs/views.py
index 21e3c470..a54edd56 100644
--- a/logs/views.py
+++ b/logs/views.py
@@ -102,15 +102,18 @@ from re2o.utils import (
all_baned,
all_has_access,
all_adherent,
+ all_active_assigned_interfaces_count,
+ all_active_interfaces_count,
+)
+from re2o.base import (
re2o_paginator,
+ SortTable
)
from re2o.acl import (
can_view_all,
can_view_app,
can_edit_history,
)
-from re2o.utils import all_active_assigned_interfaces_count
-from re2o.utils import all_active_interfaces_count, SortTable
@login_required
diff --git a/machines/forms.py b/machines/forms.py
index abc96811..94b9293a 100644
--- a/machines/forms.py
+++ b/machines/forms.py
@@ -273,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm):
self.fields['origin'].label = _("A record origin")
self.fields['origin_v6'].label = _("AAAA record origin")
self.fields['soa'].label = _("SOA record to use")
+ self.fields['dnssec'].label = _("Sign with DNSSEC")
class DelExtensionForm(FormRevMixin, Form):
diff --git a/machines/locale/fr/LC_MESSAGES/django.mo b/machines/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index c9696d92..00000000
Binary files a/machines/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/machines/migrations/0097_extension_dnssec.py b/machines/migrations/0097_extension_dnssec.py
new file mode 100644
index 00000000..48e41f77
--- /dev/null
+++ b/machines/migrations/0097_extension_dnssec.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2018-12-24 14:00
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('machines', '0096_auto_20181013_1417'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='extension',
+ name='dnssec',
+ field=models.BooleanField(default=False, help_text='Should the zone be signed with DNSSEC'),
+ ),
+ ]
diff --git a/machines/migrations/0098_auto_20190102_1745.py b/machines/migrations/0098_auto_20190102_1745.py
new file mode 100644
index 00000000..e886e8a1
--- /dev/null
+++ b/machines/migrations/0098_auto_20190102_1745.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2019-01-02 23:45
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('machines', '0097_extension_dnssec'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='specific_role',
+ field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
+ ),
+ ]
diff --git a/machines/migrations/0099_role_recursive_dns.py b/machines/migrations/0099_role_recursive_dns.py
new file mode 100644
index 00000000..c1ce3965
--- /dev/null
+++ b/machines/migrations/0099_role_recursive_dns.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2019-01-02 23:45
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+def migrate(apps, schema_editor):
+ Role = apps.get_model('machines', 'Role')
+
+ for role in Role.objects.filter(specific_role='dns-recursif-server'):
+ role.specific_role = 'dns-recursive-server'
+ role.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('machines', '0098_auto_20190102_1745'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate),
+ ]
+
+
diff --git a/machines/migrations/0100_auto_20190102_1753.py b/machines/migrations/0100_auto_20190102_1753.py
new file mode 100644
index 00000000..35f7b78d
--- /dev/null
+++ b/machines/migrations/0100_auto_20190102_1753.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2019-01-02 23:53
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('machines', '0099_role_recursive_dns'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='specific_role',
+ field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
+ ),
+ ]
diff --git a/machines/models.py b/machines/models.py
index 48e50644..a4685676 100644
--- a/machines/models.py
+++ b/machines/models.py
@@ -696,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model):
'SOA',
on_delete=models.CASCADE
)
+ dnssec = models.BooleanField(
+ default=False,
+ help_text=_("Should the zone be signed with DNSSEC")
+ )
class Meta:
permissions = (
@@ -741,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model):
.filter(cname__interface_parent__in=all_active_assigned_interfaces())
.prefetch_related('cname'))
+ def get_associated_dname_records(self):
+ return (DName.objects.filter(alias=self))
+
@staticmethod
def can_use_all(user_request, *_args, **_kwargs):
"""Superdroit qui permet d'utiliser toutes les extensions sans
@@ -1089,7 +1096,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.get_cached_value('ipv6_mode') == 'DHCPV6'):
return self.ipv6list.filter(slaac_ip=False)
else:
- return None
+ return []
def mac_bare(self):
""" Formatage de la mac type mac_bare"""
@@ -1373,7 +1380,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.filter(interface=self.interface, slaac_ip=True)
.exclude(id=self.id)):
raise ValidationError(_("A SLAAC IP address is already registered."))
- prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
+ try:
+ prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
+ except AttributeError: # Prevents from crashing when there is no defined prefix_v6
+ prefix_v6 = None
if prefix_v6:
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
IPv6Address(prefix_v6).exploded[:20]):
@@ -1602,7 +1612,7 @@ class Role(RevMixin, AclMixin, models.Model):
ROLE = (
('dhcp-server', _("DHCP server")),
('switch-conf-server', _("Switches configuration server")),
- ('dns-recursif-server', _("Recursive DNS server")),
+ ('dns-recursive-server', _("Recursive DNS server")),
('ntp-server', _("NTP server")),
('radius-server', _("RADIUS server")),
('log-server', _("Log server")),
@@ -1631,18 +1641,6 @@ class Role(RevMixin, AclMixin, models.Model):
verbose_name = _("server role")
verbose_name_plural = _("server roles")
- @classmethod
- def get_instance(cls, roleid, *_args, **_kwargs):
- """Get the Role instance with roleid.
-
- Args:
- roleid: The id
-
- Returns:
- The role.
- """
- return cls.objects.get(pk=roleid)
-
@classmethod
def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype"""
@@ -1657,14 +1655,6 @@ class Role(RevMixin, AclMixin, models.Model):
machine__interface__role=cls.objects.filter(specific_role=roletype)
)
- @classmethod
- def get_instance(cls, roleid, *_args, **_kwargs):
- """Get the Machine instance with machineid.
- :param userid: The id
- :return: The user
- """
- return cls.objects.get(pk=roleid)
-
@classmethod
def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype"""
diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html
index b8493f5f..fae4e25b 100644
--- a/machines/templates/machines/aff_extension.html
+++ b/machines/templates/machines/aff_extension.html
@@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %}
{% trans "AAAA record origin" %} |
{% endif %}
+ {% trans "DNSSEC" %} |
|
@@ -50,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %}
{{ extension.origin_v6 }} |
{% endif %}
+ {{ extension.dnssec|tick }} |
{% can_edit extension %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html
index 60e1a57a..d5a83ed3 100644
--- a/machines/templates/machines/aff_machines.html
+++ b/machines/templates/machines/aff_machines.html
@@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if machines_list.paginator %}
- {% include "pagination.html" with list=machines_list %}
+ {% include "pagination.html" with list=machines_list go_to_id="machines" %}
{% endif %}
@@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if machines_list.paginator %}
- {% include "pagination.html" with list=machines_list %}
+ {% include "pagination.html" with list=machines_list go_to_id="machines" %}
{% endif %}
diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html
index 1a8ce8b1..1e2e3700 100644
--- a/machines/templates/machines/machine.html
+++ b/machines/templates/machines/machine.html
@@ -95,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if interfaceform %}
{% trans "Interface" %}
{% if i_mbf_param %}
- {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %}
+ {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
{% else %}
- {% massive_bootstrap_form interfaceform 'ipv4,machine' %}
+ {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
{% endif %}
{% endif %}
{% if domainform %}
@@ -146,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if aliasform %}
{% trans "Alias" %}
- {% bootstrap_form aliasform %}
+ {% massive_bootstrap_form aliasform 'extension' %}
{% endif %}
{% if serviceform %}
{% trans "Service" %}
diff --git a/machines/views.py b/machines/views.py
index 8d395749..59d4bd5a 100644
--- a/machines/views.py
+++ b/machines/views.py
@@ -55,6 +55,8 @@ from re2o.acl import (
from re2o.utils import (
all_active_assigned_interfaces,
filter_active_interfaces,
+)
+from re2o.base import (
SortTable,
re2o_paginator,
)
diff --git a/preferences/forms.py b/preferences/forms.py
index 2f90927f..fd052edd 100644
--- a/preferences/forms.py
+++ b/preferences/forms.py
@@ -42,6 +42,7 @@ from .models import (
Reminder,
RadiusKey,
SwitchManagementCred,
+ RadiusOption,
)
from topologie.models import Switch
@@ -114,11 +115,6 @@ class EditOptionalTopologieForm(ModelForm):
prefix=prefix,
**kwargs
)
- self.fields['radius_general_policy'].label = _("RADIUS general policy")
- self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted"
- " by RADIUS")
- self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected"
- " by RADIUS")
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
@@ -229,6 +225,13 @@ class EditHomeOptionForm(ModelForm):
self.fields['twitter_account_name'].label = _("Twitter account name")
+class EditRadiusOptionForm(ModelForm):
+ """Edition forms for Radius options"""
+ class Meta:
+ model = RadiusOption
+ fields = '__all__'
+
+
class ServiceForm(ModelForm):
"""Edition, ajout de services sur la page d'accueil"""
class Meta:
diff --git a/preferences/locale/fr/LC_MESSAGES/django.mo b/preferences/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 1d64de15..00000000
Binary files a/preferences/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/preferences/migrations/0055_generaloption_main_site_url.py b/preferences/migrations/0055_generaloption_main_site_url.py
new file mode 100644
index 00000000..655c0b07
--- /dev/null
+++ b/preferences/migrations/0055_generaloption_main_site_url.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2018-11-14 16:46
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('preferences', '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),
+ ),
+ ]
diff --git a/preferences/migrations/0056_1_radiusoption.py b/preferences/migrations/0056_1_radiusoption.py
new file mode 100644
index 00000000..8a7cb45c
--- /dev/null
+++ b/preferences/migrations/0056_1_radiusoption.py
@@ -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'),
+ ),
+ ]
diff --git a/preferences/migrations/0056_2_radiusoption.py b/preferences/migrations/0056_2_radiusoption.py
new file mode 100644
index 00000000..1a8ecccd
--- /dev/null
+++ b/preferences/migrations/0056_2_radiusoption.py
@@ -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),
+ ]
diff --git a/preferences/migrations/0056_3_radiusoption.py b/preferences/migrations/0056_3_radiusoption.py
new file mode 100644
index 00000000..f3e5f98c
--- /dev/null
+++ b/preferences/migrations/0056_3_radiusoption.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2018-10-13 14:29
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import re2o.mixins
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('machines', '0095_auto_20180919_2225'),
+ ('preferences', '0055_generaloption_main_site_url'),
+ ('preferences', '0056_2_radiusoption'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='optionaltopologie',
+ name='radius_general_policy',
+ ),
+ migrations.RemoveField(
+ model_name='optionaltopologie',
+ name='vlan_decision_nok',
+ ),
+ migrations.RemoveField(
+ model_name='optionaltopologie',
+ name='vlan_decision_ok',
+ ),
+ ]
diff --git a/preferences/migrations/0056_4_radiusoption.py b/preferences/migrations/0056_4_radiusoption.py
new file mode 100644
index 00000000..8d93fff9
--- /dev/null
+++ b/preferences/migrations/0056_4_radiusoption.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2018-12-04 13:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('preferences', '0056_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'),
+ ),
+ ]
diff --git a/preferences/migrations/0057_optionaluser_all_users_active.py b/preferences/migrations/0057_optionaluser_all_users_active.py
new file mode 100644
index 00000000..3f0cc8c1
--- /dev/null
+++ b/preferences/migrations/0057_optionaluser_all_users_active.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2019-01-05 17:15
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('preferences', '0056_4_radiusoption'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='optionaluser',
+ name='all_users_active',
+ field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid'),
+ ),
+ ]
diff --git a/preferences/models.py b/preferences/models.py
index b21f42fe..228807a6 100644
--- a/preferences/models.py
+++ b/preferences/models.py
@@ -116,6 +116,11 @@ class OptionalUser(AclMixin, PreferencesModel):
default=False,
help_text=_("A new user can create their account on Re2o")
)
+ all_users_active = models.BooleanField(
+ default=False,
+ help_text=_("If True, all new created and connected users are active.\
+ If False, only when a valid registration has been paid")
+ )
class Meta:
permissions = (
@@ -199,25 +204,6 @@ class OptionalTopologie(AclMixin, PreferencesModel):
('tftp', 'tftp'),
)
- radius_general_policy = models.CharField(
- max_length=32,
- choices=CHOICE_RADIUS,
- default='DEFINED'
- )
- vlan_decision_ok = models.OneToOneField(
- 'machines.Vlan',
- on_delete=models.PROTECT,
- related_name='decision_ok',
- blank=True,
- null=True
- )
- vlan_decision_nok = models.OneToOneField(
- 'machines.Vlan',
- on_delete=models.PROTECT,
- related_name='decision_nok',
- blank=True,
- null=True
- )
switchs_web_management = models.BooleanField(
default=False,
help_text="Web management, activé si provision automatique"
@@ -297,19 +283,19 @@ class OptionalTopologie(AclMixin, PreferencesModel):
log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type)
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
+ dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type)
subnet = None
subnet6 = None
if self.switchs_ip_type:
subnet = self.switchs_ip_type.ip_set_full_info
subnet6 = self.switchs_ip_type.ip6_set_full_info
- return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6}
+ return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6}
@cached_property
def provision_switchs_enabled(self):
"""Return true if all settings are ok : switchs on automatic provision,
ip_type"""
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
-
class Meta:
permissions = (
("view_optionaltopologie", _("Can view the topology options")),
@@ -431,6 +417,7 @@ class GeneralOption(AclMixin, PreferencesModel):
req_expire_hrs = models.IntegerField(default=48)
site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@example.com")
+ main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
GTU_sum_up = models.TextField(
default="",
blank=True,
@@ -587,3 +574,122 @@ class MailMessageOption(AclMixin, models.Model):
)
verbose_name = _("email message options")
+
+class RadiusOption(AclMixin, PreferencesModel):
+ class Meta:
+ verbose_name = _("radius policies")
+
+ MACHINE = 'MACHINE'
+ DEFINED = 'DEFINED'
+ CHOICE_RADIUS = (
+ (MACHINE, _("On the IP range's VLAN of the machine")),
+ (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")),
+ )
+ REJECT = 'REJECT'
+ SET_VLAN = 'SET_VLAN'
+ CHOICE_POLICY = (
+ (REJECT, _('Reject the machine')),
+ (SET_VLAN, _('Place the machine on the VLAN'))
+ )
+ radius_general_policy = models.CharField(
+ max_length=32,
+ choices=CHOICE_RADIUS,
+ default='DEFINED'
+ )
+ unknown_machine = models.CharField(
+ max_length=32,
+ choices=CHOICE_POLICY,
+ default=REJECT,
+ verbose_name=_("Policy for unknown machines"),
+ )
+ unknown_machine_vlan = models.ForeignKey(
+ 'machines.Vlan',
+ on_delete=models.PROTECT,
+ related_name='unknown_machine_vlan',
+ blank=True,
+ null=True,
+ verbose_name=_('Unknown machine Vlan'),
+ help_text=_(
+ 'Vlan for unknown machines if not rejected.'
+ )
+ )
+ unknown_port = models.CharField(
+ max_length=32,
+ choices=CHOICE_POLICY,
+ default=REJECT,
+ verbose_name=_("Policy for unknown port"),
+ )
+ unknown_port_vlan = models.ForeignKey(
+ 'machines.Vlan',
+ on_delete=models.PROTECT,
+ related_name='unknown_port_vlan',
+ blank=True,
+ null=True,
+ verbose_name=_('Unknown port Vlan'),
+ help_text=_(
+ 'Vlan for unknown ports if not rejected.'
+ )
+ )
+ unknown_room = models.CharField(
+ max_length=32,
+ choices=CHOICE_POLICY,
+ default=REJECT,
+ verbose_name=_(
+ "Policy for machine connecting from "
+ "unregistered room (relevant on ports with STRICT "
+ "radius mode)"
+ ),
+ )
+ unknown_room_vlan = models.ForeignKey(
+ 'machines.Vlan',
+ related_name='unknown_room_vlan',
+ on_delete=models.PROTECT,
+ blank=True,
+ null=True,
+ verbose_name=_('Unknown room Vlan'),
+ help_text=_(
+ 'Vlan for unknown room if not rejected.'
+ )
+ )
+ non_member = models.CharField(
+ max_length=32,
+ choices=CHOICE_POLICY,
+ default=REJECT,
+ verbose_name=_("Policy non member users."),
+ )
+ non_member_vlan = models.ForeignKey(
+ 'machines.Vlan',
+ related_name='non_member_vlan',
+ on_delete=models.PROTECT,
+ blank=True,
+ null=True,
+ verbose_name=_('Non member Vlan'),
+ help_text=_(
+ 'Vlan for non members if not rejected.'
+ )
+ )
+ banned = models.CharField(
+ max_length=32,
+ choices=CHOICE_POLICY,
+ default=REJECT,
+ verbose_name=_("Policy for banned users."),
+ )
+ banned_vlan = models.ForeignKey(
+ 'machines.Vlan',
+ related_name='banned_vlan',
+ on_delete=models.PROTECT,
+ blank=True,
+ null=True,
+ verbose_name=_('Banned Vlan'),
+ help_text=_(
+ 'Vlan for banned if not rejected.'
+ )
+ )
+ vlan_decision_ok = models.OneToOneField(
+ 'machines.Vlan',
+ on_delete=models.PROTECT,
+ related_name='vlan_ok_option',
+ blank=True,
+ null=True
+ )
+
diff --git a/preferences/templates/preferences/aff_radiusoptions.html b/preferences/templates/preferences/aff_radiusoptions.html
new file mode 100644
index 00000000..17e2a869
--- /dev/null
+++ b/preferences/templates/preferences/aff_radiusoptions.html
@@ -0,0 +1,96 @@
+{% comment %}
+Re2o est un logiciel d'administration développé initiallement au rezometz. Il
+se veut agnostique au réseau considéré, de manière à être installable en
+quelques clics.
+
+Copyright © 2018 Hugo Levy-Falk
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+{% endcomment %}
+{% load i18n %}
+{% load acl %}
+{% load logs_extra %}
+
+
+
+ {% trans "General policy for VLAN setting" %} |
+ {{ radiusoptions.radius_general_policy }} |
+ {% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %} |
+
+
+ {% trans "VLAN for machines accepted by RADIUS" %} |
+ Vlan {{ radiusoptions.vlan_decision_ok }} |
+
+
+
+
+
+
+ {% trans "Situation" %} |
+ {% trans "Behavior" %} |
+
+
+
+ {% trans "Unknown machine" %} |
+
+ {% if radiusoptions.unknown_machine == 'REJECT' %}
+ {% trans "Reject" %}
+ {% else %}
+ Vlan {{ radiusoptions.unknown_machine_vlan }}
+ {% endif %}
+ |
+
+
+ {% trans "Unknown port" %} |
+
+ {% if radiusoptions.unknown_port == 'REJECT' %}
+ {% trans "Reject" %}
+ {% else %}
+ Vlan {{ radiusoptions.unknown_port_vlan }}
+ {% endif %}
+ |
+
+
+ {% trans "Unknown room" %} |
+
+ {% if radiusoptions.unknown_room == 'REJECT' %}
+ {% trans "Reject" %}
+ {% else %}
+ Vlan {{ radiusoptions.unknown_room_vlan }}
+ {% endif %}
+ |
+
+
+ {% trans "Non member" %} |
+
+ {% if radiusoptions.non_member == 'REJECT' %}
+ {% trans "Reject" %}
+ {% else %}
+ Vlan {{ radiusoptions.non_member_vlan }}
+ {% endif %}
+ |
+
+
+ {% trans "Banned user" %} |
+
+ {% if radiusoptions.unknown_port == 'REJECT' %}
+ {% trans "Reject" %}
+ {% else %}
+ Vlan {{ radiusoptions.banned_vlan }}
+ {% endif %}
+ |
+
+
+
diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html
index 1c29c595..e4321f5b 100644
--- a/preferences/templates/preferences/display_preferences.html
+++ b/preferences/templates/preferences/display_preferences.html
@@ -31,293 +31,427 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Preferences" %}{% endblock %}
{% block content %}
- {% trans "User preferences" %}
-
-
- {% trans "Edit" %}
-
-
-
- {% trans "General preferences" %}
-
-
- {% trans "Creation of members by everyone" %} |
- {{ useroptions.all_can_create_adherent|tick }} |
- {% trans "Creation of clubs by everyone" %} |
- {{ useroptions.all_can_create_club|tick }} |
-
-
- {% trans "Self registration" %} |
- {{ useroptions.self_adhesion|tick }} |
- {% trans "Delete not yet active users after" %} |
- {{ useroptions.delete_notyetactive }} days |
-
-
+
- {% trans "Users general permissions" %}
-
-
- {% trans "Default shell for users" %} |
- {{ useroptions.shell_default }} |
- {% trans "Users can edit their shell" %} |
- {{ useroptions.self_change_shell|tick }} |
-
-
- {% trans "Users can edit their room" %} |
- {{ useroptions.self_change_room|tick }} |
- {% trans "Telephone number required" %} |
- {{ useroptions.is_tel_mandatory|tick }} |
-
-
- {% trans "GPG fingerprint field" %} |
- {{ useroptions.gpg_fingerprint|tick }} |
-
-
+
+
+
+
+
+
+ {% trans "Edit" %}
+
+
+
+
+
+ {% trans "Website name" %} |
+ {{ generaloptions.site_name }} |
+ {% trans "Email address for automatic emailing" %} |
+ {{ generaloptions.email_from }} |
+
+
+ {% trans "Number of results displayed when searching" %} |
+ {{ generaloptions.search_display_page }} |
+ {% trans "Number of items per page (standard size)" %} |
+ {{ generaloptions.pagination_number }} |
+
+
+ {% trans "Number of items per page (large size)" %} |
+ {{ generaloptions.pagination_large_number }} |
+ {% trans "Time before expiration of the reset password link (in hours)" %} |
+ {{ generaloptions.req_expire_hrs }} |
+
+
+ {% trans "General message displayed on the website" %} |
+ {{ generaloptions.general_message }} |
+ {% trans "Main site url" %} |
+ {{ generaloptions.main_site_url }} |
+
+
+ {% trans "Summary of the General Terms of Use" %} |
+ {{ generaloptions.GTU_sum_up }} |
+ {% trans "General Terms of Use" %} |
+ {{ generaloptions.GTU }}
+ |
+
+
+
+ {% trans "Local email accounts enabled" %} |
+ {{ useroptions.local_email_accounts_enabled|tick }} |
+ {% trans "Local email domain" %} |
+ {{ useroptions.local_email_domain }} |
+
+
+ {% trans "Maximum number of email aliases allowed" %} |
+ {{ useroptions.max_email_address }} |
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans "Edit" %}
+
+
+
+
+
+ {% trans "Creation of members by everyone" %} |
+ {{ useroptions.all_can_create_adherent|tick }} |
+ {% trans "Creation of clubs by everyone" %} |
+ {{ useroptions.all_can_create_club|tick }} |
+
+
+ {% trans "Self registration" %} |
+ {{ useroptions.self_adhesion|tick }} |
+ {% trans "Delete not yet active users after" %} |
+ {{ useroptions.delete_notyetactive }} days |
+
+
+ {% trans "All users are active by default" %} |
+ {{ useroptions.all_users_active|tick }} |
+
+
+
+ {% trans "Users general permissions" %}
+
+
+ {% trans "Default shell for users" %} |
+ {{ useroptions.shell_default }} |
+ {% trans "Users can edit their shell" %} |
+ {{ useroptions.self_change_shell|tick }} |
+
+
+ {% trans "Users can edit their room" %} |
+ {{ useroptions.self_change_room|tick }} |
+ {% trans "Telephone number required" %} |
+ {{ useroptions.is_tel_mandatory|tick }} |
+
+
+ {% trans "GPG fingerprint field" %} |
+ {{ useroptions.gpg_fingerprint|tick }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans "Edit" %}
+
+
+
+
+ {% trans "Password per machine" %} |
+ {{ machineoptions.password_machine|tick }} |
+ {% trans "Maximum number of interfaces allowed for a standard user" %} |
+ {{ machineoptions.max_lambdauser_interfaces }} |
+
+
+ {% trans "Maximum number of DNS aliases allowed for a standard user" %} |
+ {{ machineoptions.max_lambdauser_aliases }} |
+ {% trans "IPv6 support" %} |
+ {{ machineoptions.ipv6_mode }} |
+
+
+ {% trans "Creation of machines" %} |
+ {{ machineoptions.create_machine|tick }} |
+
+
+
+
+
+
+
+
- {% trans "Email accounts preferences" %}
-
-
- {% trans "Local email accounts enabled" %} |
- {{ useroptions.local_email_accounts_enabled|tick }} |
- {% trans "Local email domain" %} |
- {{ useroptions.local_email_domain }} |
-
-
- {% trans "Maximum number of email aliases allowed" %} |
- {{ useroptions.max_email_address }} |
-
-
- {% trans "Machines preferences" %}
-
-
- {% trans "Edit" %}
-
-
-
-
-
- {% trans "Password per machine" %} |
- {{ machineoptions.password_machine|tick }} |
- {% trans "Maximum number of interfaces allowed for a standard user" %} |
- {{ machineoptions.max_lambdauser_interfaces }} |
-
-
- {% trans "Maximum number of DNS aliases allowed for a standard user" %} |
- {{ machineoptions.max_lambdauser_aliases }} |
- {% trans "IPv6 support" %} |
- {{ machineoptions.ipv6_mode }} |
-
-
- {% trans "Creation of machines" %} |
- {{ machineoptions.create_machine|tick }} |
-
-
- {% trans "Topology preferences" %}
{% trans "Edit" %}
-
-
-
-
- {% trans "General policy for VLAN setting" %} |
- {{ topologieoptions.radius_general_policy }} |
- {% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %} |
- |
-
-
- {% trans "VLAN for machines accepted by RADIUS" %} |
- {{ topologieoptions.vlan_decision_ok }} |
- {% trans "VLAN for machines rejected by RADIUS" %} |
- {{ topologieoptions.vlan_decision_nok }} |
-
-
- Placement sur ce vlan par default en cas de rejet |
- {{ topologieoptions.vlan_decision_nok }} |
-
-
+
+
+
+ {% trans "General policy for VLAN setting" %} |
+ {{ topologieoptions.radius_general_policy }} |
+ {% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %} |
+ |
+
+
+ {% trans "VLAN for machines accepted by RADIUS" %} |
+ {{ topologieoptions.vlan_decision_ok }} |
+ {% trans "VLAN for machines rejected by RADIUS" %} |
+ {{ topologieoptions.vlan_decision_nok }} |
+
+
+ {% trans "VLAN for non members machines" %} |
+ {{ topologieoptions.vlan_non_member }} |
+
+
- Clef radius
- {% can_create RadiusKey%}
- Ajouter une clef radius
- {% acl_end %}
- {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
+ Clef radius
+ {% can_create RadiusKey%}
+ Ajouter une clef radius
+ {% acl_end %}
+ {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
- Configuration des switches
-
-
- Web management, activé si provision automatique |
- {{ topologieoptions.switchs_web_management }} |
- Rest management, activé si provision auto |
- {{ topologieoptions.switchs_rest_management }} |
-
-
+
+
+
+
+
-
- {% if topologieoptions.provision_switchs_enabled %}Provision de la config des switchs{% else %}Provision de la config des switchs{% endif%}
-
-
- Switchs configurés automatiquement |
- {{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %} OK{% else %}Manquant{% endif %} |
-
-
- Plage d'ip de management des switchs |
- {{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %} OK{% else %}Manquant{% endif %} |
-
-
- Serveur des config des switchs |
- {{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} OK{% else %}Manquant{% endif %} |
-
-
- Mode de provision des switchs |
- {{ topologieoptions.switchs_provision }} |
-
-
- Mode TFTP |
- OK |
-
-
- Mode SFTP |
- {% if topologieoptions.switchs_management_sftp_creds %} OK{% else %}Creds manquants{% endif %} |
-
-
-
- Creds de management des switchs
- {% can_create SwitchManagementCred%}
- Ajouter un id/mdp de management switch
- {% acl_end %}
-
-
- {% if switchmanagementcred_list %} OK{% else %}Manquant{% endif %}
- {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
-
-
-
- {% trans "General preferences" %}
-
+
{% trans "Edit" %}
-
-
-
-
- {% trans "Website name" %} |
- {{ generaloptions.site_name }} |
- {% trans "Email address for automatic emailing" %} |
- {{ generaloptions.email_from }} |
-
-
- {% trans "Number of results displayed when searching" %} |
- {{ generaloptions.search_display_page }} |
- {% trans "Number of items per page (standard size)" %} |
- {{ generaloptions.pagination_number }} |
-
-
- {% trans "Number of items per page (large size)" %} |
- {{ generaloptions.pagination_large_number }} |
- {% trans "Time before expiration of the reset password link (in hours)" %} |
- {{ generaloptions.req_expire_hrs }} |
-
-
- {% trans "General message displayed on the website" %} |
- {{ generaloptions.general_message }} |
- {% trans "Summary of the General Terms of Use" %} |
- {{ generaloptions.GTU_sum_up }} |
-
-
- {% trans "General Terms of Use" %} |
- {{ generaloptions.GTU }}
- |
-
- {% trans "Information about the organisation" %}
+
+
+
+
+ Web management, activé si provision automatique |
+ {{ topologieoptions.switchs_web_management }} |
+ Rest management, activé si provision auto |
+ {{ topologieoptions.switchs_rest_management }} |
+
+
+
+
+
+ {% if topologieoptions.provision_switchs_enabled %}Provision de la config des switchs{% else %}Provision de la config des switchs{% endif%}
+
+
+ Switchs configurés automatiquement |
+ {{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %} OK{% else %}Manquant{% endif %} |
+
+
+ Plage d'ip de management des switchs |
+ {{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %} OK{% else %}Manquant{% endif %} |
+
+
+ Serveur des config des switchs |
+ {{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} OK{% else %}Manquant{% endif %} |
+
+
+ Mode de provision des switchs |
+ {{ topologieoptions.switchs_provision }} |
+
+
+ Mode TFTP |
+ OK |
+
+
+ Mode SFTP |
+ {% if topologieoptions.switchs_management_sftp_creds %} OK{% else %}Creds manquants{% endif %} |
+
+
+
+ Creds de management des switchs
+ {% can_create SwitchManagementCred%}
+ Ajouter un id/mdp de management switch
+ {% acl_end %}
+
+
+ {% if switchmanagementcred_list %} OK{% else %}Manquant{% endif %}
+ {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
+
+
+
+
+
+
+
+
{% trans "Edit" %}
-
-
-
-
- {% trans "Name" %} |
- {{ assooptions.name }} |
- {% trans "SIRET number" %} |
- {{ assooptions.siret }} |
-
-
- {% trans "Address" %} |
- {{ assooptions.adresse1 }}
- {{ assooptions.adresse2 }}
- |
- {% trans "Contact email address" %} |
- {{ assooptions.contact }} |
-
-
- {% trans "Telephone number" %} |
- {{ assooptions.telephone }} |
- {% trans "Usual name" %} |
- {{ assooptions.pseudo }} |
-
-
- {% trans "User object of the organisation" %} |
- {{ assooptions.utilisateur_asso }} |
- {% trans "Description of the organisation" %} |
- {{ assooptions.description|safe }} |
-
-
- {% trans "Custom email message" %}
+
+
+
+ {% trans "Name" %} |
+ {{ assooptions.name }} |
+ {% trans "SIRET number" %} |
+ {{ assooptions.siret }} |
+
+
+ {% trans "Address" %} |
+ {{ assooptions.adresse1 }}
+ {{ assooptions.adresse2 }}
+ |
+ {% trans "Contact email address" %} |
+ {{ assooptions.contact }} |
+
+
+ {% trans "Telephone number" %} |
+ {{ assooptions.telephone }} |
+ {% trans "Usual name" %} |
+ {{ assooptions.pseudo }} |
+
+
+ {% trans "User object of the organisation" %} |
+ {{ assooptions.utilisateur_asso }} |
+ {% trans "Description of the organisation" %} |
+ {{ assooptions.description|safe }} |
+
+
+
+
+
+
+
+
+
{% trans "Edit" %}
-
-
-
-
- {% trans "Welcome email (in French)" %} |
- {{ mailmessageoptions.welcome_mail_fr|safe }} |
-
-
- {% trans "Welcome email (in English)" %} |
- {{ mailmessageoptions.welcome_mail_en|safe }} |
-
-
- Options pour le mail de fin d'adhésion
+
+
+
+
+ {% trans "Welcome email (in French)" %} |
+ {{ mailmessageoptions.welcome_mail_fr|safe }} |
+
+
+ {% trans "Welcome email (in English)" %} |
+ {{ mailmessageoptions.welcome_mail_en|safe }} |
+
+
+
+
+
+
+
+
{% can_create preferences.Reminder%}
+
Ajouter un rappel
+
{% acl_end %}
{% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
+
+
- {% trans "List of services and homepage preferences" %}
+
+
+
+
+
+
+
+
+
+
+
+ {% trans "Edit" %}
+
+
+
+
+ {% trans "Twitter account URL" %} |
+ {{ homeoptions.twitter_url }} |
+ {% trans "Twitter account name" %} |
+ {{ homeoptions.twitter_account_name }} |
+
+
+ {% trans "Facebook account URL" %} |
+ {{ homeoptions.facebook_url }} |
+
+
+
+
+
{% endblock %}
diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html
index a1540f33..c3dd4652 100644
--- a/preferences/templates/preferences/edit_preferences.html
+++ b/preferences/templates/preferences/edit_preferences.html
@@ -37,6 +37,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
diff --git a/preferences/templates/preferences/sidebar.html b/preferences/templates/preferences/sidebar.html
index 4f69298b..98b597ea 100644
--- a/preferences/templates/preferences/sidebar.html
+++ b/preferences/templates/preferences/sidebar.html
@@ -22,6 +22,8 @@ You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
+{% load acl %}
+{% load i18n %}
{% block sidebar %}
diff --git a/preferences/urls.py b/preferences/urls.py
index 63ca6a39..30163868 100644
--- a/preferences/urls.py
+++ b/preferences/urls.py
@@ -66,6 +66,11 @@ urlpatterns = [
views.edit_options,
name='edit-options'
),
+ url(
+ r'^edit_options/(?PRadiusOption)$',
+ views.edit_options,
+ name='edit-options'
+ ),
url(r'^add_service/$', views.add_service, name='add-service'),
url(
r'^edit_service/(?P[0-9]+)$',
diff --git a/preferences/views.py b/preferences/views.py
index ee81ae89..586be60f 100644
--- a/preferences/views.py
+++ b/preferences/views.py
@@ -62,7 +62,8 @@ from .models import (
HomeOption,
Reminder,
RadiusKey,
- SwitchManagementCred
+ SwitchManagementCred,
+ RadiusOption,
)
from . import models
from . import forms
@@ -86,6 +87,7 @@ def display_options(request):
reminder_list = Reminder.objects.all()
radiuskey_list = RadiusKey.objects.all()
switchmanagementcred_list = SwitchManagementCred.objects.all()
+ radiusoptions, _ = RadiusOption.objects.get_or_create()
return form({
'useroptions': useroptions,
'machineoptions': machineoptions,
@@ -98,7 +100,8 @@ def display_options(request):
'mailcontact_list': mailcontact_list,
'reminder_list': reminder_list,
'radiuskey_list' : radiuskey_list,
- 'switchmanagementcred_list': switchmanagementcred_list,
+ 'switchmanagementcred_list': switchmanagementcred_list,
+ 'radiusoptions' : radiusoptions,
}, 'preferences/display_preferences.html', request)
@@ -134,7 +137,9 @@ def edit_options(request, section):
messages.success(request, _("The preferences were edited."))
return redirect(reverse('preferences:display-options'))
return form(
- {'options': options},
+ {
+ 'options': options,
+ },
'preferences/edit_preferences.html',
request
)
diff --git a/re2o/aes_field.py b/re2o/aes_field.py
index 2720f5af..5f50ddd2 100644
--- a/re2o/aes_field.py
+++ b/re2o/aes_field.py
@@ -37,8 +37,8 @@ from django.db import models
from django import forms
from django.conf import settings
-EOD = '`%EofD%`' # This should be something that will not occur in strings
-
+EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings
+EOD = EOD_asbyte.decode('utf-8')
def genstring(length=16, chars=string.printable):
""" Generate a random string of length `length` and composed of
@@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable):
return ''.join([choice(chars) for i in range(length)])
-def encrypt(key, s):
- """ AES Encrypt a secret `s` with the key `key` """
+def encrypt(key, secret):
+ """ AES Encrypt a secret with the key `key` """
obj = AES.new(key)
- datalength = len(s) + len(EOD)
+ datalength = len(secret) + len(EOD)
if datalength < 16:
saltlength = 16 - datalength
else:
saltlength = 16 - datalength % 16
- ss = ''.join([s, EOD, genstring(saltlength)])
- return obj.encrypt(ss)
+ encrypted_secret = ''.join([secret, EOD, genstring(saltlength)])
+ return obj.encrypt(encrypted_secret)
-def decrypt(key, s):
- """ AES Decrypt a secret `s` with the key `key` """
+def decrypt(key, secret):
+ """ AES Decrypt a secret with the key `key` """
obj = AES.new(key)
- ss = obj.decrypt(s)
- return ss.split(bytes(EOD, 'utf-8'))[0]
+ uncrypted_secret = obj.decrypt(secret)
+ return uncrypted_secret.split(EOD_asbyte)[0]
class AESEncryptedFormField(forms.CharField):
@@ -81,8 +81,7 @@ class AESEncryptedField(models.CharField):
if value is None:
return None
try:
- return decrypt(settings.AES_KEY,
- binascii.a2b_base64(value)).decode('utf-8')
+ return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
except Exception as e:
raise ValueError(value)
@@ -90,18 +89,14 @@ class AESEncryptedField(models.CharField):
if value is None:
return value
try:
- return decrypt(settings.AES_KEY,
- binascii.a2b_base64(value)).decode('utf-8')
+ return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
except Exception as e:
raise ValueError(value)
def get_prep_value(self, value):
if value is None:
return value
- return binascii.b2a_base64(encrypt(
- settings.AES_KEY,
- value
- )).decode('utf-8')
+ return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8')
def formfield(self, **kwargs):
defaults = {'form_class': AESEncryptedFormField}
diff --git a/re2o/base.py b/re2o/base.py
new file mode 100644
index 00000000..023a16ff
--- /dev/null
+++ b/re2o/base.py
@@ -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=""
+ help_text = help_text_template.format(
+ main=convert_datetime_format(input_formats[0]),
+ more=more_text_template.format(
+ '\n'.join(map(convert_datetime_format, input_formats))
+ )
+ )
+ return help_text
+
+
+class SortTable:
+ """ Class gathering uselful stuff to sort the colums of a table, according
+ to the column and order requested. It's used with a dict of possible
+ values and associated model_fields """
+
+ # All the possible possible values
+ # The naming convention is based on the URL or the views function
+ # The syntax to describe the sort to apply is a dict where the keys are
+ # the url value and the values are a list of model field name to use to
+ # order the request. They are applied in the order they are given.
+ # A 'default' might be provided to specify what to do if the requested col
+ # doesn't match any keys.
+
+ USERS_INDEX = {
+ 'user_name': ['name'],
+ 'user_surname': ['surname'],
+ 'user_pseudo': ['pseudo'],
+ 'user_room': ['room'],
+ 'default': ['state', 'pseudo']
+ }
+ USERS_INDEX_BAN = {
+ 'ban_user': ['user__pseudo'],
+ 'ban_start': ['date_start'],
+ 'ban_end': ['date_end'],
+ 'default': ['-date_end']
+ }
+ USERS_INDEX_WHITE = {
+ 'white_user': ['user__pseudo'],
+ 'white_start': ['date_start'],
+ 'white_end': ['date_end'],
+ 'default': ['-date_end']
+ }
+ USERS_INDEX_SCHOOL = {
+ 'school_name': ['name'],
+ 'default': ['name']
+ }
+ MACHINES_INDEX = {
+ 'machine_name': ['name'],
+ 'default': ['pk']
+ }
+ COTISATIONS_INDEX = {
+ 'cotis_user': ['user__pseudo'],
+ 'cotis_paiement': ['paiement__moyen'],
+ 'cotis_date': ['date'],
+ 'cotis_id': ['id'],
+ 'default': ['-date']
+ }
+ COTISATIONS_CUSTOM = {
+ 'invoice_date': ['date'],
+ 'invoice_id': ['id'],
+ 'invoice_recipient': ['recipient'],
+ 'invoice_address': ['address'],
+ 'invoice_payment': ['payment'],
+ 'default': ['-date']
+ }
+ COTISATIONS_CONTROL = {
+ 'control_name': ['user__adherent__name'],
+ 'control_surname': ['user__surname'],
+ 'control_paiement': ['paiement'],
+ 'control_date': ['date'],
+ 'control_valid': ['valid'],
+ 'control_control': ['control'],
+ 'control_id': ['id'],
+ 'control_user-id': ['user__id'],
+ 'default': ['-date']
+ }
+ TOPOLOGIE_INDEX = {
+ 'switch_dns': ['interface__domain__name'],
+ 'switch_ip': ['interface__ipv4__ipv4'],
+ 'switch_loc': ['switchbay__name'],
+ 'switch_ports': ['number'],
+ 'switch_stack': ['stack__name'],
+ 'default': ['switchbay', 'stack', 'stack_member_id']
+ }
+ TOPOLOGIE_INDEX_PORT = {
+ 'port_port': ['port'],
+ 'port_room': ['room__name'],
+ 'port_interface': ['machine_interface__domain__name'],
+ 'port_related': ['related__switch__name'],
+ 'port_radius': ['radius'],
+ 'port_vlan': ['vlan_force__name'],
+ 'default': ['port']
+ }
+ TOPOLOGIE_INDEX_ROOM = {
+ 'room_name': ['name'],
+ 'default': ['name']
+ }
+ TOPOLOGIE_INDEX_BUILDING = {
+ 'building_name': ['name'],
+ 'default': ['name']
+ }
+ TOPOLOGIE_INDEX_BORNE = {
+ 'ap_name': ['interface__domain__name'],
+ 'ap_ip': ['interface__ipv4__ipv4'],
+ 'ap_mac': ['interface__mac_address'],
+ 'default': ['interface__domain__name']
+ }
+ TOPOLOGIE_INDEX_STACK = {
+ 'stack_name': ['name'],
+ 'stack_id': ['stack_id'],
+ 'default': ['stack_id'],
+ }
+ TOPOLOGIE_INDEX_MODEL_SWITCH = {
+ 'model-switch_name': ['reference'],
+ 'model-switch_contructor': ['constructor__name'],
+ 'default': ['reference'],
+ }
+ TOPOLOGIE_INDEX_SWITCH_BAY = {
+ 'switch-bay_name': ['name'],
+ 'switch-bay_building': ['building__name'],
+ 'default': ['name'],
+ }
+ TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
+ 'constructor-switch_name': ['name'],
+ 'default': ['name'],
+ }
+ LOGS_INDEX = {
+ 'sum_date': ['revision__date_created'],
+ 'default': ['-revision__date_created'],
+ }
+ LOGS_STATS_LOGS = {
+ 'logs_author': ['user__name'],
+ 'logs_date': ['date_created'],
+ 'default': ['-date_created']
+ }
+
+ @staticmethod
+ def sort(request, col, order, values):
+ """ Check if the given values are possible and add .order_by() and
+ a .reverse() as specified according to those values """
+ fields = values.get(col, None)
+ if not fields:
+ fields = values.get('default', [])
+ request = request.order_by(*fields)
+ if values.get(col, None) and order == 'desc':
+ return request.reverse()
+ else:
+ return request
+
+
+def re2o_paginator(request, query_set, pagination_number):
+ """Paginator script for list display in re2o.
+ :request:
+ :query_set: Query_set to paginate
+ :pagination_number: Number of entries to display"""
+ paginator = Paginator(query_set, pagination_number)
+ page = request.GET.get('page')
+ try:
+ results = paginator.page(page)
+ except PageNotAnInteger:
+ # If page is not an integer, deliver first page.
+ results = paginator.page(1)
+ except EmptyPage:
+ # If page is out of range (e.g. 9999), deliver last page of results.
+ results = paginator.page(paginator.num_pages)
+ return results
diff --git a/re2o/locale/fr/LC_MESSAGES/django.mo b/re2o/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 83348f41..00000000
Binary files a/re2o/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/re2o/login.py b/re2o/login.py
index 471c2e02..0b552239 100644
--- a/re2o/login.py
+++ b/re2o/login.py
@@ -114,9 +114,9 @@ class CryptPasswordHasher(hashers.BasePasswordHasher):
Check password against encoded using CRYPT algorithm
"""
assert encoded.startswith(self.algorithm)
- salt = hash_password_salt(challenge_password)
- return constant_time_compare(crypt.crypt(password.encode(), salt),
- challenge.encode())
+ salt = hash_password_salt(encoded)
+ return constant_time_compare(crypt.crypt(password, salt),
+ encoded)
def safe_summary(self, encoded):
"""
diff --git a/re2o/utils.py b/re2o/utils.py
index 6f7870f0..20218a81 100644
--- a/re2o/utils.py
+++ b/re2o/utils.py
@@ -38,55 +38,11 @@ from __future__ import unicode_literals
from django.utils import timezone
from django.db.models import Q
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from cotisations.models import Cotisation, Facture, Vente
from machines.models import Interface, Machine
from users.models import Adherent, User, Ban, Whitelist
-
-# Mapping of srtftime format for better understanding
-# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior
-datetime_mapping={
- '%a': '%a',
- '%A': '%A',
- '%w': '%w',
- '%d': 'dd',
- '%b': '%b',
- '%B': '%B',
- '%m': 'mm',
- '%y': 'yy',
- '%Y': 'yyyy',
- '%H': 'HH',
- '%I': 'HH(12h)',
- '%p': 'AMPM',
- '%M': 'MM',
- '%S': 'SS',
- '%f': 'µµ',
- '%z': 'UTC(+/-HHMM)',
- '%Z': 'UTC(TZ)',
- '%j': '%j',
- '%U': 'ww',
- '%W': 'ww',
- '%c': '%c',
- '%x': '%x',
- '%X': '%X',
- '%%': '%%',
-}
-
-
-def convert_datetime_format(format):
- i=0
- new_format = ""
- while i < len(format):
- if format[i] == '%':
- char = format[i:i+2]
- new_format += datetime_mapping.get(char, char)
- i += 2
- else:
- new_format += format[i]
- i += 1
- return new_format
-
+from preferences.models import AssoOption
def all_adherent(search_time=None):
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est
@@ -103,7 +59,7 @@ def all_adherent(search_time=None):
vente__in=Vente.objects.filter(
facture__in=Facture.objects.all().exclude(valid=False)
)
- ).filter(date_end__gt=search_time)
+ ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
)
)
).distinct()
@@ -115,7 +71,7 @@ def all_baned(search_time=None):
search_time = timezone.now()
return User.objects.filter(
ban__in=Ban.objects.filter(
- date_end__gt=search_time
+ Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
)
).distinct()
@@ -126,20 +82,23 @@ def all_whitelisted(search_time=None):
search_time = timezone.now()
return User.objects.filter(
whitelist__in=Whitelist.objects.filter(
- date_end__gt=search_time
+ Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
)
).distinct()
def all_has_access(search_time=None):
- """ Renvoie tous les users beneficiant d'une connexion
- : user adherent ou whiteliste et non banni """
+ """ Return all connected users : active users and whitelisted +
+ asso_user defined in AssoOption pannel
+ ----
+ Renvoie tous les users beneficiant d'une connexion
+ : user adherent et whiteliste non banni plus l'utilisateur asso"""
if search_time is None:
search_time = timezone.now()
- return User.objects.filter(
+ filter_user = (
Q(state=User.STATE_ACTIVE) &
- ~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
- (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
+ ~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) &
+ (Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) |
Q(facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter(
@@ -148,10 +107,14 @@ def all_has_access(search_time=None):
facture__in=Facture.objects.all()
.exclude(valid=False)
)
- ).filter(date_end__gt=search_time)
+ ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
)
)))
- ).distinct()
+ )
+ asso_user = AssoOption.get_cached_value('utilisateur_asso')
+ if asso_user:
+ filter_user |= Q(id=asso_user.id)
+ return User.objects.filter(filter_user).distinct()
def filter_active_interfaces(interface_set):
@@ -203,164 +166,6 @@ def all_active_assigned_interfaces_count():
return all_active_interfaces_count().filter(ipv4__isnull=False)
-class SortTable:
- """ Class gathering uselful stuff to sort the colums of a table, according
- to the column and order requested. It's used with a dict of possible
- values and associated model_fields """
-
- # All the possible possible values
- # The naming convention is based on the URL or the views function
- # The syntax to describe the sort to apply is a dict where the keys are
- # the url value and the values are a list of model field name to use to
- # order the request. They are applied in the order they are given.
- # A 'default' might be provided to specify what to do if the requested col
- # doesn't match any keys.
-
- USERS_INDEX = {
- 'user_name': ['name'],
- 'user_surname': ['surname'],
- 'user_pseudo': ['pseudo'],
- 'user_room': ['room'],
- 'default': ['state', 'pseudo']
- }
- USERS_INDEX_BAN = {
- 'ban_user': ['user__pseudo'],
- 'ban_start': ['date_start'],
- 'ban_end': ['date_end'],
- 'default': ['-date_end']
- }
- USERS_INDEX_WHITE = {
- 'white_user': ['user__pseudo'],
- 'white_start': ['date_start'],
- 'white_end': ['date_end'],
- 'default': ['-date_end']
- }
- USERS_INDEX_SCHOOL = {
- 'school_name': ['name'],
- 'default': ['name']
- }
- MACHINES_INDEX = {
- 'machine_name': ['name'],
- 'default': ['pk']
- }
- COTISATIONS_INDEX = {
- 'cotis_user': ['user__pseudo'],
- 'cotis_paiement': ['paiement__moyen'],
- 'cotis_date': ['date'],
- 'cotis_id': ['id'],
- 'default': ['-date']
- }
- COTISATIONS_CUSTOM = {
- 'invoice_date': ['date'],
- 'invoice_id': ['id'],
- 'invoice_recipient': ['recipient'],
- 'invoice_address': ['address'],
- 'invoice_payment': ['payment'],
- 'default': ['-date']
- }
- COTISATIONS_CONTROL = {
- 'control_name': ['user__adherent__name'],
- 'control_surname': ['user__surname'],
- 'control_paiement': ['paiement'],
- 'control_date': ['date'],
- 'control_valid': ['valid'],
- 'control_control': ['control'],
- 'control_id': ['id'],
- 'control_user-id': ['user__id'],
- 'default': ['-date']
- }
- TOPOLOGIE_INDEX = {
- 'switch_dns': ['interface__domain__name'],
- 'switch_ip': ['interface__ipv4__ipv4'],
- 'switch_loc': ['switchbay__name'],
- 'switch_ports': ['number'],
- 'switch_stack': ['stack__name'],
- 'default': ['switchbay', 'stack', 'stack_member_id']
- }
- TOPOLOGIE_INDEX_PORT = {
- 'port_port': ['port'],
- 'port_room': ['room__name'],
- 'port_interface': ['machine_interface__domain__name'],
- 'port_related': ['related__switch__name'],
- 'port_radius': ['radius'],
- 'port_vlan': ['vlan_force__name'],
- 'default': ['port']
- }
- TOPOLOGIE_INDEX_ROOM = {
- 'room_name': ['name'],
- 'default': ['name']
- }
- TOPOLOGIE_INDEX_BUILDING = {
- 'building_name': ['name'],
- 'default': ['name']
- }
- TOPOLOGIE_INDEX_BORNE = {
- 'ap_name': ['interface__domain__name'],
- 'ap_ip': ['interface__ipv4__ipv4'],
- 'ap_mac': ['interface__mac_address'],
- 'default': ['interface__domain__name']
- }
- TOPOLOGIE_INDEX_STACK = {
- 'stack_name': ['name'],
- 'stack_id': ['stack_id'],
- 'default': ['stack_id'],
- }
- TOPOLOGIE_INDEX_MODEL_SWITCH = {
- 'model-switch_name': ['reference'],
- 'model-switch_contructor': ['constructor__name'],
- 'default': ['reference'],
- }
- TOPOLOGIE_INDEX_SWITCH_BAY = {
- 'switch-bay_name': ['name'],
- 'switch-bay_building': ['building__name'],
- 'default': ['name'],
- }
- TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
- 'constructor-switch_name': ['name'],
- 'default': ['name'],
- }
- LOGS_INDEX = {
- 'sum_date': ['revision__date_created'],
- 'default': ['-revision__date_created'],
- }
- LOGS_STATS_LOGS = {
- 'logs_author': ['user__name'],
- 'logs_date': ['date_created'],
- 'default': ['-date_created']
- }
-
- @staticmethod
- def sort(request, col, order, values):
- """ Check if the given values are possible and add .order_by() and
- a .reverse() as specified according to those values """
- fields = values.get(col, None)
- if not fields:
- fields = values.get('default', [])
- request = request.order_by(*fields)
- if values.get(col, None) and order == 'desc':
- return request.reverse()
- else:
- return request
-
-
-def re2o_paginator(request, query_set, pagination_number):
- """Paginator script for list display in re2o.
- :request:
- :query_set: Query_set to paginate
- :pagination_number: Number of entries to display"""
- paginator = Paginator(query_set, pagination_number)
- page = request.GET.get('page')
- try:
- results = paginator.page(page)
- except PageNotAnInteger:
- # If page is not an integer, deliver first page.
- results = paginator.page(1)
- except EmptyPage:
- # If page is out of range (e.g. 9999), deliver last page of results.
- results = paginator.page(paginator.num_pages)
- return results
-
-
def remove_user_room(room):
""" Déménage de force l'ancien locataire de la chambre """
try:
@@ -370,18 +175,3 @@ def remove_user_room(room):
user.room = None
user.save()
-
-def get_input_formats_help_text(input_formats):
- """Returns a help text about the possible input formats"""
- if len(input_formats) > 1:
- help_text_template="Format: {main} {more}"
- else:
- help_text_template="Format: {main}"
- more_text_template=""
- help_text = help_text_template.format(
- main=convert_datetime_format(input_formats[0]),
- more=more_text_template.format(
- '\n'.join(map(convert_datetime_format, input_formats))
- )
- )
- return help_text
diff --git a/search/forms.py b/search/forms.py
index 5c98415f..5fa5fca8 100644
--- a/search/forms.py
+++ b/search/forms.py
@@ -27,7 +27,7 @@ from __future__ import unicode_literals
from django import forms
from django.forms import Form
from django.utils.translation import ugettext_lazy as _
-from re2o.utils import get_input_formats_help_text
+from re2o.base import get_input_formats_help_text
CHOICES_USER = (
('0', _("Active")),
diff --git a/search/locale/fr/LC_MESSAGES/django.mo b/search/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 94a44104..00000000
Binary files a/search/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/search/views.py b/search/views.py
index a92b0105..eb0027ec 100644
--- a/search/views.py
+++ b/search/views.py
@@ -46,7 +46,7 @@ from search.forms import (
CHOICES_AFF,
initial_choices
)
-from re2o.utils import SortTable
+from re2o.base import SortTable
from re2o.acl import can_view_all
diff --git a/static/css/base.css b/static/css/base.css
index 736935b3..a13c596f 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -79,19 +79,6 @@ a > i.fa {
vertical-align: middle;
}
-/* Pull sidebars to the bottom */
-@media (min-width: 767px) {
- .row {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- }
- .row > [class*='col-'] {
- flex-direction: column;
- }
-}
-
/* On small screens, set height to 'auto' for sidenav and grid */
@media screen and (max-width: 767px) {
.sidenav {
@@ -102,7 +89,7 @@ a > i.fa {
}
.table-responsive {
- overflow-y: visible;
+ overflow: visible;
}
/* Make modal wider on wide screens */
@@ -145,3 +132,14 @@ th.long_text{
.dashboard{
text-align: center;
}
+
+/* Detailed information on profile page */
+dl.profile-info {
+ margin-top: -16px;
+ margin-bottom: 0;
+}
+
+dl.profile-info > div {
+ padding: 8px;
+ border-top: 1px solid #ddd;
+}
diff --git a/static/js/collapse-from-url.js b/static/js/collapse-from-url.js
new file mode 100644
index 00000000..6c85762b
--- /dev/null
+++ b/static/js/collapse-from-url.js
@@ -0,0 +1,33 @@
+// Re2o est un logiciel d'administration développé initiallement au rezometz. Il
+// se veut agnostique au réseau considéré, de manière à être installable en
+// quelques clics.
+//
+// Copyright © 2018 Alexandre Iooss
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+// This script makes URL hash controls Bootstrap collapse
+// e.g. if there is #information in the URL
+// then the collapse with id "information" will be open.
+
+$(document).ready(function () {
+ if(location.hash != null && location.hash !== ""){
+ // Open the collapse corresponding to URL hash
+ $(location.hash + '.collapse').collapse('show');
+ } else {
+ // Open default collapse
+ $('.collapse-default.collapse').collapse('show');
+ }
+});
diff --git a/templates/base.html b/templates/base.html
index 76ba975a..867be422 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -45,6 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_javascript %}
+
{# Load CSS #}
{% bootstrap_css %}
diff --git a/templates/locale/fr/LC_MESSAGES/django.mo b/templates/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 6f33dd5a..00000000
Binary files a/templates/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/templates/pagination.html b/templates/pagination.html
index cf488c5d..5ecced6d 100644
--- a/templates/pagination.html
+++ b/templates/pagination.html
@@ -23,23 +23,52 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load url_insert_param %}
+{% load i18n %}
{% if list.paginator.num_pages > 1 %}
-
{% endif %}
-
diff --git a/topologie/forms.py b/topologie/forms.py
index fa089507..ed6fa5b9 100644
--- a/topologie/forms.py
+++ b/topologie/forms.py
@@ -55,6 +55,8 @@ from .models import (
SwitchBay,
Building,
PortProfile,
+ ModuleSwitch,
+ ModuleOnSwitch,
)
@@ -269,3 +271,23 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
prefix=prefix,
**kwargs)
+class EditModuleForm(FormRevMixin, ModelForm):
+ """Add and edit module instance"""
+ class Meta:
+ model = ModuleSwitch
+ fields = '__all__'
+
+ def __init__(self, *args, **kwargs):
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
+
+
+class EditSwitchModuleForm(FormRevMixin, ModelForm):
+ """Add/edit a switch to a module"""
+ class Meta:
+ model = ModuleOnSwitch
+ fields = '__all__'
+
+ def __init__(self, *args, **kwargs):
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditSwitchModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
diff --git a/topologie/locale/fr/LC_MESSAGES/django.mo b/topologie/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 61d2bf80..00000000
Binary files a/topologie/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/topologie/migrations/0067_auto_20181230_1819.py b/topologie/migrations/0067_auto_20181230_1819.py
new file mode 100644
index 00000000..57f268ea
--- /dev/null
+++ b/topologie/migrations/0067_auto_20181230_1819.py
@@ -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')]),
+ ),
+ ]
diff --git a/topologie/migrations/0068_auto_20190102_1758.py b/topologie/migrations/0068_auto_20190102_1758.py
new file mode 100644
index 00000000..b03e7ae5
--- /dev/null
+++ b/topologie/migrations/0068_auto_20190102_1758.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2019-01-02 23:58
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('topologie', '0067_auto_20181230_1819'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='modelswitch',
+ name='is_itself_module',
+ field=models.BooleanField(default=False, help_text='Is the switch, itself, considered as a module'),
+ ),
+ ]
diff --git a/topologie/models.py b/topologie/models.py
index cd191d7e..e05fa50e 100644
--- a/topologie/models.py
+++ b/topologie/models.py
@@ -252,6 +252,7 @@ class Switch(AclMixin, Machine):
help_text='Provision automatique de ce switch',
)
+
class Meta:
unique_together = ('stack', 'stack_member_id')
permissions = (
@@ -281,31 +282,18 @@ class Switch(AclMixin, Machine):
def create_ports(self, begin, end):
""" Crée les ports de begin à end si les valeurs données
sont cohérentes. """
-
- s_begin = s_end = 0
- nb_ports = self.ports.count()
- if nb_ports > 0:
- ports = self.ports.order_by('port').values('port')
- s_begin = ports.first().get('port')
- s_end = ports.last().get('port')
-
if end < begin:
raise ValidationError(_("The end port is less than the start"
" port."))
- if end - begin > self.number:
+ ports_to_create = range(begin, end + 1)
+ existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True)
+ non_existing_ports = list(set(ports_to_create) - set(existing_ports))
+
+ if len(non_existing_ports) + existing_ports.count() > self.number:
raise ValidationError(_("This switch can't have that many ports."))
- begin_range = range(begin, s_begin)
- end_range = range(s_end+1, end+1)
- for i in itertools.chain(begin_range, end_range):
- port = Port()
- port.switch = self
- port.port = i
- try:
- with transaction.atomic(), reversion.create_revision():
- port.save()
- reversion.set_comment(_("Creation"))
- except IntegrityError:
- ValidationError(_("Creation of an existing port."))
+ with transaction.atomic(), reversion.create_revision():
+ reversion.set_comment(_("Creation"))
+ Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports])
def main_interface(self):
""" Returns the 'main' interface of the switch
@@ -317,7 +305,7 @@ class Switch(AclMixin, Machine):
@cached_property
def get_name(self):
- return self.name or self.main_interface().domain.name
+ return self.name or getattr(self.main_interface(), 'domain', 'Unknown')
@cached_property
def get_radius_key(self):
@@ -380,6 +368,17 @@ class Switch(AclMixin, Machine):
"""Return dict ip6:subnet for all ipv6 of the switch"""
return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
+ @cached_property
+ def list_modules(self):
+ """Return modules of that switch, list of dict (rank, reference)"""
+ modules = []
+ if getattr(self.model, 'is_modular', None):
+ if self.model.is_itself_module:
+ modules.append((1, self.model.reference))
+ for module_of_self in self.moduleonswitch_set.all():
+ modules.append((module_of_self.slot, module_of_self.module.reference))
+ return modules
+
def __str__(self):
return str(self.get_name)
@@ -402,6 +401,14 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
null=True,
blank=True
)
+ is_modular = models.BooleanField(
+ default=False,
+ help_text=_("Is this switch model modular"),
+ )
+ is_itself_module = models.BooleanField(
+ default=False,
+ help_text=_("Is the switch, itself, considered as a module"),
+ )
class Meta:
permissions = (
@@ -417,6 +424,53 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
return str(self.constructor) + ' ' + self.reference
+class ModuleSwitch(AclMixin, RevMixin, models.Model):
+ """A module of a switch"""
+ reference = models.CharField(
+ max_length=255,
+ help_text=_("Reference of a module"),
+ verbose_name=_("Module reference")
+ )
+ comment = models.CharField(
+ max_length=255,
+ null=True,
+ blank=True,
+ help_text=_("Comment"),
+ verbose_name=_("Comment")
+ )
+
+ class Meta:
+ permissions = (
+ ("view_moduleswitch", _("Can view a module object")),
+ )
+ verbose_name = _("Module of a switch")
+
+
+ def __str__(self):
+ return str(self.reference)
+
+
+class ModuleOnSwitch(AclMixin, RevMixin, models.Model):
+ """Link beetween module and switch"""
+ module = models.ForeignKey('ModuleSwitch', on_delete=models.CASCADE)
+ switch = models.ForeignKey('Switch', on_delete=models.CASCADE)
+ slot = models.CharField(
+ max_length=15,
+ help_text=_("Slot on switch"),
+ verbose_name=_("Slot")
+ )
+
+ class Meta:
+ permissions = (
+ ("view_moduleonswitch", _("Can view a moduleonswitch object")),
+ )
+ verbose_name = _("link between switchs and modules")
+ unique_together = ['slot', 'switch']
+
+ def __str__(self):
+ return 'On slot ' + str(self.slot) + ' of ' + str(self.switch)
+
+
class ConstructorSwitch(AclMixin, RevMixin, models.Model):
"""Un constructeur de switch"""
diff --git a/topologie/templates/topologie/aff_modules.html b/topologie/templates/topologie/aff_modules.html
new file mode 100644
index 00000000..0c7a3207
--- /dev/null
+++ b/topologie/templates/topologie/aff_modules.html
@@ -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 %}
+
+
+
+
+ {% trans "Reference" %} |
+ {% trans "Comment" %} |
+ {% trans "Switchs" %} |
+ |
+
+
+ {% for module in module_list %}
+
+ {{ module.reference }} |
+ {{ module.comment }} |
+
+ {% for module_switch in module.moduleonswitch_set.all %}
+ Slot {{ module_switch.slot }} of {{ module_switch.switch }}
+ {% can_edit module_switch %}
+
+
+
+ {% acl_end %}
+ {% can_delete module_switch %}
+
+
+
+ {% acl_end %}
+
+ {% endfor %}
+ |
+
+ {% can_edit module %}
+
+
+
+
+
+
+ {% acl_end %}
+ {% history_button module %}
+ {% can_delete module %}
+
+
+
+ {% acl_end %}
+ |
+
+ {% endfor %}
+
+
+{% if module_list.paginator %}
+{% include "pagination.html" with list=module_list %}
+{% endif %}
+
+{% trans "All modular switchs" %}
+
+
+
+ {% trans "Switch" %} |
+ {% trans "Reference" %} |
+ {% trans "Slot" %} |
+
+ {% for switch in modular_switchs %}
+ {% if switch.list_modules %}
+
+
+ {{ switch }}
+ |
+
+ {% for module in switch.list_modules %}
+
+ |
+ {{ module.1 }} |
+ {{ module.0 }} |
+
+ {% endfor %}
+{% endif %}
+{% endfor %}
+
diff --git a/topologie/templates/topologie/index_module.html b/topologie/templates/topologie/index_module.html
new file mode 100644
index 00000000..d9cc2925
--- /dev/null
+++ b/topologie/templates/topologie/index_module.html
@@ -0,0 +1,43 @@
+{% extends "topologie/sidebar.html" %}
+{% comment %}
+Re2o est un logiciel d'administration développé initiallement au rezometz. Il
+se veut agnostique au réseau considéré, de manière à être installable en
+quelques clics.
+
+Copyright © 2017 Gabriel Détraz
+Copyright © 2017 Goulven Kermarec
+Copyright © 2017 Augustin Lemesle
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+{% endcomment %}
+
+{% load bootstrap3 %}
+{% load acl %}
+{% load i18n %}
+
+{% block title %}{% trans "Topology" %}{% endblock %}
+
+{% block content %}
+{% trans "Modules of switchs" %}
+{% can_create ModuleSwitch %}
+{% trans " Add a module" %}
+
+{% acl_end %}
+ {% include "topologie/aff_modules.html" with module_list=module_list modular_switchs=modular_switchs %}
+
+
+
+{% endblock %}
+
diff --git a/topologie/templates/topologie/sidebar.html b/topologie/templates/topologie/sidebar.html
index a35721f9..80317a16 100644
--- a/topologie/templates/topologie/sidebar.html
+++ b/topologie/templates/topologie/sidebar.html
@@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Switches" %}
+
+
+
+ {% trans "Switches modules" %}
diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html
index bf9f760b..a7824020 100644
--- a/topologie/templates/topologie/topo.html
+++ b/topologie/templates/topologie/topo.html
@@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
diff --git a/topologie/urls.py b/topologie/urls.py
index 77d68d50..70eae8e4 100644
--- a/topologie/urls.py
+++ b/topologie/urls.py
@@ -123,4 +123,15 @@ urlpatterns = [
url(r'^edit_vlanoptions/(?P[0-9]+)$',
views.edit_vlanoptions,
name='edit-vlanoptions'),
- ]
+ url(r'^add_module/$', views.add_module, name='add-module'),
+ url(r'^edit_module/(?P[0-9]+)$',
+ views.edit_module,
+ name='edit-module'),
+ url(r'^del_module/(?P[0-9]+)$', views.del_module, name='del-module'),
+ url(r'^index_module/$', views.index_module, name='index-module'),
+ url(r'^add_module_on/$', views.add_module_on, name='add-module-on'),
+ url(r'^edit_module_on/(?P[0-9]+)$',
+ views.edit_module_on,
+ name='edit-module-on'),
+ url(r'^del_module_on/(?P[0-9]+)$', views.del_module_on, name='del-module-on'),
+]
diff --git a/topologie/views.py b/topologie/views.py
index 0bd0f6c2..55f0a060 100644
--- a/topologie/views.py
+++ b/topologie/views.py
@@ -48,7 +48,10 @@ from django.utils.translation import ugettext as _
import tempfile
from users.views import form
-from re2o.utils import re2o_paginator, SortTable
+from re2o.base import (
+ re2o_paginator,
+ SortTable,
+)
from re2o.acl import (
can_create,
can_edit,
@@ -83,6 +86,8 @@ from .models import (
Building,
Server,
PortProfile,
+ ModuleSwitch,
+ ModuleOnSwitch,
)
from .forms import (
EditPortForm,
@@ -99,6 +104,8 @@ from .forms import (
EditSwitchBayForm,
EditBuildingForm,
EditPortProfileForm,
+ EditModuleForm,
+ EditSwitchModuleForm,
)
from subprocess import (
@@ -313,6 +320,22 @@ def index_model_switch(request):
)
+@login_required
+@can_view_all(ModuleSwitch)
+def index_module(request):
+ """Display all modules of switchs"""
+ module_list = ModuleSwitch.objects.all()
+ modular_switchs = Switch.objects.filter(model__is_modular=True)
+ pagination_number = GeneralOption.get_cached_value('pagination_number')
+ module_list = re2o_paginator(request, module_list, pagination_number)
+ return render(
+ request,
+ 'topologie/index_module.html',
+ {'module_list': module_list,
+ 'modular_switchs': modular_switchs}
+ )
+
+
@login_required
@can_edit(Vlan)
def edit_vlanoptions(request, vlan_instance, **_kwargs):
@@ -530,19 +553,13 @@ def create_ports(request, switchid):
except Switch.DoesNotExist:
messages.error(request, _("Nonexistent switch"))
return redirect(reverse('topologie:index'))
-
- s_begin = s_end = 0
- nb_ports = switch.ports.count()
- if nb_ports > 0:
- ports = switch.ports.order_by('port').values('port')
- s_begin = ports.first().get('port')
- s_end = ports.last().get('port')
-
+
+ first_port = getattr(switch.ports.order_by('port').first(), 'port', 1)
+ last_port = switch.number + first_port - 1
port_form = CreatePortsForm(
request.POST or None,
- initial={'begin': s_begin, 'end': s_end}
+ initial={'begin': first_port, 'end': last_port}
)
-
if port_form.is_valid():
begin = port_form.cleaned_data['begin']
end = port_form.cleaned_data['end']
@@ -1051,6 +1068,115 @@ def del_port_profile(request, port_profile, **_kwargs):
)
+@login_required
+@can_create(ModuleSwitch)
+def add_module(request):
+ """ View used to add a Module object """
+ module = EditModuleForm(request.POST or None)
+ if module.is_valid():
+ module.save()
+ messages.success(request, _("The module was created."))
+ return redirect(reverse('topologie:index-module'))
+ return form(
+ {'topoform': module, 'action_name': _("Create a module")},
+ 'topologie/topo.html',
+ request
+ )
+
+
+@login_required
+@can_edit(ModuleSwitch)
+def edit_module(request, module_instance, **_kwargs):
+ """ View used to edit a Module object """
+ module = EditModuleForm(request.POST or None, instance=module_instance)
+ if module.is_valid():
+ if module.changed_data:
+ module.save()
+ messages.success(request, _("The module was edited."))
+ return redirect(reverse('topologie:index-module'))
+ return form(
+ {'topoform': module, 'action_name': _("Edit")},
+ 'topologie/topo.html',
+ request
+ )
+
+
+@login_required
+@can_delete(ModuleSwitch)
+def del_module(request, module, **_kwargs):
+ """Compleete delete a module"""
+ if request.method == "POST":
+ try:
+ module.delete()
+ messages.success(request, _("The module was deleted."))
+ except ProtectedError:
+ messages.error(
+ request,
+ (_("The module %s is used by another object, impossible to"
+ " deleted it.") % module)
+ )
+ return redirect(reverse('topologie:index-module'))
+ return form(
+ {'objet': module, 'objet_name': _("Module")},
+ 'topologie/delete.html',
+ request
+ )
+
+@login_required
+@can_create(ModuleOnSwitch)
+def add_module_on(request):
+ """Add a module to a switch"""
+ module_switch = EditSwitchModuleForm(request.POST or None)
+ if module_switch.is_valid():
+ module_switch.save()
+ messages.success(request, _("The module added to that switch"))
+ return redirect(reverse('topologie:index-module'))
+ return form(
+ {'topoform': module_switch, 'action_name': _("Create")},
+ 'topologie/topo.html',
+ request
+ )
+
+
+@login_required
+@can_edit(ModuleOnSwitch)
+def edit_module_on(request, module_instance, **_kwargs):
+ """ View used to edit a Module object """
+ module = EditSwitchModuleForm(request.POST or None, instance=module_instance)
+ if module.is_valid():
+ if module.changed_data:
+ module.save()
+ messages.success(request, _("The module was edited."))
+ return redirect(reverse('topologie:index-module'))
+ return form(
+ {'topoform': module, 'action_name': _("Edit")},
+ 'topologie/topo.html',
+ request
+ )
+
+
+@login_required
+@can_delete(ModuleOnSwitch)
+def del_module_on(request, module, **_kwargs):
+ """Compleete delete a module"""
+ if request.method == "POST":
+ try:
+ module.delete()
+ messages.success(request, _("The module was deleted."))
+ except ProtectedError:
+ messages.error(
+ request,
+ (_("The module %s is used by another object, impossible to"
+ " deleted it.") % module)
+ )
+ return redirect(reverse('topologie:index-module'))
+ return form(
+ {'objet': module, 'objet_name': _("Module")},
+ 'topologie/delete.html',
+ request
+ )
+
+
def make_machine_graph():
"""
Create the graph of switchs, machines and access points.
diff --git a/users/forms.py b/users/forms.py
index e2f09dca..d4110dcd 100644
--- a/users/forms.py
+++ b/users/forms.py
@@ -45,7 +45,8 @@ from django.utils.safestring import mark_safe
from machines.models import Interface, Machine, Nas
from topologie.models import Port
from preferences.models import OptionalUser
-from re2o.utils import remove_user_room, get_input_formats_help_text
+from re2o.utils import remove_user_room
+from re2o.base import get_input_formats_help_text
from re2o.mixins import FormRevMixin
from re2o.field_permissions import FieldPermissionFormMixin
@@ -116,6 +117,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
"""Changement du mot de passe"""
user = super(PassForm, self).save(commit=False)
user.set_password(self.cleaned_data.get("passwd1"))
+ user.set_active()
user.save()
@@ -323,14 +325,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['room'].empty_label = _("No room")
self.fields['school'].empty_label = _("Select a school")
- def clean_email(self):
- if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
- return self.cleaned_data.get('email').lower()
- else:
- raise forms.ValidationError(
- _("You can't use a {} address.").format(
- OptionalUser.objects.first().local_email_domain))
-
class Meta:
model = Adherent
fields = [
@@ -344,6 +338,19 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'room',
]
+ force = forms.BooleanField(
+ label=_("Force the move?"),
+ initial=False,
+ required=False
+ )
+
+ def clean_email(self):
+ if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
+ return self.cleaned_data.get('email').lower()
+ else:
+ raise forms.ValidationError(
+ _("You can't use a {} address.").format(
+ OptionalUser.objects.first().local_email_domain))
def clean_telephone(self):
"""Verifie que le tel est présent si 'option est validée
@@ -355,12 +362,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
)
return telephone
- force = forms.BooleanField(
- label=_("Force the move?"),
- initial=False,
- required=False
- )
-
def clean_force(self):
"""On supprime l'ancien user de la chambre si et seulement si la
case est cochée"""
@@ -368,6 +369,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
remove_user_room(self.cleaned_data.get('room'))
return
+
class AdherentCreationForm(AdherentForm):
"""Formulaire de création d'un user.
AdherentForm auquel on ajoute une checkbox afin d'éviter les
@@ -383,8 +385,22 @@ class AdherentCreationForm(AdherentForm):
# Checkbox for GTU
gtu_check = forms.BooleanField(required=True)
- gtu_check.label = mark_safe("{} {}{}".format(
- _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
+ #gtu_check.label = mark_safe("{} {}{}".format(
+ # _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
+
+ class Meta:
+ model = Adherent
+ fields = [
+ 'name',
+ 'surname',
+ 'pseudo',
+ 'email',
+ 'school',
+ 'comment',
+ 'telephone',
+ 'room',
+ 'state',
+ ]
def __init__(self, *args, **kwargs):
super(AdherentCreationForm, self).__init__(*args, **kwargs)
@@ -398,12 +414,6 @@ class AdherentEditForm(AdherentForm):
if 'shell' in self.fields:
self.fields['shell'].empty_label = _("Default shell")
- def clean_gpg_fingerprint(self):
- """Format the GPG fingerprint"""
- gpg_fingerprint = self.cleaned_data.get('gpg_fingerprint', None)
- if gpg_fingerprint:
- return gpg_fingerprint.replace(' ', '').upper()
-
class Meta:
model = Adherent
fields = [
@@ -429,6 +439,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['surname'].label = _("Name")
self.fields['school'].label = _("School")
self.fields['comment'].label = _("Comment")
+ self.fields['email'].label = _("Email Address")
if 'room' in self.fields:
self.fields['room'].label = _("Room")
self.fields['room'].empty_label = _("No room")
@@ -443,7 +454,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'school',
'comment',
'room',
+ 'email',
'telephone',
+ 'email',
'shell',
'mailing'
]
@@ -488,13 +501,14 @@ class PasswordForm(FormRevMixin, ModelForm):
class ServiceUserForm(FormRevMixin, ModelForm):
- """ Modification d'un service user"""
+ """Service user creation
+ force initial password set"""
password = forms.CharField(
label=_("New password"),
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
- required=False
+ required=True
)
class Meta:
@@ -506,7 +520,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
def save(self, commit=True):
- """Changement du mot de passe"""
+ """Password change"""
user = super(ServiceUserForm, self).save(commit=False)
if self.cleaned_data['password']:
user.set_password(self.cleaned_data.get("password"))
@@ -516,6 +530,14 @@ class ServiceUserForm(FormRevMixin, ModelForm):
class EditServiceUserForm(ServiceUserForm):
"""Formulaire d'edition de base d'un service user. Ne permet
d'editer que son group d'acl et son commentaire"""
+ password = forms.CharField(
+ label=_("New password"),
+ max_length=255,
+ validators=[MinLengthValidator(8)],
+ widget=forms.PasswordInput,
+ required=False
+ )
+
class Meta(ServiceUserForm.Meta):
fields = ['access_group', 'comment']
diff --git a/users/locale/fr/LC_MESSAGES/django.mo b/users/locale/fr/LC_MESSAGES/django.mo
deleted file mode 100644
index 4547ff8e..00000000
Binary files a/users/locale/fr/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/users/migrations/0079_auto_20181228_2039.py b/users/migrations/0079_auto_20181228_2039.py
new file mode 100644
index 00000000..79ab56d9
--- /dev/null
+++ b/users/migrations/0079_auto_20181228_2039.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2018-12-28 19:39
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0078_auto_20181011_1405'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='adherent',
+ name='gpg_fingerprint',
+ field=models.CharField(blank=True, max_length=49, null=True),
+ ),
+ ]
diff --git a/users/models.py b/users/models.py
index 63d0a875..a2798207 100755
--- a/users/models.py
+++ b/users/models.py
@@ -81,6 +81,7 @@ from re2o.settings import LDAP, GID_RANGES, UID_RANGES
from re2o.login import hashNT
from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin
+from re2o.base import smtp_check
from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine, regen
@@ -93,7 +94,7 @@ from preferences.models import OptionalMachine, MailMessageOption
def linux_user_check(login):
""" Validation du pseudo pour respecter les contraintes unix"""
- UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9-]*[$]?$")
+ UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$")
return UNIX_LOGIN_PATTERN.match(login)
@@ -336,7 +337,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
def set_active(self):
"""Enable this user if he subscribed successfully one time before"""
if self.state == self.STATE_NOT_YET_ACTIVE:
- if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists():
+ if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists() or OptionalUser.get_cached_value('all_users_active'):
self.state = self.STATE_ACTIVE
self.save()
@@ -474,7 +475,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
""" Renvoie si un utilisateur a accès à internet """
return (self.state == User.STATE_ACTIVE and
not self.is_ban() and
- (self.is_connected() or self.is_whitelisted()))
+ (self.is_connected() or self.is_whitelisted())) \
+ or self == AssoOption.get_cached_value('utilisateur_asso')
def end_access(self):
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
@@ -576,7 +578,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
mac_refresh : synchronise les machines de l'user
group_refresh : synchronise les group de l'user
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
- if sys.version_info[0] >= 3:
+ if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\
+ self.state != self.STATE_DISABLED:
self.refresh_from_db()
try:
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
@@ -693,10 +696,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
def autoregister_machine(self, mac_address, nas_type):
""" Fonction appellée par freeradius. Enregistre la mac pour
une machine inconnue sur le compte de l'user"""
- all_interfaces = self.user_interfaces(active=False)
- if all_interfaces.count() > OptionalMachine.get_cached_value(
- 'max_lambdauser_interfaces'
- ):
+ allowed, _message = Machine.can_create(self, self.id)
+ if not allowed:
return False, _("Maximum number of registered machines reached.")
if not nas_type:
return False, _("Re2o doesn't know wich machine type to assign.")
@@ -1025,17 +1026,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
):
raise ValidationError("This pseudo is already in use.")
if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE):
- raise ValidationError(
- {'email': (
- _("There is neither a local email address nor an external"
+ raise ValidationError(_("There is neither a local email address nor an external"
" email address for this user.")
- ), }
)
if self.local_email_redirect and not self.email:
- raise ValidationError(
- {'local_email_redirect': (
- _("You can't redirect your local emails if no external email"
- " address has been set.")), }
+ raise ValidationError(_("You can't redirect your local emails if no external email"
+ " address has been set.")
)
def __str__(self):
@@ -1054,20 +1050,27 @@ class Adherent(User):
null=True
)
gpg_fingerprint = models.CharField(
- max_length=40,
+ max_length=49,
blank=True,
null=True,
- validators=[RegexValidator(
- '^[0-9A-F]{40}$',
- message=_("A GPG fingerprint must contain 40 hexadecimal"
- " characters.")
- )]
)
class Meta(User.Meta):
verbose_name = _("member")
verbose_name_plural = _("members")
+ def format_gpgfp(self):
+ """Format gpg finger print as AAAA BBBB... from a string AAAABBBB...."""
+ self.gpg_fingerprint = ' '.join([self.gpg_fingerprint[i:i + 4] for i in range(0, len(self.gpg_fingerprint), 4)])
+
+ def validate_gpgfp(self):
+ """Validate from raw entry if is it a valid gpg fp"""
+ if self.gpg_fingerprint:
+ gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper()
+ if not re.match("^[0-9A-F]{40}$", gpg_fingerprint):
+ raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters"))
+ self.gpg_fingerprint = gpg_fingerprint
+
@classmethod
def get_instance(cls, adherentid, *_args, **_kwargs):
"""Try to find an instance of `Adherent` with the given id.
@@ -1098,6 +1101,13 @@ class Adherent(User):
_("You don't have the right to create a user.")
)
+ def clean(self, *args, **kwargs):
+ """Format the GPG fingerprint"""
+ super(Adherent, self).clean(*args, **kwargs)
+ if self.gpg_fingerprint:
+ self.validate_gpgfp()
+ self.format_gpgfp()
+
class Club(User):
""" A class representing a club (it is considered as a user
@@ -1889,6 +1899,9 @@ class EMailAddress(RevMixin, AclMixin, models.Model):
def clean(self, *args, **kwargs):
self.local_part = self.local_part.lower()
- if "@" in self.local_part:
- raise ValidationError(_("The local part must not contain @."))
+ if "@" in self.local_part or "+" in self.local_part:
+ raise ValidationError(_("The local part must not contain @ or +."))
+ result, reason = smtp_check(self.local_part)
+ if result:
+ raise ValidationError(reason)
super(EMailAddress, self).clean(*args, **kwargs)
diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html
index cdc9a9d1..b4fa7875 100644
--- a/users/templates/users/profil.html
+++ b/users/templates/users/profil.html
@@ -23,7 +23,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
-{% load bootstrap3 %}
{% load acl %}
{% load logs_extra %}
{% load design %}
@@ -78,8 +77,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if solde_activated %}
-
- {{ users.solde }}
+
+ {{ users.solde }}
|