Projet

Général

Profil

0001-migrate-to-Django-Select2-5-fixes-15604.patch

Benjamin Dauvergne, 24 mars 2017 13:57

Télécharger (22,1 ko)

Voir les différences:

Subject: [PATCH] migrate to Django-Select2>5 (fixes #15604)

 setup.py                                           |   3 +-
 src/authentic2/manager/fields.py                   | 218 ++++-----------------
 .../static/authentic2/manager/css/style.css        |  34 +---
 .../manager/templates/authentic2/manager/base.html |  18 --
 .../manager/templates/authentic2/manager/form.html |   7 +
 src/authentic2/manager/urls.py                     |   2 +-
 src/authentic2/manager/views.py                    |  62 +++++-
 src/authentic2/manager/widgets.py                  | 124 ++++++++++++
 src/authentic2/settings.py                         |   1 +
 9 files changed, 236 insertions(+), 233 deletions(-)
 create mode 100644 src/authentic2/manager/widgets.py
setup.py
114 114
          'django-model-utils>=2.4',
115 115
          'django-admin-tools>=0.6,<0.7',
116 116
          'dnspython>=1.10',
117
          'Django-Select2>=4.3.0,<5',
117
          'Django-Select2>5',
118 118
          'django-tables2>=1.0,<1.1',
119 119
          'gadjo',
120 120
          'django-import-export>=0.2.7',
......
131 131
          'cryptography',
132 132
          'XStatic-jQuery',
133 133
          'XStatic-jquery-ui',
134
          'xstatic-select2',
134 135
      ],
135 136
      extras_require = {
136 137
          'idp-openid': ['python-openid'],
src/authentic2/manager/fields.py
1
from django_select2 import AutoModelSelect2Field, \
2
    AutoModelSelect2MultipleField, NO_ERR_RESP
1
from django import forms
3 2

  
4
from django.contrib.auth.models import Group, Permission
5
from django.contrib.auth import get_user_model
6
from django.db.models.query import Q
3
from . import widgets
7 4

  
8
from django_rbac.backends import DjangoRBACBackend
9
from django_rbac.utils import get_role_model, get_ou_model
10 5

  
11
from authentic2.models import Service
12

  
13
from . import utils
14

  
15

  
16
class SecurityCheckMixin(object):
17
    operations = ['change', 'add', 'view', 'delete']
18

  
19
    @property
20
    def perms(self):
21
        model = self.queryset.model
22
        app_label = model._meta.app_label
23
        model_name = model._meta.model_name
24
        return ['%s.%s_%s' % (app_label, perm, model_name)
25
                 for perm in self.operations]
26

  
27
    def security_check(self, request, *args, **kwargs):
28
        model = self.queryset.model
29
        app_label = model._meta.app_label
30
        model_name = model._meta.model_name
31
        return request.user.is_authenticated() \
32
            and request.user.has_perm_any(self.perms)
33

  
34
    def prepare_qs_params(self, request, search_term, search_fields):
35
        '''Only search visible objects'''
36
        ors = []
37
        ands = {}
38
        for term in search_term.split():
39
            qs_params = super(SecurityCheckMixin, self).prepare_qs_params(
40
                request, term, search_fields)
41
            ors.extend(qs_params['or'])
42
            ands.update(qs_params['and'])
43
        model = self.queryset.model
44
        app_label = model._meta.app_label
45
        model_name = model._meta.model_name
46
        rbac_backend = DjangoRBACBackend()
47
        query = rbac_backend.filter_by_perm_query(
48
            request.user, self.perms, self.queryset)
49
        if query is False:
50
            ands['id'] = -1
51
        elif query is True:
52
            pass
6
class Select2Mixin(object):
7
    def __init__(self, **kwargs):
8
        if getattr(self.widget, 'queryset', None) is not None:
9
            kwargs['queryset'] = self.widget.queryset
10
        elif getattr(self.widget, 'model', None):
11
            kwargs['queryset'] = self.widget.model.objects.all()
53 12
        else:
54
            ors = [query & reduce(Q.__or__, ors)]
55
        return {'or': ors, 'and': ands}
56

  
57

  
58
class SplitSearchTermMixin(object):
59
    def prepare_qs_params(self, request, search_term, search_fields):
60
        ors = []
61
        ands = {}
62
        for term in search_term.split():
63
            qs_params = super(SplitSearchTermMixin, self).prepare_qs_params(
64
                request, term, search_fields)
65
            ors.extend(qs_params['or'])
66
            ands.update(qs_params['and'])
67
        return {'or': ors, 'and': ands}
68

  
69

  
70
class ChooseUserField(SecurityCheckMixin, SplitSearchTermMixin,
71
                      AutoModelSelect2Field):
72
    queryset = get_user_model().objects
73
    search_fields = [
74
        'username__icontains', 'first_name__icontains',
75
        'last_name__icontains', 'email__icontains'
76
    ]
77

  
78
    def get_results(self, request, term, page, context):
79
        return (NO_ERR_RESP, False, utils.search_user(term))
80

  
81

  
82
class ChooseUsersField(SecurityCheckMixin, SplitSearchTermMixin,
83
                      AutoModelSelect2MultipleField):
84
    queryset = get_user_model().objects
85
    search_fields = [
86
        'username__icontains', 'first_name__icontains',
87
        'last_name__icontains', 'email__icontains'
88
    ]
89

  
90
    def get_results(self, request, term, page, context):
91
        return (NO_ERR_RESP, False, utils.search_user(term))
92

  
93

  
94
class GroupsField(SecurityCheckMixin, SplitSearchTermMixin,
95
                  AutoModelSelect2MultipleField):
96
    queryset = Group.objects
97
    search_fields = [
98
        'name__icontains',
99
    ]
100

  
101

  
102
class PermissionChoices(SecurityCheckMixin, SplitSearchTermMixin,
103
                        AutoModelSelect2MultipleField):
104
    queryset = Permission.objects
105
    search_fields = [
106
        'name__icontains', 'codename__icontains',
107
        'content_type__name__icontains'
108
    ]
109

  
110
    def prepare_qs_params(self, request, search_term, search_fields):
111
        ors = []
112
        ands = {}
113
        for term in search_term.split():
114
            qs_params = super(PermissionChoices, self).prepare_qs_params(
115
                request, term, search_fields)
116
            ors.extend(qs_params['or'])
117
            ands.update(qs_params['and'])
118
        return {'or': ors, 'and': ands}
119

  
120
    def label_from_instance(self, instance):
121
        return instance.name
122

  
123

  
124
class RoleLabelMixin(object):
125
    def label_from_instance(self, obj):
126
        label = unicode(obj)
127
        if obj.service:
128
            label = label + ' - ' + unicode(obj.service)
129
        return label
130

  
131

  
132
class ChooseRoleField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin,
133
                      AutoModelSelect2Field):
