mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-23 15:33:45 +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):
|
if getattr(view, "_ignore_model_permissions", False):
|
||||||
return True
|
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
|
return True
|
||||||
|
|
||||||
if not request.user or not request.user.is_authenticated:
|
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.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
|
from re2o.widgets import AutocompleteModelWidget
|
||||||
from .models import (
|
from .models import (
|
||||||
Article,
|
Article,
|
||||||
Paiement,
|
Paiement,
|
||||||
|
@ -79,6 +80,10 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Facture
|
model = Facture
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"user": AutocompleteModelWidget(url="/users/user-autocomplete"),
|
||||||
|
"banque": AutocompleteModelWidget(url="/cotisations/banque-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(FactureForm, self).clean()
|
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)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -293,8 +293,7 @@ class Facture(BaseInvoice):
|
||||||
"""Returns every subscription associated with this invoice."""
|
"""Returns every subscription associated with this invoice."""
|
||||||
return Cotisation.objects.filter(
|
return Cotisation.objects.filter(
|
||||||
vente__in=self.vente_set.filter(
|
vente__in=self.vente_set.filter(
|
||||||
~(Q(duration_membership=0)) |\
|
~(Q(duration_membership=0)) | ~(Q(duration_days_membership=0))
|
||||||
~(Q(duration_days_membership=0))
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -467,16 +466,18 @@ class Vente(RevMixin, AclMixin, models.Model):
|
||||||
)
|
)
|
||||||
duration_days_connection = models.PositiveIntegerField(
|
duration_days_connection = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
validators=[MinValueValidator(0)],
|
verbose_name=_(
|
||||||
verbose_name=_("duration of the connection (in days, will be added to duration in months)"),
|
"duration of the connection (in days, will be added to duration in months)"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
duration_membership = models.PositiveIntegerField(
|
duration_membership = models.PositiveIntegerField(
|
||||||
default=0, verbose_name=_("duration of the membership (in months)")
|
default=0, verbose_name=_("duration of the membership (in months)")
|
||||||
)
|
)
|
||||||
duration_days_membership = models.PositiveIntegerField(
|
duration_days_membership = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
validators=[MinValueValidator(0)],
|
verbose_name=_(
|
||||||
verbose_name=_("duration of the membership (in days, will be added to duration in months)"),
|
"duration of the membership (in days, will be added to duration in months)"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -631,12 +632,14 @@ class Vente(RevMixin, AclMixin, models.Model):
|
||||||
return str(self.name) + " " + str(self.facture)
|
return str(self.name) + " " + str(self.facture)
|
||||||
|
|
||||||
def test_membership_or_connection(self):
|
def test_membership_or_connection(self):
|
||||||
""" Test if the purchase include membership or connecton
|
"""Test if the purchase include membership or connecton"""
|
||||||
"""
|
return (
|
||||||
return self.duration_membership or \
|
self.duration_membership
|
||||||
self.duration_days_membership or \
|
or self.duration_days_membership
|
||||||
self.duration_connection or \
|
or self.duration_connection
|
||||||
self.duration_days_connection
|
or self.duration_days_connection
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO : change vente to purchase
|
# TODO : change vente to purchase
|
||||||
@receiver(post_save, sender=Vente)
|
@receiver(post_save, sender=Vente)
|
||||||
|
@ -704,20 +707,20 @@ class Article(RevMixin, AclMixin, models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
duration_membership = models.PositiveIntegerField(
|
duration_membership = models.PositiveIntegerField(
|
||||||
validators=[MinValueValidator(0)],
|
|
||||||
verbose_name=_("duration of the membership (in months)")
|
verbose_name=_("duration of the membership (in months)")
|
||||||
)
|
)
|
||||||
duration_days_membership = models.PositiveIntegerField(
|
duration_days_membership = models.PositiveIntegerField(
|
||||||
validators=[MinValueValidator(0)],
|
verbose_name=_(
|
||||||
verbose_name=_("duration of the membership (in days, will be added to duration in months)"),
|
"duration of the membership (in days, will be added to duration in months)"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
duration_connection = models.PositiveIntegerField(
|
duration_connection = models.PositiveIntegerField(
|
||||||
validators=[MinValueValidator(0)],
|
|
||||||
verbose_name=_("duration of the connection (in months)")
|
verbose_name=_("duration of the connection (in months)")
|
||||||
)
|
)
|
||||||
duration_days_connection = models.PositiveIntegerField(
|
duration_days_connection = models.PositiveIntegerField(
|
||||||
validators=[MinValueValidator(0)],
|
verbose_name=_(
|
||||||
verbose_name=_("duration of the connection (in days, will be added to duration in months)"),
|
"duration of the connection (in days, will be added to duration in months)"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
need_membership = models.BooleanField(
|
need_membership = models.BooleanField(
|
||||||
|
@ -884,7 +887,9 @@ class Paiement(RevMixin, AclMixin, models.Model):
|
||||||
|
|
||||||
# In case a cotisation was bought, inform the user, the
|
# In case a cotisation was bought, inform the user, the
|
||||||
# cotisation time has been extended too
|
# 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(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_(
|
_(
|
||||||
|
@ -956,9 +961,13 @@ class Cotisation(RevMixin, AclMixin, models.Model):
|
||||||
vente = models.OneToOneField(
|
vente = models.OneToOneField(
|
||||||
"Vente", on_delete=models.CASCADE, null=True, verbose_name=_("purchase")
|
"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_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"))
|
date_end_memb = models.DateTimeField(verbose_name=_("end date for the membership"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -25,13 +25,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load staticfiles%}
|
{% load staticfiles%}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Creation and editing of invoices" %}{% endblock %}
|
{% block title %}{% trans "Creation and editing of invoices" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% bootstrap_form_errors factureform %}
|
{% bootstrap_form_errors factureform %}
|
||||||
|
{{ factureform.media }}
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>{% trans "Edit invoice" %}</h3>
|
<h3>{% trans "Edit invoice" %}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% massive_bootstrap_form factureform 'user' %}
|
{% bootstrap_form factureform %}
|
||||||
{{ venteform.management_form }}
|
{{ venteform.management_form }}
|
||||||
<h3>{% trans "Articles" %}</h3>
|
<h3>{% trans "Articles" %}</h3>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
|
|
|
@ -27,7 +27,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views, views_autocomplete
|
||||||
from . import payment_methods
|
from . import payment_methods
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -104,4 +104,6 @@ urlpatterns = [
|
||||||
url(r"^index_paiement/$", views.index_paiement, name="index-paiement"),
|
url(r"^index_paiement/$", views.index_paiement, name="index-paiement"),
|
||||||
url(r"^control/$", views.control, name="control"),
|
url(r"^control/$", views.control, name="control"),
|
||||||
url(r"^$", views.index, name="index"),
|
url(r"^$", views.index, name="index"),
|
||||||
|
### Autocomplete Views
|
||||||
|
url(r'^banque-autocomplete/$', views_autocomplete.BanqueAutocomplete.as_view(), name='banque-autocomplete',),
|
||||||
] + payment_methods.urls.urlpatterns
|
] + 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.forms import Form
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from re2o.base import get_input_formats_help_text
|
from re2o.base import get_input_formats_help_text
|
||||||
|
from re2o.widgets import AutocompleteModelWidget
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
@ -46,10 +47,7 @@ CHOICES_ACTION_TYPE = (
|
||||||
("all", _("All")),
|
("all", _("All")),
|
||||||
)
|
)
|
||||||
|
|
||||||
CHOICES_TYPE = (
|
CHOICES_TYPE = (("ip", _("IPv4")), ("mac", _("MAC address")))
|
||||||
("ip", _("IPv4")),
|
|
||||||
("mac", _("MAC address")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def all_classes(module):
|
def all_classes(module):
|
||||||
|
@ -87,14 +85,11 @@ def classes_for_action_type(action_type):
|
||||||
users.models.User.__name__,
|
users.models.User.__name__,
|
||||||
users.models.Adherent.__name__,
|
users.models.Adherent.__name__,
|
||||||
users.models.Club.__name__,
|
users.models.Club.__name__,
|
||||||
users.models.EMailAddress.__name__
|
users.models.EMailAddress.__name__,
|
||||||
]
|
]
|
||||||
|
|
||||||
if action_type == "machines":
|
if action_type == "machines":
|
||||||
return [
|
return [machines.models.Machine.__name__, machines.models.Interface.__name__]
|
||||||
machines.models.Machine.__name__,
|
|
||||||
machines.models.Interface.__name__
|
|
||||||
]
|
|
||||||
|
|
||||||
if action_type == "subscriptions":
|
if action_type == "subscriptions":
|
||||||
return all_classes(cotisations.models)
|
return all_classes(cotisations.models)
|
||||||
|
@ -114,40 +109,39 @@ def classes_for_action_type(action_type):
|
||||||
|
|
||||||
class ActionsSearchForm(Form):
|
class ActionsSearchForm(Form):
|
||||||
"""Form used to do an advanced search through the logs."""
|
"""Form used to do an advanced search through the logs."""
|
||||||
u = forms.ModelChoiceField(
|
|
||||||
|
user = forms.ModelChoiceField(
|
||||||
label=_("Performed by"),
|
label=_("Performed by"),
|
||||||
queryset=users.models.User.objects.all(),
|
queryset=users.models.User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
widget=AutocompleteModelWidget(url="/users/user-autocomplete"),
|
||||||
)
|
)
|
||||||
t = forms.MultipleChoiceField(
|
action_type = forms.MultipleChoiceField(
|
||||||
label=_("Action type"),
|
label=_("Action type"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
choices=CHOICES_ACTION_TYPE,
|
choices=CHOICES_ACTION_TYPE,
|
||||||
initial=[i[0] for i in CHOICES_ACTION_TYPE],
|
initial=[i[0] for i in CHOICES_ACTION_TYPE],
|
||||||
)
|
)
|
||||||
s = forms.DateField(required=False, label=_("Start date"))
|
start_date = forms.DateField(required=False, label=_("Start date"))
|
||||||
e = forms.DateField(required=False, label=_("End date"))
|
end_date = forms.DateField(required=False, label=_("End date"))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ActionsSearchForm, self).__init__(*args, **kwargs)
|
super(ActionsSearchForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["s"].help_text = get_input_formats_help_text(
|
self.fields["start_date"].help_text = get_input_formats_help_text(
|
||||||
self.fields["s"].input_formats
|
self.fields["start_date"].input_formats
|
||||||
)
|
)
|
||||||
self.fields["e"].help_text = get_input_formats_help_text(
|
self.fields["end_date"].help_text = get_input_formats_help_text(
|
||||||
self.fields["e"].input_formats
|
self.fields["end_date"].input_formats
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MachineHistorySearchForm(Form):
|
class MachineHistorySearchForm(Form):
|
||||||
"""Form used to do a search through the machine histories."""
|
"""Form used to do a search through the machine histories."""
|
||||||
q = forms.CharField(
|
|
||||||
label=_("Search"),
|
q = forms.CharField(label=_("Search"), max_length=100)
|
||||||
max_length=100,
|
|
||||||
)
|
|
||||||
t = forms.CharField(
|
t = forms.CharField(
|
||||||
label=_("Search type"),
|
label=_("Search type"), widget=forms.Select(choices=CHOICES_TYPE)
|
||||||
widget=forms.Select(choices=CHOICES_TYPE)
|
|
||||||
)
|
)
|
||||||
s = forms.DateField(required=False, label=_("Start date"))
|
s = forms.DateField(required=False, label=_("Start date"))
|
||||||
e = forms.DateField(required=False, label=_("End date"))
|
e = forms.DateField(required=False, label=_("End date"))
|
||||||
|
|
|
@ -600,10 +600,10 @@ class ActionsSearch:
|
||||||
Returns:
|
Returns:
|
||||||
The QuerySet of Revision objects corresponding to the search.
|
The QuerySet of Revision objects corresponding to the search.
|
||||||
"""
|
"""
|
||||||
user = params.get("u", None)
|
user = params.get("user", None)
|
||||||
start = params.get("s", None)
|
start = params.get("start_date", None)
|
||||||
end = params.get("e", None)
|
end = params.get("end_date", None)
|
||||||
action_types = params.get("t", None)
|
action_types = params.get("action_type", None)
|
||||||
|
|
||||||
query = Q()
|
query = Q()
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Search events" %}{% endblock %}
|
{% 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">
|
<form class="form">
|
||||||
<h3>{% trans "Search events" %}</h3>
|
<h3>{% trans "Search events" %}</h3>
|
||||||
|
|
||||||
{% massive_bootstrap_form actions_form 'u' %}
|
{% bootstrap_form actions_form %}
|
||||||
{% trans "Search" as tr_search %}
|
{% trans "Search" as tr_search %}
|
||||||
{% bootstrap_button tr_search button_type="submit" icon="search" %}
|
{% bootstrap_button tr_search button_type="submit" icon="search" %}
|
||||||
</form>
|
</form>
|
||||||
|
{{ actions_form.media }}
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
|
@ -41,6 +41,10 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from re2o.field_permissions import FieldPermissionFormMixin
|
from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
|
from re2o.widgets import (
|
||||||
|
AutocompleteModelWidget,
|
||||||
|
AutocompleteMultipleModelWidget,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
Domain,
|
Domain,
|
||||||
Machine,
|
Machine,
|
||||||
|
@ -71,6 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Machine
|
model = Machine
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {"user": AutocompleteModelWidget(url="/users/user-autocomplete")}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -91,6 +96,19 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ["machine", "machine_type", "ipv4", "mac_address", "details"]
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -139,6 +157,9 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Domain
|
model = Domain
|
||||||
fields = ["name", "extension", "ttl"]
|
fields = ["name", "extension", "ttl"]
|
||||||
|
widgets = {
|
||||||
|
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -188,6 +209,9 @@ class MachineTypeForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MachineType
|
model = MachineType
|
||||||
fields = ["name", "ip_type"]
|
fields = ["name", "ip_type"]
|
||||||
|
widgets = {
|
||||||
|
"ip_type": AutocompleteModelWidget(url="/machines/iptype-autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -222,6 +246,13 @@ class IpTypeForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IpType
|
model = IpType
|
||||||
fields = "__all__"
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -351,6 +382,10 @@ class MxForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mx
|
model = Mx
|
||||||
fields = ["zone", "priority", "name", "ttl"]
|
fields = ["zone", "priority", "name", "ttl"]
|
||||||
|
widgets = {
|
||||||
|
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||||
|
"name": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -386,6 +421,10 @@ class NsForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ns
|
model = Ns
|
||||||
fields = ["zone", "ns", "ttl"]
|
fields = ["zone", "ns", "ttl"]
|
||||||
|
widgets = {
|
||||||
|
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||||
|
"ns": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -419,6 +458,9 @@ class TxtForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Txt
|
model = Txt
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -449,6 +491,9 @@ class DNameForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DName
|
model = DName
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -479,6 +524,10 @@ class SrvForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Srv
|
model = Srv
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
|
||||||
|
"target": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -509,6 +558,14 @@ class NasForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Nas
|
model = Nas
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"nas_type": AutocompleteModelWidget(
|
||||||
|
url="/machines/machinetype-autocomplete"
|
||||||
|
),
|
||||||
|
"machine_type": AutocompleteModelWidget(
|
||||||
|
url="/machines/machinetype-autocomplete"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -539,6 +596,11 @@ class RoleForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"servers": AutocompleteMultipleModelWidget(
|
||||||
|
url="/machines/interface-autocomplete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -572,6 +634,11 @@ class ServiceForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"servers": AutocompleteMultipleModelWidget(
|
||||||
|
url="/machines/interface-autocomplete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -656,6 +723,11 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ["port_lists"]
|
fields = ["port_lists"]
|
||||||
|
widgets = {
|
||||||
|
"port_lists": AutocompleteMultipleModelWidget(
|
||||||
|
url="/machines/ouvertureportlist-autocomplete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
|
|
@ -378,6 +378,34 @@ class MachineType(RevMixin, AclMixin, models.Model):
|
||||||
)
|
)
|
||||||
return True, None, None
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -953,6 +981,32 @@ class Extension(RevMixin, AclMixin, models.Model):
|
||||||
("machines.use_all_extension",),
|
("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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -2130,6 +2184,34 @@ class IpList(RevMixin, AclMixin, models.Model):
|
||||||
self.clean()
|
self.clean()
|
||||||
super(IpList, self).save(*args, **kwargs)
|
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):
|
def __str__(self):
|
||||||
return self.ipv4
|
return self.ipv4
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Machines" %}{% endblock %}
|
{% block title %}{% trans "Machines" %}{% endblock %}
|
||||||
|
@ -33,54 +32,67 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if machineform %}
|
{% if machineform %}
|
||||||
{% bootstrap_form_errors machineform %}
|
{% bootstrap_form_errors machineform %}
|
||||||
|
{{ machineform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaceform %}
|
{% if interfaceform %}
|
||||||
{% bootstrap_form_errors interfaceform %}
|
{% bootstrap_form_errors interfaceform %}
|
||||||
|
{{ interfaceform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if domainform %}
|
{% if domainform %}
|
||||||
{% bootstrap_form_errors domainform %}
|
{% bootstrap_form_errors domainform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if iptypeform %}
|
{% if iptypeform %}
|
||||||
{% bootstrap_form_errors iptypeform %}
|
{% bootstrap_form_errors iptypeform %}
|
||||||
|
{{ iptypeform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if machinetypeform %}
|
{% if machinetypeform %}
|
||||||
{% bootstrap_form_errors machinetypeform %}
|
{% bootstrap_form_errors machinetypeform %}
|
||||||
|
{{ machinetypeform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if extensionform %}
|
{% if extensionform %}
|
||||||
{% bootstrap_form_errors extensionform %}
|
{% bootstrap_form_errors extensionform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mxform %}
|
{% if mxform %}
|
||||||
{% bootstrap_form_errors mxform %}
|
{% bootstrap_form_errors mxform %}
|
||||||
|
{{ mxform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if nsform %}
|
{% if nsform %}
|
||||||
{% bootstrap_form_errors nsform %}
|
{% bootstrap_form_errors nsform %}
|
||||||
|
{{ nsform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if txtform %}
|
{% if txtform %}
|
||||||
{% bootstrap_form_errors txtform %}
|
{% bootstrap_form_errors txtform %}
|
||||||
|
{{ txtform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if dnameform %}
|
{% if dnameform %}
|
||||||
{% bootstrap_form_errors dnameform %}
|
{% bootstrap_form_errors dnameform %}
|
||||||
|
{{ dnameform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if srvform %}
|
{% if srvform %}
|
||||||
{% bootstrap_form_errors srvform %}
|
{% bootstrap_form_errors srvform %}
|
||||||
|
{{ srvform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if aliasform %}
|
{% if aliasform %}
|
||||||
{% bootstrap_form_errors aliasform %}
|
{% bootstrap_form_errors aliasform %}
|
||||||
|
{{ aliasform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if serviceform %}
|
{% if serviceform %}
|
||||||
{% bootstrap_form_errors serviceform %}
|
{% bootstrap_form_errors serviceform %}
|
||||||
|
{{ serviceform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sshfpform %}
|
{% if sshfpform %}
|
||||||
{% bootstrap_form_errors sshfpform %}
|
{% bootstrap_form_errors sshfpform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roleform %}
|
{% if roleform %}
|
||||||
{% bootstrap_form_errors roleform %}
|
{% bootstrap_form_errors roleform %}
|
||||||
|
{{ roleform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if vlanform %}
|
{% if vlanform %}
|
||||||
{% bootstrap_form_errors vlanform %}
|
{% bootstrap_form_errors vlanform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if nasform %}
|
{% if nasform %}
|
||||||
{% bootstrap_form_errors nasform %}
|
{% bootstrap_form_errors nasform %}
|
||||||
|
{{ nasform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ipv6form %}
|
{% if ipv6form %}
|
||||||
{% bootstrap_form_errors ipv6form %}
|
{% bootstrap_form_errors ipv6form %}
|
||||||
|
@ -90,15 +102,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if machineform %}
|
{% if machineform %}
|
||||||
<h3>{% trans "Machine" %}</h3>
|
<h3>{% trans "Machine" %}</h3>
|
||||||
{% massive_bootstrap_form machineform 'user' %}
|
{% bootstrap_form machineform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaceform %}
|
{% if interfaceform %}
|
||||||
<h3>{% trans "Interface" %}</h3>
|
<h3>{% trans "Interface" %}</h3>
|
||||||
{% if i_mbf_param %}
|
{% bootstrap_form interfaceform %}
|
||||||
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
|
|
||||||
{% else %}
|
|
||||||
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if domainform %}
|
{% if domainform %}
|
||||||
<h3>{% trans "Domain" %}</h3>
|
<h3>{% trans "Domain" %}</h3>
|
||||||
|
@ -114,7 +122,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if extensionform %}
|
{% if extensionform %}
|
||||||
<h3>{% trans "Extension" %}</h3>
|
<h3>{% trans "Extension" %}</h3>
|
||||||
{% massive_bootstrap_form extensionform 'origin' %}
|
{% bootstrap_form extensionform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if soaform %}
|
{% if soaform %}
|
||||||
<h3>{% trans "SOA record" %}</h3>
|
<h3>{% trans "SOA record" %}</h3>
|
||||||
|
@ -122,11 +130,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mxform %}
|
{% if mxform %}
|
||||||
<h3>{% trans "MX record" %}</h3>
|
<h3>{% trans "MX record" %}</h3>
|
||||||
{% massive_bootstrap_form mxform 'name' %}
|
{% bootstrap_form mxform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if nsform %}
|
{% if nsform %}
|
||||||
<h3>{% trans "NS record" %}</h3>
|
<h3>{% trans "NS record" %}</h3>
|
||||||
{% massive_bootstrap_form nsform 'ns' %}
|
{% bootstrap_form nsform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if txtform %}
|
{% if txtform %}
|
||||||
<h3>{% trans "TXT record" %}</h3>
|
<h3>{% trans "TXT record" %}</h3>
|
||||||
|
@ -138,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if srvform %}
|
{% if srvform %}
|
||||||
<h3>{% trans "SRV record" %}</h3>
|
<h3>{% trans "SRV record" %}</h3>
|
||||||
{% massive_bootstrap_form srvform 'target' %}
|
{% bootstrap_form srvform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sshfpform %}
|
{% if sshfpform %}
|
||||||
<h3>{% trans "SSHFP record" %}</h3>
|
<h3>{% trans "SSHFP record" %}</h3>
|
||||||
|
@ -146,15 +154,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if aliasform %}
|
{% if aliasform %}
|
||||||
<h3>{% trans "Alias" %}</h3>
|
<h3>{% trans "Alias" %}</h3>
|
||||||
{% massive_bootstrap_form aliasform 'extension' %}
|
{% bootstrap_form aliasform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if serviceform %}
|
{% if serviceform %}
|
||||||
<h3>{% trans "Service" %}</h3>
|
<h3>{% trans "Service" %}</h3>
|
||||||
{% massive_bootstrap_form serviceform 'servers' %}
|
{% bootstrap_form serviceform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roleform %}
|
{% if roleform %}
|
||||||
<h3>Role</h3>
|
<h3>Role</h3>
|
||||||
{% massive_bootstrap_form roleform 'servers' %}
|
{% bootstrap_form roleform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if vlanform %}
|
{% if vlanform %}
|
||||||
<h3>{% trans "VLAN" %}</h3>
|
<h3>{% trans "VLAN" %}</h3>
|
||||||
|
|
|
@ -29,6 +29,7 @@ from __future__ import unicode_literals
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
from . import views_autocomplete
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^new_machine/(?P<userid>[0-9]+)$", views.new_machine, name="new-machine"),
|
url(r"^new_machine/(?P<userid>[0-9]+)$", views.new_machine, name="new-machine"),
|
||||||
|
@ -153,4 +154,14 @@ urlpatterns = [
|
||||||
views.configure_ports,
|
views.configure_ports,
|
||||||
name="port-config",
|
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
|
@login_required
|
||||||
@can_create(Machine)
|
@can_create(Machine)
|
||||||
@can_edit(User)
|
@can_edit(User)
|
||||||
|
@ -235,13 +150,11 @@ def new_machine(request, user, **_kwargs):
|
||||||
new_domain.save()
|
new_domain.save()
|
||||||
messages.success(request, _("The machine was created."))
|
messages.success(request, _("The machine was created."))
|
||||||
return redirect(reverse("users:profil", kwargs={"userid": str(user.id)}))
|
return redirect(reverse("users:profil", kwargs={"userid": str(user.id)}))
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"machineform": machine,
|
"machineform": machine,
|
||||||
"interfaceform": interface,
|
"interfaceform": interface,
|
||||||
"domainform": domain,
|
"domainform": domain,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"action_name": _("Add"),
|
"action_name": _("Add"),
|
||||||
},
|
},
|
||||||
"machines/machine.html",
|
"machines/machine.html",
|
||||||
|
@ -281,13 +194,11 @@ def edit_interface(request, interface_instance, **_kwargs):
|
||||||
kwargs={"userid": str(interface_instance.machine.user.id)},
|
kwargs={"userid": str(interface_instance.machine.user.id)},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"machineform": machine_form,
|
"machineform": machine_form,
|
||||||
"interfaceform": interface_form,
|
"interfaceform": interface_form,
|
||||||
"domainform": domain_form,
|
"domainform": domain_form,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"action_name": _("Edit"),
|
"action_name": _("Edit"),
|
||||||
},
|
},
|
||||||
"machines/machine.html",
|
"machines/machine.html",
|
||||||
|
@ -332,12 +243,10 @@ def new_interface(request, machine, **_kwargs):
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse("users:profil", kwargs={"userid": str(machine.user.id)})
|
reverse("users:profil", kwargs={"userid": str(machine.user.id)})
|
||||||
)
|
)
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"interfaceform": interface_form,
|
"interfaceform": interface_form,
|
||||||
"domainform": domain_form,
|
"domainform": domain_form,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"action_name": _("Add"),
|
"action_name": _("Add"),
|
||||||
},
|
},
|
||||||
"machines/machine.html",
|
"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
|
from .preferences.models import MultiopOption
|
||||||
|
|
||||||
|
|
||||||
class DormitoryForm(FormRevMixin, Form):
|
class DormitoryForm(FormRevMixin, Form):
|
||||||
"""Form used to select dormitories."""
|
"""Form used to select dormitories."""
|
||||||
|
|
||||||
dormitory = forms.ModelMultipleChoiceField(
|
dormitory = forms.ModelMultipleChoiceField(
|
||||||
queryset=MultiopOption.get_cached_value("enabled_dorm").all(),
|
|
||||||
label=_("Dormitory"),
|
label=_("Dormitory"),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=False,
|
required=False,
|
||||||
|
queryset=Dormitory.objects.none(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DormitoryForm, self).__init__(*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 import forms
|
||||||
from django.forms import ModelForm, Form
|
from django.forms import ModelForm, Form
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from re2o.widgets import AutocompleteMultipleModelWidget
|
||||||
|
|
||||||
from .models import MultiopOption
|
from .models import MultiopOption
|
||||||
|
|
||||||
|
@ -39,3 +40,8 @@ class EditMultiopOptionForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MultiopOption
|
model = MultiopOption
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"enabled_dorm": AutocompleteMultipleModelWidget(
|
||||||
|
url="/topologie/dormitory-autocomplete",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
django-bootstrap3==11.1.0
|
django-bootstrap3==11.1.0
|
||||||
django-macaddress==1.6.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 import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
|
from re2o.widgets import (
|
||||||
|
AutocompleteModelWidget,
|
||||||
|
AutocompleteMultipleModelWidget
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
OptionalUser,
|
OptionalUser,
|
||||||
OptionalMachine,
|
OptionalMachine,
|
||||||
|
@ -108,12 +112,19 @@ class EditOptionalTopologieForm(ModelForm):
|
||||||
"""Form used to edit the configuration of switches."""
|
"""Form used to edit the configuration of switches."""
|
||||||
|
|
||||||
automatic_provision_switchs = forms.ModelMultipleChoiceField(
|
automatic_provision_switchs = forms.ModelMultipleChoiceField(
|
||||||
Switch.objects.all(), required=False
|
Switch.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OptionalTopologie
|
model = OptionalTopologie
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"switchs_ip_type": AutocompleteModelWidget(
|
||||||
|
url="/machines/iptype-autocomplete",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -168,6 +179,11 @@ class EditAssoOptionForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssoOption
|
model = AssoOption
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"utilisateur_asso": AutocompleteModelWidget(
|
||||||
|
url="/users/user-autocomplete",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -254,6 +270,11 @@ class MandateForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mandate
|
model = Mandate
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"president": AutocompleteModelWidget(
|
||||||
|
url="/users/user-autocomplete",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -368,7 +389,9 @@ class RadiusKeyForm(FormRevMixin, ModelForm):
|
||||||
"""Form used to add and edit RADIUS keys."""
|
"""Form used to add and edit RADIUS keys."""
|
||||||
|
|
||||||
members = forms.ModelMultipleChoiceField(
|
members = forms.ModelMultipleChoiceField(
|
||||||
queryset=Switch.objects.all(), required=False
|
queryset=Switch.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -391,7 +414,11 @@ class RadiusKeyForm(FormRevMixin, ModelForm):
|
||||||
class SwitchManagementCredForm(FormRevMixin, ModelForm):
|
class SwitchManagementCredForm(FormRevMixin, ModelForm):
|
||||||
"""Form used to add and edit switch management credentials."""
|
"""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:
|
class Meta:
|
||||||
model = SwitchManagementCred
|
model = SwitchManagementCred
|
||||||
|
|
|
@ -24,19 +24,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Preferences" %}{% endblock %}
|
{% block title %}{% trans "Preferences" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% bootstrap_form_errors options %}
|
{% bootstrap_form_errors options %}
|
||||||
|
{{ options.media }}
|
||||||
|
|
||||||
<h3>{% trans "Editing of preferences" %}</h3>
|
<h3>{% trans "Editing of preferences" %}</h3>
|
||||||
|
|
||||||
<form class="form" method="post" enctype="multipart/form-data">
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
|
{% bootstrap_form options %}
|
||||||
{% if formset %}
|
{% if formset %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
{% for f in formset %}
|
{% for f in formset %}
|
||||||
|
|
|
@ -25,20 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Preferences" %}{% endblock %}
|
{% block title %}{% trans "Preferences" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if preferenceform %}
|
{% if preferenceform %}
|
||||||
{% bootstrap_form_errors preferenceform %}
|
{% bootstrap_form_errors preferenceform %}
|
||||||
|
{{ preferenceform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<form class="form" method="post" enctype="multipart/form-data">
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if preferenceform %}
|
{% if preferenceform %}
|
||||||
{% massive_bootstrap_form preferenceform 'members,president' %}
|
{% bootstrap_form preferenceform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
|
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -151,7 +151,8 @@ def display_options(request):
|
||||||
optionnal_templates_list = [
|
optionnal_templates_list = [
|
||||||
app.preferences.views.aff_preferences(request)
|
app.preferences.views.aff_preferences(request)
|
||||||
for app in optionnal_apps
|
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(
|
return form(
|
||||||
|
@ -350,7 +351,10 @@ def add_switchmanagementcred(request):
|
||||||
"The switch management credentials were added."))
|
"The switch management credentials were added."))
|
||||||
return redirect(reverse("preferences:display-options"))
|
return redirect(reverse("preferences:display-options"))
|
||||||
return form(
|
return form(
|
||||||
{"preferenceform": switchmanagementcred, "action_name": _("Add"), },
|
{
|
||||||
|
"preferenceform": switchmanagementcred,
|
||||||
|
"action_name": _("Add"),
|
||||||
|
},
|
||||||
"preferences/preferences.html",
|
"preferences/preferences.html",
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
|
@ -415,6 +419,10 @@ def add_mailcontact(request):
|
||||||
return redirect(reverse("preferences:display-options"))
|
return redirect(reverse("preferences:display-options"))
|
||||||
return form(
|
return form(
|
||||||
{"preferenceform": mailcontact, "action_name": _("Add"), },
|
{"preferenceform": mailcontact, "action_name": _("Add"), },
|
||||||
|
{
|
||||||
|
"preferenceform": mailcontact,
|
||||||
|
"action_name": _("Add"),
|
||||||
|
},
|
||||||
"preferences/preferences.html",
|
"preferences/preferences.html",
|
||||||
request,
|
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
|
"""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,
|
permission by calling model.method_name. If the flag on_instance is True,
|
||||||
tries to get an instance of the model by calling
|
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.
|
rather than model.method_name.
|
||||||
|
|
||||||
It is not intended to be used as is. It is a base for others ACL
|
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:
|
Args:
|
||||||
method_name: The name of the method which is to to be used for ACL.
|
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
|
# `wrapper` inside the `decorator` function, you need to read some
|
||||||
# documentation on decorators !
|
# documentation on decorators !
|
||||||
def decorator(view):
|
def decorator(view):
|
||||||
"""The decorator to use on a specific view
|
"""The decorator to use on a specific view"""
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(request, *args, **kwargs):
|
def wrapper(request, *args, **kwargs):
|
||||||
"""The wrapper used for a specific request"""
|
"""The wrapper used for a specific request"""
|
||||||
|
@ -259,18 +264,24 @@ ModelC)
|
||||||
for msg in error_messages:
|
for msg in error_messages:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
msg or _(
|
msg or _("You don't have the right to access this menu."),
|
||||||
"You don't have the right to access this menu."),
|
|
||||||
)
|
)
|
||||||
# And redirect the user to the right place.
|
# And redirect the user to the right place.
|
||||||
if request.user.id is not None:
|
if request.user.id is not None:
|
||||||
if not api:
|
if not api:
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse("users:profil", kwargs={
|
reverse(
|
||||||
"userid": str(request.user.id)})
|
"users:profil", kwargs={"userid": str(request.user.id)}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(data={"errors": error_messages, "warning": warning_messages}, status=403)
|
return Response(
|
||||||
|
data={
|
||||||
|
"errors": error_messages,
|
||||||
|
"warning": warning_messages,
|
||||||
|
},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return redirect(reverse("index"))
|
return redirect(reverse("index"))
|
||||||
return view(request, *chain(instances, args), **kwargs)
|
return view(request, *chain(instances, args), **kwargs)
|
||||||
|
@ -321,12 +332,10 @@ def can_delete_set(model):
|
||||||
If none of them, return an error"""
|
If none of them, return an error"""
|
||||||
|
|
||||||
def decorator(view):
|
def decorator(view):
|
||||||
"""The decorator to use on a specific view
|
"""The decorator to use on a specific view"""
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(request, *args, **kwargs):
|
def wrapper(request, *args, **kwargs):
|
||||||
"""The wrapper used for a specific request
|
"""The wrapper used for a specific request"""
|
||||||
"""
|
|
||||||
all_objects = model.objects.all()
|
all_objects = model.objects.all()
|
||||||
instances_id = []
|
instances_id = []
|
||||||
for instance in all_objects:
|
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)
|
return acl_base_decorator("can_view_all", *targets, on_instance=False)
|
||||||
|
|
||||||
|
|
||||||
def can_view_app(*apps_name):
|
def can_list(*targets):
|
||||||
"""Decorator to check if an user can view the applications.
|
"""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:
|
for app_name in apps_name:
|
||||||
assert app_name in sys.modules.keys()
|
assert app_name in sys.modules.keys()
|
||||||
return acl_base_decorator(
|
return acl_base_decorator(
|
||||||
|
@ -383,8 +400,7 @@ def can_edit_history(view):
|
||||||
"""Decorator to check if an user can edit history."""
|
"""Decorator to check if an user can edit history."""
|
||||||
|
|
||||||
def wrapper(request, *args, **kwargs):
|
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"):
|
if request.user.has_perm("admin.change_logentry"):
|
||||||
return view(request, *args, **kwargs)
|
return view(request, *args, **kwargs)
|
||||||
messages.error(request, _(
|
messages.error(request, _(
|
||||||
|
|
|
@ -81,6 +81,8 @@ class AclMixin(object):
|
||||||
:can_view: Applied on an instance, return if the user can view the
|
:can_view: Applied on an instance, return if the user can view the
|
||||||
instance
|
instance
|
||||||
:can_view_all: Applied on a class, return if the user can view all
|
: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"""
|
instances"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -210,6 +212,28 @@ class AclMixin(object):
|
||||||
(permission,),
|
(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):
|
def can_view(self, user_request, *_args, **_kwargs):
|
||||||
"""Check if a user can view an instance of an object
|
"""Check if a user can view an instance of an object
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,8 @@ LOGIN_URL = "/login/" # The URL for login page
|
||||||
LOGIN_REDIRECT_URL = "/" # The URL for redirecting after login
|
LOGIN_REDIRECT_URL = "/" # The URL for redirecting after login
|
||||||
|
|
||||||
# Application definition
|
# 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_APPS = (
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
|
@ -80,7 +82,7 @@ LOCAL_APPS = (
|
||||||
"logs",
|
"logs",
|
||||||
)
|
)
|
||||||
INSTALLED_APPS = (
|
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 = (
|
MIDDLEWARE_CLASSES = (
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
|
|
@ -141,6 +141,8 @@ def get_callback(tag_name, obj=None):
|
||||||
return acl_fct(obj.can_view_all, False)
|
return acl_fct(obj.can_view_all, False)
|
||||||
if tag_name == "cannot_view_all":
|
if tag_name == "cannot_view_all":
|
||||||
return acl_fct(obj.can_view_all, True)
|
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":
|
if tag_name == "can_view_app":
|
||||||
return acl_fct(
|
return acl_fct(
|
||||||
lambda x: (
|
lambda x: (
|
||||||
|
@ -296,6 +298,7 @@ def acl_change_filter(parser, token):
|
||||||
@register.tag("cannot_delete_all")
|
@register.tag("cannot_delete_all")
|
||||||
@register.tag("can_view_all")
|
@register.tag("can_view_all")
|
||||||
@register.tag("cannot_view_all")
|
@register.tag("cannot_view_all")
|
||||||
|
@register.tag("can_list")
|
||||||
def acl_model_filter(parser, token):
|
def acl_model_filter(parser, token):
|
||||||
"""Generic definition of an acl templatetag for acl based on model"""
|
"""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.template.context_processors import csrf
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
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 (
|
from preferences.models import (
|
||||||
Service,
|
Service,
|
||||||
|
@ -169,3 +171,30 @@ def handler500(request):
|
||||||
def handler404(request):
|
def handler404(request):
|
||||||
"""The handler view for a 404 error"""
|
"""The handler view for a 404 error"""
|
||||||
return render(request, "errors/404.html", status=404)
|
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
|
113
search/engine.py
113
search/engine.py
|
@ -42,6 +42,20 @@ from preferences.models import GeneralOption
|
||||||
from re2o.base import SortTable, re2o_paginator
|
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 Query:
|
||||||
"""Class representing a query.
|
"""Class representing a query.
|
||||||
It can contain the user-entered text, the operator for the 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
|
subqueries: list of Query objects when the current query is split in
|
||||||
several parts.
|
several parts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, text="", case_sensitive=False):
|
def __init__(self, text="", case_sensitive=False):
|
||||||
"""Initialise an instance of Query.
|
"""Initialise an instance of Query.
|
||||||
|
|
||||||
|
@ -98,27 +113,14 @@ class Query:
|
||||||
return self.operator.join([q.plaintext for q in self.subqueries])
|
return self.operator.join([q.plaintext for q in self.subqueries])
|
||||||
|
|
||||||
if self.case_sensitive:
|
if self.case_sensitive:
|
||||||
return "\"{}\"".format(self.text)
|
return '"{}"'.format(self.text)
|
||||||
|
|
||||||
return 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():
|
def empty_filters():
|
||||||
"""Build empty filters used by Django."""
|
"""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):
|
def is_int(variable):
|
||||||
|
@ -176,10 +178,9 @@ def finish_results(request, results, col, order):
|
||||||
max_result = GeneralOption.get_cached_value("search_display_page")
|
max_result = GeneralOption.get_cached_value("search_display_page")
|
||||||
for name, val in results.items():
|
for name, val in results.items():
|
||||||
page_arg = name + "_page"
|
page_arg = name + "_page"
|
||||||
results[name] = re2o_paginator(request,
|
results[name] = re2o_paginator(
|
||||||
val.distinct(),
|
request, val.distinct(), max_result, page_arg=page_arg
|
||||||
max_result,
|
)
|
||||||
page_arg=page_arg)
|
|
||||||
|
|
||||||
results.update({"max_result": max_result})
|
results.update({"max_result": max_result})
|
||||||
|
|
||||||
|
@ -206,9 +207,9 @@ def contains_filter(attribute, word, case_sensitive=False):
|
||||||
return Q(**{attr: word})
|
return Q(**{attr: word})
|
||||||
|
|
||||||
|
|
||||||
def search_single_word(word, filters, user, start, end,
|
def search_single_word(
|
||||||
user_state, email_state, aff,
|
word, filters, user, start, end, user_state, email_state, aff, case_sensitive=False
|
||||||
case_sensitive=False):
|
):
|
||||||
"""Construct the correct filters to match differents fields of some models
|
"""Construct the correct filters to match differents fields of some models
|
||||||
with the given query according to the given filters.
|
with the given query according to the given filters.
|
||||||
The match fields are either CharField or IntegerField that will be displayed
|
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
|
# Users have a name whereas clubs only have a surname
|
||||||
filter_users = (
|
filter_users = filter_clubs | contains_filter("name", word, case_sensitive)
|
||||||
filter_clubs
|
|
||||||
| contains_filter("name", word, case_sensitive)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not User.can_view_all(user)[0]:
|
if not User.can_view_all(user)[0]:
|
||||||
filter_clubs &= Q(id=user.id)
|
filter_clubs &= Q(id=user.id)
|
||||||
|
@ -252,12 +250,15 @@ def search_single_word(word, filters, user, start, end,
|
||||||
if "1" in aff:
|
if "1" in aff:
|
||||||
filter_machines = (
|
filter_machines = (
|
||||||
contains_filter("name", word, case_sensitive)
|
contains_filter("name", word, case_sensitive)
|
||||||
| (contains_filter("user__pseudo", word, case_sensitive)
|
| (
|
||||||
|
contains_filter("user__pseudo", word, case_sensitive)
|
||||||
& Q(user__state__in=user_state)
|
& Q(user__state__in=user_state)
|
||||||
& Q(user__email_state__in=email_state))
|
& Q(user__email_state__in=email_state)
|
||||||
|
)
|
||||||
| contains_filter("interface__domain__name", word, case_sensitive)
|
| contains_filter("interface__domain__name", word, case_sensitive)
|
||||||
| contains_filter("interface__domain__related_domain__name",
|
| contains_filter(
|
||||||
word, case_sensitive)
|
"interface__domain__related_domain__name", word, case_sensitive
|
||||||
|
)
|
||||||
| contains_filter("interface__mac_address", word, case_sensitive)
|
| contains_filter("interface__mac_address", word, case_sensitive)
|
||||||
| contains_filter("interface__ipv4__ipv4", 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
|
# Switch ports
|
||||||
if "6" in aff and User.can_view_all(user):
|
if "6" in aff and User.can_view_all(user):
|
||||||
filter_ports = (
|
filter_ports = (
|
||||||
contains_filter("machine_interface__domain__name",
|
contains_filter("machine_interface__domain__name", word, case_sensitive)
|
||||||
word, case_sensitive)
|
| contains_filter(
|
||||||
| contains_filter("related__switch__interface__domain__name",
|
"related__switch__interface__domain__name", word, case_sensitive
|
||||||
word, case_sensitive)
|
)
|
||||||
| contains_filter("custom_profile__name", word, case_sensitive)
|
| contains_filter("custom_profile__name", word, case_sensitive)
|
||||||
| contains_filter("custom_profile__profil_default",
|
| contains_filter("custom_profile__profil_default", word, case_sensitive)
|
||||||
word, case_sensitive)
|
|
||||||
| contains_filter("details", word, case_sensitive)
|
| contains_filter("details", word, case_sensitive)
|
||||||
# Added through annotate
|
# Added through annotate
|
||||||
| contains_filter("room_full_name", word, case_sensitive)
|
| contains_filter("room_full_name", word, case_sensitive)
|
||||||
|
@ -360,8 +360,7 @@ def search_single_word(word, filters, user, start, end,
|
||||||
filter_switches = (
|
filter_switches = (
|
||||||
contains_filter("interface__domain__name", word, case_sensitive)
|
contains_filter("interface__domain__name", word, case_sensitive)
|
||||||
| contains_filter("interface__ipv4__ipv4", word, case_sensitive)
|
| contains_filter("interface__ipv4__ipv4", word, case_sensitive)
|
||||||
| contains_filter("switchbay__building__name",
|
| contains_filter("switchbay__building__name", word, case_sensitive)
|
||||||
word, case_sensitive)
|
|
||||||
| contains_filter("stack__name", word, case_sensitive)
|
| contains_filter("stack__name", word, case_sensitive)
|
||||||
| contains_filter("model__reference", word, case_sensitive)
|
| contains_filter("model__reference", word, case_sensitive)
|
||||||
| contains_filter("model__constructor__name", word, case_sensitive)
|
| contains_filter("model__constructor__name", word, case_sensitive)
|
||||||
|
@ -399,13 +398,11 @@ def apply_filters(filters, user, aff):
|
||||||
# Users and clubs
|
# Users and clubs
|
||||||
if "0" in aff:
|
if "0" in aff:
|
||||||
results["users"] = Adherent.objects.annotate(
|
results["users"] = Adherent.objects.annotate(
|
||||||
room_full_name=Concat("room__building__name",
|
room_full_name=Concat("room__building__name", Value(" "), "room__name"),
|
||||||
Value(" "), "room__name"),
|
|
||||||
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
||||||
).filter(filters["users"])
|
).filter(filters["users"])
|
||||||
results["clubs"] = Club.objects.annotate(
|
results["clubs"] = Club.objects.annotate(
|
||||||
room_full_name=Concat("room__building__name",
|
room_full_name=Concat("room__building__name", Value(" "), "room__name"),
|
||||||
Value(" "), "room__name"),
|
|
||||||
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
||||||
).filter(filters["clubs"])
|
).filter(filters["clubs"])
|
||||||
|
|
||||||
|
@ -435,8 +432,7 @@ def apply_filters(filters, user, aff):
|
||||||
# Switch ports
|
# Switch ports
|
||||||
if "6" in aff and User.can_view_all(user):
|
if "6" in aff and User.can_view_all(user):
|
||||||
results["ports"] = Port.objects.annotate(
|
results["ports"] = Port.objects.annotate(
|
||||||
room_full_name=Concat("room__building__name",
|
room_full_name=Concat("room__building__name", Value(" "), "room__name"),
|
||||||
Value(" "), "room__name"),
|
|
||||||
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
room_full_name_stuck=Concat("room__building__name", "room__name"),
|
||||||
).filter(filters["ports"])
|
).filter(filters["ports"])
|
||||||
|
|
||||||
|
@ -455,24 +451,32 @@ def search_single_query(query, filters, user, start, end, user_state, email_stat
|
||||||
newfilters = empty_filters()
|
newfilters = empty_filters()
|
||||||
for q in query.subqueries:
|
for q in query.subqueries:
|
||||||
# Construct an independent filter for each subquery
|
# Construct an independent filter for each subquery
|
||||||
subfilters = search_single_query(q, empty_filters(), user,
|
subfilters = search_single_query(
|
||||||
start, end, user_state,
|
q, empty_filters(), user, start, end, user_state, email_state, aff
|
||||||
email_state, aff)
|
)
|
||||||
|
|
||||||
# Apply the subfilter
|
# Apply the subfilter
|
||||||
for field in filter_fields():
|
for field in FILTER_FIELDS:
|
||||||
newfilters[field] &= subfilters[field]
|
newfilters[field] &= subfilters[field]
|
||||||
|
|
||||||
# Add these filters to the existing ones
|
# Add these filters to the existing ones
|
||||||
for field in filter_fields():
|
for field in FILTER_FIELDS:
|
||||||
filters[field] |= newfilters[field]
|
filters[field] |= newfilters[field]
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
# Handle standard queries
|
# Handle standard queries
|
||||||
return search_single_word(query.text, filters, user, start, end,
|
return search_single_word(
|
||||||
user_state, email_state, aff,
|
query.text,
|
||||||
query.case_sensitive)
|
filters,
|
||||||
|
user,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
user_state,
|
||||||
|
email_state,
|
||||||
|
aff,
|
||||||
|
query.case_sensitive,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_queries(query):
|
def create_queries(query):
|
||||||
|
@ -564,4 +568,9 @@ def create_queries(query):
|
||||||
|
|
||||||
queries.append(current_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
|
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 #}
|
{# Preload JavaScript #}
|
||||||
{% bootstrap_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>
|
<script src="{% static 'js/collapse-from-url.js' %}"></script>
|
||||||
|
|
||||||
{% block custom_js %}{% endblock %}
|
{% block custom_js %}{% endblock %}
|
||||||
|
|
||||||
{# Load CSS #}
|
{# Load CSS #}
|
||||||
{% bootstrap_css %}
|
{% bootstrap_css %}
|
||||||
<link href="{% static 'css/typeaheadjs.css' %}" rel="stylesheet">
|
<link href="{% static 'css/autocomplete.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'css/bootstrap-tokenfield.css' %}" rel="stylesheet">
|
|
||||||
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
|
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
|
||||||
{# load theme #}
|
{# load theme #}
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
|
|
|
@ -29,6 +29,7 @@ from django.template.loader import render_to_string
|
||||||
from django.forms import ModelForm, Form
|
from django.forms import ModelForm, Form
|
||||||
from re2o.field_permissions import FieldPermissionFormMixin
|
from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
|
from re2o.widgets import AutocompleteModelWidget
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .models import Ticket, CommentTicket
|
from .models import Ticket, CommentTicket
|
||||||
|
@ -58,6 +59,11 @@ class EditTicketForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"user": AutocompleteModelWidget(
|
||||||
|
url="/users/user-autocomplete",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(EditTicketForm, self).__init__(*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 %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Ticket" %}{% endblock %}
|
{% 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>
|
<h2>{% trans "Ticket opening" %}</h2>
|
||||||
|
|
||||||
{% bootstrap_form_errors ticketform %}
|
{% bootstrap_form_errors ticketform %}
|
||||||
|
{{ ticketform.media }}
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -38,6 +38,10 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from machines.models import Interface
|
from machines.models import Interface
|
||||||
from machines.forms import EditMachineForm, NewMachineForm
|
from machines.forms import EditMachineForm, NewMachineForm
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
|
from re2o.widgets import (
|
||||||
|
AutocompleteModelWidget,
|
||||||
|
AutocompleteMultipleModelWidget,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Port,
|
Port,
|
||||||
|
@ -62,6 +66,17 @@ class PortForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Port
|
model = Port
|
||||||
fields = "__all__"
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -154,7 +169,7 @@ class AddAccessPointForm(NewMachineForm):
|
||||||
class EditAccessPointForm(EditMachineForm):
|
class EditAccessPointForm(EditMachineForm):
|
||||||
"""Form used to edit access points."""
|
"""Form used to edit access points."""
|
||||||
|
|
||||||
class Meta:
|
class Meta(EditMachineForm.Meta):
|
||||||
model = AccessPoint
|
model = AccessPoint
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
@ -162,9 +177,15 @@ class EditAccessPointForm(EditMachineForm):
|
||||||
class EditSwitchForm(EditMachineForm):
|
class EditSwitchForm(EditMachineForm):
|
||||||
"""Form used to edit switches."""
|
"""Form used to edit switches."""
|
||||||
|
|
||||||
class Meta:
|
class Meta(EditMachineForm.Meta):
|
||||||
model = Switch
|
model = Switch
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"switchbay": AutocompleteModelWidget(
|
||||||
|
url="/topologie/switchbay-autocomplete"
|
||||||
|
),
|
||||||
|
"user": AutocompleteModelWidget(url="/users/user-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NewSwitchForm(NewMachineForm):
|
class NewSwitchForm(NewMachineForm):
|
||||||
|
@ -180,6 +201,9 @@ class EditRoomForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Room
|
model = Room
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"building": AutocompleteModelWidget(url="/topologie/building-autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -196,7 +220,11 @@ class CreatePortsForm(forms.Form):
|
||||||
class EditModelSwitchForm(FormRevMixin, ModelForm):
|
class EditModelSwitchForm(FormRevMixin, ModelForm):
|
||||||
"""Form used to edit switch models."""
|
"""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:
|
class Meta:
|
||||||
model = ModelSwitch
|
model = ModelSwitch
|
||||||
|
@ -230,11 +258,18 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm):
|
||||||
class EditSwitchBayForm(FormRevMixin, ModelForm):
|
class EditSwitchBayForm(FormRevMixin, ModelForm):
|
||||||
"""Form used to edit switch bays."""
|
"""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:
|
class Meta:
|
||||||
model = SwitchBay
|
model = SwitchBay
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"building": AutocompleteModelWidget(url="/topologie/building-autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -279,6 +314,12 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PortProfile
|
model = PortProfile
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"vlan_tagged": AutocompleteMultipleModelWidget(
|
||||||
|
url="/machines/vlan-autocomplete"
|
||||||
|
),
|
||||||
|
"vlan_untagged": AutocompleteModelWidget(url="/machines/vlan-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
|
|
@ -731,6 +731,22 @@ class Dormitory(AclMixin, RevMixin, models.Model):
|
||||||
else:
|
else:
|
||||||
return cache.get_or_set("multiple_dorms", cls.objects.count() > 1)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -762,6 +778,22 @@ class Building(AclMixin, RevMixin, models.Model):
|
||||||
else:
|
else:
|
||||||
return self.name
|
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
|
@cached_property
|
||||||
def cached_name(self):
|
def cached_name(self):
|
||||||
return self.get_name()
|
return self.get_name()
|
||||||
|
@ -944,6 +976,22 @@ class Room(AclMixin, RevMixin, models.Model):
|
||||||
verbose_name_plural = _("rooms")
|
verbose_name_plural = _("rooms")
|
||||||
unique_together = ("name", "building")
|
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):
|
def __str__(self):
|
||||||
return self.building.cached_name + " " + self.name
|
return self.building.cached_name + " " + self.name
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Topology" %}{% endblock %}
|
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||||
|
@ -41,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if topoform %}
|
{% if topoform %}
|
||||||
<h3>{% trans "Specific settings for the switch" %}</h3>
|
<h3>{% trans "Specific settings for the switch" %}</h3>
|
||||||
{% massive_bootstrap_form topoform 'switch_interface' %}
|
{% bootstrap_form topoform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% trans "Confirm" as tr_confirm %}
|
{% trans "Confirm" as tr_confirm %}
|
||||||
{% bootstrap_button tr_confirm button_type="submit" icon='ok' button_class='btn-success' %}
|
{% 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 %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Topology" %}{% endblock %}
|
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||||
|
@ -37,11 +36,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %}
|
{% bootstrap_form topoform %}
|
||||||
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
|
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
|
||||||
</form>
|
</form>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
{{ topoform.media }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Topology" %}{% endblock %}
|
{% block title %}{% trans "Topology" %}{% endblock %}
|
||||||
|
@ -32,9 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if topoform %}
|
{% if topoform %}
|
||||||
{% bootstrap_form_errors topoform %}
|
{% bootstrap_form_errors topoform %}
|
||||||
|
{{ topoform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if machineform %}
|
{% if machineform %}
|
||||||
{% bootstrap_form_errors machineform %}
|
{% bootstrap_form_errors machineform %}
|
||||||
|
{{ machineform.media }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if domainform %}
|
{% if domainform %}
|
||||||
{% bootstrap_form_errors domainform %}
|
{% bootstrap_form_errors domainform %}
|
||||||
|
@ -46,11 +47,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if topoform %}
|
{% if topoform %}
|
||||||
<h3>{% blocktrans %}Specific settings for the {{ device }} object{% endblocktrans %}</h3>
|
<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 %}
|
{% endif %}
|
||||||
{% if machineform %}
|
{% if machineform %}
|
||||||
<h3>{% blocktrans %}General settings for the machine linked to the {{ device }} object{% endblocktrans %}</h3>
|
<h3>{% blocktrans %}General settings for the machine linked to the {{ device }} object{% endblocktrans %}</h3>
|
||||||
{% massive_bootstrap_form machineform 'user' %}
|
{% bootstrap_form machineform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if domainform %}
|
{% if domainform %}
|
||||||
<h3>{% trans "DNS name" %}</h3>
|
<h3>{% trans "DNS name" %}</h3>
|
||||||
|
|
|
@ -28,6 +28,7 @@ from __future__ import unicode_literals
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
from . import views_autocomplete
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^$", views.index, name="index"),
|
url(r"^$", views.index, name="index"),
|
||||||
|
@ -169,4 +170,12 @@ urlpatterns = [
|
||||||
views.del_module_on,
|
views.del_module_on,
|
||||||
name="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,
|
AddInterfaceForm,
|
||||||
EditOptionVlanForm,
|
EditOptionVlanForm,
|
||||||
)
|
)
|
||||||
from machines.views import generate_ipv4_mbf_param
|
|
||||||
from machines.models import Interface, Service_link, Vlan
|
from machines.models import Interface, Service_link, Vlan
|
||||||
from preferences.models import AssoOption, GeneralOption
|
from preferences.models import AssoOption, GeneralOption
|
||||||
|
|
||||||
|
@ -560,13 +559,11 @@ def new_switch(request):
|
||||||
new_domain_obj.save()
|
new_domain_obj.save()
|
||||||
messages.success(request, _("The switch was created."))
|
messages.success(request, _("The switch was created."))
|
||||||
return redirect(reverse("topologie:index"))
|
return redirect(reverse("topologie:index"))
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"topoform": interface,
|
"topoform": interface,
|
||||||
"machineform": switch,
|
"machineform": switch,
|
||||||
"domainform": domain,
|
"domainform": domain,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"device": _("switch"),
|
"device": _("switch"),
|
||||||
},
|
},
|
||||||
"topologie/topo_more.html",
|
"topologie/topo_more.html",
|
||||||
|
@ -634,14 +631,12 @@ def edit_switch(request, switch, switchid):
|
||||||
new_domain_obj.save()
|
new_domain_obj.save()
|
||||||
messages.success(request, _("The switch was edited."))
|
messages.success(request, _("The switch was edited."))
|
||||||
return redirect(reverse("topologie:index"))
|
return redirect(reverse("topologie:index"))
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"id_switch": switchid,
|
"id_switch": switchid,
|
||||||
"topoform": interface_form,
|
"topoform": interface_form,
|
||||||
"machineform": switch_form,
|
"machineform": switch_form,
|
||||||
"domainform": domain_form,
|
"domainform": domain_form,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"device": _("switch"),
|
"device": _("switch"),
|
||||||
},
|
},
|
||||||
"topologie/topo_more.html",
|
"topologie/topo_more.html",
|
||||||
|
@ -686,13 +681,11 @@ def new_ap(request):
|
||||||
new_domain_obj.save()
|
new_domain_obj.save()
|
||||||
messages.success(request, _("The access point was created."))
|
messages.success(request, _("The access point was created."))
|
||||||
return redirect(reverse("topologie:index-ap"))
|
return redirect(reverse("topologie:index-ap"))
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"topoform": interface,
|
"topoform": interface,
|
||||||
"machineform": ap,
|
"machineform": ap,
|
||||||
"domainform": domain,
|
"domainform": domain,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"device": _("access point"),
|
"device": _("access point"),
|
||||||
},
|
},
|
||||||
"topologie/topo_more.html",
|
"topologie/topo_more.html",
|
||||||
|
@ -737,13 +730,11 @@ def edit_ap(request, ap, **_kwargs):
|
||||||
new_domain_obj.save()
|
new_domain_obj.save()
|
||||||
messages.success(request, _("The access point was edited."))
|
messages.success(request, _("The access point was edited."))
|
||||||
return redirect(reverse("topologie:index-ap"))
|
return redirect(reverse("topologie:index-ap"))
|
||||||
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
|
|
||||||
return form(
|
return form(
|
||||||
{
|
{
|
||||||
"topoform": interface_form,
|
"topoform": interface_form,
|
||||||
"machineform": ap_form,
|
"machineform": ap_form,
|
||||||
"domainform": domain_form,
|
"domainform": domain_form,
|
||||||
"i_mbf_param": i_mbf_param,
|
|
||||||
"device": _("access point"),
|
"device": _("access point"),
|
||||||
},
|
},
|
||||||
"topologie/topo_more.html",
|
"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
|
|
@ -46,7 +46,10 @@ from os import walk, path
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import ModelForm, Form
|
from django.forms import ModelForm, Form
|
||||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
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.core.validators import MinLengthValidator
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -61,6 +64,10 @@ from preferences.models import OptionalUser
|
||||||
from re2o.utils import remove_user_room
|
from re2o.utils import remove_user_room
|
||||||
from re2o.base import get_input_formats_help_text
|
from re2o.base import get_input_formats_help_text
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
|
from re2o.widgets import (
|
||||||
|
AutocompleteMultipleModelWidget,
|
||||||
|
AutocompleteModelWidget,
|
||||||
|
)
|
||||||
from re2o.field_permissions import FieldPermissionFormMixin
|
from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
|
|
||||||
from preferences.models import GeneralOption
|
from preferences.models import GeneralOption
|
||||||
|
@ -156,14 +163,10 @@ class ServiceUserAdminForm(FormRevMixin, forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
password1 = forms.CharField(
|
password1 = forms.CharField(
|
||||||
label=_("Password"),
|
label=_("Password"), widget=forms.PasswordInput, max_length=255
|
||||||
widget=forms.PasswordInput,
|
|
||||||
max_length=255,
|
|
||||||
)
|
)
|
||||||
password2 = forms.CharField(
|
password2 = forms.CharField(
|
||||||
label=_("Password confirmation"),
|
label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255
|
||||||
widget=forms.PasswordInput,
|
|
||||||
max_length=255,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -215,6 +218,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
|
||||||
DjangoForm : Inherit from basic django form
|
DjangoForm : Inherit from basic django form
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
selfpasswd = forms.CharField(
|
selfpasswd = forms.CharField(
|
||||||
label=_("Current password"), max_length=255, widget=forms.PasswordInput
|
label=_("Current password"), max_length=255, widget=forms.PasswordInput
|
||||||
)
|
)
|
||||||
|
@ -222,12 +226,10 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
|
||||||
label=_("New password"),
|
label=_("New password"),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput,
|
||||||
help_text=password_validators_help_text_html()
|
help_text=password_validators_help_text_html(),
|
||||||
)
|
)
|
||||||
passwd2 = forms.CharField(
|
passwd2 = forms.CharField(
|
||||||
label=_("New password confirmation"),
|
label=_("New password confirmation"), max_length=255, widget=forms.PasswordInput
|
||||||
max_length=255,
|
|
||||||
widget=forms.PasswordInput,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -296,9 +298,7 @@ class MassArchiveForm(forms.Form):
|
||||||
|
|
||||||
date = forms.DateTimeField(help_text="%d/%m/%y")
|
date = forms.DateTimeField(help_text="%d/%m/%y")
|
||||||
full_archive = forms.BooleanField(
|
full_archive = forms.BooleanField(
|
||||||
label=_(
|
label=_("Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"),
|
||||||
"Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"
|
|
||||||
),
|
|
||||||
initial=False,
|
initial=False,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
@ -350,6 +350,16 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
"telephone",
|
"telephone",
|
||||||
"room",
|
"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(
|
force = forms.BooleanField(
|
||||||
label=_("Force the move?"), initial=False, required=False
|
label=_("Force the move?"), initial=False, required=False
|
||||||
|
@ -413,6 +423,7 @@ class AdherentCreationForm(AdherentForm):
|
||||||
Parameters:
|
Parameters:
|
||||||
DjangoForm : Inherit from basic django form
|
DjangoForm : Inherit from basic django form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Champ pour choisir si un lien est envoyé par mail pour le mot de passe
|
# Champ pour choisir si un lien est envoyé par mail pour le mot de passe
|
||||||
init_password_by_mail_info = _(
|
init_password_by_mail_info = _(
|
||||||
"If this options is set, you will receive a link to set"
|
"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(
|
init_password_by_mail = forms.BooleanField(
|
||||||
help_text=init_password_by_mail_info,
|
help_text=init_password_by_mail_info, required=False, initial=True
|
||||||
required=False,
|
|
||||||
initial=True
|
|
||||||
)
|
)
|
||||||
init_password_by_mail.label = _("Send password reset link by email.")
|
init_password_by_mail.label = _("Send password reset link by email.")
|
||||||
|
|
||||||
|
@ -438,7 +447,7 @@ class AdherentCreationForm(AdherentForm):
|
||||||
label=_("Password"),
|
label=_("Password"),
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput,
|
||||||
max_length=255,
|
max_length=255,
|
||||||
help_text=password_validators_help_text_html()
|
help_text=password_validators_help_text_html(),
|
||||||
)
|
)
|
||||||
password2 = forms.CharField(
|
password2 = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -461,7 +470,7 @@ class AdherentCreationForm(AdherentForm):
|
||||||
# Checkbox for GTU
|
# Checkbox for GTU
|
||||||
gtu_check = forms.BooleanField(required=True)
|
gtu_check = forms.BooleanField(required=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta(AdherentForm.Meta):
|
||||||
model = Adherent
|
model = Adherent
|
||||||
fields = [
|
fields = [
|
||||||
"name",
|
"name",
|
||||||
|
@ -528,8 +537,12 @@ class AdherentCreationForm(AdherentForm):
|
||||||
# Save the provided password in hashed format
|
# Save the provided password in hashed format
|
||||||
user = super(AdherentForm, self).save(commit=False)
|
user = super(AdherentForm, self).save(commit=False)
|
||||||
|
|
||||||
is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation")
|
is_set_password_allowed = OptionalUser.get_cached_value(
|
||||||
set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail")
|
"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:
|
if set_passwd:
|
||||||
user.set_password(self.cleaned_data["password1"])
|
user.set_password(self.cleaned_data["password1"])
|
||||||
|
|
||||||
|
@ -556,7 +569,7 @@ class AdherentEditForm(AdherentForm):
|
||||||
if "shell" in self.fields:
|
if "shell" in self.fields:
|
||||||
self.fields["shell"].empty_label = _("Default shell")
|
self.fields["shell"].empty_label = _("Default shell")
|
||||||
|
|
||||||
class Meta:
|
class Meta(AdherentForm.Meta):
|
||||||
model = Adherent
|
model = Adherent
|
||||||
fields = [
|
fields = [
|
||||||
"name",
|
"name",
|
||||||
|
@ -609,6 +622,11 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
|
||||||
"shell",
|
"shell",
|
||||||
"mailing",
|
"mailing",
|
||||||
]
|
]
|
||||||
|
widgets = {
|
||||||
|
"school": AutocompleteModelWidget(url="/users/school-autocomplete"),
|
||||||
|
"room": AutocompleteModelWidget(url="/topologie/room-autocomplete"),
|
||||||
|
"shell": AutocompleteModelWidget(url="/users/shell-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
def clean_telephone(self):
|
def clean_telephone(self):
|
||||||
"""Clean telephone, check if telephone is made mandatory, and
|
"""Clean telephone, check if telephone is made mandatory, and
|
||||||
|
@ -637,6 +655,14 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = ["administrators", "members"]
|
fields = ["administrators", "members"]
|
||||||
|
widgets = {
|
||||||
|
"administrators": AutocompleteMultipleModelWidget(
|
||||||
|
url="/users/adherent-autocomplete"
|
||||||
|
),
|
||||||
|
"members": AutocompleteMultipleModelWidget(
|
||||||
|
url="/users/adherent-autocomplete"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
|
||||||
|
@ -972,6 +998,7 @@ class InitialRegisterForm(forms.Form):
|
||||||
Parameters:
|
Parameters:
|
||||||
DjangoForm : Inherit from basic django form
|
DjangoForm : Inherit from basic django form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
register_room = forms.BooleanField(required=False)
|
register_room = forms.BooleanField(required=False)
|
||||||
register_machine = forms.BooleanField(required=False)
|
register_machine = forms.BooleanField(required=False)
|
||||||
|
|
||||||
|
@ -1056,4 +1083,4 @@ class ThemeForm(FormRevMixin, forms.Form):
|
||||||
if not themes:
|
if not themes:
|
||||||
themes = ["default.css"]
|
themes = ["default.css"]
|
||||||
super(ThemeForm, self).__init__(*args, **kwargs)
|
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
|
self.gpg_fingerprint = gpg_fingerprint
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""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.
|
:return: An adherent.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -2065,6 +2065,33 @@ class Adherent(User):
|
||||||
("users.add_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):
|
def clean(self, *args, **kwargs):
|
||||||
"""Method, clean and validate the gpgfp value.
|
"""Method, clean and validate the gpgfp value.
|
||||||
|
|
||||||
|
@ -2154,13 +2181,13 @@ class Club(User):
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""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: A club.
|
||||||
"""
|
"""
|
||||||
return cls.objects.get(pk=clubid)
|
return cls.objects.get(pk=object_id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Adherent)
|
@receiver(post_save, sender=Adherent)
|
||||||
|
@ -2364,6 +2391,22 @@ class School(RevMixin, AclMixin, models.Model):
|
||||||
verbose_name = _("school")
|
verbose_name = _("school")
|
||||||
verbose_name_plural = _("schools")
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -2487,6 +2530,22 @@ class ListShell(RevMixin, AclMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
return self.shell.split("/")[-1]
|
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):
|
def __str__(self):
|
||||||
return self.shell
|
return self.shell
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
|
@ -351,7 +351,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-xs-12">
|
<div class="col-md-6 col-xs-12">
|
||||||
<dt>{% trans "Theme" %}</dt>
|
<dt>{% trans "Theme" %}</dt>
|
||||||
<dd>{{ request.user.theme_name }}</dd>
|
<dd>{{ users.theme_name }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}{% trans "Users" %}{% endblock %}
|
{% 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">
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% 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' %}
|
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
|
||||||
</form>
|
</form>
|
||||||
{% if load_js_file %}
|
{% if load_js_file %}
|
||||||
|
@ -48,5 +47,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
{{ userform.media }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load massive_bootstrap_form %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}{% trans "Users" %}{% endblock %}
|
{% block title %}{% trans "Users" %}{% endblock %}
|
||||||
|
|
|
@ -31,6 +31,7 @@ from __future__ import unicode_literals
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
from . import views_autocomplete
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^new_user/$", views.new_user, name="new-user"),
|
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"^index_clubs/$", views.index_clubs, name="index-clubs"),
|
||||||
url(r"^initial_register/$", views.initial_register, name="initial-register"),
|
url(r"^initial_register/$", views.initial_register, name="initial-register"),
|
||||||
url(r"^edit_theme/(?P<userid>[0-9]+)$", views.edit_theme, name="edit-theme"),
|
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