diff --git a/circle/dashboard/autocomplete_light_registry.py b/circle/dashboard/autocomplete_light_registry.py index 96ce13d..8ff9d94 100644 --- a/circle/dashboard/autocomplete_light_registry.py +++ b/circle/dashboard/autocomplete_light_registry.py @@ -1,33 +1,88 @@ 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 -class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase): +def highlight(field, q, none_wo_match=True): + """ + >>> highlight('<b>Akkount Krokodil', 'kro', False) + u'<b>Akkount <span class="autocomplete-hl">Kro</span>kodil' + """ + + 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]) + + '<span class="autocomplete-hl">' + + escape(field[match:match_end]) + + '</span>' + 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'), + ('first_name', 'last_name', 'username', 'email', 'profile__org_id'), + ('name', 'groupprofile__org_id'), ) - autocomplete_js_attributes = {'placeholder': _("Name of group or user")} - choice_html_format = u'<span data-value="%s"><span>%s</span> %s</span>' + choice_html_format = (u'<span data-value="%s"><span style="display:none"' + u'>%s</span>%s</span>') - def choice_html(self, choice): - try: - name = choice.get_full_name() - except AttributeError: - name = _('group') - if name: - name = u'(%s)' % name + 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), name) + 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(AclUserAutocomplete, self).choices_for_request() + 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 94fe926..1f623e8 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -1055,9 +1055,29 @@ class UserCreationForm(OrgUserCreationForm): return user -class AclUserAddForm(forms.Form): +class AclUserOrGroupAddForm(forms.Form): name = forms.CharField(widget=autocomplete_light.TextWidget( - 'AclUserAutocomplete', attrs={'class': 'form-control'})) + 'AclUserGroupAutocomplete', + autocomplete_js_attributes={'placeholder': _("Name of group or user")}, + attrs={'class': 'form-control'})) + + +class TransferOwnershipForm(forms.Form): + name = forms.CharField( + widget=autocomplete_light.TextWidget( + 'AclUserAutocomplete', + autocomplete_js_attributes={"placeholder": _("Name of user")}, + attrs={'class': 'form-control'}), + label=_("E-mail address or identifier of user")) + + +class AddGroupMemberForm(forms.Form): + new_member = forms.CharField( + widget=autocomplete_light.TextWidget( + 'AclUserAutocomplete', + autocomplete_js_attributes={"placeholder": _("Name of user")}, + attrs={'class': 'form-control'}), + label=_("E-mail address or identifier of user")) class UserKeyForm(forms.ModelForm): diff --git a/circle/dashboard/static/dashboard/dashboard.css b/circle/dashboard/static/dashboard/dashboard.css index 213fa0b..be9f243 100644 --- a/circle/dashboard/static/dashboard/dashboard.css +++ b/circle/dashboard/static/dashboard/dashboard.css @@ -591,11 +591,15 @@ footer a, footer a:hover, footer a:visited { width: 100px; } +#group-detail-user-table tr:last-child td:nth-child(2) { + text-align: left; +} + #group-detail-perm-header { margin-top: 25px; } -textarea[name="list-new-namelist"] { +textarea[name="new_members"] { max-width: 500px; min-height: 80px; margin-bottom: 10px; @@ -960,3 +964,12 @@ textarea[name="list-new-namelist"] { #vm-activity-state { margin-bottom: 15px; } + +.autocomplete-hl { + color: #b20000; + font-weight: bold; +} + +.hilight .autocomplete-hl { + color: orange; +} diff --git a/circle/dashboard/templates/dashboard/_modal.html b/circle/dashboard/templates/dashboard/_modal.html index 08e5d91..cc5a5b9 100644 --- a/circle/dashboard/templates/dashboard/_modal.html +++ b/circle/dashboard/templates/dashboard/_modal.html @@ -2,6 +2,12 @@ <div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog"> <div class="modal-dialog"> <div class="modal-content"> + {% if box_title and ajax_title %} + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">{{ box_title }}</h4> + </div> + {% endif %} <div class="modal-body"> {% if template %} {% include template %} diff --git a/circle/dashboard/templates/dashboard/group-detail.html b/circle/dashboard/templates/dashboard/group-detail.html index 794691c..f683ad8 100644 --- a/circle/dashboard/templates/dashboard/group-detail.html +++ b/circle/dashboard/templates/dashboard/group-detail.html @@ -89,13 +89,12 @@ <tr> <td><i class="fa fa-plus"></i></td> <td colspan="2"> - <input type="text" class="form-control" name="list-new-name" - placeholder="{% trans "Name of user" %}"> + {{addmemberform.new_member}} </td> </tr> </tbody> </table> - <textarea name="list-new-namelist" class="form-control" + <textarea name="new_members" class="form-control" placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea> <div class="form-actions"> <button type="submit" class="btn btn-success">{% trans "Save" %}</button> diff --git a/circle/dashboard/templates/dashboard/vm-detail/access.html b/circle/dashboard/templates/dashboard/vm-detail/access.html index 3f35486..b716552 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/access.html +++ b/circle/dashboard/templates/dashboard/vm-detail/access.html @@ -9,8 +9,10 @@ {% endblocktrans %} {% endif %} {% if user == instance.owner or user.is_superuser %} + <span class="operation-wrapper"> <a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}" - class="btn btn-link">{% trans "Transfer ownership..." %}</a> + class="btn btn-link operation">{% trans "Transfer ownership..." %}</a> + </span> {% endif %} </p> <h3>{% trans "Permissions"|capfirst %}</h3> diff --git a/circle/dashboard/templates/dashboard/vm-detail/tx-owner.html b/circle/dashboard/templates/dashboard/vm-detail/tx-owner.html index cd3174c..5c0a54c 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/tx-owner.html +++ b/circle/dashboard/templates/dashboard/vm-detail/tx-owner.html @@ -1,25 +1,16 @@ -{% extends "dashboard/base.html" %} {% load i18n %} -{% block content %} - <div class="body-content"> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="no-margin"> - {% trans "Transfer ownership" %} - </h3> - </div> - <div class="panel-body"> - <div class="pull-right"> - <form action="" method="POST"> - {% csrf_token %} - <label> - {% trans "E-mail address or identifier of user" %}: - <input name="name"> - </label> - <input type="submit"> - </form> +<div class="pull-right"> + <form action="{% url "dashboard.views.vm-transfer-ownership" pk=instance.pk %}" method="POST" style="max-width: 400px;"> + {% csrf_token %} + <label> + {{ form.name.label }} + </label> + <div class="input-group"> + {{form.name}} + <div class="input-group-btn"> + <input type="submit" value="{% trans "Save" %}" class="btn btn-primary"> </div> - </div> - </div> -{% endblock %} + </div> + </form> +</div> diff --git a/circle/dashboard/tests/test_views.py b/circle/dashboard/tests/test_views.py index 895853d..e15e546 100644 --- a/circle/dashboard/tests/test_views.py +++ b/circle/dashboard/tests/test_views.py @@ -1134,7 +1134,7 @@ class GroupDetailTest(LoginMixin, TestCase): c = Client() user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + - str(self.g1.pk) + '/', {'list-new-name': 'user3'}) + str(self.g1.pk) + '/', {'new_member': 'user3'}) self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(response.status_code, 302) @@ -1144,7 +1144,7 @@ class GroupDetailTest(LoginMixin, TestCase): self.login(c, 'user3') user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + - str(self.g1.pk) + '/', {'list-new-name': 'user3'}) + str(self.g1.pk) + '/', {'new_member': 'user3'}) self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(response.status_code, 403) @@ -1153,7 +1153,7 @@ class GroupDetailTest(LoginMixin, TestCase): self.login(c, 'superuser') user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + - str(self.g1.pk) + '/', {'list-new-name': 'user3'}) + str(self.g1.pk) + '/', {'new_member': 'user3'}) self.assertEqual(user_in_group + 1, self.g1.user_set.count()) self.assertEqual(response.status_code, 302) @@ -1162,7 +1162,7 @@ class GroupDetailTest(LoginMixin, TestCase): self.login(c, 'user0') user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + - str(self.g1.pk) + '/', {'list-new-name': 'user3'}) + str(self.g1.pk) + '/', {'new_member': 'user3'}) self.assertEqual(user_in_group + 1, self.g1.user_set.count()) self.assertEqual(response.status_code, 302) @@ -1172,7 +1172,7 @@ class GroupDetailTest(LoginMixin, TestCase): user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + str(self.g1.pk) + '/', - {'list-new-namelist': 'user1\r\nuser2'}) + {'new_members': 'user1\r\nuser2'}) self.assertEqual(user_in_group + 2, self.g1.user_set.count()) self.assertEqual(response.status_code, 302) @@ -1182,7 +1182,7 @@ class GroupDetailTest(LoginMixin, TestCase): user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + str(self.g1.pk) + '/', - {'list-new-namelist': 'user1\r\nnoname\r\nuser2'}) + {'new_members': 'user1\r\nnoname\r\nuser2'}) self.assertEqual(user_in_group + 2, self.g1.user_set.count()) self.assertEqual(response.status_code, 302) @@ -1192,7 +1192,7 @@ class GroupDetailTest(LoginMixin, TestCase): user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + str(self.g1.pk) + '/', - {'list-new-namelist': 'user1\r\nuser2'}) + {'new_members': 'user1\r\nuser2'}) self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(response.status_code, 403) @@ -1201,7 +1201,7 @@ class GroupDetailTest(LoginMixin, TestCase): user_in_group = self.g1.user_set.count() response = c.post('/dashboard/group/' + str(self.g1.pk) + '/', - {'list-new-namelist': 'user1\r\nuser2'}) + {'new_members': 'user1\r\nuser2'}) self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(response.status_code, 302) @@ -1471,8 +1471,8 @@ class TransferOwnershipViewTest(LoginMixin, TestCase): c2 = self.u2.notification_set.count() c = Client() self.login(c, 'user2') - response = c.post('/dashboard/vm/1/tx/') - assert response.status_code == 400 + response = c.post('/dashboard/vm/1/tx/', {'name': 'userx'}) + assert response.status_code == 403 self.assertEqual(self.u2.notification_set.count(), c2) def test_owned_offer(self): diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index c1afcf1..c28052c 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -70,9 +70,10 @@ from .forms import ( UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm, VmSaveForm, UserKeyForm, VmRenewForm, VmStateChangeForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, - TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm, + TraitsForm, RawDataForm, GroupPermissionForm, AclUserOrGroupAddForm, VmResourcesForm, VmAddInterfaceForm, VmListSearchForm, - TemplateListSearchForm, ConnectCommandForm + TemplateListSearchForm, ConnectCommandForm, + TransferOwnershipForm, AddGroupMemberForm ) from .tables import ( @@ -390,7 +391,7 @@ class VmDetailView(CheckedDetailView): ).all() context['acl'] = AclUpdateView.get_acl_data( instance, self.request.user, 'dashboard.views.vm-acl') - context['aclform'] = AclUserAddForm() + context['aclform'] = AclUserOrGroupAddForm() context['os_type_icon'] = instance.os_type.replace("unknown", "question") # ipv6 infos @@ -1282,7 +1283,8 @@ class GroupDetailView(CheckedDetailView): context['acl'] = AclUpdateView.get_acl_data( self.object.profile, self.request.user, 'dashboard.views.group-acl') - context['aclform'] = AclUserAddForm() + context['aclform'] = AclUserOrGroupAddForm() + context['addmemberform'] = AddGroupMemberForm() context['group_profile_form'] = GroupProfileUpdate.get_form_object( self.request, self.object.profile) @@ -1299,17 +1301,15 @@ class GroupDetailView(CheckedDetailView): if request.POST.get('new_name'): return self.__set_name(request) - if request.POST.get('list-new-name'): + if request.POST.get('new_member'): return self.__add_user(request) - if request.POST.get('list-new-namelist'): + if request.POST.get('new_members'): return self.__add_list(request) - if (request.POST.get('list-new-name') is not None) and \ - (request.POST.get('list-new-namelist') is not None): - return redirect(reverse_lazy("dashboard.views.group-detail", - kwargs={'pk': self.get_object().pk})) + return redirect(reverse_lazy("dashboard.views.group-detail", + kwargs={'pk': self.get_object().pk})) def __add_user(self, request): - name = request.POST['list-new-name'] + name = request.POST['new_member'] self.__add_username(request, name) return redirect(reverse_lazy("dashboard.views.group-detail", kwargs={'pk': self.object.pk})) @@ -1328,9 +1328,7 @@ class GroupDetailView(CheckedDetailView): messages.warning(request, _('User "%s" not found.') % name) def __add_list(self, request): - if not self.get_has_level()(request.user, 'operator'): - raise PermissionDenied() - userlist = request.POST.get('list-new-namelist').split('\r\n') + userlist = request.POST.get('new_members').split('\r\n') for line in userlist: self.__add_username(request, line) return redirect(reverse_lazy("dashboard.views.group-detail", @@ -1717,7 +1715,7 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): obj, self.request.user, 'dashboard.views.template-acl') context['disks'] = obj.disks.all() context['is_owner'] = obj.has_level(self.request.user, 'owner') - context['aclform'] = AclUserAddForm() + context['aclform'] = AclUserOrGroupAddForm() return context def get_success_url(self): @@ -2768,11 +2766,30 @@ class FavouriteView(TemplateView): return HttpResponse("Added.") -class TransferOwnershipView(LoginRequiredMixin, DetailView): +class TransferOwnershipView(CheckedDetailView, DetailView): model = Instance - template_name = 'dashboard/vm-detail/tx-owner.html' + + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/_modal.html'] + else: + return ['dashboard/nojs-wrapper.html'] + + def get_context_data(self, *args, **kwargs): + context = super(TransferOwnershipView, self).get_context_data( + *args, **kwargs) + context['form'] = TransferOwnershipForm() + context.update({ + 'box_title': _("Transfer ownership"), + 'ajax_title': True, + 'template': "dashboard/vm-detail/tx-owner.html", + }) + return context def post(self, request, *args, **kwargs): + form = TransferOwnershipForm(request.POST) + if not form.is_valid(): + return self.get(request) try: new_owner = search_user(request.POST['name']) except User.DoesNotExist: diff --git a/circle/network/views.py b/circle/network/views.py index 28f3579..123de84 100644 --- a/circle/network/views.py +++ b/circle/network/views.py @@ -42,7 +42,7 @@ from operator import itemgetter from itertools import chain import json from dashboard.views import AclUpdateView -from dashboard.forms import AclUserAddForm +from dashboard.forms import AclUserOrGroupAddForm class SuccessMessageMixin(FormMixin): @@ -660,7 +660,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, context['vlan_vid'] = self.kwargs.get('vid') context['acl'] = AclUpdateView.get_acl_data( self.object, self.request.user, 'network.vlan-acl') - context['aclform'] = AclUserAddForm() + context['aclform'] = AclUserOrGroupAddForm() return context success_url = reverse_lazy('network.vlan_list')