134
    queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True)
135
    search_fields = [
136
        'name__icontains',
137
        'service__name__icontains',
138
    ]
139

  
140

  
141
class ChooseRolesField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin,
142
                      AutoModelSelect2MultipleField):
143
    queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True)
144
    search_fields = [
145
        'name__icontains',
146
        'service__name__icontains',
147
    ]
148

  
149

  
150
class ChooseRolesForChangeField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin,
151
                      AutoModelSelect2MultipleField):
152
    operations = ['change']
153
    queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True)
154
    search_fields = [
155
        'name__icontains',
156
        'service__name__icontains',
157
    ]
158

  
159
class ChooseOUField(SecurityCheckMixin, SplitSearchTermMixin,
160
                    AutoModelSelect2Field):
161
    queryset = get_ou_model().objects
162
    search_fields = [
163
        'name__icontains',
164
    ]
165

  
166

  
167
class ChooseServiceField(SecurityCheckMixin, SplitSearchTermMixin,
168
                         AutoModelSelect2Field):
169
    queryset = Service.objects
170
    search_fields = [
171
        'name__icontains',
172
    ]
173

  
174

  
175
class ChooseUserRoleField(RoleLabelMixin, SecurityCheckMixin, SplitSearchTermMixin,
176
                      AutoModelSelect2Field):
177
    operations = ['change']
178
    queryset = get_role_model().objects
179
    search_fields = [
180
        'name__icontains',
181
        'service__name__icontains',
182
    ]
13
            raise NotImplementedError
14
        assert kwargs['queryset'] is not None
15
        super(Select2Mixin, self).__init__(**kwargs)
16

  
17
    def __setattr__(self, key, value):
18
        if key == 'queryset':
19
            self.widget.queryset = value
20
        super(Select2Mixin, self).__setattr__(key, value)
21

  
22

  
23
class Select2ModelChoiceField(Select2Mixin, forms.ModelChoiceField):
24
    pass
25

  
26

  
27
class Select2ModelMultipleChoiceField(Select2Mixin, forms.ModelMultipleChoiceField):
28
    pass
29

  
30

  
31
for key in dir(widgets):
32
    cls = getattr(widgets, key)
33
    if not isinstance(cls, type):
34
        continue
35
    if issubclass(cls, widgets.ModelSelect2MultipleWidget):
36
        cls_name = key.replace('Widget', 'Field')
