0001-profile_views-address-autocomplete-field-41919.patch
src/authentic2/api_urls.py | ||
---|---|---|
28 | 28 |
name='a2-api-role-members'), |
29 | 29 |
url(r'^check-password/$', api_views.check_password, name='a2-api-check-password'), |
30 | 30 |
url(r'^validate-password/$', api_views.validate_password, name='a2-api-validate-password'), |
31 |
url(r'^address-autocomplete/$', api_views.address_autocomplete, name='a2-api-address-autocomplete'), |
|
31 | 32 |
] |
32 | 33 | |
33 | 34 |
urlpatterns += api_views.router.urls |
src/authentic2/api_views.py | ||
---|---|---|
21 | 21 |
from pytz.exceptions import AmbiguousTimeError |
22 | 22 |
import django |
23 | 23 |
from django.db import models |
24 |
from django.conf import settings |
|
24 | 25 |
from django.contrib.auth import get_user_model |
25 | 26 |
from django.contrib.auth.hashers import identify_hasher |
26 | 27 |
from django.core.exceptions import MultipleObjectsReturned |
... | ... | |
33 | 34 |
from django.shortcuts import get_object_or_404 |
34 | 35 | |
35 | 36 |
from django_rbac.utils import get_ou_model, get_role_model |
37 |
import requests |
|
38 |
from requests.exceptions import RequestException |
|
36 | 39 | |
37 | 40 |
from rest_framework import serializers, pagination |
38 | 41 |
from rest_framework.validators import UniqueTogetherValidator |
... | ... | |
1038 | 1041 |
return result, status.HTTP_200_OK |
1039 | 1042 | |
1040 | 1043 |
validate_password = ValidatePasswordAPI.as_view() |
1044 | ||
1045 | ||
1046 |
class AddressAutocompleteAPI(APIView): |
|
1047 |
permission_classes = (permissions.IsAuthenticated,) |
|
1048 | ||
1049 |
def get(self, request): |
|
1050 |
if not getattr(settings, 'ADDRESS_AUTOCOMPLETE_URL', None): |
|
1051 |
return Response({}) |
|
1052 |
try: |
|
1053 |
response = requests.get( |
|
1054 |
settings.ADDRESS_AUTOCOMPLETE_URL, |
|
1055 |
params=request.GET |
|
1056 |
) |
|
1057 |
response.raise_for_status() |
|
1058 |
return Response(response.json()) |
|
1059 |
except RequestException: |
|
1060 |
return Response({}) |
|
1061 | ||
1062 | ||
1063 |
address_autocomplete = AddressAutocompleteAPI.as_view() |
src/authentic2/attribute_kinds.py | ||
---|---|---|
23 | 23 |
from itertools import chain |
24 | 24 | |
25 | 25 |
from django import forms |
26 |
from django.conf import settings |
|
26 | 27 |
from django.core.exceptions import ValidationError |
27 | 28 |
from django.core.validators import RegexValidator |
29 |
from django.urls import reverse |
|
28 | 30 |
from django.utils import six, formats |
29 | 31 |
from django.utils.translation import ugettext_lazy as _, pgettext_lazy |
30 | 32 |
from django.utils import html |
... | ... | |
110 | 112 |
] |
111 | 113 | |
112 | 114 | |
115 |
class AddressAutocompleteInput(forms.Select): |
|
116 |
template_name = 'authentic2/widgets/address_autocomplete.html' |
|
117 | ||
118 |
class Media: |
|
119 |
js = [ |
|
120 |
settings.SELECT2_JS, |
|
121 |
'authentic2/js/address_autocomplete.js', |
|
122 |
] |
|
123 |
css = { |
|
124 |
'screen': [settings.SELECT2_CSS], |
|
125 |
} |
|
126 | ||
127 |
def __init__(self, **kwargs): |
|
128 |
super().__init__(**kwargs) |
|
129 |
self.attrs['data-select2-url'] = reverse('a2-api-address-autocomplete') |
|
130 |
self.attrs['class'] = 'address-autocomplete' |
|
131 | ||
132 | ||
133 |
class AddressAutocompleteField(forms.CharField): |
|
134 |
widget = AddressAutocompleteInput |
|
135 | ||
136 | ||
113 | 137 |
@to_iter |
114 | 138 |
def get_title_choices(): |
115 | 139 |
return app_settings.A2_ATTRIBUTE_KIND_TITLE_CHOICES or DEFAULT_TITLE_CHOICES |
... | ... | |
269 | 293 |
'rest_framework_field_class': BirthdateRestField, |
270 | 294 |
'free_text_search': date_free_text_search, |
271 | 295 |
}, |
296 |
{ |
|
297 |
'label': _('address (autocomplete)'), |
|
298 |
'name': 'address_auto', |
|
299 |
'field_class': AddressAutocompleteField, |
|
300 |
}, |
|
272 | 301 |
{ |
273 | 302 |
'label': _('french postcode'), |
274 | 303 |
'name': 'fr_postcode', |
src/authentic2/manager/static/authentic2/manager/css/style.css | ||
---|---|---|
248 | 248 |
margin-right: 1em; |
249 | 249 |
} |
250 | 250 | |
251 |
.ui-dialog-content span.select2-container { |
|
251 |
.ui-dialog-content span.select2-container, |
|
252 |
form .widget span.select2-container { |
|
252 | 253 |
width: 100% !important; |
253 | 254 |
} |
254 | 255 |
src/authentic2/manager/user_views.py | ||
---|---|---|
315 | 315 |
if not self.object.username and self.object.ou and not self.object.ou.show_username: |
316 | 316 |
fields.remove('username') |
317 | 317 |
for attribute in Attribute.objects.all(): |
318 |
if attribute.name == 'address_autocomplete': |
|
319 |
continue |
|
318 | 320 |
fields.append(attribute.name) |
319 | 321 |
if self.request.user.is_superuser and \ |
320 | 322 |
'is_superuser' not in self.fields: |
src/authentic2/static/authentic2/js/address_autocomplete.js | ||
---|---|---|
1 |
$(function() { |
|
2 |
$('select.address-autocomplete').select2({ |
|
3 |
ajax: { |
|
4 |
delay: 250, |
|
5 |
dataType: 'json', |
|
6 |
data: function(params) { |
|
7 |
return {q: params.term, page_limit: 10}; |
|
8 |
}, |
|
9 |
processResults: function (data, params) { |
|
10 |
return {results: data.data}; |
|
11 |
}, |
|
12 |
url: function (params) { |
|
13 |
return $(this).data('select2-url') |
|
14 |
} |
|
15 |
} |
|
16 |
}).on('select2:select', function(e) { |
|
17 |
var data = e.params.data; |
|
18 |
if (data) { |
|
19 |
var address = undefined; |
|
20 |
if (typeof data.address == "object") { |
|
21 |
address = data.address; |
|
22 |
} else { |
|
23 |
address = data; |
|
24 |
} |
|
25 |
var road = address.road || address.nom_rue; |
|
26 |
var house_number = address.house_number || address.numero; |
|
27 |
var city = address.city || address.nom_commune; |
|
28 |
var postcode = address.postcode || address.code_postal; |
|
29 |
var number_and_street = null; |
|
30 |
if (house_number && road) { |
|
31 |
number_and_street = house_number + ' ' + road; |
|
32 |
} else { |
|
33 |
number_and_street = road; |
|
34 |
} |
|
35 |
$('#id_address').val(number_and_street); |
|
36 |
$('#id_city').val(city); |
|
37 |
$('#id_zipcode').val(postcode); |
|
38 |
} |
|
39 |
}); |
|
40 |
$('#id_address, #id_city, #id_zipcode').attr('disabled', 'disabled'); |
|
41 |
$('#manual-address').on('change', function() { |
|
42 |
$('#id_address, #id_city, #id_zipcode').attr('disabled', this.checked ? null : 'disabled'); |
|
43 |
}); |
|
44 |
if ($('#id_address').val() || $('#id_city').val() || $('#id_zipcode').val()) { |
|
45 |
var data = { |
|
46 |
id: 1, |
|
47 |
text: '' |
|
48 |
} |
|
49 |
$.each(['#id_address', '#id_zipcode', '#id_city'], function(idx, value) { |
|
50 |
if ($(value).val()) { |
|
51 |
if (data.text) { |
|
52 |
data.text += ' '; |
|
53 |
} |
|
54 |
data.text += $(value).val(); |
|
55 |
} |
|
56 |
}) |
|
57 |
var newOption = new Option(data.text, data.id, false, false); |
|
58 |
$('select.address-autocomplete').append(newOption).trigger('change'); |
|
59 |
} |
|
60 |
}); |
src/authentic2/templates/authentic2/widgets/address_autocomplete.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
{% include "django/forms/widgets/select.html" %} |
|
3 |
<div> |
|
4 |
<label><input id="manual-address" type="checkbox">{% trans "Manually enter the address" %}</label> |
|
5 |
</div> |
tests/test_api.py | ||
---|---|---|
18 | 18 | |
19 | 19 |
import datetime |
20 | 20 |
import json |
21 |
import mock |
|
21 | 22 |
import pytest |
22 | 23 |
import random |
23 | 24 |
import uuid |
... | ... | |
36 | 37 | |
37 | 38 |
from django_rbac.models import SEARCH_OP |
38 | 39 |
from django_rbac.utils import get_role_model, get_ou_model |
40 |
from requests.models import Response |
|
39 | 41 | |
40 | 42 |
from authentic2.a2_rbac.models import Role |
41 | 43 |
from authentic2.a2_rbac.utils import get_default_ou |
... | ... | |
1926 | 1928 |
resp = app.get('/api/users/find_duplicates/', params=params) |
1927 | 1929 |
assert len(resp.json['data']) == 2 |
1928 | 1930 |
assert resp.json['data'][0]['id'] == homonym.pk |
1931 | ||
1932 | ||
1933 |
class MockedRequestResponse(mock.Mock): |
|
1934 |
status_code = 200 |
|
1935 | ||
1936 |
def json(self): |
|
1937 |
return json.loads(self.content) |
|
1938 | ||
1939 | ||
1940 |
def test_api_address_autocomplete(app, admin, settings): |
|
1941 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
1942 | ||
1943 |
settings.ADDRESS_AUTOCOMPLETE_URL = 'example.com' |
|
1944 | ||
1945 |
params = {'q': '42 avenue'} |
|
1946 |
with mock.patch('authentic2.api_views.requests.get') as requests_get: |
|
1947 |
mock_resp = Response() |
|
1948 |
mock_resp.status_code = 500 |
|
1949 |
requests_get.return_value = mock_resp |
|
1950 |
resp = app.get('/api/address-autocomplete/', params=params) |
|
1951 |
assert resp.json == {} |
|
1952 |
assert requests_get.call_args_list[0][0][0] == 'example.com' |
|
1953 |
assert requests_get.call_args_list[0][1]['params'] == {'q': ['42 avenue']} |
|
1954 |
with mock.patch('authentic2.api_views.requests.get') as requests_get: |
|
1955 |
mock_resp = Response() |
|
1956 |
mock_resp.status_code = 404 |
|
1957 |
requests_get.return_value = mock_resp |
|
1958 |
resp = app.get('/api/address-autocomplete/', params=params) |
|
1959 |
assert resp.json == {} |
|
1960 |
with mock.patch('authentic2.api_views.requests.get') as requests_get: |
|
1961 |
requests_get.return_value = MockedRequestResponse(content=json.dumps({'data': {'foo': 'bar'}})) |
|
1962 |
resp = app.get('/api/address-autocomplete/', params=params) |
|
1963 |
assert resp.json == {'data': {'foo': 'bar'}} |
|
1964 | ||
1965 |
settings.ADDRESS_AUTOCOMPLETE_URL = None |
|
1966 |
with mock.patch('authentic2.api_views.requests.get') as requests_get: |
|
1967 |
resp = app.get('/api/address-autocomplete/', params=params) |
|
1968 |
assert resp.json == {} |
|
1969 |
assert requests_get.call_args_list == [] |
|
1970 | ||
1971 |
del settings.ADDRESS_AUTOCOMPLETE_URL |
|
1972 |
with mock.patch('authentic2.api_views.requests.get') as requests_get: |
|
1973 |
resp = app.get('/api/address-autocomplete/', params=params) |
|
1974 |
assert resp.json == {} |
|
1975 |
assert requests_get.call_args_list == [] |
tests/test_user_manager.py | ||
---|---|---|
733 | 733 |
assert not user.email_verified |
734 | 734 | |
735 | 735 | |
736 |
def test_manager_edit_user_address_autocomplete(app, simple_user, superuser_or_admin): |
|
737 |
url = u'/manage/users/%s/edit/' % simple_user.pk |
|
738 |
login(app, superuser_or_admin, '/manage/') |
|
739 | ||
740 |
Attribute.objects.create( |
|
741 |
name='address_autocomplete', label='Address (autocomplete)', |
|
742 |
kind='address_auto', user_visible=True, user_editable=True) |
|
743 | ||
744 |
resp = app.get(url) |
|
745 |
assert resp.html.find('select', {'name': 'address_autocomplete'}) |
|
746 |
assert resp.html.find('input', {'id': 'manual-address'}) |
|
747 | ||
748 | ||
736 | 749 |
def test_manager_email_verified_column_user(app, simple_user, superuser_or_admin): |
737 | 750 |
login(app, superuser_or_admin, '/manage/') |
738 | 751 | |
... | ... | |
793 | 806 |
assert resp.html.find('input', {'name': 'username'}) |
794 | 807 | |
795 | 808 | |
809 |
def test_manager_user_address_autocomplete_field(app, superuser, simple_user): |
|
810 |
login(app, superuser, '/manage/') |
|
811 |
Attribute.objects.create( |
|
812 |
name='address_autocomplete', label='Address (autocomplete)', |
|
813 |
kind='address_auto', user_visible=True, user_editable=True) |
|
814 |
resp = app.get(reverse('a2-manager-user-detail', kwargs={'pk': simple_user.id})) |
|
815 |
assert not resp.html.find('select', {'name': 'address_autocomplete'}) |
|
816 |
assert not resp.html.find('input', {'id': 'manual-address'}) |
|
817 | ||
818 | ||
796 | 819 |
def test_manager_user_roles_visibility(app, simple_user, admin, ou1, ou2): |
797 | 820 |
Role = get_role_model() |
798 | 821 |
role1 = Role.objects.create(name='Role 1', slug='role1', ou=ou1) |
799 |
- |