0001-authenticators-set-order-through-dragndrop-65479.patch
src/authentic2/apps/authenticators/forms.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
from django import forms |
18 | 18 |
from django.core.exceptions import ValidationError |
19 |
from django.db.models import Max |
|
19 | 20 |
from django.utils.translation import ugettext as _ |
20 | 21 | |
21 | 22 |
from authentic2.forms.mixins import SlugMixin |
... | ... | |
35 | 36 |
return condition |
36 | 37 | |
37 | 38 | |
39 |
class AuthenticatorsOrderForm(forms.Form): |
|
40 |
order = forms.CharField(widget=forms.HiddenInput) |
|
41 | ||
42 | ||
38 | 43 |
class AuthenticatorAddForm(SlugMixin, forms.ModelForm): |
39 | 44 |
field_order = ('authenticator', 'name', 'ou') |
40 | 45 | |
... | ... | |
55 | 60 |
] |
56 | 61 | |
57 | 62 |
def save(self): |
63 |
max_order = BaseAuthenticator.objects.aggregate(max=Max('order'))['max'] or 0 |
|
64 | ||
58 | 65 |
Authenticator = self.authenticators[self.cleaned_data['authenticator']] |
59 |
self.instance = Authenticator(name=self.cleaned_data['name'], ou=self.cleaned_data['ou']) |
|
66 |
self.instance = Authenticator( |
|
67 |
name=self.cleaned_data['name'], ou=self.cleaned_data['ou'], order=max_order + 1 |
|
68 |
) |
|
60 | 69 |
return super().save() |
61 | 70 | |
62 | 71 |
src/authentic2/apps/authenticators/manager_urls.py | ||
---|---|---|
76 | 76 |
views.journal, |
77 | 77 |
name='a2-manager-authenticator-journal', |
78 | 78 |
), |
79 |
path( |
|
80 |
'authenticators/order/', |
|
81 |
views.order, |
|
82 |
name='a2-manager-authenticators-order', |
|
83 |
), |
|
79 | 84 |
], |
80 | 85 |
) |
src/authentic2/apps/authenticators/migrations/0001_initial.py | ||
---|---|---|
26 | 26 |
('uuid', models.CharField(default=uuid.uuid4, editable=False, max_length=255, unique=True)), |
27 | 27 |
('name', models.CharField(max_length=128, blank=True, verbose_name='Name')), |
28 | 28 |
('slug', models.SlugField(unique=True)), |
29 |
('order', models.IntegerField(default=0, verbose_name='Order')), |
|
29 |
('order', models.IntegerField(default=0, verbose_name='Order', editable=False)),
|
|
30 | 30 |
('enabled', models.BooleanField(default=False, editable=False)), |
31 | 31 |
( |
32 | 32 |
'show_condition', |
... | ... | |
56 | 56 |
), |
57 | 57 |
], |
58 | 58 |
options={ |
59 |
'ordering': ('-enabled', 'name', 'slug', 'ou'), |
|
59 |
'ordering': ('-enabled', 'order', 'name', 'slug', 'ou'),
|
|
60 | 60 |
}, |
61 | 61 |
), |
62 | 62 |
] |
src/authentic2/apps/authenticators/models.py | ||
---|---|---|
44 | 44 |
blank=True, |
45 | 45 |
on_delete=models.CASCADE, |
46 | 46 |
) |
47 |
order = models.IntegerField(_('Order'), default=0) |
|
47 |
order = models.IntegerField(_('Order'), default=0, editable=False)
|
|
48 | 48 |
enabled = models.BooleanField(default=False, editable=False) |
49 | 49 |
show_condition = models.CharField( |
50 | 50 |
_('Show condition'), |
... | ... | |
69 | 69 |
description_fields = ['show_condition'] |
70 | 70 | |
71 | 71 |
class Meta: |
72 |
ordering = ('-enabled', 'name', 'slug', 'ou') |
|
72 |
ordering = ('-enabled', 'order', 'name', 'slug', 'ou')
|
|
73 | 73 | |
74 | 74 |
def __str__(self): |
75 | 75 |
if self.name: |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticators.html | ||
---|---|---|
4 | 4 |
{% block appbar %} |
5 | 5 |
{{ block.super }} |
6 | 6 |
<span class="actions"> |
7 |
<a href="{% url 'a2-manager-authenticators-order' %}" rel="popup">{% trans "Edit order" %}</a> |
|
7 | 8 |
<a href="{% url 'a2-manager-authenticator-add' %}" rel="popup">{% trans "Add new authenticator" %}</a> |
8 | 9 |
</span> |
9 | 10 |
{% endblock %} |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticators_order_form.html | ||
---|---|---|
1 |
{% extends "authentic2/authenticators/authenticator_common.html" %} |
|
2 |
{% load i18n gadjo %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="#"></a> |
|
7 |
{% endblock %} |
|
8 | ||
9 |
{% block content %} |
|
10 |
<form method="post" enctype="multipart/form-data"> |
|
11 | ||
12 |
<ul class="objects-list" id="authenticators-ordered-list"> |
|
13 |
{% for authenticator in authenticators %} |
|
14 |
<li data-authenticator-id="{{ authenticator.pk }}"> |
|
15 |
<span class="handle">⣿</span> {{ authenticator }} |
|
16 |
</li> |
|
17 |
{% endfor %} |
|
18 |
</ul> |
|
19 | ||
20 |
{% csrf_token %} |
|
21 |
{{ form|with_template }} |
|
22 |
<div class="buttons"> |
|
23 |
<button>{% trans "Save" %}</button> |
|
24 |
<a class="cancel" href="{% url 'a2-manager-authenticators' %}">{% trans 'Cancel' %}</a> |
|
25 |
</div> |
|
26 | ||
27 |
<script> |
|
28 |
$(function () { |
|
29 |
function set_order_field_value () { |
|
30 |
var new_order = $('#authenticators-ordered-list li').map( |
|
31 |
function() { return $(this).data('authenticator-id'); } |
|
32 |
).get().join(); |
|
33 |
$('#id_order').val(new_order); |
|
34 |
} |
|
35 |
set_order_field_value(); |
|
36 | ||
37 |
$('#authenticators-ordered-list').sortable({ |
|
38 |
handle: '.handle', |
|
39 |
stop: function(event, ui) { |
|
40 |
set_order_field_value(); |
|
41 |
} |
|
42 |
}).sortable('refresh'); |
|
43 |
}); |
|
44 |
</script> |
|
45 |
</form> |
|
46 |
{% endblock %} |
src/authentic2/apps/authenticators/views.py | ||
---|---|---|
21 | 21 |
from django.urls import reverse, reverse_lazy |
22 | 22 |
from django.utils.functional import cached_property |
23 | 23 |
from django.utils.translation import ugettext as _ |
24 |
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView |
|
24 |
from django.views.generic import CreateView, DeleteView, DetailView, FormView, UpdateView
|
|
25 | 25 |
from django.views.generic.list import ListView |
26 | 26 | |
27 | 27 |
from authentic2.apps.authenticators import forms |
... | ... | |
32 | 32 | |
33 | 33 | |
34 | 34 |
class AuthenticatorsMixin(MediaMixin, TitleMixin): |
35 |
model = BaseAuthenticator |
|
36 | ||
35 | 37 |
def get_queryset(self): |
36 | 38 |
return self.model.authenticators.all() |
37 | 39 | |
38 | 40 | |
39 | 41 |
class AuthenticatorsView(AuthenticatorsMixin, ListView): |
40 | 42 |
template_name = 'authentic2/authenticators/authenticators.html' |
41 |
model = BaseAuthenticator |
|
42 | 43 |
title = _('Authenticators') |
43 | 44 | |
44 | 45 | |
... | ... | |
64 | 65 | |
65 | 66 |
class AuthenticatorDetailView(AuthenticatorsMixin, DetailView): |
66 | 67 |
template_name = 'authentic2/authenticators/authenticator_detail.html' |
67 |
model = BaseAuthenticator |
|
68 | 68 | |
69 | 69 |
@property |
70 | 70 |
def title(self): |
... | ... | |
77 | 77 |
class AuthenticatorEditView(AuthenticatorsMixin, UpdateView): |
78 | 78 |
template_name = 'authentic2/authenticators/authenticator_edit_form.html' |
79 | 79 |
title = _('Edit authenticator') |
80 |
model = BaseAuthenticator |
|
81 | 80 | |
82 | 81 |
def get_form_class(self): |
83 | 82 |
return self.object.manager_form_class |
... | ... | |
93 | 92 |
class AuthenticatorDeleteView(AuthenticatorsMixin, DeleteView): |
94 | 93 |
template_name = 'authentic2/authenticators/authenticator_delete_form.html' |
95 | 94 |
title = _('Delete authenticator') |
96 |
model = BaseAuthenticator |
|
97 | 95 |
success_url = reverse_lazy('a2-manager-authenticators') |
98 | 96 | |
99 | 97 |
def dispatch(self, *args, **kwargs): |
... | ... | |
110 | 108 | |
111 | 109 | |
112 | 110 |
class AuthenticatorToggleView(AuthenticatorsMixin, DetailView): |
113 |
model = BaseAuthenticator |
|
114 | ||
115 | 111 |
def dispatch(self, *args, **kwargs): |
116 | 112 |
self.authenticator = self.get_object() |
117 | 113 |
if self.authenticator.protected or not self.authenticator.has_valid_configuration(): |
... | ... | |
153 | 149 | |
154 | 150 | |
155 | 151 |
journal = AuthenticatorJournal.as_view() |
152 | ||
153 | ||
154 |
class AuthenticatorsOrderView(AuthenticatorsMixin, FormView): |
|
155 |
template_name = 'authentic2/authenticators/authenticators_order_form.html' |
|
156 |
title = _('Configure display order') |
|
157 |
form_class = forms.AuthenticatorsOrderForm |
|
158 |
success_url = reverse_lazy('a2-manager-authenticators') |
|
159 | ||
160 |
def form_valid(self, form): |
|
161 |
order_by_pk = {pk: i for i, pk in enumerate(form.cleaned_data['order'].split(','))} |
|
162 | ||
163 |
authenticators = list(self.get_queryset()) |
|
164 |
for authenticator in authenticators: |
|
165 |
authenticator.order = order_by_pk[str(authenticator.pk)] |
|
166 | ||
167 |
BaseAuthenticator.objects.bulk_update(authenticators, ['order']) |
|
168 |
return super().form_valid(form) |
|
169 | ||
170 |
def get_context_data(self, **kwargs): |
|
171 |
context = super().get_context_data(**kwargs) |
|
172 |
context['authenticators'] = self.get_queryset() |
|
173 |
return context |
|
174 | ||
175 |
def get_queryset(self): |
|
176 |
qs = super().get_queryset() |
|
177 |
return qs.filter(enabled=True) |
|
178 | ||
179 | ||
180 |
order = AuthenticatorsOrderView.as_view() |
src/authentic2/manager/static/authentic2/manager/css/style.scss | ||
---|---|---|
297 | 297 |
-webkit-column-width: 20em; |
298 | 298 |
column-width: 20em; |
299 | 299 |
} |
300 | ||
301 |
span.handle { |
|
302 |
cursor: move; |
|
303 |
display: inline-block; |
|
304 |
padding: 0.5ex; |
|
305 |
text-align: center; |
|
306 |
width: 1em; |
|
307 |
} |
tests/test_manager_authenticators.py | ||
---|---|---|
17 | 17 |
import pytest |
18 | 18 |
from django import VERSION as DJ_VERSION |
19 | 19 | |
20 |
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator |
|
20 |
from authentic2.apps.authenticators.models import BaseAuthenticator, LoginPasswordAuthenticator
|
|
21 | 21 |
from authentic2_auth_fc.models import FcAuthenticator |
22 | 22 |
from authentic2_auth_oidc.models import OIDCProvider |
23 | 23 |
from authentic2_auth_saml.models import SAMLAuthenticator |
... | ... | |
54 | 54 |
resp = resp.click('Edit') |
55 | 55 |
assert list(resp.form.fields) == [ |
56 | 56 |
'csrfmiddlewaretoken', |
57 |
'order', |
|
58 | 57 |
'show_condition', |
59 | 58 |
'remember_me', |
60 | 59 |
'include_ou_selector', |
... | ... | |
209 | 208 |
resp = resp.click('Edit') |
210 | 209 |
assert list(resp.form.fields) == [ |
211 | 210 |
'csrfmiddlewaretoken', |
212 |
'order', |
|
213 | 211 |
'show_condition', |
214 | 212 |
'platform', |
215 | 213 |
'client_id', |
... | ... | |
275 | 273 | |
276 | 274 |
resp = resp.click('Enable').follow() |
277 | 275 |
assert 'Authenticator has been enabled.' in resp.text |
276 | ||
277 | ||
278 |
def test_authenticators_order(app, superuser): |
|
279 |
resp = login(app, superuser, path='/manage/authenticators/') |
|
280 | ||
281 |
saml_authenticator = SAMLAuthenticator.objects.create(name='Test', slug='test', enabled=True, order=42) |
|
282 |
SAMLAuthenticator.objects.create(name='Test disabled', slug='test-disabled', enabled=False) |
|
283 |
fc_authenticator = FcAuthenticator.objects.create(slug='fc-authenticator', enabled=True, order=-1) |
|
284 |
password_authenticator = LoginPasswordAuthenticator.objects.get() |
|
285 | ||
286 |
assert fc_authenticator.order == -1 |
|
287 |
assert password_authenticator.order == 0 |
|
288 |
assert saml_authenticator.order == 42 |
|
289 | ||
290 |
resp = resp.click('Edit order') |
|
291 |
assert resp.text.index('FranceConnect') < resp.text.index('Password') < resp.text.index('SAML - Test') |
|
292 |
assert 'SAML - Test disabled' not in resp.text |
|
293 | ||
294 |
resp.form['order'] = '%s,%s,%s' % (saml_authenticator.pk, password_authenticator.pk, fc_authenticator.pk) |
|
295 |
resp.form.submit() |
|
296 | ||
297 |
fc_authenticator.refresh_from_db() |
|
298 |
password_authenticator.refresh_from_db() |
|
299 |
saml_authenticator.refresh_from_db() |
|
300 |
assert fc_authenticator.order == 2 |
|
301 |
assert password_authenticator.order == 1 |
|
302 |
assert saml_authenticator.order == 0 |
|
303 | ||
304 | ||
305 |
def test_authenticators_add_last(app, superuser): |
|
306 |
resp = login(app, superuser, path='/manage/authenticators/') |
|
307 | ||
308 |
BaseAuthenticator.objects.all().delete() |
|
309 | ||
310 |
resp = resp.click('Add new authenticator') |
|
311 |
resp.form['name'] = 'Test' |
|
312 |
resp.form['authenticator'] = 'saml' |
|
313 |
resp.form.submit() |
|
314 | ||
315 |
authenticator = SAMLAuthenticator.objects.get() |
|
316 |
assert authenticator.order == 1 |
|
317 | ||
318 |
authenticator.order = 42 |
|
319 |
authenticator.save() |
|
320 |
resp = app.get('/manage/authenticators/add/') |
|
321 |
resp.form['name'] = 'Test 2' |
|
322 |
resp.form['authenticator'] = 'saml' |
|
323 |
resp.form.submit() |
|
324 | ||
325 |
authenticator = SAMLAuthenticator.objects.filter(slug='test-2').get() |
|
326 |
assert authenticator.order == 43 |
|
278 |
- |