37
        vars()[cls_name] = type(cls_name, (Select2ModelMultipleChoiceField,), {
38
            'widget': cls,
39
        })
40
    elif issubclass(cls, widgets.ModelSelect2Widget):
41
        cls_name = key.replace('Widget', 'Field')
42
        vars()[cls_name] = type(cls_name, (Select2ModelChoiceField,), {
43
            'widget': cls,
44
        })
src/authentic2/manager/static/authentic2/manager/css/style.css
56 56
	align-items: baseline;
57 57
}
58 58

  
59
.manager-m2m-add-form .select2-container {
60
	flex-grow: 1;
61
	margin: 0 1em;
62
}
63

  
64 59
table.main th.name, table.main td.name, #user-table .link, #user-table .username, #user-table
65 60
.email, #user-table .first_name, #user-table .last_name, #user-table .ou {
66 61
        text-align: left;
......
205 200
  width: calc(100% - 28px);
206 201
}
207 202

  
208
form p .select2-container {
209
  width: calc(100% - 10px);
210
  margin-left: 10px;
211
}
212

  
213 203
form input[type="checkbox"] {
214 204
  width: auto;
215 205
}
......
226 216
  margin-left: 50%;
227 217
}
228 218

  
229
# Override jquery-ui default
230
.ui-widget select {
231
  font-family: inherit;
232
  font-size: inherit;
233
}
234

  
235
div.ui-dialog form p select, form p select {
236
  width: calc(100% - 28px);
237
}
238 219

  
239 220
#id_generate_password_p label, #id_reset_password_at_next_login_p label,
240 221
#id_send_mail_p label, #id_is_superuser_p label {
......
259 240
  width: 50%;
260 241
}
261 242

  
262
.ui-widget-content {
263
	color: #3c3c33;
264
}
265

  
266
.ui-widget, .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button {
267
	font-family: "Bitstream Vera Sans","Verdana",sans-serif;
268
	font-size: inherit;
269
}
270

  
271 243
.waiting {
272 244
    position: fixed;
273 245
    top: 0;
......
287 259
    margin-right: 1ex;
288 260
}
289 261
.role-inheritance { margin: 1em 0px; }
262

  
263
/* Select2 styling */
264

  
265
.select2-container {
266
	width: 100% !important;
267
}
src/authentic2/manager/templates/authentic2/manager/base.html
1 1
{% extends "gadjo/base.html" %}
2 2
{% load i18n staticfiles %}
3
{% load django_select2_tags %}
4 3
{% load firstof from future %}
5 4

  
6 5
{% block page-title %}{% firstof site_title "Authentic2" %}{% endblock %}
......
19 18
  <h2>{% block page_title %}{% endblock %}</h2>
20 19
{% endblock %}
21 20

  
22
{% block css %}
23
  {{ block.super }}
24
  <link rel="stylesheet" type="text/css" media="all" href="{% static "authentic2/manager/css/style.css" %}"/>
25
{% endblock %}
26

  
27 21
{% block extrascripts %}
28 22
  {{ block.super }}
29
  {% if debug %}
30
    <script src="{% static "jquery/js/jquery.form.js" %}"></script>
31
  {% else %}
32
    <script src="{% static "jquery/js/jquery.form.min.js" %}"></script>
33
  {% endif %}
34
  <script type="text/javascript" src="{% static "admin/js/urlify.js" %}"></script>
35
  <script type="text/javascript" src="{% static "authentic2/js/purl.js" %}"></script>
36
  <script type="text/javascript" src="{% static "authentic2/manager/js/manager.js" %}"></script>
37
  {% import_django_select2_js %}
38
  {% import_django_select2_css %}
39
  <script type="text/javascript" src="{% url 'a2-manager-javascript-catalog' %}"></script>
40
  <script type="text/javascript" src="{% static "authentic2/manager/js/select2_locale.js" %}"></script>
41 23
  <script>
42 24
    window.csrf_token = '{{ csrf_token }}';
43 25
  </script>
src/authentic2/manager/templates/authentic2/manager/form.html
73 73
        {% endif %}
74 74
      {% endblock %}
75 75
      </div>
76
      <script>
77
        $(function () {
78
          if ($.fn.djangoSelect2) {
79
            $('.django-select2').djangoSelect2();
80
          }
81
        })
82
      </script>
76 83
{% endblock %}
src/authentic2/manager/urls.py
92 92
        url(r'^jsi18n/$', javascript_catalog,
93 93
            {'packages': ('authentic2.manager',)},
94 94
            name='a2-manager-javascript-catalog'),
95
        url(r'^', include('django_select2.urls')),
