mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-23 07:23:46 +00:00
Merge branch 'dev' of https://gitlab.federez.net/re2o/re2o into new_radius_api
This commit is contained in:
commit
5315a8964e
57 changed files with 1353 additions and 4928 deletions
|
@ -239,7 +239,9 @@ class AutodetectACLPermission(permissions.BasePermission):
|
|||
if getattr(view, "_ignore_model_permissions", False):
|
||||
return True
|
||||
|
||||
if not getattr(view, "queryset", getattr(view, "get_queryset", None)):
|
||||
# Bypass permission verifications if it is a functional view
|
||||
# (permissions are handled by ACL)
|
||||
if not hasattr(view, "queryset") and not hasattr(view, "get_queryset"):
|
||||
return True
|
||||
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
|
|
|
@ -46,6 +46,7 @@ from django.shortcuts import get_object_or_404
|
|||
|
||||
from re2o.field_permissions import FieldPermissionFormMixin
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.widgets import AutocompleteModelWidget
|
||||
from .models import (
|
||||
Article,
|
||||
Paiement,
|
||||
|
@ -79,6 +80,10 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Facture
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"user": AutocompleteModelWidget(url="/users/user-autocomplete"),
|
||||
"banque": AutocompleteModelWidget(url="/cotisations/banque-autocomplete"),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(FactureForm, self).clean()
|
||||
|
|
59
cotisations/migrations/0051_auto_20201228_1636.py
Normal file
59
cotisations/migrations/0051_auto_20201228_1636.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.29 on 2020-12-28 15:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("cotisations", "0050_auto_20201102_2342"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="article",
|
||||
name="duration_connection",
|
||||
field=models.PositiveIntegerField(
|
||||
verbose_name="duration of the connection (in months)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="article",
|
||||
name="duration_days_connection",
|
||||
field=models.PositiveIntegerField(
|
||||
verbose_name="duration of the connection (in days, will be added to duration in months)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="article",
|
||||
name="duration_days_membership",
|
||||
field=models.PositiveIntegerField(
|
||||
verbose_name="duration of the membership (in days, will be added to duration in months)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="article",
|
||||
name="duration_membership",
|
||||
field=models.PositiveIntegerField(
|
||||
verbose_name="duration of the membership (in months)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="vente",
|
||||
name="duration_days_connection",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name="duration of the connection (in days, will be added to duration in months)",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="vente",
|
||||
name="duration_days_membership",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name="duration of the membership (in days, will be added to duration in months)",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -98,7 +98,7 @@ class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|||
|
||||
def name_detailed(self):
|
||||
"""
|
||||
Return:
|
||||
Return:
|
||||
- a list of strings with the name of all article in the invoice
|
||||
and their quantity.
|
||||
"""
|
||||
|
@ -248,8 +248,8 @@ class Facture(BaseInvoice):
|
|||
|
||||
@staticmethod
|
||||
def can_change_control(user_request, *_args, **_kwargs):
|
||||
""" Returns True if the user can change the 'controlled' status of
|
||||
this invoice """
|
||||
"""Returns True if the user can change the 'controlled' status of
|
||||
this invoice"""
|
||||
can = user_request.has_perm("cotisations.change_facture_control")
|
||||
return (
|
||||
can,
|
||||
|
@ -293,8 +293,7 @@ class Facture(BaseInvoice):
|
|||
"""Returns every subscription associated with this invoice."""
|
||||
return Cotisation.objects.filter(
|
||||
vente__in=self.vente_set.filter(
|
||||
~(Q(duration_membership=0)) |\
|
||||
~(Q(duration_days_membership=0))
|
||||
~(Q(duration_membership=0)) | ~(Q(duration_days_membership=0))
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -454,7 +453,7 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
number = models.IntegerField(
|
||||
validators=[MinValueValidator(1)], verbose_name=_("amount")
|
||||
)
|
||||
# TODO : change this field for a ForeinKey to Article
|
||||
# TODO : change this field for a ForeinKey to Article
|
||||
# Note: With a foreign key, modifing an Article modifis the Purchase, wich is bad.
|
||||
# To use a foreign key, you need to make Article read only
|
||||
name = models.CharField(max_length=255, verbose_name=_("article"))
|
||||
|
@ -467,16 +466,18 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
)
|
||||
duration_days_connection = models.PositiveIntegerField(
|
||||
default=0,
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_("duration of the connection (in days, will be added to duration in months)"),
|
||||
verbose_name=_(
|
||||
"duration of the connection (in days, will be added to duration in months)"
|
||||
),
|
||||
)
|
||||
duration_membership = models.PositiveIntegerField(
|
||||
default=0, verbose_name=_("duration of the membership (in months)")
|
||||
)
|
||||
duration_days_membership = models.PositiveIntegerField(
|
||||
default=0,
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_("duration of the membership (in days, will be added to duration in months)"),
|
||||
verbose_name=_(
|
||||
"duration of the membership (in days, will be added to duration in months)"
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -513,7 +514,7 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
|
||||
def create_cotis(self, date_start_con=False, date_start_memb=False):
|
||||
"""
|
||||
Creates a cotisation without initializing the dates (start and end ar set to self.facture.facture.date)
|
||||
Creates a cotisation without initializing the dates (start and end ar set to self.facture.facture.date)
|
||||
and without saving it. You should use Facture.reorder_purchases to set the right dates.
|
||||
"""
|
||||
try:
|
||||
|
@ -631,12 +632,14 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
return str(self.name) + " " + str(self.facture)
|
||||
|
||||
def test_membership_or_connection(self):
|
||||
""" Test if the purchase include membership or connecton
|
||||
"""
|
||||
return self.duration_membership or \
|
||||
self.duration_days_membership or \
|
||||
self.duration_connection or \
|
||||
self.duration_days_connection
|
||||
"""Test if the purchase include membership or connecton"""
|
||||
return (
|
||||
self.duration_membership
|
||||
or self.duration_days_membership
|
||||
or self.duration_connection
|
||||
or self.duration_days_connection
|
||||
)
|
||||
|
||||
|
||||
# TODO : change vente to purchase
|
||||
@receiver(post_save, sender=Vente)
|
||||
|
@ -704,20 +707,20 @@ class Article(RevMixin, AclMixin, models.Model):
|
|||
)
|
||||
|
||||
duration_membership = models.PositiveIntegerField(
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_("duration of the membership (in months)")
|
||||
)
|
||||
duration_days_membership = models.PositiveIntegerField(
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_("duration of the membership (in days, will be added to duration in months)"),
|
||||
verbose_name=_(
|
||||
"duration of the membership (in days, will be added to duration in months)"
|
||||
),
|
||||
)
|
||||
duration_connection = models.PositiveIntegerField(
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_("duration of the connection (in months)")
|
||||
)
|
||||
duration_days_connection = models.PositiveIntegerField(
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_("duration of the connection (in days, will be added to duration in months)"),
|
||||
verbose_name=_(
|
||||
"duration of the connection (in days, will be added to duration in months)"
|
||||
),
|
||||
)
|
||||
|
||||
need_membership = models.BooleanField(
|
||||
|
@ -793,8 +796,8 @@ class Article(RevMixin, AclMixin, models.Model):
|
|||
if target_user is not None and not target_user.is_adherent():
|
||||
objects_pool = objects_pool.filter(
|
||||
Q(duration_membership__gt=0)
|
||||
|Q(duration_days_membership__gt=0)
|
||||
|Q(need_membership=False)
|
||||
| Q(duration_days_membership__gt=0)
|
||||
| Q(need_membership=False)
|
||||
)
|
||||
if user.has_perm("cotisations.buy_every_article"):
|
||||
return objects_pool
|
||||
|
@ -884,7 +887,9 @@ class Paiement(RevMixin, AclMixin, models.Model):
|
|||
|
||||
# In case a cotisation was bought, inform the user, the
|
||||
# cotisation time has been extended too
|
||||
if any(sell.test_membership_or_connection() for sell in invoice.vente_set.all()):
|
||||
if any(
|
||||
sell.test_membership_or_connection() for sell in invoice.vente_set.all()
|
||||
):
|
||||
messages.success(
|
||||
request,
|
||||
_(
|
||||
|
@ -956,9 +961,13 @@ class Cotisation(RevMixin, AclMixin, models.Model):
|
|||
vente = models.OneToOneField(
|
||||
"Vente", on_delete=models.CASCADE, null=True, verbose_name=_("purchase")
|
||||
)
|
||||
date_start_con = models.DateTimeField(verbose_name=_("start date for the connection"))
|
||||
date_start_con = models.DateTimeField(
|
||||
verbose_name=_("start date for the connection")
|
||||
)
|
||||
date_end_con = models.DateTimeField(verbose_name=_("end date for the connection"))
|
||||
date_start_memb = models.DateTimeField(verbose_name=_("start date for the membership"))
|
||||
date_start_memb = models.DateTimeField(
|
||||
verbose_name=_("start date for the membership")
|
||||
)
|
||||
date_end_memb = models.DateTimeField(verbose_name=_("end date for the membership"))
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -25,13 +25,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Creation and editing of invoices" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors factureform %}
|
||||
{{ factureform.media }}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% else %}
|
||||
<h3>{% trans "Edit invoice" %}</h3>
|
||||
{% endif %}
|
||||
{% massive_bootstrap_form factureform 'user' %}
|
||||
{% bootstrap_form factureform %}
|
||||
{{ venteform.management_form }}
|
||||
<h3>{% trans "Articles" %}</h3>
|
||||
<table class="table table-striped">
|
||||
|
|
|
@ -27,7 +27,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
from . import views, views_autocomplete
|
||||
from . import payment_methods
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -104,4 +104,6 @@ urlpatterns = [
|
|||
url(r"^index_paiement/$", views.index_paiement, name="index-paiement"),
|
||||
url(r"^control/$", views.control, name="control"),
|
||||
url(r"^$", views.index, name="index"),
|
||||
### Autocomplete Views
|
||||
url(r'^banque-autocomplete/$', views_autocomplete.BanqueAutocomplete.as_view(), name='banque-autocomplete',),
|
||||
] + payment_methods.urls.urlpatterns
|
||||
|
|
50
cotisations/views_autocomplete.py
Normal file
50
cotisations/views_autocomplete.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||
# quelques clics.
|
||||
#
|
||||
# Copyright © 2017-2020 Gabriel Détraz
|
||||
# Copyright © 2017-2020 Jean-Romain Garnier
|
||||
#
|
||||
# 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.
|
||||
|
||||
# App de gestion des users pour re2o
|
||||
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin
|
||||
# Gplv2
|
||||
"""
|
||||
Django views autocomplete view
|
||||
|
||||
Here are defined the autocomplete class based view.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Q, Value, CharField
|
||||
|
||||
from .models import (
|
||||
Banque
|
||||
)
|
||||
|
||||
from re2o.views import AutocompleteViewMixin
|
||||
|
||||
from re2o.acl import (
|
||||
can_view_all,
|
||||
)
|
||||
|
||||
|
||||
class BanqueAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Banque
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ from django import forms
|
|||
from django.forms import Form
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from re2o.base import get_input_formats_help_text
|
||||
from re2o.widgets import AutocompleteModelWidget
|
||||
|
||||
import inspect
|
||||
|
||||
|
@ -46,10 +47,7 @@ CHOICES_ACTION_TYPE = (
|
|||
("all", _("All")),
|
||||
)
|
||||
|
||||
CHOICES_TYPE = (
|
||||
("ip", _("IPv4")),
|
||||
("mac", _("MAC address")),
|
||||
)
|
||||
CHOICES_TYPE = (("ip", _("IPv4")), ("mac", _("MAC address")))
|
||||
|
||||
|
||||
def all_classes(module):
|
||||
|
@ -87,14 +85,11 @@ def classes_for_action_type(action_type):
|
|||
users.models.User.__name__,
|
||||
users.models.Adherent.__name__,
|
||||
users.models.Club.__name__,
|
||||
users.models.EMailAddress.__name__
|
||||
users.models.EMailAddress.__name__,
|
||||
]
|
||||
|
||||
if action_type == "machines":
|
||||
return [
|
||||
machines.models.Machine.__name__,
|
||||
machines.models.Interface.__name__
|
||||
]
|
||||
return [machines.models.Machine.__name__, machines.models.Interface.__name__]
|
||||
|
||||
if action_type == "subscriptions":
|
||||
return all_classes(cotisations.models)
|
||||
|
@ -114,40 +109,39 @@ def classes_for_action_type(action_type):
|
|||
|
||||
class ActionsSearchForm(Form):
|
||||
"""Form used to do an advanced search through the logs."""
|
||||
u = forms.ModelChoiceField(
|
||||
|
||||
user = forms.ModelChoiceField(
|
||||
label=_("Performed by"),
|
||||
queryset=users.models.User.objects.all(),
|
||||
required=False,
|
||||
widget=AutocompleteModelWidget(url="/users/user-autocomplete"),
|
||||
)
|
||||
t = forms.MultipleChoiceField(
|
||||
action_type = forms.MultipleChoiceField(
|
||||
label=_("Action type"),
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
choices=CHOICES_ACTION_TYPE,
|
||||
initial=[i[0] for i in CHOICES_ACTION_TYPE],
|
||||
)
|
||||
s = forms.DateField(required=False, label=_("Start date"))
|
||||
e = forms.DateField(required=False, label=_("End date"))
|
||||
start_date = forms.DateField(required=False, label=_("Start date"))
|
||||
end_date = forms.DateField(required=False, label=_("End date"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ActionsSearchForm, self).__init__(*args, **kwargs)
|
||||
self.fields["s"].help_text = get_input_formats_help_text(
|
||||
self.fields["s"].input_formats
|
||||
self.fields["start_date"].help_text = get_input_formats_help_text(
|
||||
self.fields["start_date"].input_formats
|
||||
)
|
||||
self.fields["e"].help_text = get_input_formats_help_text(
|
||||
self.fields["e"].input_formats
|
||||
self.fields["end_date"].help_text = get_input_formats_help_text(
|
||||
self.fields["end_date"].input_formats
|
||||
)
|
||||
|
||||
|
||||
class MachineHistorySearchForm(Form):
|
||||
"""Form used to do a search through the machine histories."""
|
||||
q = forms.CharField(
|
||||
label=_("Search"),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
q = forms.CharField(label=_("Search"), max_length=100)
|
||||
t = forms.CharField(
|
||||
label=_("Search type"),
|
||||
widget=forms.Select(choices=CHOICES_TYPE)
|
||||
label=_("Search type"), widget=forms.Select(choices=CHOICES_TYPE)
|
||||
)
|
||||
s = forms.DateField(required=False, label=_("Start date"))
|
||||
e = forms.DateField(required=False, label=_("End date"))
|
||||
|
|
|
@ -600,10 +600,10 @@ class ActionsSearch:
|
|||
Returns:
|
||||
The QuerySet of Revision objects corresponding to the search.
|
||||
"""
|
||||
user = params.get("u", None)
|
||||
start = params.get("s", None)
|
||||
end = params.get("e", None)
|
||||
action_types = params.get("t", None)
|
||||
user = params.get("user", None)
|
||||
start = params.get("start_date", None)
|
||||
end = params.get("end_date", None)
|
||||
action_types = params.get("action_type", None)
|
||||
|
||||
query = Q()
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Search events" %}{% endblock %}
|
||||
|
@ -32,10 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<form class="form">
|
||||
<h3>{% trans "Search events" %}</h3>
|
||||
|
||||
{% massive_bootstrap_form actions_form 'u' %}
|
||||
{% bootstrap_form actions_form %}
|
||||
{% trans "Search" as tr_search %}
|
||||
{% bootstrap_button tr_search button_type="submit" icon="search" %}
|
||||
</form>
|
||||
{{ actions_form.media }}
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
|
|
@ -41,6 +41,10 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from re2o.field_permissions import FieldPermissionFormMixin
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.widgets import (
|
||||
AutocompleteModelWidget,
|
||||
AutocompleteMultipleModelWidget,
|
||||
)
|
||||
from .models import (
|
||||
Domain,
|
||||
Machine,
|
||||
|
@ -71,6 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Machine
|
||||
fields = "__all__"
|
||||
widgets = {"user": AutocompleteModelWidget(url="/users/user-autocomplete")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -91,6 +96,19 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Interface
|
||||
fields = ["machine", "machine_type", "ipv4", "mac_address", "details"]
|
||||
widgets = {
|
||||
"machine": AutocompleteModelWidget(url="/machines/machine-autocomplete"),
|
||||
"machine_type": AutocompleteModelWidget(
|
||||
url="/machines/machinetype-autocomplete"
|
||||
),
|
||||
"ipv4": AutocompleteModelWidget(
|
||||
url="/machines/iplist-autocomplete",
|
||||
forward=["machine_type"],
|
||||
attrs={
|
||||
"data-placeholder": "Automatic assigment. Type to choose specific ip."
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -139,6 +157,9 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Domain
|
||||
fields = ["name", "extension", "ttl"]
|
||||
widgets = {
|
||||
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -188,6 +209,9 @@ class MachineTypeForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = MachineType
|
||||
fields = ["name", "ip_type"]
|
||||
widgets = {
|
||||
"ip_type": AutocompleteModelWidget(url="/machines/iptype-autocomplete")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -222,6 +246,13 @@ class IpTypeForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = IpType
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"vlan": AutocompleteModelWidget(url="/machines/vlan-autocomplete"),
|
||||
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||
"ouverture_ports": AutocompleteModelWidget(
|
||||
url="/machines/ouvertureportlist-autocomplete"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -351,6 +382,10 @@ class MxForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Mx
|
||||
fields = ["zone", "priority", "name", "ttl"]
|
||||
widgets = {
|
||||
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||
"name": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -386,6 +421,10 @@ class NsForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Ns
|
||||
fields = ["zone", "ns", "ttl"]
|
||||
widgets = {
|
||||
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||
"ns": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -419,6 +458,9 @@ class TxtForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Txt
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -449,6 +491,9 @@ class DNameForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = DName
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -479,6 +524,10 @@ class SrvForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Srv
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||
"target": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -509,6 +558,14 @@ class NasForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Nas
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"nas_type": AutocompleteModelWidget(
|
||||
url="/machines/machinetype-autocomplete"
|
||||
),
|
||||
"machine_type": AutocompleteModelWidget(
|
||||
url="/machines/machinetype-autocomplete"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -539,6 +596,11 @@ class RoleForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Role
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"servers": AutocompleteMultipleModelWidget(
|
||||
url="/machines/interface-autocomplete"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -572,6 +634,11 @@ class ServiceForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Service
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"servers": AutocompleteMultipleModelWidget(
|
||||
url="/machines/interface-autocomplete"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -656,6 +723,11 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Interface
|
||||
fields = ["port_lists"]
|
||||
widgets = {
|
||||
"port_lists": AutocompleteMultipleModelWidget(
|
||||
url="/machines/ouvertureportlist-autocomplete"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
|
|
@ -378,6 +378,34 @@ class MachineType(RevMixin, AclMixin, models.Model):
|
|||
)
|
||||
return True, None, None
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list unprivileged machinetypes
|
||||
Only members of privileged groups can list all.
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
can, _message, _group = cls.can_use_all(user_request)
|
||||
if can:
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
True,
|
||||
_("You don't have the right to use all machine types."),
|
||||
("machines.use_all_machinetype",),
|
||||
cls.objects.filter(
|
||||
ip_type__in=IpType.objects.filter(need_infra=False)
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -953,6 +981,32 @@ class Extension(RevMixin, AclMixin, models.Model):
|
|||
("machines.use_all_extension",),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list unprivileged extensions
|
||||
Only members of privileged groups can list all.
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
can, _message, _group = cls.can_use_all(user_request)
|
||||
if can:
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
True,
|
||||
_("You don't have the right to list all extensions."),
|
||||
("machines.use_all_extension",),
|
||||
cls.objects.filter(need_infra=False),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -2130,6 +2184,34 @@ class IpList(RevMixin, AclMixin, models.Model):
|
|||
self.clean()
|
||||
super(IpList, self).save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""Only privilged users can list all ipv4.
|
||||
Others can list Ipv4 related with unprivileged type.
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
can, _message, _group = IpType.can_use_all(user_request)
|
||||
if can:
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
True,
|
||||
_("You don't have the right to use all machine types."),
|
||||
("machines.use_all_machinetype",),
|
||||
cls.objects.filter(
|
||||
ip_type__in=IpType.objects.filter(need_infra=False)
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.ipv4
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Machines" %}{% endblock %}
|
||||
|
@ -33,54 +32,67 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% block content %}
|
||||
{% if machineform %}
|
||||
{% bootstrap_form_errors machineform %}
|
||||
{{ machineform.media }}
|
||||
{% endif %}
|
||||
{% if interfaceform %}
|
||||
{% bootstrap_form_errors interfaceform %}
|
||||
{{ interfaceform.media }}
|
||||
{% endif %}
|
||||
{% if domainform %}
|
||||
{% bootstrap_form_errors domainform %}
|
||||
{% endif %}
|
||||
{% if iptypeform %}
|
||||
{% bootstrap_form_errors iptypeform %}
|
||||
{{ iptypeform.media }}
|
||||
{% endif %}
|
||||
{% if machinetypeform %}
|
||||
{% bootstrap_form_errors machinetypeform %}
|
||||
{{ machinetypeform.media }}
|
||||
{% endif %}
|
||||
{% if extensionform %}
|
||||
{% bootstrap_form_errors extensionform %}
|
||||
{% endif %}
|
||||
{% if mxform %}
|
||||
{% bootstrap_form_errors mxform %}
|
||||
{{ mxform.media }}
|
||||
{% endif %}
|
||||
{% if nsform %}
|
||||
{% bootstrap_form_errors nsform %}
|
||||
{{ nsform.media }}
|
||||
{% endif %}
|
||||
{% if txtform %}
|
||||
{% bootstrap_form_errors txtform %}
|
||||
{{ txtform.media }}
|
||||
{% endif %}
|
||||
{% if dnameform %}
|
||||
{% bootstrap_form_errors dnameform %}
|
||||
{{ dnameform.media }}
|
||||
{% endif %}
|
||||
{% if srvform %}
|
||||
{% bootstrap_form_errors srvform %}
|
||||
{{ srvform.media }}
|
||||
{% endif %}
|
||||
{% if aliasform %}
|
||||
{% bootstrap_form_errors aliasform %}
|
||||
{{ aliasform.media }}
|
||||
{% endif %}
|
||||
{% if serviceform %}
|
||||
{% bootstrap_form_errors serviceform %}
|
||||
{{ serviceform.media }}
|
||||
{% endif %}
|
||||
{% if sshfpform %}
|
||||
{% bootstrap_form_errors sshfpform %}
|
||||
{% endif %}
|
||||
{% if roleform %}
|
||||
{% bootstrap_form_errors roleform %}
|
||||
{{ roleform.media }}
|
||||
{% endif %}
|
||||
{% if vlanform %}
|
||||
{% bootstrap_form_errors vlanform %}
|
||||
{% endif %}
|
||||
{% if nasform %}
|
||||
{% bootstrap_form_errors nasform %}
|
||||
{{ nasform.media }}
|
||||
{% endif %}
|
||||
{% if ipv6form %}
|
||||
{% bootstrap_form_errors ipv6form %}
|
||||
|
@ -90,15 +102,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% csrf_token %}
|
||||
{% if machineform %}
|
||||
<h3>{% trans "Machine" %}</h3>
|
||||
{% massive_bootstrap_form machineform 'user' %}
|
||||
{% bootstrap_form machineform %}
|
||||
{% endif %}
|
||||
{% if interfaceform %}
|
||||
<h3>{% trans "Interface" %}</h3>
|
||||
{% if i_mbf_param %}
|
||||
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
|
||||
{% else %}
|
||||
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
|
||||
{% endif %}
|
||||
{% bootstrap_form interfaceform %}
|
||||
{% endif %}
|
||||
{% if domainform %}
|
||||
<h3>{% trans "Domain" %}</h3>
|
||||
|
@ -114,7 +122,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
{% if extensionform %}
|
||||
<h3>{% trans "Extension" %}</h3>
|
||||
{% massive_bootstrap_form extensionform 'origin' %}
|
||||
{% bootstrap_form extensionform %}
|
||||
{% endif %}
|
||||
{% if soaform %}
|
||||
<h3>{% trans "SOA record" %}</h3>
|
||||
|
@ -122,11 +130,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
{% if mxform %}
|
||||
<h3>{% trans "MX record" %}</h3>
|
||||
{% massive_bootstrap_form mxform 'name' %}
|
||||
{% bootstrap_form mxform %}
|
||||
{% endif %}
|
||||
{% if nsform %}
|
||||
<h3>{% trans "NS record" %}</h3>
|
||||
{% massive_bootstrap_form nsform 'ns' %}
|
||||
{% bootstrap_form nsform %}
|
||||
{% endif %}
|
||||
{% if txtform %}
|
||||
<h3>{% trans "TXT record" %}</h3>
|
||||
|
@ -138,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
{% if srvform %}
|
||||
<h3>{% trans "SRV record" %}</h3>
|
||||
{% massive_bootstrap_form srvform 'target' %}
|
||||
{% bootstrap_form srvform %}
|
||||
{% endif %}
|
||||
{% if sshfpform %}
|
||||
<h3>{% trans "SSHFP record" %}</h3>
|
||||
|
@ -146,15 +154,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
{% if aliasform %}
|
||||
<h3>{% trans "Alias" %}</h3>
|
||||
{% massive_bootstrap_form aliasform 'extension' %}
|
||||
{% bootstrap_form aliasform %}
|
||||
{% endif %}
|
||||
{% if serviceform %}
|
||||
<h3>{% trans "Service" %}</h3>
|
||||
{% massive_bootstrap_form serviceform 'servers' %}
|
||||
{% bootstrap_form serviceform %}
|
||||
{% endif %}
|
||||
{% if roleform %}
|
||||
<h3>Role</h3>
|
||||
{% massive_bootstrap_form roleform 'servers' %}
|
||||
{% bootstrap_form roleform %}
|
||||
{% endif %}
|
||||
{% if vlanform %}
|
||||
<h3>{% trans "VLAN" %}</h3>
|
||||
|
|
|
@ -29,6 +29,7 @@ from __future__ import unicode_literals
|
|||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
from . import views_autocomplete
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^new_machine/(?P<userid>[0-9]+)$", views.new_machine, name="new-machine"),
|
||||
|
@ -153,4 +154,14 @@ urlpatterns = [
|
|||
views.configure_ports,
|
||||
name="port-config",
|
||||
),
|
||||
### Autocomplete Views
|
||||
url(r'^vlan-autocomplete/$', views_autocomplete.VlanAutocomplete.as_view(), name='vlan-autocomplete',),
|
||||
url(r'^interface-autocomplete/$', views_autocomplete.InterfaceAutocomplete.as_view(), name='interface-autocomplete',),
|
||||
url(r'^machine-autocomplete/$', views_autocomplete.MachineAutocomplete.as_view(), name='machine-autocomplete',),
|
||||
url(r'^machinetype-autocomplete/$', views_autocomplete.MachineTypeAutocomplete.as_view(), name='machinetype-autocomplete',),
|
||||
url(r'^iptype-autocomplete/$', views_autocomplete.IpTypeAutocomplete.as_view(), name='iptype-autocomplete',),
|
||||
url(r'^extension-autocomplete/$', views_autocomplete.ExtensionAutocomplete.as_view(), name='extension-autocomplete',),
|
||||
url(r'^domain-autocomplete/$', views_autocomplete.DomainAutocomplete.as_view(), name='domain-autocomplete',),
|
||||
url(r'^ouvertureportlist-autocomplete/$', views_autocomplete.OuverturePortListAutocomplete.as_view(), name='ouvertureportlist-autocomplete',),
|
||||
url(r'^iplist-autocomplete/$', views_autocomplete.IpListAutocomplete.as_view(), name='iplist-autocomplete',),
|
||||
]
|
||||
|
|
|
@ -124,91 +124,6 @@ from .models import (
|
|||
)
|
||||
|
||||
|
||||
|
||||
def f_type_id(is_type_tt):
|
||||
""" The id that will be used in HTML to store the value of the field
|
||||
type. Depends on the fact that type is generate using typeahead or not
|
||||
"""
|
||||
return (
|
||||
"id_Interface-machine_type_hidden"
|
||||
if is_type_tt
|
||||
else "id_Interface-machine_type"
|
||||
)
|
||||
|
||||
|
||||
def generate_ipv4_choices(form_obj):
|
||||
""" Generate the parameter choices for the massive_bootstrap_form tag
|
||||
"""
|
||||
f_ipv4 = form_obj.fields["ipv4"]
|
||||
used_mtype_id = []
|
||||
choices = '{"":[{key:"",value:"' + _("Select a machine type first.") + '"}'
|
||||
mtype_id = -1
|
||||
|
||||
for ip in f_ipv4.queryset.annotate(mtype_id=F("ip_type__machinetype__id")).order_by(
|
||||
"mtype_id", "id"
|
||||
):
|
||||
if mtype_id != ip.mtype_id:
|
||||
mtype_id = ip.mtype_id
|
||||
used_mtype_id.append(mtype_id)
|
||||
choices += '],"{t}":[{{key:"",value:"{v}"}},'.format(
|
||||
t=mtype_id, v=f_ipv4.empty_label or '""'
|
||||
)
|
||||
choices += '{{key:{k},value:"{v}"}},'.format(k=ip.id, v=ip.ipv4)
|
||||
|
||||
for t in form_obj.fields["machine_type"].queryset.exclude(id__in=used_mtype_id):
|
||||
choices += '], "' + str(t.id) + '": ['
|
||||
choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},'
|
||||
choices += "]}"
|
||||
return choices
|
||||
|
||||
|
||||
def generate_ipv4_engine(is_type_tt):
|
||||
""" Generate the parameter engine for the massive_bootstrap_form tag
|
||||
"""
|
||||
return (
|
||||
"new Bloodhound( {{"
|
||||
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),'
|
||||
"queryTokenizer: Bloodhound.tokenizers.whitespace,"
|
||||
'local: choices_ipv4[ $( "#{machine_type_id}" ).val() ],'
|
||||
"identify: function( obj ) {{ return obj.key; }}"
|
||||
"}} )"
|
||||
).format(machine_type_id=f_type_id(is_type_tt))
|
||||
|
||||
|
||||
def generate_ipv4_match_func(is_type_tt):
|
||||
""" Generate the parameter match_func for the massive_bootstrap_form tag
|
||||
"""
|
||||
return (
|
||||
"function(q, sync) {{"
|
||||
'if (q === "") {{'
|
||||
'var first = choices_ipv4[$("#{machine_type_id}").val()].slice(0, 5);'
|
||||
"first = first.map( function (obj) {{ return obj.key; }} );"
|
||||
"sync(engine_ipv4.get(first));"
|
||||
"}} else {{"
|
||||
"engine_ipv4.search(q, sync);"
|
||||
"}}"
|
||||
"}}"
|
||||
).format(machine_type_id=f_type_id(is_type_tt))
|
||||
|
||||
|
||||
def generate_ipv4_mbf_param(form_obj, is_type_tt):
|
||||
""" Generate all the parameters to use with the massive_bootstrap_form
|
||||
tag """
|
||||
i_choices = {"ipv4": generate_ipv4_choices(form_obj)}
|
||||
i_engine = {"ipv4": generate_ipv4_engine(is_type_tt)}
|
||||
i_match_func = {"ipv4": generate_ipv4_match_func(is_type_tt)}
|
||||
i_update_on = {"ipv4": [f_type_id(is_type_tt)]}
|
||||
i_gen_select = {"ipv4": False}
|
||||
i_mbf_param = {
|
||||
"choices": i_choices,
|
||||
"engine": i_engine,
|
||||
"match_func": i_match_func,
|
||||
"update_on": i_update_on,
|
||||
"gen_select": i_gen_select,
|
||||
}
|
||||
return i_mbf_param
|
||||
|
||||
|
||||
@login_required
|
||||
@can_create(Machine)
|
||||
@can_edit(User)
|
||||
|
@ -235,13 +150,11 @@ def new_machine(request, user, **_kwargs):
|
|||
new_domain.save()
|
||||
messages.success(request, _("The machine was created."))
|
||||
return redirect(reverse("users:profil", kwargs={"userid": str(user.id)}))
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface, False)
|
||||
return form(
|
||||
{
|
||||
"machineform": machine,
|
||||
"interfaceform": interface,
|
||||
"domainform": domain,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"action_name": _("Add"),
|
||||
},
|
||||
"machines/machine.html",
|
||||
|
@ -281,13 +194,11 @@ def edit_interface(request, interface_instance, **_kwargs):
|
|||
kwargs={"userid": str(interface_instance.machine.user.id)},
|
||||
)
|
||||
)
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
||||
return form(
|
||||
{
|
||||
"machineform": machine_form,
|
||||
"interfaceform": interface_form,
|
||||
"domainform": domain_form,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"action_name": _("Edit"),
|
||||
},
|
||||
"machines/machine.html",
|
||||
|
@ -332,12 +243,10 @@ def new_interface(request, machine, **_kwargs):
|
|||
return redirect(
|
||||
reverse("users:profil", kwargs={"userid": str(machine.user.id)})
|
||||
)
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
||||
return form(
|
||||
{
|
||||
"interfaceform": interface_form,
|
||||
"domainform": domain_form,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"action_name": _("Add"),
|
||||
},
|
||||
"machines/machine.html",
|
||||
|
|
105
machines/views_autocomplete.py
Normal file
105
machines/views_autocomplete.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||
# quelques clics.
|
||||
#
|
||||
# Copyright © 2017-2020 Gabriel Détraz
|
||||
# Copyright © 2017-2020 Jean-Romain Garnier
|
||||
#
|
||||
# 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.
|
||||
|
||||
# App de gestion des users pour re2o
|
||||
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin
|
||||
# Gplv2
|
||||
"""
|
||||
Django views autocomplete view
|
||||
|
||||
Here are defined the autocomplete class based view.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Q, Value, CharField
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from .models import (
|
||||
Interface,
|
||||
Machine,
|
||||
Vlan,
|
||||
MachineType,
|
||||
IpType,
|
||||
Extension,
|
||||
Domain,
|
||||
OuverturePortList,
|
||||
IpList,
|
||||
)
|
||||
|
||||
from re2o.views import AutocompleteViewMixin
|
||||
|
||||
|
||||
class VlanAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Vlan
|
||||
|
||||
|
||||
class MachineAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Machine
|
||||
|
||||
|
||||
class MachineTypeAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = MachineType
|
||||
|
||||
|
||||
class IpTypeAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = IpType
|
||||
|
||||
|
||||
class ExtensionAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Extension
|
||||
|
||||
|
||||
class DomainAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Domain
|
||||
|
||||
|
||||
class OuverturePortListAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = OuverturePortList
|
||||
|
||||
|
||||
class InterfaceAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Interface
|
||||
|
||||
# Precision on search to add annotations so search behaves more like users expect it to
|
||||
def filter_results(self):
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(domain__name__icontains=self.q) | Q(machine__name__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class IpListAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = IpList
|
||||
|
||||
# Precision on search to add annotations so search behaves more like users expect it to
|
||||
def filter_results(self):
|
||||
machine_type = self.forwarded.get("machine_type", None)
|
||||
self.query_set = self.query_set.filter(interface__isnull=True)
|
||||
|
||||
if machine_type:
|
||||
self.query_set = self.query_set.filter(
|
||||
ip_type__machinetype__id=machine_type
|
||||
)
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(Q(ipv4__startswith=self.q))
|
|
@ -36,15 +36,17 @@ from topologie.models import Dormitory
|
|||
|
||||
from .preferences.models import MultiopOption
|
||||
|
||||
|
||||
class DormitoryForm(FormRevMixin, Form):
|
||||
"""Form used to select dormitories."""
|
||||
|
||||
dormitory = forms.ModelMultipleChoiceField(
|
||||
queryset=MultiopOption.get_cached_value("enabled_dorm").all(),
|
||||
label=_("Dormitory"),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False,
|
||||
queryset=Dormitory.objects.none(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DormitoryForm, self).__init__(*args, **kwargs)
|
||||
self.fields["dormitory"].queryset = MultiopOption.get_cached_value("enabled_dorm").all()
|
||||
|
|
|
@ -29,6 +29,7 @@ each.
|
|||
from django import forms
|
||||
from django.forms import ModelForm, Form
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from re2o.widgets import AutocompleteMultipleModelWidget
|
||||
|
||||
from .models import MultiopOption
|
||||
|
||||
|
@ -39,3 +40,8 @@ class EditMultiopOptionForm(ModelForm):
|
|||
class Meta:
|
||||
model = MultiopOption
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"enabled_dorm": AutocompleteMultipleModelWidget(
|
||||
url="/topologie/dormitory-autocomplete",
|
||||
),
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
django-bootstrap3==11.1.0
|
||||
django-macaddress==1.6.0
|
||||
django-autocomplete-light==3.8.1
|
||||
|
|
|
@ -30,6 +30,10 @@ from django.db.models import Q
|
|||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.widgets import (
|
||||
AutocompleteModelWidget,
|
||||
AutocompleteMultipleModelWidget
|
||||
)
|
||||
from .models import (
|
||||
OptionalUser,
|
||||
OptionalMachine,
|
||||
|
@ -108,12 +112,19 @@ class EditOptionalTopologieForm(ModelForm):
|
|||
"""Form used to edit the configuration of switches."""
|
||||
|
||||
automatic_provision_switchs = forms.ModelMultipleChoiceField(
|
||||
Switch.objects.all(), required=False
|
||||
Switch.objects.all(),
|
||||
required=False,
|
||||
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = OptionalTopologie
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"switchs_ip_type": AutocompleteModelWidget(
|
||||
url="/machines/iptype-autocomplete",
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -168,6 +179,11 @@ class EditAssoOptionForm(ModelForm):
|
|||
class Meta:
|
||||
model = AssoOption
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"utilisateur_asso": AutocompleteModelWidget(
|
||||
url="/users/user-autocomplete",
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -254,6 +270,11 @@ class MandateForm(ModelForm):
|
|||
class Meta:
|
||||
model = Mandate
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"president": AutocompleteModelWidget(
|
||||
url="/users/user-autocomplete",
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -368,7 +389,9 @@ class RadiusKeyForm(FormRevMixin, ModelForm):
|
|||
"""Form used to add and edit RADIUS keys."""
|
||||
|
||||
members = forms.ModelMultipleChoiceField(
|
||||
queryset=Switch.objects.all(), required=False
|
||||
queryset=Switch.objects.all(),
|
||||
required=False,
|
||||
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -391,7 +414,11 @@ class RadiusKeyForm(FormRevMixin, ModelForm):
|
|||
class SwitchManagementCredForm(FormRevMixin, ModelForm):
|
||||
"""Form used to add and edit switch management credentials."""
|
||||
|
||||
members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False)
|
||||
members = forms.ModelMultipleChoiceField(
|
||||
Switch.objects.all(),
|
||||
required=False,
|
||||
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SwitchManagementCred
|
||||
|
|
|
@ -24,19 +24,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Preferences" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors options %}
|
||||
{{ options.media }}
|
||||
|
||||
<h3>{% trans "Editing of preferences" %}</h3>
|
||||
|
||||
<form class="form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
|
||||
{% bootstrap_form options %}
|
||||
{% if formset %}
|
||||
{{ formset.management_form }}
|
||||
{% for f in formset %}
|
||||
|
|
|
@ -25,20 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load massive_bootstrap_form %}
|
||||
|
||||
{% block title %}{% trans "Preferences" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if preferenceform %}
|
||||
{% bootstrap_form_errors preferenceform %}
|
||||
{{ preferenceform.media }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<form class="form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% if preferenceform %}
|
||||
{% massive_bootstrap_form preferenceform 'members,president' %}
|
||||
{% bootstrap_form preferenceform %}
|
||||
{% endif %}
|
||||
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
|
||||
</form>
|
||||
|
|
|
@ -151,7 +151,8 @@ def display_options(request):
|
|||
optionnal_templates_list = [
|
||||
app.preferences.views.aff_preferences(request)
|
||||
for app in optionnal_apps
|
||||
if hasattr(app, "preferences") and hasattr(app.preferences.views, "aff_preferences")
|
||||
if hasattr(app, "preferences")
|
||||
and hasattr(app.preferences.views, "aff_preferences")
|
||||
]
|
||||
|
||||
return form(
|
||||
|
@ -350,7 +351,10 @@ def add_switchmanagementcred(request):
|
|||
"The switch management credentials were added."))
|
||||
return redirect(reverse("preferences:display-options"))
|
||||
return form(
|
||||
{"preferenceform": switchmanagementcred, "action_name": _("Add"), },
|
||||
{
|
||||
"preferenceform": switchmanagementcred,
|
||||
"action_name": _("Add"),
|
||||
},
|
||||
"preferences/preferences.html",
|
||||
request,
|
||||
)
|
||||
|
@ -415,6 +419,10 @@ def add_mailcontact(request):
|
|||
return redirect(reverse("preferences:display-options"))
|
||||
return form(
|
||||
{"preferenceform": mailcontact, "action_name": _("Add"), },
|
||||
{
|
||||
"preferenceform": mailcontact,
|
||||
"action_name": _("Add"),
|
||||
},
|
||||
"preferences/preferences.html",
|
||||
request,
|
||||
)
|
||||
|
|
50
re2o/acl.py
50
re2o/acl.py
|
@ -68,11 +68,17 @@ def acl_base_decorator(method_name, *targets, on_instance=True, api=False):
|
|||
"""Base decorator for acl. It checks if the `request.user` has the
|
||||
permission by calling model.method_name. If the flag on_instance is True,
|
||||
tries to get an instance of the model by calling
|
||||
`model.get_instance(*args, **kwargs)` and runs `instance.mehod_name`
|
||||
`model.get_instance(obj_id, *args, **kwargs)` and runs `instance.mehod_name`
|
||||
rather than model.method_name.
|
||||
|
||||
It is not intended to be used as is. It is a base for others ACL
|
||||
decorators.
|
||||
decorators. Beware, if you redefine the `get_instance` method for your
|
||||
model, give it a signature such as
|
||||
`def get_instance(cls, object_id, *_args, **_kwargs)`, because you will
|
||||
likely have an url with a named parameter "userid" if *e.g.* your model
|
||||
is an user. Otherwise, if the parameter name in `get_instance` was also
|
||||
`userid`, then `get_instance` would end up having two identical parameter
|
||||
passed on, and this would result in a `TypeError` exception.
|
||||
|
||||
Args:
|
||||
method_name: The name of the method which is to to be used for ACL.
|
||||
|
@ -176,8 +182,7 @@ ModelC)
|
|||
# `wrapper` inside the `decorator` function, you need to read some
|
||||
# documentation on decorators !
|
||||
def decorator(view):
|
||||
"""The decorator to use on a specific view
|
||||
"""
|
||||
"""The decorator to use on a specific view"""
|
||||
|
||||
def wrapper(request, *args, **kwargs):
|
||||
"""The wrapper used for a specific request"""
|
||||
|
@ -259,18 +264,24 @@ ModelC)
|
|||
for msg in error_messages:
|
||||
messages.error(
|
||||
request,
|
||||
msg or _(
|
||||
"You don't have the right to access this menu."),
|
||||
msg or _("You don't have the right to access this menu."),
|
||||
)
|
||||
# And redirect the user to the right place.
|
||||
if request.user.id is not None:
|
||||
if not api:
|
||||
return redirect(
|
||||
reverse("users:profil", kwargs={
|
||||
"userid": str(request.user.id)})
|
||||
reverse(
|
||||
"users:profil", kwargs={"userid": str(request.user.id)}
|
||||
)
|
||||
)
|
||||
else:
|
||||
return Response(data={"errors": error_messages, "warning": warning_messages}, status=403)
|
||||
return Response(
|
||||
data={
|
||||
"errors": error_messages,
|
||||
"warning": warning_messages,
|
||||
},
|
||||
status=403,
|
||||
)
|
||||
else:
|
||||
return redirect(reverse("index"))
|
||||
return view(request, *chain(instances, args), **kwargs)
|
||||
|
@ -321,12 +332,10 @@ def can_delete_set(model):
|
|||
If none of them, return an error"""
|
||||
|
||||
def decorator(view):
|
||||
"""The decorator to use on a specific view
|
||||
"""
|
||||
"""The decorator to use on a specific view"""
|
||||
|
||||
def wrapper(request, *args, **kwargs):
|
||||
"""The wrapper used for a specific request
|
||||
"""
|
||||
"""The wrapper used for a specific request"""
|
||||
all_objects = model.objects.all()
|
||||
instances_id = []
|
||||
for instance in all_objects:
|
||||
|
@ -367,9 +376,17 @@ def can_view_all(*targets):
|
|||
return acl_base_decorator("can_view_all", *targets, on_instance=False)
|
||||
|
||||
|
||||
def can_view_app(*apps_name):
|
||||
"""Decorator to check if an user can view the applications.
|
||||
def can_list(*targets):
|
||||
"""Decorator to check if an user can list a class of model.
|
||||
It runs `acl_base_decorator` with the flag `on_instance=False` and the
|
||||
method 'can_list'. See `acl_base_decorator` documentation for further
|
||||
details.
|
||||
"""
|
||||
return acl_base_decorator("can_list", *targets, on_instance=False)
|
||||
|
||||
|
||||
def can_view_app(*apps_name):
|
||||
"""Decorator to check if an user can view the applications."""
|
||||
for app_name in apps_name:
|
||||
assert app_name in sys.modules.keys()
|
||||
return acl_base_decorator(
|
||||
|
@ -383,8 +400,7 @@ def can_edit_history(view):
|
|||
"""Decorator to check if an user can edit history."""
|
||||
|
||||
def wrapper(request, *args, **kwargs):
|
||||
"""The wrapper used for a specific request
|
||||
"""
|
||||
"""The wrapper used for a specific request"""
|
||||
if request.user.has_perm("admin.change_logentry"):
|
||||
return view(request, *args, **kwargs)
|
||||
messages.error(request, _(
|
||||
|
|
|
@ -30,9 +30,9 @@ from django.utils.translation import ugettext as _
|
|||
|
||||
|
||||
class RevMixin(object):
|
||||
""" A mixin to subclass the save and delete function of a model
|
||||
"""A mixin to subclass the save and delete function of a model
|
||||
to enforce the versioning of the object before those actions
|
||||
really happen """
|
||||
really happen"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Creates a version of this object and save it to database """
|
||||
|
@ -50,8 +50,8 @@ class RevMixin(object):
|
|||
|
||||
|
||||
class FormRevMixin(object):
|
||||
""" A mixin to subclass the save function of a form
|
||||
to enforce the versionning of the object before it is really edited """
|
||||
"""A mixin to subclass the save function of a form
|
||||
to enforce the versionning of the object before it is really edited"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Create a version of this object and save it to database """
|
||||
|
@ -81,6 +81,8 @@ class AclMixin(object):
|
|||
:can_view: Applied on an instance, return if the user can view the
|
||||
instance
|
||||
:can_view_all: Applied on a class, return if the user can view all
|
||||
instances
|
||||
:can_list: Applied on a class, return if the user can list all
|
||||
instances"""
|
||||
|
||||
@classmethod
|
||||
|
@ -131,7 +133,7 @@ class AclMixin(object):
|
|||
|
||||
Parameters:
|
||||
user_request: User calling for this action
|
||||
self: Instance to edit
|
||||
self: Instance to edit
|
||||
|
||||
Returns:
|
||||
Boolean: True if user_request has the right access to do it, else
|
||||
|
@ -152,7 +154,7 @@ class AclMixin(object):
|
|||
|
||||
Parameters:
|
||||
user_request: User calling for this action
|
||||
self: Instance to delete
|
||||
self: Instance to delete
|
||||
|
||||
Returns:
|
||||
Boolean: True if user_request has the right access to do it, else
|
||||
|
@ -210,12 +212,34 @@ class AclMixin(object):
|
|||
(permission,),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""Check if a user can list all instances of an object
|
||||
|
||||
Parameters:
|
||||
user_request: User calling for this action
|
||||
|
||||
Returns:
|
||||
Boolean: True if user_request has the right access to do it, else
|
||||
false with reason for reject authorization
|
||||
"""
|
||||
permission = cls.get_modulename() + ".view_" + cls.get_classname()
|
||||
can = user_request.has_perm(permission)
|
||||
return (
|
||||
can,
|
||||
_("You don't have the right to list every %s object.") % cls.get_classname()
|
||||
if not can
|
||||
else None,
|
||||
(permission,),
|
||||
cls.objects.all() if can else None,
|
||||
)
|
||||
|
||||
def can_view(self, user_request, *_args, **_kwargs):
|
||||
"""Check if a user can view an instance of an object
|
||||
|
||||
Parameters:
|
||||
user_request: User calling for this action
|
||||
self: Instance to view
|
||||
self: Instance to view
|
||||
|
||||
Returns:
|
||||
Boolean: True if user_request has the right access to do it, else
|
||||
|
|
|
@ -59,6 +59,8 @@ LOGIN_URL = "/login/" # The URL for login page
|
|||
LOGIN_REDIRECT_URL = "/" # The URL for redirecting after login
|
||||
|
||||
# Application definition
|
||||
# dal_legacy_static only needed for Django < 2.0 (https://django-autocomplete-light.readthedocs.io/en/master/install.html#django-versions-earlier-than-2-0)
|
||||
EARLY_EXTERNAL_CONTRIB_APPS = ("dal", "dal_select2", "dal_legacy_static") # Need to be added before django.contrib.admin (https://django-autocomplete-light.readthedocs.io/en/master/install.html#configuration)
|
||||
DJANGO_CONTRIB_APPS = (
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
|
@ -80,7 +82,7 @@ LOCAL_APPS = (
|
|||
"logs",
|
||||
)
|
||||
INSTALLED_APPS = (
|
||||
DJANGO_CONTRIB_APPS + EXTERNAL_CONTRIB_APPS + LOCAL_APPS + OPTIONNAL_APPS
|
||||
EARLY_EXTERNAL_CONTRIB_APPS + DJANGO_CONTRIB_APPS + EXTERNAL_CONTRIB_APPS + LOCAL_APPS + OPTIONNAL_APPS
|
||||
)
|
||||
MIDDLEWARE_CLASSES = (
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
|
|
|
@ -141,6 +141,8 @@ def get_callback(tag_name, obj=None):
|
|||
return acl_fct(obj.can_view_all, False)
|
||||
if tag_name == "cannot_view_all":
|
||||
return acl_fct(obj.can_view_all, True)
|
||||
if tag_name == "can_list":
|
||||
return acl_fct(obj.can_list, False)
|
||||
if tag_name == "can_view_app":
|
||||
return acl_fct(
|
||||
lambda x: (
|
||||
|
@ -296,6 +298,7 @@ def acl_change_filter(parser, token):
|
|||
@register.tag("cannot_delete_all")
|
||||
@register.tag("can_view_all")
|
||||
@register.tag("cannot_view_all")
|
||||
@register.tag("can_list")
|
||||
def acl_model_filter(parser, token):
|
||||
"""Generic definition of an acl templatetag for acl based on model"""
|
||||
|
||||
|
|
|
@ -1,752 +0,0 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||
# quelques clics.
|
||||
#
|
||||
# Copyright © 2017 Maël Kervella
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
""" Templatetag used to render massive django form selects into bootstrap
|
||||
forms that can still be manipulating even if there is multiple tens of
|
||||
thousands of elements in the select. It's made possible using JS libaries
|
||||
Twitter Typeahead and Splitree's Tokenfield.
|
||||
See docstring of massive_bootstrap_form for a detailed explaantion on how
|
||||
to use this templatetag.
|
||||
"""
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.forms import TextInput
|
||||
from django.forms.widgets import Select
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from bootstrap3.utils import render_tag
|
||||
from bootstrap3.forms import render_field
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def massive_bootstrap_form(form, mbf_fields, *args, **kwargs):
|
||||
"""
|
||||
Render a form where some specific fields are rendered using Twitter
|
||||
Typeahead and/or splitree's Bootstrap Tokenfield to improve the
|
||||
performance, the speed and UX when dealing with very large datasets
|
||||
(select with 50k+ elts for instance).
|
||||
When the fields specified should normally be rendered as a select with
|
||||
single selectable option, Twitter Typeahead is used for a better display
|
||||
and the matching query engine. When dealing with multiple selectable
|
||||
options, sliptree's Bootstrap Tokenfield in addition with Typeahead.
|
||||
For convenience, it accepts the same parameters as a standard bootstrap
|
||||
can accept.
|
||||
|
||||
**Tag name**::
|
||||
|
||||
massive_bootstrap_form
|
||||
|
||||
**Parameters**:
|
||||
|
||||
form (required)
|
||||
The form that is to be rendered
|
||||
|
||||
mbf_fields (optional)
|
||||
A list of field names (comma separated) that should be rendered
|
||||
with Typeahead/Tokenfield instead of the default bootstrap
|
||||
renderer.
|
||||
If not specified, all fields will be rendered as a normal bootstrap
|
||||
field.
|
||||
|
||||
mbf_param (optional)
|
||||
A dict of parameters for the massive_bootstrap_form tag. The
|
||||
possible parameters are the following.
|
||||
|
||||
choices (optional)
|
||||
A dict of strings representing the choices in JS. The keys of
|
||||
the dict are the names of the concerned fields. The choices
|
||||
must be an array of objects. Each of those objects must at
|
||||
least have the fields 'key' (value to send) and 'value' (value
|
||||
to display). Other fields can be added as desired.
|
||||
For a more complex structure you should also consider
|
||||
reimplementing the engine and the match_func.
|
||||
If not specified, the key is the id of the object and the value
|
||||
is its string representation as in a normal bootstrap form.
|
||||
Example :
|
||||
'choices' : {
|
||||
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]',
|
||||
'field_B':...,
|
||||
...
|
||||
}
|
||||
|
||||
engine (optional)
|
||||
A dict of strings representating the engine used for matching
|
||||
queries and possible values with typeahead. The keys of the
|
||||
dict are the names of the concerned fields. The string is valid
|
||||
JS code.
|
||||
If not specified, BloodHound with relevant basic properties is
|
||||
used.
|
||||
Example :
|
||||
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...}
|
||||
|
||||
match_func (optional)
|
||||
A dict of strings representing a valid JS function used in the
|
||||
dataset to overload the matching engine. The keys of the dict
|
||||
are the names of the concerned fields. This function is used
|
||||
the source of the dataset. This function receives 2 parameters,
|
||||
the query and the synchronize function as specified in
|
||||
typeahead.js documentation. If needed, the local variables
|
||||
'choices_<fieldname>' and 'engine_<fieldname>' contains
|
||||
respectively the array of all possible values and the engine
|
||||
to match queries with possible values.
|
||||
If not specified, the function used display up to the 10 first
|
||||
elements if the query is empty and else the matching results.
|
||||
Example :
|
||||
'match_func' : {
|
||||
'field_A': 'function(q, sync) { engine.search(q, sync); }',
|
||||
'field_B': ...,
|
||||
...
|
||||
}
|
||||
|
||||
update_on (optional)
|
||||
A dict of list of ids that the values depends on. The engine
|
||||
and the typeahead properties are recalculated and reapplied.
|
||||
Example :
|
||||
'update_on' : {
|
||||
'field_A' : [ 'id0', 'id1', ... ] ,
|
||||
'field_B' : ... ,
|
||||
...
|
||||
}
|
||||
|
||||
gen_select (optional)
|
||||
A dict of boolean telling if the form should either generate
|
||||
the normal select (set to true) and then use it to generate
|
||||
the possible choices and then remove it or either (set to
|
||||
false) generate the choices variable in this tag and do not
|
||||
send any select.
|
||||
Sending the select before can be usefull to permit the use
|
||||
without any JS enabled but it will execute more code locally
|
||||
for the client so the loading might be slower.
|
||||
If not specified, this variable is set to true for each field
|
||||
Example :
|
||||
'gen_select' : {
|
||||
'field_A': True ,
|
||||
'field_B': ... ,
|
||||
...
|
||||
}
|
||||
|
||||
See boostrap_form_ for other arguments
|
||||
|
||||
**Usage**::
|
||||
|
||||
{% massive_bootstrap_form
|
||||
form
|
||||
[ '<field1>[,<field2>[,...]]' ]
|
||||
[ mbf_param = {
|
||||
[ 'choices': {
|
||||
[ '<field1>': '<choices1>'
|
||||
[, '<field2>': '<choices2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
[, 'engine': {
|
||||
[ '<field1>': '<engine1>'
|
||||
[, '<field2>': '<engine2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
[, 'match_func': {
|
||||
[ '<field1>': '<match_func1>'
|
||||
[, '<field2>': '<match_func2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
[, 'update_on': {
|
||||
[ '<field1>': '<update_on1>'
|
||||
[, '<field2>': '<update_on2>'
|
||||
[, ... ] ] ]
|
||||
} ],
|
||||
[, 'gen_select': {
|
||||
[ '<field1>': '<gen_select1>'
|
||||
[, '<field2>': '<gen_select2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
} ]
|
||||
[ <standard boostrap_form parameters> ]
|
||||
%}
|
||||
|
||||
**Example**:
|
||||
|
||||
{% massive_bootstrap_form form 'ipv4' choices='[...]' %}
|
||||
"""
|
||||
|
||||
mbf_form = MBFForm(form, mbf_fields.split(","), *args, **kwargs)
|
||||
return mbf_form.render()
|
||||
|
||||
|
||||
class MBFForm:
|
||||
""" An object to hold all the information and useful methods needed to
|
||||
create and render a massive django form into an actual HTML and JS
|
||||
code able to handle it correctly.
|
||||
Every field that is not listed is rendered as a normal bootstrap_field.
|
||||
"""
|
||||
|
||||
def __init__(self, form, mbf_fields, *args, **kwargs):
|
||||
# The django form object
|
||||
self.form = form
|
||||
# The fields on which to use JS
|
||||
self.fields = mbf_fields
|
||||
|
||||
# Other bootstrap_form arguments to render the fields
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
# Fields to exclude form the form rendering
|
||||
self.exclude = self.kwargs.get("exclude", "").split(",")
|
||||
|
||||
# All the mbf parameters specified byt the user
|
||||
param = kwargs.pop("mbf_param", {})
|
||||
self.choices = param.get("choices", {})
|
||||
self.engine = param.get("engine", {})
|
||||
self.match_func = param.get("match_func", {})
|
||||
self.update_on = param.get("update_on", {})
|
||||
self.gen_select = param.get("gen_select", {})
|
||||
self.hidden_fields = [h.name for h in self.form.hidden_fields()]
|
||||
|
||||
# HTML code to insert inside a template
|
||||
self.html = ""
|
||||
|
||||
def render(self):
|
||||
""" HTML code for the fully rendered form with all the necessary form
|
||||
"""
|
||||
for name, field in self.form.fields.items():
|
||||
if name not in self.exclude:
|
||||
|
||||
if name in self.fields and name not in self.hidden_fields:
|
||||
mbf_field = MBFField(
|
||||
name,
|
||||
field,
|
||||
field.get_bound_field(self.form, name),
|
||||
self.choices.get(name, None),
|
||||
self.engine.get(name, None),
|
||||
self.match_func.get(name, None),
|
||||
self.update_on.get(name, None),
|
||||
self.gen_select.get(name, True),
|
||||
*self.args,
|
||||
**self.kwargs
|
||||
)
|
||||
self.html += mbf_field.render()
|
||||
|
||||
else:
|
||||
f = field.get_bound_field(self.form, name), self.args, self.kwargs
|
||||
self.html += render_field(
|
||||
field.get_bound_field(self.form, name),
|
||||
*self.args,
|
||||
**self.kwargs
|
||||
)
|
||||
|
||||
return mark_safe(self.html)
|
||||
|
||||
|
||||
class MBFField:
|
||||
""" An object to hold all the information and useful methods needed to
|
||||
create and render a massive django form field into an actual HTML and JS
|
||||
code able to handle it correctly.
|
||||
Twitter Typeahead is used for the display and the matching of queries and
|
||||
in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage
|
||||
multiple values.
|
||||
A div with only non visible elements is created after the div containing
|
||||
the displayed input. It's used to store the actual data that will be sent
|
||||
to the server """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name_,
|
||||
field_,
|
||||
bound_,
|
||||
choices_,
|
||||
engine_,
|
||||
match_func_,
|
||||
update_on_,
|
||||
gen_select_,
|
||||
*args_,
|
||||
**kwargs_
|
||||
):
|
||||
|
||||
# Verify this field is a Select (or MultipleSelect) (only supported)
|
||||
if not isinstance(field_.widget, Select):
|
||||
raise ValueError(
|
||||
(
|
||||
"Field named {f_name} is not a Select and"
|
||||
"can't be rendered with massive_bootstrap_form."
|
||||
).format(f_name=name_)
|
||||
)
|
||||
|
||||
# Name of the field
|
||||
self.name = name_
|
||||
# Django field object
|
||||
self.field = field_
|
||||
# Bound Django field associated with field
|
||||
self.bound = bound_
|
||||
|
||||
# Id for the main visible input
|
||||
self.input_id = self.bound.auto_id
|
||||
# Id for a hidden input used to store the value
|
||||
self.hidden_id = self.input_id + "_hidden"
|
||||
# Id for another div containing hidden inputs and script
|
||||
self.div2_id = self.input_id + "_div"
|
||||
|
||||
# Should the standard select should be generated
|
||||
self.gen_select = gen_select_
|
||||
# Is it select with multiple values possible (use of tokenfield)
|
||||
self.multiple = self.field.widget.allow_multiple_selected
|
||||
# JS for the choices variable (user specified or default)
|
||||
self.choices = choices_ or self.default_choices()
|
||||
# JS for the engine variable (typeahead) (user specified or default)
|
||||
self.engine = engine_ or self.default_engine()
|
||||
# JS for the matching function (typeahead) (user specified or default)
|
||||
self.match_func = match_func_ or self.default_match_func()
|
||||
# JS for the datasets variable (typeahead) (user specified or default)
|
||||
self.datasets = self.default_datasets()
|
||||
# Ids of other fields to bind a reset/reload with when changed
|
||||
self.update_on = update_on_ or []
|
||||
|
||||
# Whole HTML code to insert in the template
|
||||
self.html = ""
|
||||
# JS code in the script tag
|
||||
self.js_script = ""
|
||||
# Input tag to display instead of select
|
||||
self.replace_input = None
|
||||
|
||||
# Other bootstrap_form arguments to render the fields
|
||||
self.args = args_
|
||||
self.kwargs = kwargs_
|
||||
|
||||
def default_choices(self):
|
||||
""" JS code of the variable choices_<fieldname> """
|
||||
|
||||
if self.gen_select:
|
||||
return (
|
||||
"function plop(o) {{"
|
||||
"var c = [];"
|
||||
"for( let i=0 ; i<o.length ; i++) {{"
|
||||
" c.push( {{ key: o[i].value, value: o[i].text }} );"
|
||||
"}}"
|
||||
"return c;"
|
||||
'}} ($("#{select_id}")[0].options)'
|
||||
).format(select_id=self.input_id)
|
||||
|
||||
else:
|
||||
return "[{objects}]".format(
|
||||
objects=",".join(
|
||||
[
|
||||
'{{key:{k},value:"{v}"}}'.format(
|
||||
k=choice[0] if choice[0] != "" else '""', v=choice[1]
|
||||
)
|
||||
for choice in self.field.choices
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def default_engine(self):
|
||||
""" Default JS code of the variable engine_<field_name> """
|
||||
return (
|
||||
"new Bloodhound({{"
|
||||
' datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
|
||||
" queryTokenizer: Bloodhound.tokenizers.whitespace,"
|
||||
" local: choices_{name},"
|
||||
" identify: function(obj) {{ return obj.key; }}"
|
||||
"}})"
|
||||
).format(name=self.name)
|
||||
|
||||
def default_datasets(self):
|
||||
""" Default JS script of the datasets to use with typeahead """
|
||||
return (
|
||||
"{{"
|
||||
" hint: true,"
|
||||
" highlight: true,"
|
||||
" minLength: 0"
|
||||
"}},"
|
||||
"{{"
|
||||
' display: "value",'
|
||||
' name: "{name}",'
|
||||
" source: {match_func}"
|
||||
"}}"
|
||||
).format(name=self.name, match_func=self.match_func)
|
||||
|
||||
def default_match_func(self):
|
||||
""" Default JS code of the matching function to use with typeahed """
|
||||
return (
|
||||
"function ( q, sync ) {{"
|
||||
' if ( q === "" ) {{'
|
||||
" var first = choices_{name}.slice( 0, 5 ).map("
|
||||
" function ( obj ) {{ return obj.key; }}"
|
||||
" );"
|
||||
" sync( engine_{name}.get( first ) );"
|
||||
" }} else {{"
|
||||
" engine_{name}.search( q, sync );"
|
||||
" }}"
|
||||
"}}"
|
||||
).format(name=self.name)
|
||||
|
||||
def render(self):
|
||||
""" HTML code for the fully rendered field """
|
||||
self.gen_displayed_div()
|
||||
self.gen_hidden_div()
|
||||
return mark_safe(self.html)
|
||||
|
||||
def gen_displayed_div(self):
|
||||
""" Generate HTML code for the div that contains displayed tags """
|
||||
if self.gen_select:
|
||||
self.html += render_field(self.bound, *self.args, **self.kwargs)
|
||||
|
||||
self.field.widget = TextInput(
|
||||
attrs={
|
||||
"name": "mbf_" + self.name,
|
||||
"placeholder": getattr(self.field, "empty_label", _("Nothing")),
|
||||
}
|
||||
)
|
||||
self.replace_input = render_field(self.bound, *self.args, **self.kwargs)
|
||||
|
||||
if not self.gen_select:
|
||||
self.html += self.replace_input
|
||||
|
||||
def gen_hidden_div(self):
|
||||
""" Generate HTML code for the div that contains hidden tags """
|
||||
self.gen_full_js()
|
||||
|
||||
content = self.js_script
|
||||
if not self.multiple and not self.gen_select:
|
||||
content += self.hidden_input()
|
||||
|
||||
self.html += render_tag("div", content=content, attrs={"id": self.div2_id})
|
||||
|
||||
def hidden_input(self):
|
||||
""" HTML for the hidden input element """
|
||||
return render_tag(
|
||||
"input",
|
||||
attrs={
|
||||
"id": self.hidden_id,
|
||||
"name": self.bound.html_name,
|
||||
"type": "hidden",
|
||||
"value": self.bound.value() or "",
|
||||
},
|
||||
)
|
||||
|
||||
def gen_full_js(self):
|
||||
""" Generate the full script tag containing the JS code """
|
||||
self.create_js()
|
||||
self.fill_js()
|
||||
self.get_script()
|
||||
|
||||
def create_js(self):
|
||||
""" Generate a template for the whole script to use depending on
|
||||
gen_select and multiple """
|
||||
if self.gen_select:
|
||||
if self.multiple:
|
||||
self.js_script = (
|
||||
'$( "#{input_id}" ).ready( function() {{'
|
||||
" var choices_{f_name} = {choices};"
|
||||
" {del_select}"
|
||||
" var engine_{f_name};"
|
||||
" var setup_{f_name} = function() {{"
|
||||
" engine_{f_name} = {engine};"
|
||||
' $( "#{input_id}" ).tokenfield( "destroy" );'
|
||||
' $( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
|
||||
" }};"
|
||||
' $( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );'
|
||||
' $( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
|
||||
' $( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
|
||||
" {tok_updates}"
|
||||
" setup_{f_name}();"
|
||||
" {tok_init_input}"
|
||||
"}} );"
|
||||
)
|
||||
else:
|
||||
self.js_script = (
|
||||
'$( "#{input_id}" ).ready( function() {{'
|
||||
" var choices_{f_name} = {choices};"
|
||||
" {del_select}"
|
||||
" {gen_hidden}"
|
||||
" var engine_{f_name};"
|
||||
" var setup_{f_name} = function() {{"
|
||||
" engine_{f_name} = {engine};"
|
||||
' $( "#{input_id}" ).typeahead( "destroy" );'
|
||||
' $( "#{input_id}" ).typeahead( {datasets} );'
|
||||
" }};"
|
||||
' $( "#{input_id}" ).bind( "typeahead:select", {typ_select} );'
|
||||
' $( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
|
||||
" {typ_updates}"
|
||||
" setup_{f_name}();"
|
||||
" {typ_init_input}"
|
||||
"}} );"
|
||||
)
|
||||
else:
|
||||
if self.multiple:
|
||||
self.js_script = (
|
||||
"var choices_{f_name} = {choices};"
|
||||
"var engine_{f_name};"
|
||||
"var setup_{f_name} = function() {{"
|
||||
" engine_{f_name} = {engine};"
|
||||
' $( "#{input_id}" ).tokenfield( "destroy" );'
|
||||
' $( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
|
||||
"}};"
|
||||
'$( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );'
|
||||
'$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
|
||||
'$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
|
||||
"{tok_updates}"
|
||||
'$( "#{input_id}" ).ready( function() {{'
|
||||
" setup_{f_name}();"
|
||||
" {tok_init_input}"
|
||||
"}} );"
|
||||
)
|
||||
else:
|
||||
self.js_script = (
|
||||
"var choices_{f_name} ={choices};"
|
||||
"var engine_{f_name};"
|
||||
"var setup_{f_name} = function() {{"
|
||||
" engine_{f_name} = {engine};"
|
||||
' $( "#{input_id}" ).typeahead( "destroy" );'
|
||||
' $( "#{input_id}" ).typeahead( {datasets} );'
|
||||
"}};"
|
||||
'$( "#{input_id}" ).bind( "typeahead:select", {typ_select} );'
|
||||
'$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
|
||||
"{typ_updates}"
|
||||
'$( "#{input_id}" ).ready( function() {{'
|
||||
" setup_{f_name}();"
|
||||
" {typ_init_input}"
|
||||
"}} );"
|
||||
)
|
||||
|
||||
# Make sure the visible element doesn't have the same name as the hidden elements
|
||||
# Otherwise, in the POST request, they collide and an incoherent value is sent
|
||||
self.js_script += (
|
||||
'$( "#{input_id}" ).ready( function() {{'
|
||||
' $( "#{input_id}" ).attr("name", "mbf_{f_name}");'
|
||||
"}} );"
|
||||
)
|
||||
|
||||
def fill_js(self):
|
||||
""" Fill the template with the correct values """
|
||||
self.js_script = self.js_script.format(
|
||||
f_name=self.name,
|
||||
choices=self.choices,
|
||||
del_select=self.del_select(),
|
||||
gen_hidden=self.gen_hidden(),
|
||||
engine=self.engine,
|
||||
input_id=self.input_id,
|
||||
datasets=self.datasets,
|
||||
typ_select=self.typeahead_select(),
|
||||
typ_change=self.typeahead_change(),
|
||||
tok_create=self.tokenfield_create(),
|
||||
tok_edit=self.tokenfield_edit(),
|
||||
tok_remove=self.tokenfield_remove(),
|
||||
typ_updates=self.typeahead_updates(),
|
||||
tok_updates=self.tokenfield_updates(),
|
||||
tok_init_input=self.tokenfield_init_input(),
|
||||
typ_init_input=self.typeahead_init_input(),
|
||||
)
|
||||
|
||||
def get_script(self):
|
||||
""" Insert the JS code inside a script tag """
|
||||
self.js_script = render_tag("script", content=mark_safe(self.js_script))
|
||||
|
||||
def del_select(self):
|
||||
""" JS code to delete the select if it has been generated and replace
|
||||
it with an input. """
|
||||
return (
|
||||
'var p = $("#{select_id}").parent()[0];'
|
||||
"var new_input = `{replace_input}`;"
|
||||
"p.innerHTML = new_input;"
|
||||
).format(select_id=self.input_id, replace_input=self.replace_input)
|
||||
|
||||
def gen_hidden(self):
|
||||
""" JS code to add a hidden tag to store the value. """
|
||||
return (
|
||||
'var d = $("#{div2_id}")[0];'
|
||||
'var i = document.createElement("input");'
|
||||
'i.id = "{hidden_id}";'
|
||||
'i.name = "{html_name}";'
|
||||
'i.value = "";'
|
||||
'i.type = "hidden";'
|
||||
"d.appendChild(i);"
|
||||
).format(
|
||||
div2_id=self.div2_id,
|
||||
hidden_id=self.hidden_id,
|
||||
html_name=self.bound.html_name,
|
||||
)
|
||||
|
||||
def typeahead_init_input(self):
|
||||
""" JS code to init the fields values """
|
||||
init_key = self.bound.value() or '""'
|
||||
return (
|
||||
'$( "#{input_id}" ).typeahead("val", {init_val});'
|
||||
'$( "#{hidden_id}" ).val( {init_key} );'
|
||||
).format(
|
||||
input_id=self.input_id,
|
||||
init_val='""'
|
||||
if init_key == '""'
|
||||
else "engine_{name}.get( {init_key} )[0].value".format(
|
||||
name=self.name, init_key=init_key
|
||||
),
|
||||
init_key=init_key,
|
||||
hidden_id=self.hidden_id,
|
||||
)
|
||||
|
||||
def typeahead_reset_input(self):
|
||||
""" JS code to reset the fields values """
|
||||
return (
|
||||
'$( "#{input_id}" ).typeahead("val", "");' '$( "#{hidden_id}" ).val( "" );'
|
||||
).format(input_id=self.input_id, hidden_id=self.hidden_id)
|
||||
|
||||
def typeahead_select(self):
|
||||
""" JS code to create the function triggered when an item is selected
|
||||
through typeahead """
|
||||
return (
|
||||
"function(evt, item) {{"
|
||||
' $( "#{hidden_id}" ).val( item.key );'
|
||||
' $( "#{hidden_id}" ).change();'
|
||||
" return item;"
|
||||
"}}"
|
||||
).format(hidden_id=self.hidden_id)
|
||||
|
||||
def typeahead_change(self):
|
||||
""" JS code of the function triggered when an item is changed (i.e.
|
||||
looses focus and value has changed since the moment it gained focus )
|
||||
"""
|
||||
return (
|
||||
"function(evt) {{"
|
||||
' if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{'
|
||||
' $( "#{hidden_id}" ).val( "" );'
|
||||
' $( "#{hidden_id}" ).change();'
|
||||
" }}"
|
||||
"}}"
|
||||
).format(input_id=self.input_id, hidden_id=self.hidden_id)
|
||||
|
||||
def typeahead_updates(self):
|
||||
""" JS code for binding external fields changes with a reset """
|
||||
reset_input = self.typeahead_reset_input()
|
||||
updates = [
|
||||
(
|
||||
'$( "#{u_id}" ).change( function() {{'
|
||||
" setup_{name}();"
|
||||
" {reset_input}"
|
||||
"}} );"
|
||||
).format(u_id=u_id, name=self.name, reset_input=reset_input)
|
||||
for u_id in self.update_on
|
||||
]
|
||||
return "".join(updates)
|
||||
|
||||
def tokenfield_init_input(self):
|
||||
""" JS code to init the fields values """
|
||||
init_key = self.bound.value() or '""'
|
||||
return ('$( "#{input_id}" ).tokenfield("setTokens", {init_val});').format(
|
||||
input_id=self.input_id,
|
||||
init_val='""'
|
||||
if init_key == '""'
|
||||
else (
|
||||
"engine_{name}.get( {init_key} ).map("
|
||||
" function(o) {{ return o.value; }}"
|
||||
")"
|
||||
).format(name=self.name, init_key=init_key),
|
||||
)
|
||||
|
||||
def tokenfield_reset_input(self):
|
||||
""" JS code to reset the fields values """
|
||||
return ('$( "#{input_id}" ).tokenfield("setTokens", "");').format(
|
||||
input_id=self.input_id
|
||||
)
|
||||
|
||||
def tokenfield_create(self):
|
||||
""" JS code triggered when a new token is created in tokenfield. """
|
||||
return (
|
||||
"function(evt) {{"
|
||||
" var k = evt.attrs.key;"
|
||||
" if (!k) {{"
|
||||
" var data = evt.attrs.value;"
|
||||
" var i = 0;"
|
||||
" while ( i<choices_{name}.length &&"
|
||||
" choices_{name}[i].value !== data ) {{"
|
||||
" i++;"
|
||||
" }}"
|
||||
" if ( i === choices_{name}.length ) {{ return false; }}"
|
||||
" k = choices_{name}[i].key;"
|
||||
" }}"
|
||||
' var new_input = document.createElement("input");'
|
||||
' new_input.type = "hidden";'
|
||||
' new_input.id = "{hidden_id}_"+k.toString();'
|
||||
" new_input.value = k.toString();"
|
||||
' new_input.name = "{html_name}";'
|
||||
' $( "#{div2_id}" ).append(new_input);'
|
||||
"}}"
|
||||
).format(
|
||||
name=self.name,
|
||||
hidden_id=self.hidden_id,
|
||||
html_name=self.bound.html_name,
|
||||
div2_id=self.div2_id,
|
||||
)
|
||||
|
||||
def tokenfield_edit(self):
|
||||
""" JS code triggered when a token is edited in tokenfield. """
|
||||
return (
|
||||
"function(evt) {{"
|
||||
" var k = evt.attrs.key;"
|
||||
" if (!k) {{"
|
||||
" var data = evt.attrs.value;"
|
||||
" var i = 0;"
|
||||
" while ( i<choices_{name}.length &&"
|
||||
" choices_{name}[i].value !== data ) {{"
|
||||
" i++;"
|
||||
" }}"
|
||||
" if ( i === choices_{name}.length ) {{ return true; }}"
|
||||
" k = choices_{name}[i].key;"
|
||||
" }}"
|
||||
" var old_input = document.getElementById("
|
||||
' "{hidden_id}_"+k.toString()'
|
||||
" );"
|
||||
" old_input.parentNode.removeChild(old_input);"
|
||||
"}}"
|
||||
).format(name=self.name, hidden_id=self.hidden_id)
|
||||
|
||||
def tokenfield_remove(self):
|
||||
""" JS code trigggered when a token is removed from tokenfield. """
|
||||
return (
|
||||
"function(evt) {{"
|
||||
" var k = evt.attrs.key;"
|
||||
" if (!k) {{"
|
||||
" var data = evt.attrs.value;"
|
||||
" var i = 0;"
|
||||
" while ( i<choices_{name}.length &&"
|
||||
" choices_{name}[i].value !== data ) {{"
|
||||
" i++;"
|
||||
" }}"
|
||||
" if ( i === choices_{name}.length ) {{ return true; }}"
|
||||
" k = choices_{name}[i].key;"
|
||||
" }}"
|
||||
" var old_input = document.getElementById("
|
||||
' "{hidden_id}_"+k.toString()'
|
||||
" );"
|
||||
" old_input.parentNode.removeChild(old_input);"
|
||||
"}}"
|
||||
).format(name=self.name, hidden_id=self.hidden_id)
|
||||
|
||||
def tokenfield_updates(self):
|
||||
""" JS code for binding external fields changes with a reset """
|
||||
reset_input = self.tokenfield_reset_input()
|
||||
updates = [
|
||||
(
|
||||
'$( "#{u_id}" ).change( function() {{'
|
||||
" setup_{name}();"
|
||||
" {reset_input}"
|
||||
"}} );"
|
||||
).format(u_id=u_id, name=self.name, reset_input=reset_input)
|
||||
for u_id in self.update_on
|
||||
]
|
||||
return "".join(updates)
|
|
@ -32,7 +32,9 @@ from django.shortcuts import render
|
|||
from django.template.context_processors import csrf
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.utils.decorators import method_decorator
|
||||
from dal import autocomplete
|
||||
|
||||
from preferences.models import (
|
||||
Service,
|
||||
|
@ -169,3 +171,30 @@ def handler500(request):
|
|||
def handler404(request):
|
||||
"""The handler view for a 404 error"""
|
||||
return render(request, "errors/404.html", status=404)
|
||||
|
||||
|
||||
class AutocompleteLoggedOutViewMixin(autocomplete.Select2QuerySetView):
|
||||
obj_type = None # This MUST be overridden by child class
|
||||
query_set = None
|
||||
query_filter = "name__icontains" # Override this if necessary
|
||||
|
||||
def get_queryset(self):
|
||||
can, reason, _permission, query_set = self.obj_type.can_list(self.request.user)
|
||||
|
||||
if query_set:
|
||||
self.query_set = query_set
|
||||
else:
|
||||
self.query_set = self.obj_type.objects.none()
|
||||
|
||||
if hasattr(self, "filter_results"):
|
||||
self.filter_results()
|
||||
else:
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(**{self.query_filter: self.q})
|
||||
|
||||
return self.query_set
|
||||
|
||||
|
||||
class AutocompleteViewMixin(LoginRequiredMixin, AutocompleteLoggedOutViewMixin):
|
||||
pass
|
||||
|
||||
|
|
77
re2o/widgets.py
Normal file
77
re2o/widgets.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||
# quelques clics.
|
||||
#
|
||||
# Copyright © 2021 Gabriel Détraz
|
||||
# Copyright © 2021 Jean-Romain Garnier
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
Re2o Forms and ModelForms Widgets.
|
||||
|
||||
Used in others forms for using autocomplete engine.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from dal import autocomplete
|
||||
|
||||
|
||||
class AutocompleteModelWidget(autocomplete.ModelSelect2):
|
||||
""" A mixin subclassing django-autocomplete-light's Select2 model to pass default options
|
||||
See https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#passing-options-to-select2
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
select2_attrs = kwargs.get("attrs", {})
|
||||
kwargs["attrs"] = self.fill_default_select2_attrs(select2_attrs)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def fill_default_select2_attrs(self, attrs):
|
||||
"""
|
||||
See https://select2.org/configuration/options-api
|
||||
"""
|
||||
# Display the "x" button to clear the input by default
|
||||
attrs["data-allow-clear"] = attrs.get("data-allow-clear", "true")
|
||||
# If there are less than 10 results, just show all of them (no need to autocomplete)
|
||||
attrs["data-minimum-results-for-search"] = attrs.get(
|
||||
"data-minimum-results-for-search", 10
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
class AutocompleteMultipleModelWidget(autocomplete.ModelSelect2Multiple):
|
||||
""" A mixin subclassing django-autocomplete-light's Select2 model to pass default options
|
||||
See https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#passing-options-to-select2
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
select2_attrs = kwargs.get("attrs", {})
|
||||
kwargs["attrs"] = self.fill_default_select2_attrs(select2_attrs)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def fill_default_select2_attrs(self, attrs):
|
||||
"""
|
||||
See https://select2.org/configuration/options-api
|
||||
"""
|
||||
# Display the "x" button to clear the input by default
|
||||
attrs["data-allow-clear"] = attrs.get("data-allow-clear", "true")
|
||||
# If there are less than 10 results, just show all of them (no need to autocomplete)
|
||||
attrs["data-minimum-results-for-search"] = attrs.get(
|
||||
"data-minimum-results-for-search", 10
|
||||
)
|
||||
return attrs
|
115
search/engine.py
115
search/engine.py
|
@ -42,6 +42,20 @@ from preferences.models import GeneralOption
|
|||
from re2o.base import SortTable, re2o_paginator
|
||||
|
||||
|
||||
# List of fields the search applies to
|
||||
FILTER_FIELDS = [
|
||||
"users",
|
||||
"clubs",
|
||||
"machines",
|
||||
"factures",
|
||||
"bans",
|
||||
"whitelists",
|
||||
"rooms",
|
||||
"ports",
|
||||
"switches",
|
||||
]
|
||||
|
||||
|
||||
class Query:
|
||||
"""Class representing a query.
|
||||
It can contain the user-entered text, the operator for the query,
|
||||
|
@ -53,6 +67,7 @@ class Query:
|
|||
subqueries: list of Query objects when the current query is split in
|
||||
several parts.
|
||||
"""
|
||||
|
||||
def __init__(self, text="", case_sensitive=False):
|
||||
"""Initialise an instance of Query.
|
||||
|
||||
|
@ -98,27 +113,14 @@ class Query:
|
|||
return self.operator.join([q.plaintext for q in self.subqueries])
|
||||
|
||||
if self.case_sensitive:
|
||||
return "\"{}\"".format(self.text)
|
||||
return '"{}"'.format(self.text)
|
||||
|
||||
return self.text
|
||||
|
||||
|
||||
def filter_fields():
|
||||
"""Return the list of fields the search applies to."""
|
||||
return ["users",
|
||||
"clubs",
|
||||
"machines",
|
||||
"factures",
|
||||
"bans",
|
||||
"whitelists",
|
||||
"rooms",
|
||||
"ports",
|
||||
"switches"]
|
||||
|
||||
|
||||
def empty_filters():
|
||||
"""Build empty filters used by Django."""
|
||||
return {f: Q() for f in filter_fields()}
|
||||
return {f: Q() for f in FILTER_FIELDS}
|
||||
|
||||
|
||||
def is_int(variable):
|
||||
|
@ -176,10 +178,9 @@ def finish_results(request, results, col, order):
|
|||
max_result = GeneralOption.get_cached_value("search_display_page")
|
||||
for name, val in results.items():
|
||||
page_arg = name + "_page"
|
||||
results[name] = re2o_paginator(request,
|
||||
val.distinct(),
|
||||
max_result,
|
||||
page_arg=page_arg)
|
||||
results[name] = re2o_paginator(
|
||||
request, val.distinct(), max_result, page_arg=page_arg
|
||||
)
|
||||
|
||||
results.update({"max_result": max_result})
|
||||
|
||||
|
@ -206,9 +207,9 @@ def contains_filter(attribute, word, case_sensitive=False):
|
|||
return Q(**{attr: word})
|
||||
|
||||
|
||||
def search_single_word(word, filters, user, start, end,
|
||||
user_state, email_state, aff,
|
||||
case_sensitive=False):
|
||||
def search_single_word(
|
||||
word, filters, user, start, end, user_state, email_state, aff, case_sensitive=False
|
||||
):
|
||||
"""Construct the correct filters to match differents fields of some models
|
||||
with the given query according to the given filters.
|
||||
The match fields are either CharField or IntegerField that will be displayed
|
||||
|
@ -230,10 +231,7 @@ def search_single_word(word, filters, user, start, end,
|
|||
)
|
||||
|
||||
# Users have a name whereas clubs only have a surname
|
||||
filter_users = (
|
||||
filter_clubs
|
||||
| contains_filter("name", word, case_sensitive)
|
||||
)
|
||||
filter_users = filter_clubs | contains_filter("name", word, case_sensitive)
|
||||
|
||||
if not User.can_view_all(user)[0]:
|
||||
filter_clubs &= Q(id=user.id)
|
||||
|
@ -252,12 +250,15 @@ def search_single_word(word, filters, user, start, end,
|
|||
if "1" in aff:
|
||||
filter_machines = (
|
||||
contains_filter("name", word, case_sensitive)
|
||||
| (contains_filter("user__pseudo", word, case_sensitive)
|
||||
& Q(user__state__in=user_state)
|
||||
& Q(user__email_state__in=email_state))
|
||||
| (
|
||||
contains_filter("user__pseudo", word, case_sensitive)
|
||||
& Q(user__state__in=user_state)
|
||||
& Q(user__email_state__in=email_state)
|
||||
)
|
||||
| contains_filter("interface__domain__name", word, case_sensitive)
|
||||
| contains_filter("interface__domain__related_domain__name",
|
||||
word, case_sensitive)
|
||||
| contains_filter(
|
||||
"interface__domain__related_domain__name", word, case_sensitive
|
||||
)
|
||||
| contains_filter("interface__mac_address", word, case_sensitive)
|
||||
| contains_filter("interface__ipv4__ipv4", word, case_sensitive)
|
||||
)
|
||||
|
@ -339,13 +340,12 @@ def search_single_word(word, filters, user, start, end,
|
|||
# Switch ports
|
||||
if "6" in aff and User.can_view_all(user):
|
||||
filter_ports = (
|
||||
contains_filter("machine_interface__domain__name",
|
||||
word, case_sensitive)
|
||||
| contains_filter("related__switch__interface__domain__name",
|
||||
word, case_sensitive)
|
||||
contains_filter("machine_interface__domain__name", word, case_sensitive)
|
||||
| contains_filter(
|
||||
"related__switch__interface__domain__name", word, case_sensitive
|
||||
)
|
||||
| contains_filter("custom_profile__name", word, case_sensitive)
|
||||
| contains_filter("custom_profile__profil_default",
|
||||
word, case_sensitive)
|
||||
| contains_filter("custom_profile__profil_default", word, case_sensitive)
|
||||
| contains_filter("details", word, case_sensitive)
|
||||
# Added through annotate
|
||||
| contains_filter("room_full_name", word, case_sensitive)
|
||||
|
@ -360,8 +360,7 @@ def search_single_word(word, filters, user, start, end,
|
|||
filter_switches = (
|
||||
contains_filter("interface__domain__name", word, case_sensitive)
|
||||
| contains_filter("interface__ipv4__ipv4", word, case_sensitive)
|
||||
| contains_filter("switchbay__building__name",
|
||||
word, case_sensitive)
|
||||
| contains_filter("switchbay__building__name", word, case_sensitive)
|
||||
| contains_filter("stack__name", word, case_sensitive)
|
||||
| contains_filter("model__reference", word, case_sensitive)
|
||||
| contains_filter("model__constructor__name", word, case_sensitive)
|
||||
|
@ -399,13 +398,11 @@ def apply_filters(filters, user, aff):
|
|||
# Users and clubs
|
||||
if "0" in aff:
|
||||
results["users"] = Adherent.objects.annotate(
|
||||
room_full_name=Concat("room__building__name",
|
||||
Value(" "), "room__name"),
|
||||
room_full_name=Concat("room__building__name", Value(" "), "room__name"),
|
||||
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
||||
).filter(filters["users"])
|
||||
results["clubs"] = Club.objects.annotate(
|
||||
room_full_name=Concat("room__building__name",
|
||||
Value(" "), "room__name"),
|
||||
room_full_name=Concat("room__building__name", Value(" "), "room__name"),
|
||||
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
||||
).filter(filters["clubs"])
|
||||
|
||||
|
@ -435,8 +432,7 @@ def apply_filters(filters, user, aff):
|
|||
# Switch ports
|
||||
if "6" in aff and User.can_view_all(user):
|
||||
results["ports"] = Port.objects.annotate(
|
||||
room_full_name=Concat("room__building__name",
|
||||
Value(" "), "room__name"),
|
||||
room_full_name=Concat("room__building__name", Value(" "), "room__name"),
|
||||
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
||||
).filter(filters["ports"])
|
||||
|
||||
|
@ -455,24 +451,32 @@ def search_single_query(query, filters, user, start, end, user_state, email_stat
|
|||
newfilters = empty_filters()
|
||||
for q in query.subqueries:
|
||||
# Construct an independent filter for each subquery
|
||||
subfilters = search_single_query(q, empty_filters(), user,
|
||||
start, end, user_state,
|
||||
email_state, aff)
|
||||
subfilters = search_single_query(
|
||||
q, empty_filters(), user, start, end, user_state, email_state, aff
|
||||
)
|
||||
|
||||
# Apply the subfilter
|
||||
for field in filter_fields():
|
||||
for field in FILTER_FIELDS:
|
||||
newfilters[field] &= subfilters[field]
|
||||
|
||||
# Add these filters to the existing ones
|
||||
for field in filter_fields():
|
||||
for field in FILTER_FIELDS:
|
||||
filters[field] |= newfilters[field]
|
||||
|
||||
return filters
|
||||
|
||||
# Handle standard queries
|
||||
return search_single_word(query.text, filters, user, start, end,
|
||||
user_state, email_state, aff,
|
||||
query.case_sensitive)
|
||||
return search_single_word(
|
||||
query.text,
|
||||
filters,
|
||||
user,
|
||||
start,
|
||||
end,
|
||||
user_state,
|
||||
email_state,
|
||||
aff,
|
||||
query.case_sensitive,
|
||||
)
|
||||
|
||||
|
||||
def create_queries(query):
|
||||
|
@ -564,4 +568,9 @@ def create_queries(query):
|
|||
|
||||
queries.append(current_query)
|
||||
|
||||
# Make sure there is at least one query, even if it's empty
|
||||
# Otherwise, display filters (for advanced search) won't work
|
||||
# when the search text field is empty
|
||||
queries = queries or [Query()]
|
||||
|
||||
return queries
|
||||
|
|
49
static/css/autocomplete.css
Normal file
49
static/css/autocomplete.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Don't blame me for all the '!important's
|
||||
See github.com/yourlabs/django-autocomplete-light/issues/1149
|
||||
*/
|
||||
|
||||
/* dal bootstrap css fix */
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
min-width: 10em !important;
|
||||
}
|
||||
|
||||
/* django-addanother bootstrap css fix */
|
||||
.related-widget-wrapper{
|
||||
padding-right: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link{
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.select2-container .select2-selection--single {
|
||||
height: 34px !important;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 100% !important;
|
||||
display: inline !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
.select2-container .select2-selection--multiple {
|
||||
min-height: 45px !important;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
height: 100% !important;
|
||||
display: inline !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
overflow: auto !important;
|
||||
}
|
210
static/css/bootstrap-tokenfield.css
vendored
210
static/css/bootstrap-tokenfield.css
vendored
|
@ -1,210 +0,0 @@
|
|||
/*!
|
||||
* bootstrap-tokenfield
|
||||
* https://github.com/sliptree/bootstrap-tokenfield
|
||||
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
|
||||
*/
|
||||
@-webkit-keyframes blink {
|
||||
0% {
|
||||
border-color: #ededed;
|
||||
}
|
||||
100% {
|
||||
border-color: #b94a48;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes blink {
|
||||
0% {
|
||||
border-color: #ededed;
|
||||
}
|
||||
100% {
|
||||
border-color: #b94a48;
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {
|
||||
border-color: #ededed;
|
||||
}
|
||||
100% {
|
||||
border-color: #b94a48;
|
||||
}
|
||||
}
|
||||
.tokenfield {
|
||||
height: auto;
|
||||
min-height: 34px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.tokenfield.focus {
|
||||
border-color: #66afe9;
|
||||
outline: 0;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
.tokenfield .token {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #ededed;
|
||||
white-space: nowrap;
|
||||
margin: -1px 5px 5px 0;
|
||||
height: 22px;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
}
|
||||
.tokenfield .token:hover {
|
||||
border-color: #b9b9b9;
|
||||
}
|
||||
.tokenfield .token.active {
|
||||
border-color: #52a8ec;
|
||||
border-color: rgba(82, 168, 236, 0.8);
|
||||
}
|
||||
.tokenfield .token.duplicate {
|
||||
border-color: #ebccd1;
|
||||
-webkit-animation-name: blink;
|
||||
animation-name: blink;
|
||||
-webkit-animation-duration: 0.1s;
|
||||
animation-duration: 0.1s;
|
||||
-webkit-animation-direction: normal;
|
||||
animation-direction: normal;
|
||||
-webkit-animation-timing-function: ease;
|
||||
animation-timing-function: ease;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.tokenfield .token.invalid {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
-webkit-border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px dotted #d9534f;
|
||||
}
|
||||
.tokenfield .token.invalid.active {
|
||||
background: #ededed;
|
||||
border: 1px solid #ededed;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.tokenfield .token .token-label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.tokenfield .token .close {
|
||||
font-family: Arial;
|
||||
display: inline-block;
|
||||
line-height: 100%;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.49em;
|
||||
margin-left: 5px;
|
||||
float: none;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.tokenfield .token-input {
|
||||
background: none;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
border: 0;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.tokenfield .token-input:focus {
|
||||
border-color: transparent;
|
||||
outline: 0;
|
||||
/* IE6-9 */
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.tokenfield.disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
.tokenfield.disabled .token-input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.tokenfield.disabled .token:hover {
|
||||
cursor: not-allowed;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
.tokenfield.disabled .token:hover .close {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.2;
|
||||
filter: alpha(opacity=20);
|
||||
}
|
||||
.has-warning .tokenfield.focus {
|
||||
border-color: #66512c;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
|
||||
}
|
||||
.has-error .tokenfield.focus {
|
||||
border-color: #843534;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
|
||||
}
|
||||
.has-success .tokenfield.focus {
|
||||
border-color: #2b542c;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
|
||||
}
|
||||
.tokenfield.input-sm,
|
||||
.input-group-sm .tokenfield {
|
||||
min-height: 30px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.input-group-sm .token,
|
||||
.tokenfield.input-sm .token {
|
||||
height: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.input-group-sm .token-input,
|
||||
.tokenfield.input-sm .token-input {
|
||||
height: 18px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.tokenfield.input-lg,
|
||||
.input-group-lg .tokenfield {
|
||||
height: auto;
|
||||
min-height: 45px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.input-group-lg .token,
|
||||
.tokenfield.input-lg .token {
|
||||
height: 25px;
|
||||
}
|
||||
.input-group-lg .token-label,
|
||||
.tokenfield.input-lg .token-label {
|
||||
line-height: 23px;
|
||||
}
|
||||
.input-group-lg .token .close,
|
||||
.tokenfield.input-lg .token .close {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.input-group-lg .token-input,
|
||||
.tokenfield.input-lg .token-input {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
margin-bottom: 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.tokenfield.rtl {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
.tokenfield.rtl .token {
|
||||
margin: -1px 0 5px 5px;
|
||||
}
|
||||
.tokenfield.rtl .token .token-label {
|
||||
padding-left: 0px;
|
||||
padding-right: 4px;
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
span.twitter-typeahead .tt-menu,
|
||||
span.twitter-typeahead .tt-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
span.twitter-typeahead .tt-suggestion {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 1.42857143;
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
span.twitter-typeahead .tt-suggestion.tt-cursor,
|
||||
span.twitter-typeahead .tt-suggestion:hover,
|
||||
span.twitter-typeahead .tt-suggestion:focus {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
.input-group.input-group-lg span.twitter-typeahead .form-control {
|
||||
height: 46px;
|
||||
padding: 10px 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.3333333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.input-group.input-group-sm span.twitter-typeahead .form-control {
|
||||
height: 30px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
span.twitter-typeahead {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group span.twitter-typeahead {
|
||||
display: block !important;
|
||||
height: 34px;
|
||||
}
|
||||
.input-group span.twitter-typeahead .tt-menu,
|
||||
.input-group span.twitter-typeahead .tt-dropdown-menu {
|
||||
top: 32px !important;
|
||||
}
|
||||
.input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control {
|
||||
border-radius: 0;
|
||||
}
|
||||
.input-group span.twitter-typeahead:first-child .form-control {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group span.twitter-typeahead:last-child .form-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.input-group.input-group-sm span.twitter-typeahead {
|
||||
height: 30px;
|
||||
}
|
||||
.input-group.input-group-sm span.twitter-typeahead .tt-menu,
|
||||
.input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu {
|
||||
top: 30px !important;
|
||||
}
|
||||
.input-group.input-group-lg span.twitter-typeahead {
|
||||
height: 46px;
|
||||
}
|
||||
.input-group.input-group-lg span.twitter-typeahead .tt-menu,
|
||||
.input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu {
|
||||
top: 46px !important;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#### Sliptree
|
||||
- by Illimar Tambek for [Sliptree](http://sliptree.com)
|
||||
- Copyright (c) 2013 by Sliptree
|
||||
|
||||
Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
1042
static/js/bootstrap-tokenfield/bootstrap-tokenfield.js
vendored
1042
static/js/bootstrap-tokenfield/bootstrap-tokenfield.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -1,19 +0,0 @@
|
|||
Copyright (c) 2013-2014 Twitter, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
File diff suppressed because it is too large
Load diff
|
@ -45,16 +45,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{# Preload JavaScript #}
|
||||
{% bootstrap_javascript %}
|
||||
<script src="/static/js/typeahead/typeahead.js"></script>
|
||||
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
|
||||
<script src="{% static 'js/collapse-from-url.js' %}"></script>
|
||||
|
||||
{% block custom_js %}{% endblock %}
|
||||
|
||||
{# Load CSS #}
|
||||
{% bootstrap_css %}
|
||||
<link href="{% static 'css/typeaheadjs.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/bootstrap-tokenfield.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/autocomplete.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
|
||||
{# load theme #}
|
||||
{% if request.user.is_authenticated %}
|
||||
|
@ -118,4 +115,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -29,6 +29,7 @@ from django.template.loader import render_to_string
|
|||
from django.forms import ModelForm, Form
|
||||
from re2o.field_permissions import FieldPermissionFormMixin
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.widgets import AutocompleteModelWidget
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .models import Ticket, CommentTicket
|
||||
|
@ -58,6 +59,11 @@ class EditTicketForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Ticket
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"user": AutocompleteModelWidget(
|
||||
url="/users/user-autocomplete",
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
||||
|
|
|
@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Ticket" %}{% endblock %}
|
||||
|
@ -34,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<h2>{% trans "Ticket opening" %}</h2>
|
||||
|
||||
{% bootstrap_form_errors ticketform %}
|
||||
{{ ticketform.media }}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -38,6 +38,10 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from machines.models import Interface
|
||||
from machines.forms import EditMachineForm, NewMachineForm
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.widgets import (
|
||||
AutocompleteModelWidget,
|
||||
AutocompleteMultipleModelWidget,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Port,
|
||||
|
@ -62,6 +66,17 @@ class PortForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Port
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"switch": AutocompleteModelWidget(url="/topologie/switch-autocomplete"),
|
||||
"room": AutocompleteModelWidget(url="/topologie/room-autocomplete"),
|
||||
"machine_interface": AutocompleteModelWidget(
|
||||
url="/machine/machine-autocomplete"
|
||||
),
|
||||
"related": AutocompleteModelWidget(url="/topologie/port-autocomplete"),
|
||||
"custom_profile": AutocompleteModelWidget(
|
||||
url="/topologie/portprofile-autocomplete"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -154,7 +169,7 @@ class AddAccessPointForm(NewMachineForm):
|
|||
class EditAccessPointForm(EditMachineForm):
|
||||
"""Form used to edit access points."""
|
||||
|
||||
class Meta:
|
||||
class Meta(EditMachineForm.Meta):
|
||||
model = AccessPoint
|
||||
fields = "__all__"
|
||||
|
||||
|
@ -162,9 +177,15 @@ class EditAccessPointForm(EditMachineForm):
|
|||
class EditSwitchForm(EditMachineForm):
|
||||
"""Form used to edit switches."""
|
||||
|
||||
class Meta:
|
||||
class Meta(EditMachineForm.Meta):
|
||||
model = Switch
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"switchbay": AutocompleteModelWidget(
|
||||
url="/topologie/switchbay-autocomplete"
|
||||
),
|
||||
"user": AutocompleteModelWidget(url="/users/user-autocomplete"),
|
||||
}
|
||||
|
||||
|
||||
class NewSwitchForm(NewMachineForm):
|
||||
|
@ -180,6 +201,9 @@ class EditRoomForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = Room
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"building": AutocompleteModelWidget(url="/topologie/building-autocomplete")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -196,7 +220,11 @@ class CreatePortsForm(forms.Form):
|
|||
class EditModelSwitchForm(FormRevMixin, ModelForm):
|
||||
"""Form used to edit switch models."""
|
||||
|
||||
members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False)
|
||||
members = forms.ModelMultipleChoiceField(
|
||||
Switch.objects.all(),
|
||||
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModelSwitch
|
||||
|
@ -230,11 +258,18 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm):
|
|||
class EditSwitchBayForm(FormRevMixin, ModelForm):
|
||||
"""Form used to edit switch bays."""
|
||||
|
||||
members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False)
|
||||
members = forms.ModelMultipleChoiceField(
|
||||
Switch.objects.all(),
|
||||
required=False,
|
||||
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SwitchBay
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"building": AutocompleteModelWidget(url="/topologie/building-autocomplete")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -279,6 +314,12 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
|
|||
class Meta:
|
||||
model = PortProfile
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"vlan_tagged": AutocompleteMultipleModelWidget(
|
||||
url="/machines/vlan-autocomplete"
|
||||
),
|
||||
"vlan_untagged": AutocompleteModelWidget(url="/machines/vlan-autocomplete"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
|
|
@ -731,6 +731,22 @@ class Dormitory(AclMixin, RevMixin, models.Model):
|
|||
else:
|
||||
return cache.get_or_set("multiple_dorms", cls.objects.count() > 1)
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list dormitory
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -762,6 +778,22 @@ class Building(AclMixin, RevMixin, models.Model):
|
|||
else:
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list building
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def cached_name(self):
|
||||
return self.get_name()
|
||||
|
@ -944,6 +976,22 @@ class Room(AclMixin, RevMixin, models.Model):
|
|||
verbose_name_plural = _("rooms")
|
||||
unique_together = ("name", "building")
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list room
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.building.cached_name + " " + self.name
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||
|
@ -41,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% csrf_token %}
|
||||
{% if topoform %}
|
||||
<h3>{% trans "Specific settings for the switch" %}</h3>
|
||||
{% massive_bootstrap_form topoform 'switch_interface' %}
|
||||
{% bootstrap_form topoform %}
|
||||
{% endif %}
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type="submit" icon='ok' button_class='btn-success' %}
|
||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||
|
@ -37,11 +36,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %}
|
||||
{% bootstrap_form topoform %}
|
||||
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
|
||||
</form>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{{ topoform.media }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||
|
@ -32,9 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% block content %}
|
||||
{% if topoform %}
|
||||
{% bootstrap_form_errors topoform %}
|
||||
{{ topoform.media }}
|
||||
{% endif %}
|
||||
{% if machineform %}
|
||||
{% bootstrap_form_errors machineform %}
|
||||
{{ machineform.media }}
|
||||
{% endif %}
|
||||
{% if domainform %}
|
||||
{% bootstrap_form_errors domainform %}
|
||||
|
@ -46,11 +47,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% csrf_token %}
|
||||
{% if topoform %}
|
||||
<h3>{% blocktrans %}Specific settings for the {{ device }} object{% endblocktrans %}</h3>
|
||||
{% massive_bootstrap_form topoform 'ipv4,machine' mbf_param=i_mbf_param%}
|
||||
{% bootstrap_form topoform %}
|
||||
{% endif %}
|
||||
{% if machineform %}
|
||||
<h3>{% blocktrans %}General settings for the machine linked to the {{ device }} object{% endblocktrans %}</h3>
|
||||
{% massive_bootstrap_form machineform 'user' %}
|
||||
{% bootstrap_form machineform %}
|
||||
{% endif %}
|
||||
{% if domainform %}
|
||||
<h3>{% trans "DNS name" %}</h3>
|
||||
|
|
|
@ -28,6 +28,7 @@ from __future__ import unicode_literals
|
|||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
from . import views_autocomplete
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", views.index, name="index"),
|
||||
|
@ -169,4 +170,12 @@ urlpatterns = [
|
|||
views.del_module_on,
|
||||
name="del-module-on",
|
||||
),
|
||||
### Autocomplete Views
|
||||
url(r'^room-autocomplete/$', views_autocomplete.RoomAutocomplete.as_view(), name='room-autocomplete',),
|
||||
url(r'^building-autocomplete/$', views_autocomplete.BuildingAutocomplete.as_view(), name='building-autocomplete',),
|
||||
url(r'^dormitory-autocomplete/$', views_autocomplete.DormitoryAutocomplete.as_view(), name='dormitory-autocomplete',),
|
||||
url(r'^switch-autocomplete/$', views_autocomplete.SwitchAutocomplete.as_view(), name='switch-autocomplete',),
|
||||
url(r'^port-autocomplete/$', views_autocomplete.PortAutocomplete.as_view(), name='profile-autocomplete',),
|
||||
url(r'^portprofile-autocomplete/$', views_autocomplete.PortProfileAutocomplete.as_view(), name='portprofile-autocomplete',),
|
||||
url(r'^switchbay-autocomplete/$', views_autocomplete.SwitchBayAutocomplete.as_view(), name='switchbay-autocomplete',),
|
||||
]
|
||||
|
|
|
@ -56,7 +56,6 @@ from machines.forms import (
|
|||
AddInterfaceForm,
|
||||
EditOptionVlanForm,
|
||||
)
|
||||
from machines.views import generate_ipv4_mbf_param
|
||||
from machines.models import Interface, Service_link, Vlan
|
||||
from preferences.models import AssoOption, GeneralOption
|
||||
|
||||
|
@ -560,13 +559,11 @@ def new_switch(request):
|
|||
new_domain_obj.save()
|
||||
messages.success(request, _("The switch was created."))
|
||||
return redirect(reverse("topologie:index"))
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface, False)
|
||||
return form(
|
||||
{
|
||||
"topoform": interface,
|
||||
"machineform": switch,
|
||||
"domainform": domain,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"device": _("switch"),
|
||||
},
|
||||
"topologie/topo_more.html",
|
||||
|
@ -634,14 +631,12 @@ def edit_switch(request, switch, switchid):
|
|||
new_domain_obj.save()
|
||||
messages.success(request, _("The switch was edited."))
|
||||
return redirect(reverse("topologie:index"))
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
||||
return form(
|
||||
{
|
||||
"id_switch": switchid,
|
||||
"topoform": interface_form,
|
||||
"machineform": switch_form,
|
||||
"domainform": domain_form,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"device": _("switch"),
|
||||
},
|
||||
"topologie/topo_more.html",
|
||||
|
@ -686,13 +681,11 @@ def new_ap(request):
|
|||
new_domain_obj.save()
|
||||
messages.success(request, _("The access point was created."))
|
||||
return redirect(reverse("topologie:index-ap"))
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface, False)
|
||||
return form(
|
||||
{
|
||||
"topoform": interface,
|
||||
"machineform": ap,
|
||||
"domainform": domain,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"device": _("access point"),
|
||||
},
|
||||
"topologie/topo_more.html",
|
||||
|
@ -737,13 +730,11 @@ def edit_ap(request, ap, **_kwargs):
|
|||
new_domain_obj.save()
|
||||
messages.success(request, _("The access point was edited."))
|
||||
return redirect(reverse("topologie:index-ap"))
|
||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
||||
return form(
|
||||
{
|
||||
"topoform": interface_form,
|
||||
"machineform": ap_form,
|
||||
"domainform": domain_form,
|
||||
"i_mbf_param": i_mbf_param,
|
||||
"device": _("access point"),
|
||||
},
|
||||
"topologie/topo_more.html",
|
||||
|
|
169
topologie/views_autocomplete.py
Normal file
169
topologie/views_autocomplete.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||
# quelques clics.
|
||||
#
|
||||
# Copyright © 2017-2020 Gabriel Détraz
|
||||
# Copyright © 2017-2020 Jean-Romain Garnier
|
||||
#
|
||||
# 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.
|
||||
|
||||
# App de gestion des users pour re2o
|
||||
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin
|
||||
# Gplv2
|
||||
"""
|
||||
Django views autocomplete view
|
||||
|
||||
Here are defined the autocomplete class based view.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Q, Value, CharField
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from .models import Room, Dormitory, Building, Switch, PortProfile, Port, SwitchBay
|
||||
|
||||
from re2o.views import AutocompleteViewMixin, AutocompleteLoggedOutViewMixin
|
||||
|
||||
|
||||
class RoomAutocomplete(AutocompleteLoggedOutViewMixin):
|
||||
obj_type = Room
|
||||
|
||||
# Precision on search to add annotations so search behaves more like users expect it to
|
||||
def filter_results(self):
|
||||
# Suppose we have a dorm named Dorm, a building named B, and rooms from 001 - 999
|
||||
# Comments explain what we try to match
|
||||
self.query_set = self.query_set.annotate(
|
||||
full_name=Concat(
|
||||
"building__name", Value(" "), "name"
|
||||
), # Match when the user searches "B 001"
|
||||
full_name_stuck=Concat("building__name", "name"), # Match "B001"
|
||||
dorm_name=Concat(
|
||||
"building__dormitory__name", Value(" "), "name"
|
||||
), # Match "Dorm 001"
|
||||
dorm_full_name=Concat(
|
||||
"building__dormitory__name",
|
||||
Value(" "),
|
||||
"building__name",
|
||||
Value(" "),
|
||||
"name",
|
||||
), # Match "Dorm B 001"
|
||||
dorm_full_colon_name=Concat(
|
||||
"building__dormitory__name",
|
||||
Value(" : "),
|
||||
"building__name",
|
||||
Value(" "),
|
||||
"name",
|
||||
), # Match "Dorm : B 001" (see Room's full_name property)
|
||||
).all()
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(full_name__icontains=self.q)
|
||||
| Q(full_name_stuck__icontains=self.q)
|
||||
| Q(dorm_name__icontains=self.q)
|
||||
| Q(dorm_full_name__icontains=self.q)
|
||||
| Q(dorm_full_colon_name__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class DormitoryAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Dormitory
|
||||
|
||||
|
||||
class BuildingAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Building
|
||||
|
||||
def filter_results(self):
|
||||
# We want to be able to filter by dorm so it's easier
|
||||
self.query_set = self.query_set.annotate(
|
||||
full_name=Concat("dormitory__name", Value(" "), "name"),
|
||||
full_name_colon=Concat("dormitory__name", Value(" : "), "name"),
|
||||
).all()
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(full_name__icontains=self.q) | Q(full_name_colon__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class SwitchAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Switch
|
||||
|
||||
|
||||
class PortAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Port
|
||||
|
||||
def filter_results(self):
|
||||
# We want to enter the switch name, not just the port number
|
||||
# Because we're concatenating a CharField and an Integer, we have to specify the output_field
|
||||
self.query_set = self.query_set.annotate(
|
||||
full_name=Concat(
|
||||
"switch__name", Value(" "), "port", output_field=CharField()
|
||||
),
|
||||
full_name_stuck=Concat("switch__name", "port", output_field=CharField()),
|
||||
full_name_dash=Concat(
|
||||
"switch__name", Value(" - "), "port", output_field=CharField()
|
||||
),
|
||||
).all()
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(full_name__icontains=self.q)
|
||||
| Q(full_name_stuck__icontains=self.q)
|
||||
| Q(full_name_dash__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class SwitchBayAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = SwitchBay
|
||||
|
||||
def filter_results(self):
|
||||
# See RoomAutocomplete.filter_results
|
||||
self.query_set = self.query_set.annotate(
|
||||
full_name=Concat(
|
||||
"building__name", Value(" "), "name"
|
||||
),
|
||||
dorm_name=Concat(
|
||||
"building__dormitory__name", Value(" "), "name"
|
||||
),
|
||||
dorm_full_name=Concat(
|
||||
"building__dormitory__name",
|
||||
Value(" "),
|
||||
"building__name",
|
||||
Value(" "),
|
||||
"name",
|
||||
),
|
||||
dorm_full_colon_name=Concat(
|
||||
"building__dormitory__name",
|
||||
Value(" : "),
|
||||
"building__name",
|
||||
Value(" "),
|
||||
"name",
|
||||
),
|
||||
).all()
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(full_name__icontains=self.q)
|
||||
| Q(dorm_name__icontains=self.q)
|
||||
| Q(dorm_full_name__icontains=self.q)
|
||||
| Q(dorm_full_colon_name__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class PortProfileAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = PortProfile
|
121
users/forms.py
121
users/forms.py
|
@ -46,7 +46,10 @@ from os import walk, path
|
|||
from django import forms
|
||||
from django.forms import ModelForm, Form
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.contrib.auth.password_validation import validate_password, password_validators_help_text_html
|
||||
from django.contrib.auth.password_validation import (
|
||||
validate_password,
|
||||
password_validators_help_text_html,
|
||||
)
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
@ -61,6 +64,10 @@ from preferences.models import OptionalUser
|
|||
from re2o.utils import remove_user_room
|
||||
from re2o.base import get_input_formats_help_text
|
||||
from re2o.mixins import FormRevMixin
|
||||
from re2o.widgets import (
|
||||
AutocompleteMultipleModelWidget,
|
||||
AutocompleteModelWidget,
|
||||
)
|
||||
from re2o.field_permissions import FieldPermissionFormMixin
|
||||
|
||||
from preferences.models import GeneralOption
|
||||
|
@ -156,14 +163,10 @@ class ServiceUserAdminForm(FormRevMixin, forms.ModelForm):
|
|||
"""
|
||||
|
||||
password1 = forms.CharField(
|
||||
label=_("Password"),
|
||||
widget=forms.PasswordInput,
|
||||
max_length=255,
|
||||
label=_("Password"), widget=forms.PasswordInput, max_length=255
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label=_("Password confirmation"),
|
||||
widget=forms.PasswordInput,
|
||||
max_length=255,
|
||||
label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -215,6 +218,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
|
|||
DjangoForm : Inherit from basic django form
|
||||
|
||||
"""
|
||||
|
||||
selfpasswd = forms.CharField(
|
||||
label=_("Current password"), max_length=255, widget=forms.PasswordInput
|
||||
)
|
||||
|
@ -222,12 +226,10 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
|
|||
label=_("New password"),
|
||||
max_length=255,
|
||||
widget=forms.PasswordInput,
|
||||
help_text=password_validators_help_text_html()
|
||||
help_text=password_validators_help_text_html(),
|
||||
)
|
||||
passwd2 = forms.CharField(
|
||||
label=_("New password confirmation"),
|
||||
max_length=255,
|
||||
widget=forms.PasswordInput,
|
||||
label=_("New password confirmation"), max_length=255, widget=forms.PasswordInput
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -280,7 +282,7 @@ class ResetPasswordForm(forms.Form):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
pseudo = forms.CharField(label=_("Username"), max_length=255)
|
||||
email = forms.EmailField(max_length=255)
|
||||
|
@ -292,13 +294,11 @@ class MassArchiveForm(forms.Form):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
date = forms.DateTimeField(help_text="%d/%m/%y")
|
||||
full_archive = forms.BooleanField(
|
||||
label=_(
|
||||
"Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"
|
||||
),
|
||||
label=_("Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"),
|
||||
initial=False,
|
||||
required=False,
|
||||
)
|
||||
|
@ -323,7 +323,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -350,6 +350,16 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
"telephone",
|
||||
"room",
|
||||
]
|
||||
widgets = {
|
||||
"school": AutocompleteModelWidget(url="/users/school-autocomplete"),
|
||||
"room": AutocompleteModelWidget(
|
||||
url="/topologie/room-autocomplete",
|
||||
attrs={
|
||||
"data-minimum-input-length": 3 # Only trigger autocompletion after 3 characters have been typed
|
||||
},
|
||||
),
|
||||
"shell": AutocompleteModelWidget(url="/users/shell-autocomplete"),
|
||||
}
|
||||
|
||||
force = forms.BooleanField(
|
||||
label=_("Force the move?"), initial=False, required=False
|
||||
|
@ -412,7 +422,8 @@ class AdherentCreationForm(AdherentForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
# Champ pour choisir si un lien est envoyé par mail pour le mot de passe
|
||||
init_password_by_mail_info = _(
|
||||
"If this options is set, you will receive a link to set"
|
||||
|
@ -425,9 +436,7 @@ class AdherentCreationForm(AdherentForm):
|
|||
)
|
||||
|
||||
init_password_by_mail = forms.BooleanField(
|
||||
help_text=init_password_by_mail_info,
|
||||
required=False,
|
||||
initial=True
|
||||
help_text=init_password_by_mail_info, required=False, initial=True
|
||||
)
|
||||
init_password_by_mail.label = _("Send password reset link by email.")
|
||||
|
||||
|
@ -438,7 +447,7 @@ class AdherentCreationForm(AdherentForm):
|
|||
label=_("Password"),
|
||||
widget=forms.PasswordInput,
|
||||
max_length=255,
|
||||
help_text=password_validators_help_text_html()
|
||||
help_text=password_validators_help_text_html(),
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
required=False,
|
||||
|
@ -461,7 +470,7 @@ class AdherentCreationForm(AdherentForm):
|
|||
# Checkbox for GTU
|
||||
gtu_check = forms.BooleanField(required=True)
|
||||
|
||||
class Meta:
|
||||
class Meta(AdherentForm.Meta):
|
||||
model = Adherent
|
||||
fields = [
|
||||
"name",
|
||||
|
@ -528,8 +537,12 @@ class AdherentCreationForm(AdherentForm):
|
|||
# Save the provided password in hashed format
|
||||
user = super(AdherentForm, self).save(commit=False)
|
||||
|
||||
is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation")
|
||||
set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail")
|
||||
is_set_password_allowed = OptionalUser.get_cached_value(
|
||||
"allow_set_password_during_user_creation"
|
||||
)
|
||||
set_passwd = is_set_password_allowed and not self.cleaned_data.get(
|
||||
"init_password_by_mail"
|
||||
)
|
||||
if set_passwd:
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
|
||||
|
@ -544,7 +557,7 @@ class AdherentEditForm(AdherentForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdherentEditForm, self).__init__(*args, **kwargs)
|
||||
|
@ -556,7 +569,7 @@ class AdherentEditForm(AdherentForm):
|
|||
if "shell" in self.fields:
|
||||
self.fields["shell"].empty_label = _("Default shell")
|
||||
|
||||
class Meta:
|
||||
class Meta(AdherentForm.Meta):
|
||||
model = Adherent
|
||||
fields = [
|
||||
"name",
|
||||
|
@ -580,7 +593,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -609,6 +622,11 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
"shell",
|
||||
"mailing",
|
||||
]
|
||||
widgets = {
|
||||
"school": AutocompleteModelWidget(url="/users/school-autocomplete"),
|
||||
"room": AutocompleteModelWidget(url="/topologie/room-autocomplete"),
|
||||
"shell": AutocompleteModelWidget(url="/users/shell-autocomplete"),
|
||||
}
|
||||
|
||||
def clean_telephone(self):
|
||||
"""Clean telephone, check if telephone is made mandatory, and
|
||||
|
@ -632,11 +650,19 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = ["administrators", "members"]
|
||||
widgets = {
|
||||
"administrators": AutocompleteMultipleModelWidget(
|
||||
url="/users/adherent-autocomplete"
|
||||
),
|
||||
"members": AutocompleteMultipleModelWidget(
|
||||
url="/users/adherent-autocomplete"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -648,7 +674,7 @@ class PasswordForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -665,7 +691,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
password = forms.CharField(
|
||||
label=_("New password"),
|
||||
|
@ -704,7 +730,7 @@ class EditServiceUserForm(ServiceUserForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
password = forms.CharField(
|
||||
label=_("New password"),
|
||||
|
@ -724,7 +750,7 @@ class StateForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -742,7 +768,7 @@ class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
groups = forms.ModelMultipleChoiceField(
|
||||
Group.objects.all(), widget=forms.CheckboxSelectMultiple, required=False
|
||||
|
@ -764,7 +790,7 @@ class SchoolForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = School
|
||||
|
@ -781,7 +807,7 @@ class ShellForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ListShell
|
||||
|
@ -800,7 +826,7 @@ class ListRightForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
permissions = forms.ModelMultipleChoiceField(
|
||||
Permission.objects.all().select_related("content_type"),
|
||||
|
@ -824,7 +850,7 @@ class NewListRightForm(ListRightForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
class Meta(ListRightForm.Meta):
|
||||
fields = ("name", "unix_name", "gid", "critical", "permissions", "details")
|
||||
|
@ -842,7 +868,7 @@ class DelListRightForm(Form):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
listrights = forms.ModelMultipleChoiceField(
|
||||
queryset=ListRight.objects.none(),
|
||||
|
@ -865,7 +891,7 @@ class DelSchoolForm(Form):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
schools = forms.ModelMultipleChoiceField(
|
||||
queryset=School.objects.none(),
|
||||
|
@ -887,7 +913,7 @@ class BanForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -906,7 +932,7 @@ class WhitelistForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -926,7 +952,7 @@ class EMailAddressForm(FormRevMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -947,7 +973,7 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||
|
@ -971,7 +997,8 @@ class InitialRegisterForm(forms.Form):
|
|||
|
||||
Parameters:
|
||||
DjangoForm : Inherit from basic django form
|
||||
"""
|
||||
"""
|
||||
|
||||
register_room = forms.BooleanField(required=False)
|
||||
register_machine = forms.BooleanField(required=False)
|
||||
|
||||
|
@ -1052,8 +1079,8 @@ class ThemeForm(FormRevMixin, forms.Form):
|
|||
theme = forms.ChoiceField(widget=forms.Select())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_, _ ,themes = next(walk(path.join(settings.STATIC_ROOT, "css/themes")))
|
||||
_, _, themes = next(walk(path.join(settings.STATIC_ROOT, "css/themes")))
|
||||
if not themes:
|
||||
themes = ["default.css"]
|
||||
super(ThemeForm, self).__init__(*args, **kwargs)
|
||||
self.fields['theme'].choices = [(theme, theme) for theme in themes]
|
||||
self.fields["theme"].choices = [(theme, theme) for theme in themes]
|
||||
|
|
|
@ -2027,10 +2027,10 @@ class Adherent(User):
|
|||
self.gpg_fingerprint = gpg_fingerprint
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, adherentid, *_args, **_kwargs):
|
||||
def get_instance(cls, object_id, *_args, **_kwargs):
|
||||
"""Try to find an instance of `Adherent` with the given id.
|
||||
|
||||
:param adherentid: The id of the adherent we are looking for.
|
||||
:param object_id: The id of the adherent we are looking for.
|
||||
:return: An adherent.
|
||||
|
||||
"""
|
||||
|
@ -2065,6 +2065,33 @@ class Adherent(User):
|
|||
("users.add_user",),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""Users can list adherent only if they are :
|
||||
- Members of view acl,
|
||||
- Club administrator.
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
can, _message, _group = Club.can_view_all(user_request)
|
||||
if user_request.has_perm("users.view_user") or can:
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
True,
|
||||
_("You don't have the right to list all adherents."),
|
||||
("users.view_user",),
|
||||
cls.objects.none(),
|
||||
)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
"""Method, clean and validate the gpgfp value.
|
||||
|
||||
|
@ -2154,13 +2181,13 @@ class Club(User):
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, clubid, *_args, **_kwargs):
|
||||
def get_instance(cls, object_id, *_args, **_kwargs):
|
||||
"""Try to find an instance of `Club` with the given id.
|
||||
|
||||
:param clubid: The id of the adherent we are looking for.
|
||||
:param object_id: The id of the adherent we are looking for.
|
||||
:return: A club.
|
||||
"""
|
||||
return cls.objects.get(pk=clubid)
|
||||
return cls.objects.get(pk=object_id)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Adherent)
|
||||
|
@ -2364,6 +2391,22 @@ class School(RevMixin, AclMixin, models.Model):
|
|||
verbose_name = _("school")
|
||||
verbose_name_plural = _("schools")
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list schools
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -2487,6 +2530,22 @@ class ListShell(RevMixin, AclMixin, models.Model):
|
|||
"""
|
||||
return self.shell.split("/")[-1]
|
||||
|
||||
@classmethod
|
||||
def can_list(cls, user_request, *_args, **_kwargs):
|
||||
"""All users can list shells
|
||||
|
||||
:param user_request: The user who wants to view the list.
|
||||
:return: True if the user can view the list and an explanation
|
||||
message.
|
||||
|
||||
"""
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
cls.objects.all()
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.shell
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
|
|
|
@ -351,7 +351,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<dt>{% trans "Theme" %}</dt>
|
||||
<dd>{{ request.user.theme_name }}</dd>
|
||||
<dd>{{ users.theme_name }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
|
@ -34,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
<form class="form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% massive_bootstrap_form userform 'room,school,administrators,members' %}
|
||||
{% bootstrap_form userform %}
|
||||
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
|
||||
</form>
|
||||
{% if load_js_file %}
|
||||
|
@ -48,5 +47,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
{{ userform.media }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
|
|
|
@ -31,6 +31,7 @@ from __future__ import unicode_literals
|
|||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
from . import views_autocomplete
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^new_user/$", views.new_user, name="new-user"),
|
||||
|
@ -128,4 +129,10 @@ urlpatterns = [
|
|||
url(r"^index_clubs/$", views.index_clubs, name="index-clubs"),
|
||||
url(r"^initial_register/$", views.initial_register, name="initial-register"),
|
||||
url(r"^edit_theme/(?P<userid>[0-9]+)$", views.edit_theme, name="edit-theme"),
|
||||
### Autocomplete Views
|
||||
url(r'^user-autocomplete/$', views_autocomplete.UserAutocomplete.as_view(), name='user-autocomplete',),
|
||||
url(r'^adherent-autocomplete/$', views_autocomplete.AdherentAutocomplete.as_view(), name='adherent-autocomplete',),
|
||||
url(r'^club-autocomplete/$', views_autocomplete.ClubAutocomplete.as_view(), name='club-autocomplete',),
|
||||
url(r'^school-autocomplete/$', views_autocomplete.SchoolAutocomplete.as_view(), name='school-autocomplete',),
|
||||
url(r'^shell-autocomplete/$', views_autocomplete.ShellAutocomplete.as_view(), name='shell-autocomplete',),
|
||||
]
|
||||
|
|
98
users/views_autocomplete.py
Normal file
98
users/views_autocomplete.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||
# quelques clics.
|
||||
#
|
||||
# Copyright © 2017-2020 Gabriel Détraz
|
||||
# Copyright © 2017-2020 Jean-Romain Garnier
|
||||
#
|
||||
# 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.
|
||||
|
||||
# App de gestion des users pour re2o
|
||||
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin
|
||||
# Gplv2
|
||||
"""
|
||||
Django views autocomplete view
|
||||
|
||||
Here are defined the autocomplete class based view.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .models import User, School, Adherent, Club, ListShell
|
||||
|
||||
from re2o.views import AutocompleteViewMixin, AutocompleteLoggedOutViewMixin
|
||||
|
||||
from django.db.models import Q, Value, CharField
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
|
||||
class SchoolAutocomplete(AutocompleteLoggedOutViewMixin):
|
||||
obj_type = School
|
||||
|
||||
|
||||
class UserAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = User
|
||||
|
||||
# Precision on search to add annotations so search behaves more like users expect it to
|
||||
def filter_results(self):
|
||||
# Comments explain what we try to match
|
||||
self.query_set = self.query_set.annotate(
|
||||
full_name=Concat(
|
||||
"adherent__name", Value(" "), "surname"
|
||||
), # Match when the user searches "Toto Passoir"
|
||||
full_name_reverse=Concat(
|
||||
"surname", Value(" "), "adherent__name"
|
||||
), # Match when the user searches "Passoir Toto"
|
||||
).all()
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(pseudo__icontains=self.q)
|
||||
| Q(full_name__icontains=self.q)
|
||||
| Q(full_name_reverse__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class AdherentAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Adherent
|
||||
|
||||
# Precision on search to add annotations so search behaves more like users expect it to
|
||||
def filter_results(self):
|
||||
# Comments explain what we try to match
|
||||
self.query_set = self.query_set.annotate(
|
||||
full_name=Concat(
|
||||
"name", Value(" "), "surname"
|
||||
), # Match when the user searches "Toto Passoir"
|
||||
full_name_reverse=Concat(
|
||||
"surname", Value(" "), "name"
|
||||
), # Match when the user searches "Passoir Toto"
|
||||
).all()
|
||||
|
||||
if self.q:
|
||||
self.query_set = self.query_set.filter(
|
||||
Q(pseudo__icontains=self.q)
|
||||
| Q(full_name__icontains=self.q)
|
||||
| Q(full_name_reverse__icontains=self.q)
|
||||
)
|
||||
|
||||
|
||||
class ClubAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = Club
|
||||
|
||||
|
||||
class ShellAutocomplete(AutocompleteViewMixin):
|
||||
obj_type = ListShell
|
||||
query_filter = "shell__icontains"
|
Loading…
Reference in a new issue