8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-23 07:23:46 +00:00

Merge branch 'autocomplete-light' into 'dev'

Autocomplete light

See merge request re2o/re2o!582
This commit is contained in:
chirac 2021-01-02 23:59:16 +01:00
commit be8e6925af
50 changed files with 1143 additions and 4813 deletions

View file

@ -46,6 +46,7 @@ from django.shortcuts import get_object_or_404
from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin
from re2o.widgets import AutocompleteModelWidget
from .models import (
Article,
Paiement,
@ -79,6 +80,10 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
class Meta:
model = Facture
fields = "__all__"
widgets = {
"user": AutocompleteModelWidget(url="/users/user-autocomplete"),
"banque": AutocompleteModelWidget(url="/cotisations/banque-autocomplete"),
}
def clean(self):
cleaned_data = super(FactureForm, self).clean()

View file

@ -25,13 +25,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %}
{% load staticfiles%}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Creation and editing of invoices" %}{% endblock %}
{% block content %}
{% bootstrap_form_errors factureform %}
{{ factureform.media }}
<form class="form" method="post">
{% csrf_token %}
@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% else %}
<h3>{% trans "Edit invoice" %}</h3>
{% endif %}
{% massive_bootstrap_form factureform 'user' %}
{% bootstrap_form factureform %}
{{ venteform.management_form }}
<h3>{% trans "Articles" %}</h3>
<table class="table table-striped">

View file