95
    url(r'^select2.json$', views.select2, name='django_select2-json'),
96 96
)
src/authentic2/manager/views.py
2 2

  
3 3
from django.core.exceptions import PermissionDenied
4 4
from django.views.generic.base import ContextMixin
5
from django.views.generic import TemplateView, FormView, UpdateView, \
6
    CreateView, DeleteView, TemplateView
5
from django.views.generic.edit import FormMixinBase
6
from django.views.generic import (FormView, UpdateView, CreateView, DeleteView, TemplateView)
7 7
from django.views.generic.detail import SingleObjectMixin
8 8
from django.http import HttpResponse, Http404
9 9
from django.utils.encoding import force_text
10 10
from django.utils.translation import ugettext_lazy as _
11 11
from django.utils.timezone import now
12
from django.core.urlresolvers import reverse
12
from django.core.urlresolvers import reverse, reverse_lazy
13 13
from django.contrib.messages.views import SuccessMessageMixin
14
from django.forms import MediaDefiningClass
14 15

  
15 16
from django_tables2 import SingleTableView, SingleTableMixin
16 17

  
18
from django_select2.views import AutoResponseView
19

  
17 20
from django_rbac.utils import get_ou_model
18 21

  
19 22
from authentic2.forms import modelform_factory
......
23 26
from . import app_settings
24 27

  
25 28

  
29
class MediaMixinBase(MediaDefiningClass, FormMixinBase):
30
    pass
31

  
32

  
33
class MediaMixin(object):
34
    __metaclass__ = MediaMixinBase
35

  
36
    class Media:
37
        js = (
38
            reverse_lazy('a2-manager-javascript-catalog'),
39
            'xstatic/jquery.js',
40
            'jquery/js/jquery.form.js',
41
            'admin/js/urlify.js',
42
            'authentic2/js/purl.js',
43
            'authentic2/manager/js/manager.js',
44
        )
45
        css = {
46
            'all': (
47
                'authentic2/manager/css/style.css',
48
            )
49
        }
50

  
51
    def get_context_data(self, **kwargs):
52
        kwargs['media'] = self.media
53
        ctx = super(MediaMixin, self).get_context_data(**kwargs)
54
        if 'form' in ctx:
55
            ctx['media'] += ctx['form'].media
56
        return ctx
57

  
58

  
26 59
class PermissionMixin(object):
27 60
    permissions = None
28 61

  
......
266 299
        return response
267 300

  
268 301

  
269
class ModelNameMixin(object):
302
class ModelNameMixin(MediaMixin):
270 303
    def get_model_name(self):
271 304
        return self.model._meta.verbose_name
272 305

  
......
281 314
                    TableQuerysetMixin, SingleTableView):
282 315
    pass
