From 61ce7e689ca5cddf39a42dc1df2ae069f0ba181d Mon Sep 17 00:00:00 2001 From: Czémán Arnold Date: Fri, 7 Jul 2017 21:03:16 +0200 Subject: [PATCH] dashboard, circle, network, request: upgrade and rework autocompletion, remove deprecated 'pattern' from url patterns --- circle/circle/settings/base.py | 12 ++++++------ circle/circle/urls.py | 31 ++++++++++++++----------------- circle/dashboard/autocomplete_light_registry.py | 105 --------------------------------------------------------------------------------------------------------- circle/dashboard/forms.py | 31 +++++++++++++++++-------------- circle/dashboard/static/dashboard/dashboard.js | 7 ------- circle/dashboard/static/dashboard/dashboard.less | 2 +- circle/dashboard/urls.py | 46 +++++++++++++++++++++++----------------------- circle/dashboard/views/__init__.py | 1 + circle/dashboard/views/autocomplete.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/network/urls.py | 7 +++---- circle/request/urls.py | 7 +++---- 11 files changed, 166 insertions(+), 181 deletions(-) delete mode 100644 circle/dashboard/autocomplete_light_registry.py create mode 100644 circle/dashboard/views/autocomplete.py diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index 4cd38a1..56bac76 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -183,7 +183,8 @@ PIPELINE = { "template.less", "dashboard/dashboard.less", "network/network.less", - "autocomplete_light/style.css", + "autocomplete_light/vendor/select2/dist/css/select2.css", + "autocomplete_light/select2.css", ), "output_filename": "all.css", } @@ -198,6 +199,10 @@ PIPELINE = { "jquery-simple-slider/js/simple-slider.js", "favico.js/favico.js", "datatables/media/js/jquery.dataTables.js", + "autocomplete_light/jquery.init.js", + "autocomplete_light/autocomplete.init.js", + "autocomplete_light/vendor/select2/dist/js/select2.js", + "autocomplete_light/select2.js", "dashboard/dashboard.js", "dashboard/activity.js", "dashboard/group-details.js", @@ -217,11 +222,6 @@ PIPELINE = { "js/network.js", "js/switch-port.js", "js/host-list.js", - "autocomplete_light/autocomplete.js", - "autocomplete_light/widget.js", - "autocomplete_light/addanother.js", - "autocomplete_light/text_widget.js", - "autocomplete_light/remote.js", ), "output_filename": "all.js", }, diff --git a/circle/circle/urls.py b/circle/circle/urls.py index 0130af6..a9a81bc 100644 --- a/circle/circle/urls.py +++ b/circle/circle/urls.py @@ -15,13 +15,16 @@ # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see . -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.views.generic import TemplateView from django.conf import settings from django.contrib import admin from django.core.urlresolvers import reverse from django.shortcuts import redirect +from django.contrib.auth.views import ( + password_reset_confirm, password_reset +) from circle.settings.base import get_env_variable @@ -33,9 +36,7 @@ from firewall.views import add_blacklist_item admin.autodiscover() -urlpatterns = patterns( - '', - +urlpatterns = [ url(r'^$', lambda x: redirect(reverse("dashboard.index"))), url(r'^network/', include('network.urls')), url(r'^blacklist-add/', add_blacklist_item), @@ -45,12 +46,11 @@ urlpatterns = patterns( # django/contrib/auth/urls.py (care when new version) url((r'^accounts/reset/(?P[0-9A-Za-z_\-]+)/' r'(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'), - 'django.contrib.auth.views.password_reset_confirm', + password_reset_confirm, {'set_password_form': CircleSetPasswordForm}, name='accounts.password_reset_confirm' ), - url(r'^accounts/password/reset/$', ("django.contrib.auth.views." - "password_reset"), + url(r'^accounts/password/reset/$', password_reset, {'password_reset_form': CirclePasswordResetForm}, name="accounts.password-reset", ), @@ -73,27 +73,24 @@ urlpatterns = patterns( name="info.support"), url(r'^info/resize-how-to/$', ResizeHelpView.as_view(), name="info.resize"), -) +] if 'rosetta' in settings.INSTALLED_APPS: - urlpatterns += patterns( - '', + urlpatterns += [ url(r'^rosetta/', include('rosetta.urls')), - ) + ] if settings.ADMIN_ENABLED: - urlpatterns += patterns( - '', + urlpatterns += [ url(r'^admin/', include(admin.site.urls)), - ) + ] if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': - urlpatterns += patterns( - '', + urlpatterns += [ (r'^saml2/', include('djangosaml2.urls')), - ) + ] handler500 = 'common.views.handler500' handler403 = 'common.views.handler403' diff --git a/circle/dashboard/autocomplete_light_registry.py b/circle/dashboard/autocomplete_light_registry.py deleted file mode 100644 index 47e5d8e..0000000 --- a/circle/dashboard/autocomplete_light_registry.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2014 Budapest University of Technology and Economics (BME IK) -# -# This file is part of CIRCLE Cloud. -# -# CIRCLE 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 3 of the License, or (at your option) -# any later version. -# -# CIRCLE 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 CIRCLE. If not, see . - -import autocomplete_light -from django.contrib.auth.models import User -from django.utils.html import escape -from django.utils.translation import ugettext as _ - -from .views import AclUpdateView -from .models import Profile - - -def highlight(field, q, none_wo_match=True): - """ - >>> highlight('Akkount Krokodil', 'kro', False) - u'<b>Akkount Krokodil' - """ - - if not field: - return None - try: - match = field.lower().index(q.lower()) - except ValueError: - match = None - if q and match is not None: - match_end = match + len(q) - return (escape(field[:match]) + - '' + - escape(field[match:match_end]) + - '' + escape(field[match_end:])) - elif none_wo_match: - return None - else: - return escape(field) - - -class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase): - search_fields = ( - ('first_name', 'last_name', 'username', 'email', 'profile__org_id'), - ('name', 'groupprofile__org_id'), - ) - choice_html_format = (u'%s%s') - - def choice_displayed_text(self, choice): - q = unicode(self.request.GET.get('q', '')) - name = highlight(unicode(choice), q, False) - if isinstance(choice, User): - extra_fields = [highlight(choice.get_full_name(), q, False), - highlight(choice.email, q)] - try: - extra_fields.append(highlight(choice.profile.org_id, q)) - except Profile.DoesNotExist: - pass - return '%s (%s)' % (name, ', '.join(f for f in extra_fields - if f)) - else: - return _('%s (group)') % name - - def choice_html(self, choice): - return self.choice_html_format % ( - self.choice_value(choice), self.choice_label(choice), - self.choice_displayed_text(choice)) - - def choices_for_request(self): - user = self.request.user - self.choices = (AclUpdateView.get_allowed_users(user), - AclUpdateView.get_allowed_groups(user)) - return super(AclUserGroupAutocomplete, self).choices_for_request() - - def autocomplete_html(self): - html = [] - - for choice in self.choices_for_request(): - html.append(self.choice_html(choice)) - - if not html: - html = self.empty_html_format % _('no matches found').capitalize() - - return self.autocomplete_html_format % ''.join(html) - - -class AclUserAutocomplete(AclUserGroupAutocomplete): - def choices_for_request(self): - user = self.request.user - self.choices = (AclUpdateView.get_allowed_users(user), ) - return super(AclUserGroupAutocomplete, self).choices_for_request() - - -autocomplete_light.register(AclUserGroupAutocomplete) -autocomplete_light.register(AclUserAutocomplete) diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index b3270a6..091a3d0 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -31,7 +31,7 @@ from django.contrib.auth.models import User, Group from django.core.validators import URLValidator from django.core.exceptions import PermissionDenied, ValidationError -import autocomplete_light +from dal import autocomplete from crispy_forms.helper import FormHelper from crispy_forms.layout import ( Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset @@ -43,7 +43,6 @@ from crispy_forms.bootstrap import FormActions from django import forms from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm from django.forms.widgets import TextInput, HiddenInput -from django.template import Context from django.template.loader import render_to_string from django.utils.html import escape, format_html from django.utils.safestring import mark_safe @@ -67,6 +66,7 @@ from .validators import domain_validator from dashboard.models import ConnectCommand, create_profile + LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) for l in LANGUAGES) @@ -1180,8 +1180,7 @@ class AnyTag(Div): fields += render_field(field, form, form_style, context, template_pack=template_pack) - return render_to_string(self.template, Context({'tag': self, - 'fields': fields})) + return render_to_string(self.template, {'tag': self, 'fields': fields}) class WorkingBaseInput(BaseInput): @@ -1334,27 +1333,31 @@ class UserEditForm(forms.ModelForm): class AclUserOrGroupAddForm(forms.Form): - name = forms.CharField(widget=autocomplete_light.TextWidget( - 'AclUserGroupAutocomplete', - attrs={'class': 'form-control', - 'placeholder': _("Name of group or user")})) + name = forms.CharField( + widget=autocomplete.ListSelect2( + url='autocomplete.acl.user-group', + attrs={'class': 'form-control', + 'data-html': 'true', + 'data-placeholder': _("Name of group or user")})) class TransferOwnershipForm(forms.Form): name = forms.CharField( - widget=autocomplete_light.TextWidget( - 'AclUserAutocomplete', + widget=autocomplete.ListSelect2( + url='autocomplete.acl.user', attrs={'class': 'form-control', - 'placeholder': _("Name of user")}), + 'data-html': 'true', + 'data-placeholder': _("Name of user")}), label=_("E-mail address or identifier of user")) class AddGroupMemberForm(forms.Form): new_member = forms.CharField( - widget=autocomplete_light.TextWidget( - 'AclUserAutocomplete', + widget=autocomplete.ListSelect2( + url='autocomplete.acl.user', attrs={'class': 'form-control', - 'placeholder': _("Name of user")}), + 'data-html': 'true', + 'data-placeholder': _("Name of user")}), label=_("E-mail address or identifier of user")) diff --git a/circle/dashboard/static/dashboard/dashboard.js b/circle/dashboard/static/dashboard/dashboard.js index f2c65df..4db99a6 100644 --- a/circle/dashboard/static/dashboard/dashboard.js +++ b/circle/dashboard/static/dashboard/dashboard.js @@ -508,13 +508,6 @@ $.ajaxSetup({ } }); -/* for autocomplete */ -$(function() { - yourlabs.TextWidget.prototype.getValue = function(choice) { - return choice.children().html(); - }; -}); - var tagsToReplace = { '&': '&', '<': '<', diff --git a/circle/dashboard/static/dashboard/dashboard.less b/circle/dashboard/static/dashboard/dashboard.less index 548ad83..3c0447f 100644 --- a/circle/dashboard/static/dashboard/dashboard.less +++ b/circle/dashboard/static/dashboard/dashboard.less @@ -1031,7 +1031,7 @@ textarea[name="new_members"] { font-weight: bold; } -.hilight .autocomplete-hl { +.select2-results__option--highlighted .autocomplete-hl { color: orange; } diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index ab0fa2e..d465676 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -16,9 +16,8 @@ # with CIRCLE. If not, see . from __future__ import absolute_import -from django.conf.urls import patterns, url, include +from django.conf.urls import url -import autocomplete_light from vm.models import Instance from .views import ( AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete, @@ -56,14 +55,13 @@ from .views import ( StorageDetail, DiskDetail, MessageList, MessageDetail, MessageCreate, MessageDelete, EnableTwoFactorView, DisableTwoFactorView, + AclUserGroupAutocomplete, AclUserAutocomplete, ) from .views.vm import vm_ops, vm_mass_ops from .views.node import node_ops -autocomplete_light.autodiscover() -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^$', IndexView.as_view(), name="dashboard.index"), url(r"^profile/list/$", UserList.as_view(), name="dashboard.views.user-list"), @@ -217,8 +215,6 @@ urlpatterns = patterns( ConnectCommandCreate.as_view(), name="dashboard.views.connect-command-create"), - url(r'^autocomplete/', include('autocomplete_light.urls')), - url(r"^store/list/$", StoreList.as_view(), name="dashboard.views.store-list"), url(r"^store/download/$", store_download, @@ -253,22 +249,26 @@ urlpatterns = patterns( name="dashboard.views.message-create"), url(r'^message/delete/(?P\d+)/$', MessageDelete.as_view(), name="dashboard.views.message-delete"), -) -urlpatterns += patterns( - '', - *(url(r'^vm/(?P\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname()) - for op, v in vm_ops.iteritems()) -) + url(r'^autocomplete/acl/user-group/$', + AclUserGroupAutocomplete.as_view(), + name='autocomplete.acl.user-group'), + url(r'^autocomplete/acl/user/$', + AclUserAutocomplete.as_view(), + name='autocomplete.acl.user'), +] -urlpatterns += patterns( - '', - *(url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname()) - for op, v in vm_mass_ops.iteritems()) -) +urlpatterns += [ + url(r'^vm/(?P\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname()) + for op, v in vm_ops.iteritems() +] -urlpatterns += patterns( - '', - *(url(r'^node/(?P\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname()) - for op, v in node_ops.iteritems()) -) +urlpatterns += [ + url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname()) + for op, v in vm_mass_ops.iteritems() +] + +urlpatterns += [ + url(r'^node/(?P\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname()) + for op, v in node_ops.iteritems() +] diff --git a/circle/dashboard/views/__init__.py b/circle/dashboard/views/__init__.py index f799d8d..13af4ac 100644 --- a/circle/dashboard/views/__init__.py +++ b/circle/dashboard/views/__init__.py @@ -15,3 +15,4 @@ from graph import * from storage import * from request import * from message import * +from autocomplete import * diff --git a/circle/dashboard/views/autocomplete.py b/circle/dashboard/views/autocomplete.py new file mode 100644 index 0000000..be6984b --- /dev/null +++ b/circle/dashboard/views/autocomplete.py @@ -0,0 +1,98 @@ +# Copyright 2014 Budapest University of Technology and Economics (BME IK) +# +# This file is part of CIRCLE Cloud. +# +# CIRCLE 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 3 of the License, or (at your option) +# any later version. +# +# CIRCLE 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 CIRCLE. If not, see . + +import json +from dal import autocomplete +from django.contrib.auth.models import User +from django.utils.html import escape +from django.utils.translation import ugettext as _ +from django.db.models import Q +from django.http import HttpResponse + +from ..views import AclUpdateView +from ..models import Profile + + +def highlight(field, q, none_wo_match=True): + """ + >>> highlight('Akkount Krokodil', 'kro', False) + u'<b>Akkount Krokodil' + """ + + if not field: + return None + try: + match = field.lower().index(q.lower()) + except ValueError: + match = None + if q and match is not None: + match_end = match + len(q) + return (escape(field[:match]) + + '' + + escape(field[match:match_end]) + + '' + escape(field[match_end:])) + elif none_wo_match: + return None + else: + return escape(field) + + +class AclUserAutocomplete(autocomplete.Select2ListView): + search_fields = ('first_name', 'last_name', 'username', + 'email', 'profile__org_id') + + def filter(self, qs, search_fields): + if self.q: + condition = Q() + for field in search_fields: + condition |= Q(**{field + '__icontains': unicode(self.q)}) + return list(qs.filter(condition)) + return [] + + def get_list(self): + users = AclUpdateView.get_allowed_users(self.request.user) + return self.filter(users, self.search_fields) + + def choice_displayed_text(self, choice): + q = unicode(self.request.GET.get('q', '')) + name = highlight(unicode(choice), q, False) + if isinstance(choice, User): + extra_fields = [highlight(choice.get_full_name(), q, False), + highlight(choice.email, q)] + try: + extra_fields.append(highlight(choice.profile.org_id, q)) + except Profile.DoesNotExist: + pass + return '%s (%s)' % (name, ', '.join(f for f in extra_fields + if f)) + else: + return _('%s (group)') % name + + def get(self, *args, **kwargs): + return HttpResponse(json.dumps({ + 'results': [dict(id=unicode(r), text=self.choice_displayed_text(r)) + for r in self.get_list()] + }), content_type="application/json") + + +class AclUserGroupAutocomplete(AclUserAutocomplete): + group_search_fields = ('name', 'groupprofile__org_id') + + def get_list(self): + groups = AclUpdateView.get_allowed_groups(self.request.user) + groups = self.filter(groups, self.group_search_fields) + return super(AclUserGroupAutocomplete, self).get_list() + groups diff --git a/circle/network/urls.py b/circle/network/urls.py index dc13f31..cdedbbf 100644 --- a/circle/network/urls.py +++ b/circle/network/urls.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see . -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( IndexView, HostList, HostDetail, HostCreate, HostDelete, @@ -33,8 +33,7 @@ from .views import ( VlanAclUpdateView ) -urlpatterns = patterns( - '', +urlpatterns = [ url('^$', IndexView.as_view(), name='network.index'), # blacklist url('^blacklist/$', BlacklistList.as_view(), @@ -135,4 +134,4 @@ urlpatterns = patterns( remove_switch_port_device, name='network.remove_switch_port_device'), url('^switchports/(?P\d+)/add/$', add_switch_port_device, name='network.add_switch_port_device'), -) +] diff --git a/circle/request/urls.py b/circle/request/urls.py index c69aa8d..7bf5cf5 100644 --- a/circle/request/urls.py +++ b/circle/request/urls.py @@ -16,7 +16,7 @@ # with CIRCLE. If not, see . from __future__ import absolute_import -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( RequestList, RequestDetail, RequestTypeList, @@ -26,8 +26,7 @@ from .views import ( LeaseTypeDelete, TemplateAccessTypeDelete, ResizeRequestView, ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^list/$', RequestList.as_view(), name="request.views.request-list"), url(r'^(?P\d+)/$', RequestDetail.as_view(), @@ -62,4 +61,4 @@ urlpatterns = patterns( name="request.views.request-resource"), url(r'resize/(?P\d+)/(?P\d+)/$', ResizeRequestView.as_view(), name="request.views.request-resize"), -) +] -- libgit2 0.26.0