Projet

Général

Profil

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

Valentin Deniaud, 07 juillet 2022 17:42

Télécharger (14,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 |  4 +-
 src/authentic2/apps/authenticators/models.py  |  4 +-
 .../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(+), 15 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',
......
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
-