283 316

  
284
class SubTableViewMixin(FormatsContextData, PermissionMixin,
317

  
318
class SubTableViewMixin(FormatsContextData, ModelNameMixin, PermissionMixin,
285 319
                        SearchFormMixin, FilterTableQuerysetByPermMixin,
286 320
                        TableQuerysetMixin, SingleObjectMixin,
287 321
                        SingleTableMixin, ContextMixin):
288 322
    pass
289 323

  
324

  
290 325
class SimpleSubTableView(SubTableViewMixin, TemplateView):
291 326
    pass
292 327

  
328

  
293 329
class BaseSubTableView(TitleMixin, SubTableViewMixin, FormView):
294 330
    success_url = '.'
295 331

  
......
308 344
        return '../../'
309 345

  
310 346

  
311
class ModelFormView(object):
347
class ModelFormView(MediaMixin):
312 348
    fields = None
313 349
    form_class = None
314 350

  
......
349 385
        return '..'
350 386

  
351 387

  
352
class HomepageView(PermissionMixin, TemplateView):
388
class HomepageView(PermissionMixin, MediaMixin, TemplateView):
353 389
    template_name = 'authentic2/manager/homepage.html'
354 390
    permissions = ['a2_rbac.view_role', 'a2_rbac.view_organizationalunit',
355 391
                   'auth.view_group', 'custom_user.view_user']
......
406 442
        if exclude_ou:
407 443
            kwargs['exclude'] = ['ou']
408 444
        return super(HideOUColumnMixin, self).get_table(**kwargs)
445

  
446

  
447
class Select2View(AutoResponseView):
448
    def get_widget_or_404(self):
449
        widget = super(Select2View, self).get_widget_or_404()
450
        widget.view = self
451
        if hasattr(widget, 'security_check'):
452
            if not widget.security_check(self.request, *self.args, **self.kwargs):
453
                raise PermissionDenied
454
        return widget
455

  
456
select2 = Select2View.as_view()
src/authentic2/manager/widgets.py
1
from django_select2.forms import ModelSelect2Widget, ModelSelect2MultipleWidget
2

  
3
from django.contrib.auth import get_user_model
4

  
5
from django_rbac.backends import DjangoRBACBackend
6
from django_rbac.utils import get_role_model, get_ou_model
7

  
8
from authentic2.models import Service
9

  
10
from . import utils
11

  
12

  
13
class SplitTermMixin(object):
14
    def filter_queryset(self, term, queryset=None):
15
        if queryset is not None:
16
            qs = queryset.none()
17
        else:
18
            qs = self.get_queryset().none()
19
        for term in term.split():
20
            qs |= super(SplitTermMixin, self).filter_queryset(term, queryset=queryset)
21
        return qs
22

  
23

  
24
class SecurityCheckMixin(SplitTermMixin):
25
    operations = ['change', 'add', 'view', 'delete']
26

  
27
    @property
28
    def perms(self):
29
        model = self.queryset.model
30
        app_label = model._meta.app_label
31
        model_name = model._meta.model_name
32
        return ['%s.%s_%s' % (app_label, perm, model_name)
33
                for perm in self.operations]
34

  
35
    def security_check(self, request, *args, **kwargs):
36
        return request.user.is_authenticated() \
37
            and request.user.has_perm_any(self.perms)
38

  
39
    def filter_queryset(self, term, queryset=None):
40
        '''Only search visible objects'''
41
        if not hasattr(self, 'view'):
42
            return []
43
        request = self.view.request
44
        qs = super(SecurityCheckMixin, self).filter_queryset(term, queryset=queryset)
45
        rbac_backend = DjangoRBACBackend()
46
        return rbac_backend.filter_by_perm(request.user, self.perms, qs)
47

  
48

  
49
class RoleLabelMixin(object):
50
    def label_from_instance(self, obj):
51
        label = unicode(obj)
52
        if obj.service:
53
            label = label + ' - ' + unicode(obj.service)
54
        return label
55

  
56

  
57
class ChooseUserWidget(SecurityCheckMixin, ModelSelect2Widget):
58
    model = get_user_model()
59
    search_fields = [
60
        'username__icontains', 'first_name__icontains',
61
        'last_name__icontains', 'email__icontains'
62
    ]
63

  
64
    def label_from_instance(self, user):
65
        return utils.label_from_user(user)
66

  
67

  
68
class ChooseUsersWidget(SecurityCheckMixin, ModelSelect2MultipleWidget):
69
    model = get_user_model()
70
    search_fields = [
71
        'username__icontains', 'first_name__icontains',
72
        'last_name__icontains', 'email__icontains'
73
    ]
74

  
75
    def label_from_instance(self, user):
76
        return utils.label_from_user(user)
77

  
78

  
79
class ChooseRoleWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2Widget):
80
    queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True)
81
    search_fields = [
82
        'name__icontains',
83
        'service__name__icontains',
84
    ]
85

  
86

  
87
class ChooseRolesWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2MultipleWidget):
88
    queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True)
89
    search_fields = [
90
        'name__icontains',
91
        'service__name__icontains',
92
    ]
93

  
94

  
95
class ChooseRolesForChangeWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2MultipleWidget):
96
    operations = ['change']
97
    queryset = get_role_model().objects.filter(admin_scope_ct__isnull=True)
98
    search_fields = [
99
        'name__icontains',
100
        'service__name__icontains',
101
    ]
102

  
103

  
104
class ChooseOUWidget(SecurityCheckMixin, ModelSelect2Widget):
105
    model = get_ou_model()
106
    search_fields = [
107
        'name__icontains',
108
    ]
109

  
110

  
111
class ChooseServiceWidget(SecurityCheckMixin, ModelSelect2Widget):
112
    model = Service
113
    search_fields = [
114
        'name__icontains',
115
    ]
116

  
117

  
118
class ChooseUserRoleWidget(RoleLabelMixin, SecurityCheckMixin, ModelSelect2Widget):
119
    operations = ['change']
120
    model = get_role_model()
121
    search_fields = [
122
        'name__icontains',
123
        'service__name__icontains',
124
    ]
src/authentic2/settings.py
121 121
    'rest_framework',
122 122
    'xstatic.pkg.jquery',
123 123
    'xstatic.pkg.jquery_ui',
124
    'xstatic.pkg.select2',
124 125
)
125 126

  
126 127
INSTALLED_APPS = tuple(plugins.register_plugins_installed_apps(INSTALLED_APPS))
127
-