Projet

Général

Profil

0001-authenticators-set-order-through-dragndrop-65479.patch

Valentin Deniaud, 07 juillet 2022 16:58

Télécharger (13,6 ko)

Voir les différences:

Subject: [PATCH] authenticators: set order through dragndrop (#65479)

 src/authentic2/apps/authenticators/forms.py   | 11 +++-
 .../apps/authenticators/manager_urls.py       |  5 ++
 .../authenticators/migrations/0001_initial.py |  2 +-
 src/authentic2/apps/authenticators/models.py  |  2 +-
 .../authenticators/authenticators.html        |  1 +
 .../authenticators_order_form.html            | 46 ++++++++++++++++
 src/authentic2/apps/authenticators/views.py   | 39 ++++++++++---
 .../static/authentic2/manager/css/style.scss  |  8 +++
 tests/test_manager_authenticators.py          | 55 ++++++++++++++++++-
 9 files changed, 158 insertions(+), 11 deletions(-)
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticators_order_form.html
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',
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'),
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 "Configure 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).order_by('order')
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
......
275 275

  
276 276
    resp = resp.click('Enable').follow()
277 277
    assert 'Authenticator has been enabled.' in resp.text
278

  
279

  
280
def test_authenticators_order(app, superuser):
281
    resp = login(app, superuser, path='/manage/authenticators/')
282

  
283
    saml_authenticator = SAMLAuthenticator.objects.create(name='Test', slug='test', enabled=True, order=42)
284
    disabled_saml_authenticator = SAMLAuthenticator.objects.create(
285
        name='Test disabled', slug='test-disabled', enabled=False
286
    )
287
    fc_authenticator = FcAuthenticator.objects.create(slug='fc-authenticator', enabled=True, order=-1)
288
    password_authenticator = LoginPasswordAuthenticator.objects.get()
289

  
290
    assert fc_authenticator.order == -1
291
    assert password_authenticator.order == 0
292
    assert saml_authenticator.order == 42
293

  
294
    resp = resp.click('Configure order')
295
    assert resp.text.index('FranceConnect') < resp.text.index('Password') < resp.text.index('SAML - Test')
296
    assert 'SAML - Test disabled' not in resp.text
297

  
298
    resp.form['order'] = '%s,%s,%s' % (saml_authenticator.pk, password_authenticator.pk, fc_authenticator.pk)
299
    resp.form.submit()
300

  
301
    fc_authenticator.refresh_from_db()
302
    password_authenticator.refresh_from_db()
303
    saml_authenticator.refresh_from_db()
304
    assert fc_authenticator.order == 2
305
    assert password_authenticator.order == 1
306
    assert saml_authenticator.order == 0
307

  
308

  
309
def test_authenticators_add_last(app, superuser):
310
    resp = login(app, superuser, path='/manage/authenticators/')
311

  
312
    BaseAuthenticator.objects.all().delete()
313

  
314
    resp = resp.click('Add new authenticator')
315
    resp.form['name'] = 'Test'
316
    resp.form['authenticator'] = 'saml'
317
    resp.form.submit()
318

  
319
    authenticator = SAMLAuthenticator.objects.get()
320
    assert authenticator.order == 1
321

  
322
    authenticator.order = 42
323
    authenticator.save()
324
    resp = app.get('/manage/authenticators/add/')
325
    resp.form['name'] = 'Test 2'
326
    resp.form['authenticator'] = 'saml'
327
    resp.form.submit()
328

  
329
    authenticator = SAMLAuthenticator.objects.filter(slug='test-2').get()
330
    assert authenticator.order == 43
278
-