From 73b835b6417cfc3af6c2e27e956ab8ab1a251522 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 7 Mar 2017 22:32:00 +0100 Subject: [PATCH] migrate to Django-Select2>5 (fixes #15604) --- setup.py | 3 +- src/authentic2/manager/fields.py | 218 ++++----------------- .../static/authentic2/manager/css/style.css | 34 +--- .../manager/templates/authentic2/manager/base.html | 18 -- .../manager/templates/authentic2/manager/form.html | 7 + src/authentic2/manager/urls.py | 2 +- src/authentic2/manager/views.py | 62 +++++- src/authentic2/manager/widgets.py | 124 ++++++++++++ src/authentic2/settings.py | 1 + 9 files changed, 236 insertions(+), 233 deletions(-) create mode 100644 src/authentic2/manager/widgets.py diff --git a/setup.py b/setup.py index b7dfdf3..540412d 100755 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ setup(name="authentic2", 'django-model-utils>=2.4', 'django-admin-tools>=0.6,<0.7', 'dnspython>=1.10', - 'Django-Select2>=4.3.0,<5', + 'Django-Select2>5', 'django-tables2>=1.0,<1.1', 'gadjo', 'django-import-export>=0.2.7', @@ -131,6 +131,7 @@ setup(name="authentic2", 'cryptography', 'XStatic-jQuery', 'XStatic-jquery-ui', + 'xstatic-select2', ], extras_require = { 'idp-openid': ['python-openid'], diff --git a/src/authentic2/manager/fields.py b/src/authentic2/manager/fields.py index 1224ddf..09d68ba 100644 --- a/src/authentic2/manager/fields.py +++ b/src/authentic2/manager/fields.py @@ -1,182 +1,44 @@ -from django_select2 import AutoModelSelect2Field, \ - AutoModelSelect2MultipleField, NO_ERR_RESP +from django import forms -from django.contrib.auth.models import Group, Permission -from django.contrib.auth import get_user_model -from django.db.models.query import Q +from . import widgets -from django_rbac.backends import DjangoRBACBackend -from django_rbac.utils import get_role_model, get_ou_model -from authentic2.models import Service - -from . import utils - - -class SecurityCheckMixin(object): - operations = ['change', 'add', 'view', 'delete'] - - @property - def perms(self): - model = self.queryset.model - app_label = model._meta.app_label - model_name = model._meta.model_name - return ['%s.%s_%s' % (app_label, perm, model_name) - for perm in self.operations] - - def security_check(self, request, *args, **kwargs): - model = self.queryset.model - app_label = model._meta.app_label - model_name = model._meta.model_name - return request.user.is_authenticated() \ - and request.user.has_perm_any(self.perms) - - def prepare_qs_params(self, request, search_term, search_fields): - '''Only search visible objects''' - ors = [] - ands = {} - for term in search_term.split(): - qs_params = super(SecurityCheckMixin, self).prepare_qs_params( - request, term, search_fields) - ors.extend(qs_params['or']) - ands.update(qs_params['and']) - model = self.queryset.model - app_label = model._meta.app_label - model_name = model._meta.model_name - rbac_backend = DjangoRBACBackend() - query = rbac_backend.filter_by_perm_query( - request.user, self.perms, self.queryset) - if query is False: - ands['id'] = -1 - elif query is True: - pass +class Select2Mixin(object): + def __init__(self, **kwargs): + if getattr(self.widget, 'queryset', None) is not None: + kwargs['queryset'] = self.widget.queryset + elif getattr(self.widget, 'model', None): + kwargs['queryset'] = self.widget.model.objects.all() else: - ors = [query & reduce(Q.__or__, ors)] - return {'or': ors, 'and': ands} - - -class SplitSearchTermMixin(object): - def prepare_qs_params(self, request, search_term, search_fields): - ors = [] - ands = {} - for term in search_term.split(): - qs_params = super(SplitSearchTermMixin, self).prepare_qs_params( - request, term, search_fields) - ors.extend(qs_params['or']) - ands.update(qs_params['and']) - return {'or': ors, 'and': ands} - - -class ChooseUserField(SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2Field): - queryset = get_user_model().objects - search_fields = [ - 'username__icontains', 'first_name__icontains', - 'last_name__icontains', 'email__icontains' - ] - - def get_results(self, request, term, page, context): - return (NO_ERR_RESP, False, utils.search_user(term)) - - -class ChooseUsersField(SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2MultipleField): - queryset = get_user_model().objects - search_fields = [ - 'username__icontains', 'first_name__icontains', - 'last_name__icontains', 'email__icontains' - ] - - def get_results(self, request, term, page, context): - return (NO_ERR_RESP, False, utils.search_user(term)) - - -class GroupsField(SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2MultipleField): - queryset = Group.objects - search_fields = [ - 'name__icontains', - ] - - -class PermissionChoices(SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2MultipleField): - queryset = Permission.objects - search_fields = [ - 'name__icontains', 'codename__icontains', - 'content_type__name__icontains' - ] - - def prepare_qs_params(self, request, search_term, search_fields): - ors = [] - ands = {} - for term in search_term.split(): - qs_params = super(PermissionChoices, self).prepare_qs_params( - request, term, search_fields) - ors.extend(qs_params['or']) - ands.update(qs_params['and']) - return {'or': ors, 'and': ands} - - def label_from_instance(self, instance): - return instance.name - - -class RoleLabelMixin(object): - def label_from_instance(self, obj): - label = unicode(obj) - if obj.service: - label = label + ' - ' + unicode(obj.service) - return label - - -class ChooseRoleField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2Field): - queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True) - search_fields = [ - 'name__icontains', - 'service__name__icontains', - ] - - -class ChooseRolesField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2MultipleField): - queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True) - search_fields = [ - 'name__icontains', - 'service__name__icontains', - ] - - -class ChooseRolesForChangeField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2MultipleField): - operations = ['change'] - queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True) - search_fields = [ - 'name__icontains', - 'service__name__icontains', - ] - -class ChooseOUField(SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2Field): - queryset = get_ou_model().objects - search_fields = [ - 'name__icontains', - ] - - -class ChooseServiceField(SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2Field): - queryset = Service.objects - search_fields = [ - 'name__icontains', - ] - - -class ChooseUserRoleField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin, - AutoModelSelect2Field): - operations = ['change'] - queryset = get_role_model().objects - search_fields = [ - 'name__icontains', - 'service__name__icontains', - ] + raise NotImplementedError + assert kwargs['queryset'] is not None + super(Select2Mixin, self).__init__(**kwargs) + + def __setattr__(self, key, value): + if key == 'queryset': + self.widget.queryset = value + super(Select2Mixin, self).__setattr__(key, value) + + +class Select2ModelChoiceField(Select2Mixin, forms.ModelChoiceField): + pass + + +class Select2ModelMultipleChoiceField(Select2Mixin, forms.ModelMultipleChoiceField): + pass + + +for key in dir(widgets): + cls = getattr(widgets, key) + if not isinstance(cls, type): + continue + if issubclass(cls, widgets.ModelSelect2MultipleWidget): + cls_name = key.replace('Widget', 'Field') + vars()[cls_name] = type(cls_name, (Select2ModelMultipleChoiceField,), { + 'widget': cls, + }) + elif issubclass(cls, widgets.ModelSelect2Widget): + cls_name = key.replace('Widget', 'Field') + vars()[cls_name] = type(cls_name, (Select2ModelChoiceField,), { + 'widget': cls, + }) diff --git a/src/authentic2/manager/static/authentic2/manager/css/style.css b/src/authentic2/manager/static/authentic2/manager/css/style.css index 8b8087c..dced300 100644 --- a/src/authentic2/manager/static/authentic2/manager/css/style.css +++ b/src/authentic2/manager/static/authentic2/manager/css/style.css @@ -56,11 +56,6 @@ div.role-info h3 { align-items: baseline; } -.manager-m2m-add-form .select2-container { - flex-grow: 1; - margin: 0 1em; -} - table.main th.name, table.main td.name, #user-table .link, #user-table .username, #user-table .email, #user-table .first_name, #user-table .last_name, #user-table .ou { text-align: left; @@ -205,11 +200,6 @@ form p input, form p textarea { width: calc(100% - 28px); } -form p .select2-container { - width: calc(100% - 10px); - margin-left: 10px; -} - form input[type="checkbox"] { width: auto; } @@ -226,15 +216,6 @@ form input[type="checkbox"] { margin-left: 50%; } -# Override jquery-ui default -.ui-widget select { - font-family: inherit; - font-size: inherit; -} - -div.ui-dialog form p select, form p select { - width: calc(100% - 28px); -} #id_generate_password_p label, #id_reset_password_at_next_login_p label, #id_send_mail_p label, #id_is_superuser_p label { @@ -259,15 +240,6 @@ span.errorlist span { width: 50%; } -.ui-widget-content { - color: #3c3c33; -} - -.ui-widget, .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { - font-family: "Bitstream Vera Sans","Verdana",sans-serif; - font-size: inherit; -} - .waiting { position: fixed; top: 0; @@ -287,3 +259,9 @@ span.errorlist span { margin-right: 1ex; } .role-inheritance { margin: 1em 0px; } + +/* Select2 styling */ + +.select2-container { + width: 100% !important; +} diff --git a/src/authentic2/manager/templates/authentic2/manager/base.html b/src/authentic2/manager/templates/authentic2/manager/base.html index d32a140..cf803ba 100644 --- a/src/authentic2/manager/templates/authentic2/manager/base.html +++ b/src/authentic2/manager/templates/authentic2/manager/base.html @@ -1,6 +1,5 @@ {% extends "gadjo/base.html" %} {% load i18n staticfiles %} -{% load django_select2_tags %} {% load firstof from future %} {% block page-title %}{% firstof site_title "Authentic2" %}{% endblock %} @@ -19,25 +18,8 @@

{% block page_title %}{% endblock %}

{% endblock %} -{% block css %} - {{ block.super }} - -{% endblock %} - {% block extrascripts %} {{ block.super }} - {% if debug %} - - {% else %} - - {% endif %} - - - - {% import_django_select2_js %} - {% import_django_select2_css %} - - diff --git a/src/authentic2/manager/templates/authentic2/manager/form.html b/src/authentic2/manager/templates/authentic2/manager/form.html index 2d06bba..a45e944 100644 --- a/src/authentic2/manager/templates/authentic2/manager/form.html +++ b/src/authentic2/manager/templates/authentic2/manager/form.html @@ -73,4 +73,11 @@ {% endif %} {% endblock %} + {% endblock %} diff --git a/src/authentic2/manager/urls.py b/src/authentic2/manager/urls.py index 9779680..c1f24ad 100644 --- a/src/authentic2/manager/urls.py +++ b/src/authentic2/manager/urls.py @@ -92,5 +92,5 @@ urlpatterns += patterns('', url(r'^jsi18n/$', javascript_catalog, {'packages': ('authentic2.manager',)}, name='a2-manager-javascript-catalog'), - url(r'^', include('django_select2.urls')), + url(r'^select2.json$', views.select2, name='django_select2-json'), ) diff --git a/src/authentic2/manager/views.py b/src/authentic2/manager/views.py index 95c6318..cdba490 100644 --- a/src/authentic2/manager/views.py +++ b/src/authentic2/manager/views.py @@ -2,18 +2,21 @@ import json from django.core.exceptions import PermissionDenied from django.views.generic.base import ContextMixin -from django.views.generic import TemplateView, FormView, UpdateView, \ - CreateView, DeleteView, TemplateView +from django.views.generic.edit import FormMixinBase +from django.views.generic import (FormView, UpdateView, CreateView, DeleteView, TemplateView) from django.views.generic.detail import SingleObjectMixin from django.http import HttpResponse, Http404 from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy from django.contrib.messages.views import SuccessMessageMixin +from django.forms import MediaDefiningClass from django_tables2 import SingleTableView, SingleTableMixin +from django_select2.views import AutoResponseView + from django_rbac.utils import get_ou_model from authentic2.forms import modelform_factory @@ -23,6 +26,36 @@ from authentic2.decorators import json as json_view from . import app_settings +class MediaMixinBase(MediaDefiningClass, FormMixinBase): + pass + + +class MediaMixin(object): + __metaclass__ = MediaMixinBase + + class Media: + js = ( + reverse_lazy('a2-manager-javascript-catalog'), + 'xstatic/jquery.js', + 'jquery/js/jquery.form.js', + 'admin/js/urlify.js', + 'authentic2/js/purl.js', + 'authentic2/manager/js/manager.js', + ) + css = { + 'all': ( + 'authentic2/manager/css/style.css', + ) + } + + def get_context_data(self, **kwargs): + kwargs['media'] = self.media + ctx = super(MediaMixin, self).get_context_data(**kwargs) + if 'form' in ctx: + ctx['media'] += ctx['form'].media + return ctx + + class PermissionMixin(object): permissions = None @@ -266,7 +299,7 @@ class ExportMixin(object): return response -class ModelNameMixin(object): +class ModelNameMixin(MediaMixin): def get_model_name(self): return self.model._meta.verbose_name @@ -281,15 +314,18 @@ class BaseTableView(FormatsContextData, ModelNameMixin, PermissionMixin, TableQuerysetMixin, SingleTableView): pass -class SubTableViewMixin(FormatsContextData, PermissionMixin, + +class SubTableViewMixin(FormatsContextData, ModelNameMixin, PermissionMixin, SearchFormMixin, FilterTableQuerysetByPermMixin, TableQuerysetMixin, SingleObjectMixin, SingleTableMixin, ContextMixin): pass + class SimpleSubTableView(SubTableViewMixin, TemplateView): pass + class BaseSubTableView(TitleMixin, SubTableViewMixin, FormView): success_url = '.' @@ -308,7 +344,7 @@ class BaseDeleteView(TitleMixin, ModelNameMixin, PermissionMixin, return '../../' -class ModelFormView(object): +class ModelFormView(MediaMixin): fields = None form_class = None @@ -349,7 +385,7 @@ class BaseEditView(SuccessMessageMixin, TitleMixin, ModelNameMixin, PermissionMi return '..' -class HomepageView(PermissionMixin, TemplateView): +class HomepageView(PermissionMixin, MediaMixin, TemplateView): template_name = 'authentic2/manager/homepage.html' permissions = ['a2_rbac.view_role', 'a2_rbac.view_organizationalunit', 'auth.view_group', 'custom_user.view_user'] @@ -406,3 +442,15 @@ class HideOUColumnMixin(object): if exclude_ou: kwargs['exclude'] = ['ou'] return super(HideOUColumnMixin, self).get_table(**kwargs) + + +class Select2View(AutoResponseView): + def get_widget_or_404(self): + widget = super(Select2View, self).get_widget_or_404() + widget.view = self + if hasattr(widget, 'security_check'): + if not widget.security_check(self.request, *self.args, **self.kwargs): + raise PermissionDenied + return widget + +select2 = Select2View.as_view() diff --git a/src/authentic2/manager/widgets.py b/src/authentic2/manager/widgets.py new file mode 100644 index 0000000..e4debb7 --- /dev/null +++ b/src/authentic2/manager/widgets.py @@ -0,0 +1,124 @@ +from django_select2.forms import ModelSelect2Widget, ModelSelect2MultipleWidget + +from django.contrib.auth import get_user_model + +from django_rbac.backends import DjangoRBACBackend +from django_rbac.utils import get_role_model, get_ou_model + +from authentic2.models import Service + +from . import utils + + +class SplitTermMixin(object): + def filter_queryset(self, term, queryset=None): + if queryset is not None: + qs = queryset.none() + else: + qs = self.get_queryset().none() + for term in term.split(): + qs |= super(SplitTermMixin, self).filter_queryset(term, queryset=queryset) + return qs + + +class SecurityCheckMixin(SplitTermMixin): + operations = ['change', 'add', 'view', 'delete'] + + @property + def perms(self): + model = self.queryset.model + app_label = model._meta.app_label + model_name = model._meta.model_name + return ['%s.%s_%s' % (app_label, perm, model_name) + for perm in self.operations] + + def security_check(self, request, *args, **kwargs): + return request.user.is_authenticated() \ + and request.user.has_perm_any(self.perms) + + def filter_queryset(self, term, queryset=None): + '''Only search visible objects''' + if not hasattr(self, 'view'): + return [] + request = self.view.request + qs = super(SecurityCheckMixin, self).filter_queryset(term, queryset=queryset) + rbac_backend = DjangoRBACBackend() + return rbac_backend.filter_by_perm(request.user, self.perms, qs) + + +class RoleLabelMixin(object): + def label_from_instance(self, obj): + label = unicode(obj) + if obj.service: + label = label + ' - ' + unicode(obj.service) + return label + + +class ChooseUserWidget(SecurityCheckMixin, ModelSelect2Widget): + model = get_user_model() + search_fields = [ + 'username__icontains', 'first_name__icontains', + 'last_name__icontains', 'email__icontains' + ] + + def label_from_instance(self, user): + return utils.label_from_user(user) + + +class ChooseUsersWidget(SecurityCheckMixin, ModelSelect2MultipleWidget): + model = get_user_model() + search_fields = [ + 'username__icontains', 'first_name__icontains', + 'last_name__icontains', 'email__icontains' + ] + + def label_from_instance(self, user): + return utils.label_from_user(user) + + +class ChooseRoleWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2Widget): + queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True) + search_fields = [ + 'name__icontains', + 'service__name__icontains', + ] + + +class ChooseRolesWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2MultipleWidget): + queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True) + search_fields = [ + 'name__icontains', + 'service__name__icontains', + ] + + +class ChooseRolesForChangeWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2MultipleWidget): + operations = ['change'] + queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True) + search_fields = [ + 'name__icontains', + 'service__name__icontains', + ] + + +class ChooseOUWidget(SecurityCheckMixin, ModelSelect2Widget): + model = get_ou_model() + search_fields = [ + 'name__icontains', + ] + + +class ChooseServiceWidget(SecurityCheckMixin, ModelSelect2Widget): + model = Service + search_fields = [ + 'name__icontains', + ] + + +class ChooseUserRoleWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2Widget): + operations = ['change'] + model = get_role_model() + search_fields = [ + 'name__icontains', + 'service__name__icontains', + ] diff --git a/src/authentic2/settings.py b/src/authentic2/settings.py index 583f587..91c0440 100644 --- a/src/authentic2/settings.py +++ b/src/authentic2/settings.py @@ -121,6 +121,7 @@ INSTALLED_APPS = ( 'rest_framework', 'xstatic.pkg.jquery', 'xstatic.pkg.jquery_ui', + 'xstatic.pkg.select2', ) INSTALLED_APPS = tuple(plugins.register_plugins_installed_apps(INSTALLED_APPS)) -- 2.1.4