@ -27,7 +27,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from . import views
from . import views, views_autocomplete
from . import payment_methods
urlpatterns = [
@ -104,4 +104,6 @@ urlpatterns = [
url(r"^index_paiement/$", views.index_paiement, name="index-paiement"),
url(r"^control/$", views.control, name="control"),
url(r"^$", views.index, name="index"),
### Autocomplete Views
url(r'^banque-autocomplete/$', views_autocomplete.BanqueAutocomplete.as_view(), name='banque-autocomplete',),
] + payment_methods.urls.urlpatterns

View 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

View file

@ -25,6 +25,7 @@ from django import forms
from django.forms import Form
from django.utils.translation import ugettext_lazy as _
from re2o.base import get_input_formats_help_text
from re2o.widgets import AutocompleteModelWidget
import inspect
@ -46,10 +47,7 @@ CHOICES_ACTION_TYPE = (
("all", _("All")),
)
CHOICES_TYPE = (
("ip", _("IPv4")),
("mac", _("MAC address")),
)
CHOICES_TYPE = (("ip", _("IPv4")), ("mac", _("MAC address")))
def all_classes(module):
@ -87,14 +85,11 @@ def classes_for_action_type(action_type):
users.models.User.__name__,
users.models.Adherent.__name__,
users.models.Club.__name__,
users.models.EMailAddress.__name__
users.models.EMailAddress.__name__,
]
if action_type == "machines":
return [
machines.models.Machine.__name__,
machines.models.Interface.__name__
]
return [machines.models.Machine.__name__, machines.models.Interface.__name__]
if action_type == "subscriptions":
return all_classes(cotisations.models)
@ -114,40 +109,39 @@ def classes_for_action_type(action_type):
class ActionsSearchForm(Form):
"""Form used to do an advanced search through the logs."""
u = forms.ModelChoiceField(
user = forms.ModelChoiceField(
label=_("Performed by"),
queryset=users.models.User.objects.all(),
required=False,
widget=AutocompleteModelWidget(url="/users/user-autocomplete"),
)
t = forms.MultipleChoiceField(
action_type = forms.MultipleChoiceField(
label=_("Action type"),
required=False,
widget=forms.CheckboxSelectMultiple,
choices=CHOICES_ACTION_TYPE,
initial=[i[0] for i in CHOICES_ACTION_TYPE],
)
s = forms.DateField(required=False, label=_("Start date"))
e = forms.DateField(required=False, label=_("End date"))
start_date = forms.DateField(required=False, label=_("Start date"))
end_date = forms.DateField(required=False, label=_("End date"))
def __init__(self, *args, **kwargs):
super(ActionsSearchForm, self).__init__(*args, **kwargs)
self.fields["s"].help_text = get_input_formats_help_text(
self.fields["s"].input_formats
self.fields["start_date"].help_text = get_input_formats_help_text(
self.fields["start_date"].input_formats
)
self.fields["e"].help_text = get_input_formats_help_text(
self.fields["e"].input_formats
self.fields["end_date"].help_text = get_input_formats_help_text(
self.fields["end_date"].input_formats
)
class MachineHistorySearchForm(Form):
"""Form used to do a search through the machine histories."""
q = forms.CharField(
label=_("Search"),
max_length=100,
)
q = forms.CharField(label=_("Search"), max_length=100)
t = forms.CharField(
label=_("Search type"),
widget=forms.Select(choices=CHOICES_TYPE)
label=_("Search type"), widget=forms.Select(choices=CHOICES_TYPE)
)
s = forms.DateField(required=False, label=_("Start date"))
e = forms.DateField(required=False, label=_("End date"))

View file

@ -600,10 +600,10 @@ class ActionsSearch:
Returns:
The QuerySet of Revision objects corresponding to the search.
"""
user = params.get("u", None)
start = params.get("s", None)
end = params.get("e", None)
action_types = params.get("t", None)
user = params.get("user", None)
start = params.get("start_date", None)
end = params.get("end_date", None)
action_types = params.get("action_type", None)
query = Q()

View file

@ -22,7 +22,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Search events" %}{% endblock %}
@ -32,10 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form">
<h3>{% trans "Search events" %}</h3>
{% massive_bootstrap_form actions_form 'u' %}
{% bootstrap_form actions_form %}
{% trans "Search" as tr_search %}
{% bootstrap_button tr_search button_type="submit" icon="search" %}
</form>
{{ actions_form.media }}
<br />
<br />
<br />

View file

@ -41,6 +41,10 @@ from django.utils.translation import ugettext_lazy as _
from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin
from re2o.widgets import (
AutocompleteModelWidget,
AutocompleteMultipleModelWidget,
)
from .models import (
Domain,
Machine,
@ -71,6 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class Meta:
model = Machine
fields = "__all__"
widgets = {"user": AutocompleteModelWidget(url="/users/user-autocomplete")}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -91,6 +96,19 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class Meta:
model = Interface
fields = ["machine", "machine_type", "ipv4", "mac_address", "details"]
widgets = {
"machine": AutocompleteModelWidget(url="/machines/machine-autocomplete"),
"machine_type": AutocompleteModelWidget(
url="/machines/machinetype-autocomplete"
),
"ipv4": AutocompleteModelWidget(
url="/machines/iplist-autocomplete",
forward=["machine_type"],
attrs={
"data-placeholder": "Automatic assigment. Type to choose specific ip."
},
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -139,6 +157,9 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class Meta:
model = Domain
fields = ["name", "extension", "ttl"]
widgets = {
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete")
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -188,6 +209,9 @@ class MachineTypeForm(FormRevMixin, ModelForm):
class Meta:
model = MachineType
fields = ["name", "ip_type"]
widgets = {
"ip_type": AutocompleteModelWidget(url="/machines/iptype-autocomplete")
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -222,6 +246,13 @@ class IpTypeForm(FormRevMixin, ModelForm):
class Meta:
model = IpType
fields = "__all__"
widgets = {
"vlan": AutocompleteModelWidget(url="/machines/vlan-autocomplete"),
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
"ouverture_ports": AutocompleteModelWidget(
url="/machines/ouvertureportlist-autocomplete"
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -351,6 +382,10 @@ class MxForm(FormRevMixin, ModelForm):
class Meta:
model = Mx
fields = ["zone", "priority", "name", "ttl"]
widgets = {
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
"name": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -386,6 +421,10 @@ class NsForm(FormRevMixin, ModelForm):
class Meta:
model = Ns
fields = ["zone", "ns", "ttl"]
widgets = {
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
"ns": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -419,6 +458,9 @@ class TxtForm(FormRevMixin, ModelForm):
class Meta:
model = Txt
fields = "__all__"
widgets = {
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete")
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -449,6 +491,9 @@ class DNameForm(FormRevMixin, ModelForm):
class Meta:
model = DName
fields = "__all__"
widgets = {
"zone": AutocompleteModelWidget(url="/machines/extension-autocomplete")
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -479,6 +524,10 @@ class SrvForm(FormRevMixin, ModelForm):
class Meta:
model = Srv
fields = "__all__"
widgets = {
"extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"),
"target": AutocompleteModelWidget(url="/machines/domain-autocomplete"),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -509,6 +558,14 @@ class NasForm(FormRevMixin, ModelForm):
class Meta:
model = Nas
fields = "__all__"
widgets = {
"nas_type": AutocompleteModelWidget(
url="/machines/machinetype-autocomplete"
),
"machine_type": AutocompleteModelWidget(
url="/machines/machinetype-autocomplete"
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -539,6 +596,11 @@ class RoleForm(FormRevMixin, ModelForm):
class Meta:
model = Role
fields = "__all__"
widgets = {
"servers": AutocompleteMultipleModelWidget(
url="/machines/interface-autocomplete"
)
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -572,6 +634,11 @@ class ServiceForm(FormRevMixin, ModelForm):
class Meta:
model = Service
fields = "__all__"
widgets = {
"servers": AutocompleteMultipleModelWidget(
url="/machines/interface-autocomplete"
)
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -656,6 +723,11 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm):
class Meta:
model = Interface
fields = ["port_lists"]
widgets = {
"port_lists": AutocompleteMultipleModelWidget(
url="/machines/ouvertureportlist-autocomplete"
)
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)

View file

@ -378,6 +378,34 @@ class MachineType(RevMixin, AclMixin, models.Model):
)
return True, None, None
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list unprivileged machinetypes
Only members of privileged groups can list all.
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
can, _message, _group = cls.can_use_all(user_request)
if can:
return (
True,
None,
None,
cls.objects.all()
)
else:
return (
True,
_("You don't have the right to use all machine types."),
("machines.use_all_machinetype",),
cls.objects.filter(
ip_type__in=IpType.objects.filter(need_infra=False)
),
)
def __str__(self):
return self.name
@ -953,6 +981,32 @@ class Extension(RevMixin, AclMixin, models.Model):
("machines.use_all_extension",),
)
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list unprivileged extensions
Only members of privileged groups can list all.
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
can, _message, _group = cls.can_use_all(user_request)
if can:
return (
True,
None,
None,
cls.objects.all()
)
else:
return (
True,
_("You don't have the right to list all extensions."),
("machines.use_all_extension",),
cls.objects.filter(need_infra=False),
)
def __str__(self):
return self.name
@ -2130,6 +2184,34 @@ class IpList(RevMixin, AclMixin, models.Model):
self.clean()
super(IpList, self).save(*args, **kwargs)
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""Only privilged users can list all ipv4.
Others can list Ipv4 related with unprivileged type.
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
can, _message, _group = IpType.can_use_all(user_request)
if can:
return (
True,
None,
None,
cls.objects.all()
)
else:
return (
True,
_("You don't have the right to use all machine types."),
("machines.use_all_machinetype",),
cls.objects.filter(
ip_type__in=IpType.objects.filter(need_infra=False)
),
)
def __str__(self):
return self.ipv4

View file

@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Machines" %}{% endblock %}
@ -33,54 +32,67 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
{% if machineform %}
{% bootstrap_form_errors machineform %}
{{ machineform.media }}
{% endif %}
{% if interfaceform %}
{% bootstrap_form_errors interfaceform %}
{{ interfaceform.media }}
{% endif %}
{% if domainform %}
{% bootstrap_form_errors domainform %}
{% endif %}
{% if iptypeform %}
{% bootstrap_form_errors iptypeform %}
{{ iptypeform.media }}
{% endif %}
{% if machinetypeform %}
{% bootstrap_form_errors machinetypeform %}
{{ machinetypeform.media }}
{% endif %}
{% if extensionform %}
{% bootstrap_form_errors extensionform %}
{% endif %}
{% if mxform %}
{% bootstrap_form_errors mxform %}
{{ mxform.media }}
{% endif %}
{% if nsform %}
{% bootstrap_form_errors nsform %}
{{ nsform.media }}
{% endif %}
{% if txtform %}
{% bootstrap_form_errors txtform %}
{{ txtform.media }}
{% endif %}
{% if dnameform %}
{% bootstrap_form_errors dnameform %}
{{ dnameform.media }}
{% endif %}
{% if srvform %}
{% bootstrap_form_errors srvform %}
{{ srvform.media }}
{% endif %}
{% if aliasform %}
{% bootstrap_form_errors aliasform %}
{{ aliasform.media }}
{% endif %}
{% if serviceform %}
{% bootstrap_form_errors serviceform %}
{{ serviceform.media }}
{% endif %}
{% if sshfpform %}
{% bootstrap_form_errors sshfpform %}
{% endif %}
{% if roleform %}
{% bootstrap_form_errors roleform %}
{{ roleform.media }}
{% endif %}
{% if vlanform %}
{% bootstrap_form_errors vlanform %}
{% endif %}
{% if nasform %}
{% bootstrap_form_errors nasform %}
{{ nasform.media }}
{% endif %}
{% if ipv6form %}
{% bootstrap_form_errors ipv6form %}
@ -90,15 +102,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
{% if machineform %}
<h3>{% trans "Machine" %}</h3>
{% massive_bootstrap_form machineform 'user' %}
{% bootstrap_form machineform %}
{% endif %}
{% if interfaceform %}
<h3>{% trans "Interface" %}</h3>
{% if i_mbf_param %}
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
{% else %}
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
{% endif %}
{% bootstrap_form interfaceform %}
{% endif %}
{% if domainform %}
<h3>{% trans "Domain" %}</h3>
@ -114,7 +122,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if extensionform %}
<h3>{% trans "Extension" %}</h3>
{% massive_bootstrap_form extensionform 'origin' %}
{% bootstrap_form extensionform %}
{% endif %}
{% if soaform %}
<h3>{% trans "SOA record" %}</h3>
@ -122,11 +130,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if mxform %}
<h3>{% trans "MX record" %}</h3>
{% massive_bootstrap_form mxform 'name' %}
{% bootstrap_form mxform %}
{% endif %}
{% if nsform %}
<h3>{% trans "NS record" %}</h3>
{% massive_bootstrap_form nsform 'ns' %}
{% bootstrap_form nsform %}
{% endif %}
{% if txtform %}
<h3>{% trans "TXT record" %}</h3>
@ -138,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if srvform %}
<h3>{% trans "SRV record" %}</h3>
{% massive_bootstrap_form srvform 'target' %}
{% bootstrap_form srvform %}
{% endif %}
{% if sshfpform %}
<h3>{% trans "SSHFP record" %}</h3>
@ -146,15 +154,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if aliasform %}
<h3>{% trans "Alias" %}</h3>
{% massive_bootstrap_form aliasform 'extension' %}
{% bootstrap_form aliasform %}
{% endif %}
{% if serviceform %}
<h3>{% trans "Service" %}</h3>
{% massive_bootstrap_form serviceform 'servers' %}
{% bootstrap_form serviceform %}
{% endif %}
{% if roleform %}
<h3>Role</h3>
{% massive_bootstrap_form roleform 'servers' %}
{% bootstrap_form roleform %}
{% endif %}
{% if vlanform %}
<h3>{% trans "VLAN" %}</h3>

View file

@ -29,6 +29,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from . import views
from . import views_autocomplete
urlpatterns = [
url(r"^new_machine/(?P<userid>[0-9]+)$", views.new_machine, name="new-machine"),
@ -153,4 +154,14 @@ urlpatterns = [
views.configure_ports,
name="port-config",
),
### Autocomplete Views
url(r'^vlan-autocomplete/$', views_autocomplete.VlanAutocomplete.as_view(), name='vlan-autocomplete',),
url(r'^interface-autocomplete/$', views_autocomplete.InterfaceAutocomplete.as_view(), name='interface-autocomplete',),
url(r'^machine-autocomplete/$', views_autocomplete.MachineAutocomplete.as_view(), name='machine-autocomplete',),
url(r'^machinetype-autocomplete/$', views_autocomplete.MachineTypeAutocomplete.as_view(), name='machinetype-autocomplete',),
url(r'^iptype-autocomplete/$', views_autocomplete.IpTypeAutocomplete.as_view(), name='iptype-autocomplete',),
url(r'^extension-autocomplete/$', views_autocomplete.ExtensionAutocomplete.as_view(), name='extension-autocomplete',),
url(r'^domain-autocomplete/$', views_autocomplete.DomainAutocomplete.as_view(), name='domain-autocomplete',),
url(r'^ouvertureportlist-autocomplete/$', views_autocomplete.OuverturePortListAutocomplete.as_view(), name='ouvertureportlist-autocomplete',),
url(r'^iplist-autocomplete/$', views_autocomplete.IpListAutocomplete.as_view(), name='iplist-autocomplete',),
]

View file

@ -124,91 +124,6 @@ from .models import (
)
def f_type_id(is_type_tt):
""" The id that will be used in HTML to store the value of the field
type. Depends on the fact that type is generate using typeahead or not
"""
return (
"id_Interface-machine_type_hidden"
if is_type_tt
else "id_Interface-machine_type"
)
def generate_ipv4_choices(form_obj):
""" Generate the parameter choices for the massive_bootstrap_form tag
"""
f_ipv4 = form_obj.fields["ipv4"]
used_mtype_id = []
choices = '{"":[{key:"",value:"' + _("Select a machine type first.") + '"}'
mtype_id = -1
for ip in f_ipv4.queryset.annotate(mtype_id=F("ip_type__machinetype__id")).order_by(
"mtype_id", "id"
):
if mtype_id != ip.mtype_id:
mtype_id = ip.mtype_id
used_mtype_id.append(mtype_id)
choices += '],"{t}":[{{key:"",value:"{v}"}},'.format(
t=mtype_id, v=f_ipv4.empty_label or '""'
)
choices += '{{key:{k},value:"{v}"}},'.format(k=ip.id, v=ip.ipv4)
for t in form_obj.fields["machine_type"].queryset.exclude(id__in=used_mtype_id):
choices += '], "' + str(t.id) + '": ['
choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},'
choices += "]}"
return choices
def generate_ipv4_engine(is_type_tt):
""" Generate the parameter engine for the massive_bootstrap_form tag
"""
return (
"new Bloodhound( {{"
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),'
"queryTokenizer: Bloodhound.tokenizers.whitespace,"
'local: choices_ipv4[ $( "#{machine_type_id}" ).val() ],'
"identify: function( obj ) {{ return obj.key; }}"
"}} )"
).format(machine_type_id=f_type_id(is_type_tt))
def generate_ipv4_match_func(is_type_tt):
""" Generate the parameter match_func for the massive_bootstrap_form tag
"""
return (
"function(q, sync) {{"
'if (q === "") {{'
'var first = choices_ipv4[$("#{machine_type_id}").val()].slice(0, 5);'
"first = first.map( function (obj) {{ return obj.key; }} );"
"sync(engine_ipv4.get(first));"
"}} else {{"
"engine_ipv4.search(q, sync);"
"}}"
"}}"
).format(machine_type_id=f_type_id(is_type_tt))
def generate_ipv4_mbf_param(form_obj, is_type_tt):
""" Generate all the parameters to use with the massive_bootstrap_form
tag """
i_choices = {"ipv4": generate_ipv4_choices(form_obj)}
i_engine = {"ipv4": generate_ipv4_engine(is_type_tt)}
i_match_func = {"ipv4": generate_ipv4_match_func(is_type_tt)}
i_update_on = {"ipv4": [f_type_id(is_type_tt)]}
i_gen_select = {"ipv4": False}
i_mbf_param = {
"choices": i_choices,
"engine": i_engine,
"match_func": i_match_func,
"update_on": i_update_on,
"gen_select": i_gen_select,
}
return i_mbf_param
@login_required
@can_create(Machine)
@can_edit(User)
@ -235,13 +150,11 @@ def new_machine(request, user, **_kwargs):
new_domain.save()
messages.success(request, _("The machine was created."))
return redirect(reverse("users:profil", kwargs={"userid": str(user.id)}))
i_mbf_param = generate_ipv4_mbf_param(interface, False)
return form(
{
"machineform": machine,
"interfaceform": interface,
"domainform": domain,
"i_mbf_param": i_mbf_param,
"action_name": _("Add"),
},
"machines/machine.html",
@ -281,13 +194,11 @@ def edit_interface(request, interface_instance, **_kwargs):
kwargs={"userid": str(interface_instance.machine.user.id)},
)
)
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
return form(
{
"machineform": machine_form,
"interfaceform": interface_form,
"domainform": domain_form,
"i_mbf_param": i_mbf_param,
"action_name": _("Edit"),
},
"machines/machine.html",
@ -332,12 +243,10 @@ def new_interface(request, machine, **_kwargs):
return redirect(
reverse("users:profil", kwargs={"userid": str(machine.user.id)})
)
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
return form(
{
"interfaceform": interface_form,
"domainform": domain_form,
"i_mbf_param": i_mbf_param,
"action_name": _("Add"),
},
"machines/machine.html",

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

View file

@ -29,6 +29,7 @@ each.
from django import forms
from django.forms import ModelForm, Form
from django.utils.translation import ugettext_lazy as _
from re2o.widgets import AutocompleteMultipleModelWidget
from .models import MultiopOption
@ -39,3 +40,8 @@ class EditMultiopOptionForm(ModelForm):
class Meta:
model = MultiopOption
fields = "__all__"
widgets = {
"enabled_dorm": AutocompleteMultipleModelWidget(
url="/topologie/dormitory-autocomplete",
),
}

View file

@ -1,2 +1,3 @@
django-bootstrap3==11.1.0
django-macaddress==1.6.0
django-autocomplete-light==3.8.1

View file

@ -30,6 +30,10 @@ from django.db.models import Q
from django import forms
from django.utils.translation import ugettext_lazy as _
from re2o.mixins import FormRevMixin
from re2o.widgets import (
AutocompleteModelWidget,
AutocompleteMultipleModelWidget
)
from .models import (
OptionalUser,
OptionalMachine,
@ -108,12 +112,19 @@ class EditOptionalTopologieForm(ModelForm):
"""Form used to edit the configuration of switches."""
automatic_provision_switchs = forms.ModelMultipleChoiceField(
Switch.objects.all(), required=False
Switch.objects.all(),
required=False,
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
)
class Meta:
model = OptionalTopologie
fields = "__all__"
widgets = {
"switchs_ip_type": AutocompleteModelWidget(
url="/machines/iptype-autocomplete",
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -168,6 +179,11 @@ class EditAssoOptionForm(ModelForm):
class Meta:
model = AssoOption
fields = "__all__"
widgets = {
"utilisateur_asso": AutocompleteModelWidget(
url="/users/user-autocomplete",
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -254,6 +270,11 @@ class MandateForm(ModelForm):
class Meta:
model = Mandate
fields = "__all__"
widgets = {
"president": AutocompleteModelWidget(
url="/users/user-autocomplete",
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -368,7 +389,9 @@ class RadiusKeyForm(FormRevMixin, ModelForm):
"""Form used to add and edit RADIUS keys."""
members = forms.ModelMultipleChoiceField(
queryset=Switch.objects.all(), required=False
queryset=Switch.objects.all(),
required=False,
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
)
class Meta:
@ -391,7 +414,11 @@ class RadiusKeyForm(FormRevMixin, ModelForm):
class SwitchManagementCredForm(FormRevMixin, ModelForm):
"""Form used to add and edit switch management credentials."""
members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False)
members = forms.ModelMultipleChoiceField(
Switch.objects.all(),
required=False,
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
)
class Meta:
model = SwitchManagementCred

View file

@ -24,19 +24,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Preferences" %}{% endblock %}
{% block content %}
{% bootstrap_form_errors options %}
{{ options.media }}
<h3>{% trans "Editing of preferences" %}</h3>
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
{% bootstrap_form options %}
{% if formset %}
{{ formset.management_form }}
{% for f in formset %}

View file

@ -25,20 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %}
{% load i18n %}
{% load massive_bootstrap_form %}
{% block title %}{% trans "Preferences" %}{% endblock %}
{% block content %}
{% if preferenceform %}
{% bootstrap_form_errors preferenceform %}
{{ preferenceform.media }}
{% endif %}
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if preferenceform %}
{% massive_bootstrap_form preferenceform 'members,president' %}
{% bootstrap_form preferenceform %}
{% endif %}
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
</form>

View file

@ -369,6 +369,15 @@ def can_view_all(*targets):
return acl_base_decorator("can_view_all", *targets, on_instance=False)
def can_list(*targets):
"""Decorator to check if an user can list a class of model.
It runs `acl_base_decorator` with the flag `on_instance=False` and the
method 'can_list'. See `acl_base_decorator` documentation for further
details.
"""
return acl_base_decorator("can_list", *targets, on_instance=False)
def can_view_app(*apps_name):
"""Decorator to check if an user can view the applications."""
for app_name in apps_name:

View file

@ -79,6 +79,8 @@ class AclMixin(object):
:can_view: Applied on an instance, return if the user can view the
instance
:can_view_all: Applied on a class, return if the user can view all
instances
:can_list: Applied on a class, return if the user can list all
instances"""
@classmethod
@ -208,6 +210,28 @@ class AclMixin(object):
(permission,),
)
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""Check if a user can list all instances of an object
Parameters:
user_request: User calling for this action
Returns:
Boolean: True if user_request has the right access to do it, else
false with reason for reject authorization
"""
permission = cls.get_modulename() + ".view_" + cls.get_classname()
can = user_request.has_perm(permission)
return (
can,
_("You don't have the right to list every %s object.") % cls.get_classname()
if not can
else None,
(permission,),
cls.objects.all() if can else None,
)
def can_view(self, user_request, *_args, **_kwargs):
"""Check if a user can view an instance of an object
@ -228,3 +252,4 @@ class AclMixin(object):
else None,
(permission,),
)

View file

@ -59,6 +59,8 @@ LOGIN_URL = "/login/" # The URL for login page
LOGIN_REDIRECT_URL = "/" # The URL for redirecting after login
# Application definition
# dal_legacy_static only needed for Django < 2.0 (https://django-autocomplete-light.readthedocs.io/en/master/install.html#django-versions-earlier-than-2-0)
EARLY_EXTERNAL_CONTRIB_APPS = ("dal", "dal_select2", "dal_legacy_static") # Need to be added before django.contrib.admin (https://django-autocomplete-light.readthedocs.io/en/master/install.html#configuration)
DJANGO_CONTRIB_APPS = (
"django.contrib.admin",
"django.contrib.auth",
@ -80,7 +82,7 @@ LOCAL_APPS = (
"logs",
)
INSTALLED_APPS = (
DJANGO_CONTRIB_APPS + EXTERNAL_CONTRIB_APPS + LOCAL_APPS + OPTIONNAL_APPS
EARLY_EXTERNAL_CONTRIB_APPS + DJANGO_CONTRIB_APPS + EXTERNAL_CONTRIB_APPS + LOCAL_APPS + OPTIONNAL_APPS
)
MIDDLEWARE_CLASSES = (
"django.contrib.sessions.middleware.SessionMiddleware",

View file

@ -141,6 +141,8 @@ def get_callback(tag_name, obj=None):
return acl_fct(obj.can_view_all, False)
if tag_name == "cannot_view_all":
return acl_fct(obj.can_view_all, True)
if tag_name == "can_list":
return acl_fct(obj.can_list, False)
if tag_name == "can_view_app":
return acl_fct(
lambda x: (
@ -296,6 +298,7 @@ def acl_change_filter(parser, token):
@register.tag("cannot_delete_all")
@register.tag("can_view_all")
@register.tag("cannot_view_all")
@register.tag("can_list")
def acl_model_filter(parser, token):
"""Generic definition of an acl templatetag for acl based on model"""

View file

@ -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)

View file

@ -32,7 +32,9 @@ from django.shortcuts import render
from django.template.context_processors import csrf
from django.conf import settings
from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_page
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.decorators import method_decorator
from dal import autocomplete
from preferences.models import (
Service,
@ -169,3 +171,30 @@ def handler500(request):
def handler404(request):
"""The handler view for a 404 error"""
return render(request, "errors/404.html", status=404)
class AutocompleteLoggedOutViewMixin(autocomplete.Select2QuerySetView):
obj_type = None # This MUST be overridden by child class
query_set = None
query_filter = "name__icontains" # Override this if necessary
def get_queryset(self):
can, reason, _permission, query_set = self.obj_type.can_list(self.request.user)
if query_set:
self.query_set = query_set
else:
self.query_set = self.obj_type.objects.none()
if hasattr(self, "filter_results"):
self.filter_results()
else:
if self.q:
self.query_set = self.query_set.filter(**{self.query_filter: self.q})
return self.query_set
class AutocompleteViewMixin(LoginRequiredMixin, AutocompleteLoggedOutViewMixin):
pass

77
re2o/widgets.py Normal file
View 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

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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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.

File diff suppressed because it is too large Load diff

View file

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

View file

@ -45,16 +45,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{# Preload JavaScript #}
{% bootstrap_javascript %}
<script src="/static/js/typeahead/typeahead.js"></script>
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
<script src="{% static 'js/collapse-from-url.js' %}"></script>
{% block custom_js %}{% endblock %}
{# Load CSS #}
{% bootstrap_css %}
<link href="{% static 'css/typeaheadjs.css' %}" rel="stylesheet">
<link href="{% static 'css/bootstrap-tokenfield.css' %}" rel="stylesheet">
<link href="{% static 'css/autocomplete.css' %}" rel="stylesheet">
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
{# load theme #}
{% if request.user.is_authenticated %}
@ -118,4 +115,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<script src="/static/js/main.js"></script>
</body>
</html>
</html>

View file

@ -29,6 +29,7 @@ from django.template.loader import render_to_string
from django.forms import ModelForm, Form
from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin
from re2o.widgets import AutocompleteModelWidget
from django.utils.translation import ugettext_lazy as _
from .models import Ticket, CommentTicket
@ -58,6 +59,11 @@ class EditTicketForm(FormRevMixin, ModelForm):
class Meta:
model = Ticket
fields = "__all__"
widgets = {
"user": AutocompleteModelWidget(
url="/users/user-autocomplete",
),
}
def __init__(self, *args, **kwargs):
super(EditTicketForm, self).__init__(*args, **kwargs)

View file

@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Ticket" %}{% endblock %}
@ -34,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h2>{% trans "Ticket opening" %}</h2>
{% bootstrap_form_errors ticketform %}
{{ ticketform.media }}
<form class="form" method="post">
{% csrf_token %}

View file

@ -38,6 +38,10 @@ from django.utils.translation import ugettext_lazy as _
from machines.models import Interface
from machines.forms import EditMachineForm, NewMachineForm
from re2o.mixins import FormRevMixin
from re2o.widgets import (
AutocompleteModelWidget,
AutocompleteMultipleModelWidget,
)
from .models import (
Port,
@ -62,6 +66,17 @@ class PortForm(FormRevMixin, ModelForm):
class Meta:
model = Port
fields = "__all__"
widgets = {
"switch": AutocompleteModelWidget(url="/topologie/switch-autocomplete"),
"room": AutocompleteModelWidget(url="/topologie/room-autocomplete"),
"machine_interface": AutocompleteModelWidget(
url="/machine/machine-autocomplete"
),
"related": AutocompleteModelWidget(url="/topologie/port-autocomplete"),
"custom_profile": AutocompleteModelWidget(
url="/topologie/portprofile-autocomplete"
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -154,7 +169,7 @@ class AddAccessPointForm(NewMachineForm):
class EditAccessPointForm(EditMachineForm):
"""Form used to edit access points."""
class Meta:
class Meta(EditMachineForm.Meta):
model = AccessPoint
fields = "__all__"
@ -162,9 +177,15 @@ class EditAccessPointForm(EditMachineForm):
class EditSwitchForm(EditMachineForm):
"""Form used to edit switches."""
class Meta:
class Meta(EditMachineForm.Meta):
model = Switch
fields = "__all__"
widgets = {
"switchbay": AutocompleteModelWidget(
url="/topologie/switchbay-autocomplete"
),
"user": AutocompleteModelWidget(url="/users/user-autocomplete"),
}
class NewSwitchForm(NewMachineForm):
@ -180,6 +201,9 @@ class EditRoomForm(FormRevMixin, ModelForm):
class Meta:
model = Room
fields = "__all__"
widgets = {
"building": AutocompleteModelWidget(url="/topologie/building-autocomplete")
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -196,7 +220,11 @@ class CreatePortsForm(forms.Form):
class EditModelSwitchForm(FormRevMixin, ModelForm):
"""Form used to edit switch models."""
members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False)
members = forms.ModelMultipleChoiceField(
Switch.objects.all(),
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
required=False,
)
class Meta:
model = ModelSwitch
@ -230,11 +258,18 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm):
class EditSwitchBayForm(FormRevMixin, ModelForm):
"""Form used to edit switch bays."""
members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False)
members = forms.ModelMultipleChoiceField(
Switch.objects.all(),
required=False,
widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"),
)
class Meta:
model = SwitchBay
fields = "__all__"
widgets = {
"building": AutocompleteModelWidget(url="/topologie/building-autocomplete")
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -279,6 +314,12 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
class Meta:
model = PortProfile
fields = "__all__"
widgets = {
"vlan_tagged": AutocompleteMultipleModelWidget(
url="/machines/vlan-autocomplete"
),
"vlan_untagged": AutocompleteModelWidget(url="/machines/vlan-autocomplete"),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)

View file

@ -731,6 +731,22 @@ class Dormitory(AclMixin, RevMixin, models.Model):
else:
return cache.get_or_set("multiple_dorms", cls.objects.count() > 1)
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list dormitory
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
return (
True,
None,
None,
cls.objects.all()
)
def __str__(self):
return self.name
@ -762,6 +778,22 @@ class Building(AclMixin, RevMixin, models.Model):
else:
return self.name
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list building
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
return (
True,
None,
None,
cls.objects.all()
)
@cached_property
def cached_name(self):
return self.get_name()
@ -944,6 +976,22 @@ class Room(AclMixin, RevMixin, models.Model):
verbose_name_plural = _("rooms")
unique_together = ("name", "building")
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list room
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
return (
True,
None,
None,
cls.objects.all()
)
def __str__(self):
return self.building.cached_name + " " + self.name

View file

@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Topology" %}{% endblock %}
@ -41,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
{% if topoform %}
<h3>{% trans "Specific settings for the switch" %}</h3>
{% massive_bootstrap_form topoform 'switch_interface' %}
{% bootstrap_form topoform %}
{% endif %}
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon='ok' button_class='btn-success' %}

View file

@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Topology" %}{% endblock %}
@ -37,11 +36,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
<form class="form" method="post">
{% csrf_token %}
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %}
{% bootstrap_form topoform %}
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
<br />
{{ topoform.media }}
{% endblock %}

View file

@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load i18n %}
{% block title %}{% trans "Topology" %}{% endblock %}
@ -32,9 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
{% if topoform %}
{% bootstrap_form_errors topoform %}
{{ topoform.media }}
{% endif %}
{% if machineform %}
{% bootstrap_form_errors machineform %}
{{ machineform.media }}
{% endif %}
{% if domainform %}
{% bootstrap_form_errors domainform %}
@ -46,11 +47,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
{% if topoform %}
<h3>{% blocktrans %}Specific settings for the {{ device }} object{% endblocktrans %}</h3>
{% massive_bootstrap_form topoform 'ipv4,machine' mbf_param=i_mbf_param%}
{% bootstrap_form topoform %}
{% endif %}
{% if machineform %}
<h3>{% blocktrans %}General settings for the machine linked to the {{ device }} object{% endblocktrans %}</h3>
{% massive_bootstrap_form machineform 'user' %}
{% bootstrap_form machineform %}
{% endif %}
{% if domainform %}
<h3>{% trans "DNS name" %}</h3>

View file

@ -28,6 +28,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from . import views
from . import views_autocomplete
urlpatterns = [
url(r"^$", views.index, name="index"),
@ -169,4 +170,12 @@ urlpatterns = [
views.del_module_on,
name="del-module-on",
),
### Autocomplete Views
url(r'^room-autocomplete/$', views_autocomplete.RoomAutocomplete.as_view(), name='room-autocomplete',),
url(r'^building-autocomplete/$', views_autocomplete.BuildingAutocomplete.as_view(), name='building-autocomplete',),
url(r'^dormitory-autocomplete/$', views_autocomplete.DormitoryAutocomplete.as_view(), name='dormitory-autocomplete',),
url(r'^switch-autocomplete/$', views_autocomplete.SwitchAutocomplete.as_view(), name='switch-autocomplete',),
url(r'^port-autocomplete/$', views_autocomplete.PortAutocomplete.as_view(), name='profile-autocomplete',),
url(r'^portprofile-autocomplete/$', views_autocomplete.PortProfileAutocomplete.as_view(), name='portprofile-autocomplete',),
url(r'^switchbay-autocomplete/$', views_autocomplete.SwitchBayAutocomplete.as_view(), name='switchbay-autocomplete',),
]

View file

@ -56,7 +56,6 @@ from machines.forms import (
AddInterfaceForm,
EditOptionVlanForm,
)
from machines.views import generate_ipv4_mbf_param
from machines.models import Interface, Service_link, Vlan
from preferences.models import AssoOption, GeneralOption
@ -560,13 +559,11 @@ def new_switch(request):
new_domain_obj.save()
messages.success(request, _("The switch was created."))
return redirect(reverse("topologie:index"))
i_mbf_param = generate_ipv4_mbf_param(interface, False)
return form(
{
"topoform": interface,
"machineform": switch,
"domainform": domain,
"i_mbf_param": i_mbf_param,
"device": _("switch"),
},
"topologie/topo_more.html",
@ -634,14 +631,12 @@ def edit_switch(request, switch, switchid):
new_domain_obj.save()
messages.success(request, _("The switch was edited."))
return redirect(reverse("topologie:index"))
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
return form(
{
"id_switch": switchid,
"topoform": interface_form,
"machineform": switch_form,
"domainform": domain_form,
"i_mbf_param": i_mbf_param,
"device": _("switch"),
},
"topologie/topo_more.html",
@ -686,13 +681,11 @@ def new_ap(request):
new_domain_obj.save()
messages.success(request, _("The access point was created."))
return redirect(reverse("topologie:index-ap"))
i_mbf_param = generate_ipv4_mbf_param(interface, False)
return form(
{
"topoform": interface,
"machineform": ap,
"domainform": domain,
"i_mbf_param": i_mbf_param,
"device": _("access point"),
},
"topologie/topo_more.html",
@ -737,13 +730,11 @@ def edit_ap(request, ap, **_kwargs):
new_domain_obj.save()
messages.success(request, _("The access point was edited."))
return redirect(reverse("topologie:index-ap"))
i_mbf_param = generate_ipv4_mbf_param(interface_form, False)
return form(
{
"topoform": interface_form,
"machineform": ap_form,
"domainform": domain_form,
"i_mbf_param": i_mbf_param,
"device": _("access point"),
},
"topologie/topo_more.html",

View 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

View file

@ -46,7 +46,10 @@ from os import walk, path
from django import forms
from django.forms import ModelForm, Form
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth.password_validation import validate_password, password_validators_help_text_html
from django.contrib.auth.password_validation import (
validate_password,
password_validators_help_text_html,
)
from django.core.validators import MinLengthValidator
from django.conf import settings
from django.utils import timezone
@ -61,6 +64,10 @@ from preferences.models import OptionalUser
from re2o.utils import remove_user_room
from re2o.base import get_input_formats_help_text
from re2o.mixins import FormRevMixin
from re2o.widgets import (
AutocompleteMultipleModelWidget,
AutocompleteModelWidget,
)
from re2o.field_permissions import FieldPermissionFormMixin
from preferences.models import GeneralOption
@ -156,14 +163,10 @@ class ServiceUserAdminForm(FormRevMixin, forms.ModelForm):
"""
password1 = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput,
max_length=255,
label=_("Password"), widget=forms.PasswordInput, max_length=255
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput,
max_length=255,
label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255
)
def __init__(self, *args, **kwargs):
@ -215,6 +218,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
DjangoForm : Inherit from basic django form
"""
selfpasswd = forms.CharField(
label=_("Current password"), max_length=255, widget=forms.PasswordInput
)
@ -222,12 +226,10 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
label=_("New password"),
max_length=255,
widget=forms.PasswordInput,
help_text=password_validators_help_text_html()
help_text=password_validators_help_text_html(),
)
passwd2 = forms.CharField(
label=_("New password confirmation"),
max_length=255,
widget=forms.PasswordInput,
label=_("New password confirmation"), max_length=255, widget=forms.PasswordInput
)
class Meta:
@ -280,7 +282,7 @@ class ResetPasswordForm(forms.Form):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
pseudo = forms.CharField(label=_("Username"), max_length=255)
email = forms.EmailField(max_length=255)
@ -292,13 +294,11 @@ class MassArchiveForm(forms.Form):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
date = forms.DateTimeField(help_text="%d/%m/%y")
full_archive = forms.BooleanField(
label=_(
"Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"
),
label=_("Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"),
initial=False,
required=False,
)
@ -323,7 +323,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -350,6 +350,16 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"telephone",
"room",
]
widgets = {
"school": AutocompleteModelWidget(url="/users/school-autocomplete"),
"room": AutocompleteModelWidget(
url="/topologie/room-autocomplete",
attrs={
"data-minimum-input-length": 3 # Only trigger autocompletion after 3 characters have been typed
},
),
"shell": AutocompleteModelWidget(url="/users/shell-autocomplete"),
}
force = forms.BooleanField(
label=_("Force the move?"), initial=False, required=False
@ -412,7 +422,8 @@ class AdherentCreationForm(AdherentForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
# Champ pour choisir si un lien est envoyé par mail pour le mot de passe
init_password_by_mail_info = _(
"If this options is set, you will receive a link to set"
@ -425,9 +436,7 @@ class AdherentCreationForm(AdherentForm):
)
init_password_by_mail = forms.BooleanField(
help_text=init_password_by_mail_info,
required=False,
initial=True
help_text=init_password_by_mail_info, required=False, initial=True
)
init_password_by_mail.label = _("Send password reset link by email.")
@ -438,7 +447,7 @@ class AdherentCreationForm(AdherentForm):
label=_("Password"),
widget=forms.PasswordInput,
max_length=255,
help_text=password_validators_help_text_html()
help_text=password_validators_help_text_html(),
)
password2 = forms.CharField(
required=False,
@ -461,7 +470,7 @@ class AdherentCreationForm(AdherentForm):
# Checkbox for GTU
gtu_check = forms.BooleanField(required=True)
class Meta:
class Meta(AdherentForm.Meta):
model = Adherent
fields = [
"name",
@ -528,8 +537,12 @@ class AdherentCreationForm(AdherentForm):
# Save the provided password in hashed format
user = super(AdherentForm, self).save(commit=False)
is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation")
set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail")
is_set_password_allowed = OptionalUser.get_cached_value(
"allow_set_password_during_user_creation"
)
set_passwd = is_set_password_allowed and not self.cleaned_data.get(
"init_password_by_mail"
)
if set_passwd:
user.set_password(self.cleaned_data["password1"])
@ -544,7 +557,7 @@ class AdherentEditForm(AdherentForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
super(AdherentEditForm, self).__init__(*args, **kwargs)
@ -556,7 +569,7 @@ class AdherentEditForm(AdherentForm):
if "shell" in self.fields:
self.fields["shell"].empty_label = _("Default shell")
class Meta:
class Meta(AdherentForm.Meta):
model = Adherent
fields = [
"name",
@ -580,7 +593,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -609,6 +622,11 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"shell",
"mailing",
]
widgets = {
"school": AutocompleteModelWidget(url="/users/school-autocomplete"),
"room": AutocompleteModelWidget(url="/topologie/room-autocomplete"),
"shell": AutocompleteModelWidget(url="/users/shell-autocomplete"),
}
def clean_telephone(self):
"""Clean telephone, check if telephone is made mandatory, and
@ -632,11 +650,19 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
class Meta:
model = Club
fields = ["administrators", "members"]
widgets = {
"administrators": AutocompleteMultipleModelWidget(
url="/users/adherent-autocomplete"
),
"members": AutocompleteMultipleModelWidget(
url="/users/adherent-autocomplete"
),
}
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -648,7 +674,7 @@ class PasswordForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
class Meta:
model = User
@ -665,7 +691,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
password = forms.CharField(
label=_("New password"),
@ -704,7 +730,7 @@ class EditServiceUserForm(ServiceUserForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
password = forms.CharField(
label=_("New password"),
@ -724,7 +750,7 @@ class StateForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
class Meta:
model = User
@ -742,7 +768,7 @@ class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
groups = forms.ModelMultipleChoiceField(
Group.objects.all(), widget=forms.CheckboxSelectMultiple, required=False
@ -764,7 +790,7 @@ class SchoolForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
class Meta:
model = School
@ -781,7 +807,7 @@ class ShellForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
class Meta:
model = ListShell
@ -800,7 +826,7 @@ class ListRightForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
permissions = forms.ModelMultipleChoiceField(
Permission.objects.all().select_related("content_type"),
@ -824,7 +850,7 @@ class NewListRightForm(ListRightForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
class Meta(ListRightForm.Meta):
fields = ("name", "unix_name", "gid", "critical", "permissions", "details")
@ -842,7 +868,7 @@ class DelListRightForm(Form):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
listrights = forms.ModelMultipleChoiceField(
queryset=ListRight.objects.none(),
@ -865,7 +891,7 @@ class DelSchoolForm(Form):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
schools = forms.ModelMultipleChoiceField(
queryset=School.objects.none(),
@ -887,7 +913,7 @@ class BanForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -906,7 +932,7 @@ class WhitelistForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -926,7 +952,7 @@ class EMailAddressForm(FormRevMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -947,7 +973,7 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
@ -971,7 +997,8 @@ class InitialRegisterForm(forms.Form):
Parameters:
DjangoForm : Inherit from basic django form
"""
"""
register_room = forms.BooleanField(required=False)
register_machine = forms.BooleanField(required=False)
@ -1052,8 +1079,8 @@ class ThemeForm(FormRevMixin, forms.Form):
theme = forms.ChoiceField(widget=forms.Select())
def __init__(self, *args, **kwargs):
_, _ ,themes = next(walk(path.join(settings.STATIC_ROOT, "css/themes")))
_, _, themes = next(walk(path.join(settings.STATIC_ROOT, "css/themes")))
if not themes:
themes = ["default.css"]
super(ThemeForm, self).__init__(*args, **kwargs)
self.fields['theme'].choices = [(theme, theme) for theme in themes]
self.fields["theme"].choices = [(theme, theme) for theme in themes]

View file

@ -2065,6 +2065,33 @@ class Adherent(User):
("users.add_user",),
)
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""Users can list adherent only if they are :
- Members of view acl,
- Club administrator.
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
can, _message, _group = Club.can_view_all(user_request)
if user_request.has_perm("users.view_user") or can:
return (
True,
None,
None,
cls.objects.all()
)
else:
return (
True,
_("You don't have the right to list all adherents."),
("users.view_user",),
cls.objects.none(),
)
def clean(self, *args, **kwargs):
"""Method, clean and validate the gpgfp value.
@ -2364,6 +2391,22 @@ class School(RevMixin, AclMixin, models.Model):
verbose_name = _("school")
verbose_name_plural = _("schools")
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list schools
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
return (
True,
None,
None,
cls.objects.all()
)
def __str__(self):
return self.name
@ -2487,6 +2530,22 @@ class ListShell(RevMixin, AclMixin, models.Model):
"""
return self.shell.split("/")[-1]
@classmethod
def can_list(cls, user_request, *_args, **_kwargs):
"""All users can list shells
:param user_request: The user who wants to view the list.
:return: True if the user can view the list and an explanation
message.
"""
return (
True,
None,
None,
cls.objects.all()
)
def __str__(self):
return self.shell

View file

@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% load i18n %}

View file

@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Users" %}{% endblock %}
@ -34,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% massive_bootstrap_form userform 'room,school,administrators,members' %}
{% bootstrap_form userform %}
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
</form>
{% if load_js_file %}
@ -48,5 +47,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<br/>
<br/>
<br/>
{{ userform.media }}
{% endblock %}

View file

@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Users" %}{% endblock %}

View file

@ -31,6 +31,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from . import views
from . import views_autocomplete
urlpatterns = [
url(r"^new_user/$", views.new_user, name="new-user"),
@ -128,4 +129,10 @@ urlpatterns = [
url(r"^index_clubs/$", views.index_clubs, name="index-clubs"),
url(r"^initial_register/$", views.initial_register, name="initial-register"),
url(r"^edit_theme/(?P<userid>[0-9]+)$", views.edit_theme, name="edit-theme"),
### Autocomplete Views
url(r'^user-autocomplete/$', views_autocomplete.UserAutocomplete.as_view(), name='user-autocomplete',),
url(r'^adherent-autocomplete/$', views_autocomplete.AdherentAutocomplete.as_view(), name='adherent-autocomplete',),
url(r'^club-autocomplete/$', views_autocomplete.ClubAutocomplete.as_view(), name='club-autocomplete',),
url(r'^school-autocomplete/$', views_autocomplete.SchoolAutocomplete.as_view(), name='school-autocomplete',),
url(r'^shell-autocomplete/$', views_autocomplete.ShellAutocomplete.as_view(), name='shell-autocomplete',),
]

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