profile_views: address autocomplete field (#41919)
This commit is contained in:
parent
3b6d2cc4cd
commit
7b130d6ffc
|
@ -28,6 +28,7 @@ urlpatterns = [
|
|||
name='a2-api-role-members'),
|
||||
url(r'^check-password/$', api_views.check_password, name='a2-api-check-password'),
|
||||
url(r'^validate-password/$', api_views.validate_password, name='a2-api-validate-password'),
|
||||
url(r'^address-autocomplete/$', api_views.address_autocomplete, name='a2-api-address-autocomplete'),
|
||||
]
|
||||
|
||||
urlpatterns += api_views.router.urls
|
||||
|
|
|
@ -21,6 +21,7 @@ import smtplib
|
|||
from pytz.exceptions import AmbiguousTimeError
|
||||
import django
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.hashers import identify_hasher
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
|
@ -33,6 +34,8 @@ from django.views.decorators.cache import cache_control
|
|||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from django_rbac.utils import get_ou_model, get_role_model
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from rest_framework import serializers, pagination
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
@ -1038,3 +1041,23 @@ class ValidatePasswordAPI(BaseRpcView):
|
|||
return result, status.HTTP_200_OK
|
||||
|
||||
validate_password = ValidatePasswordAPI.as_view()
|
||||
|
||||
|
||||
class AddressAutocompleteAPI(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get(self, request):
|
||||
if not getattr(settings, 'ADDRESS_AUTOCOMPLETE_URL', None):
|
||||
return Response({})
|
||||
try:
|
||||
response = requests.get(
|
||||
settings.ADDRESS_AUTOCOMPLETE_URL,
|
||||
params=request.GET
|
||||
)
|
||||
response.raise_for_status()
|
||||
return Response(response.json())
|
||||
except RequestException:
|
||||
return Response({})
|
||||
|
||||
|
||||
address_autocomplete = AddressAutocompleteAPI.as_view()
|
||||
|
|
|
@ -23,8 +23,10 @@ import os
|
|||
from itertools import chain
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.urls import reverse
|
||||
from django.utils import six, formats
|
||||
from django.utils.translation import ugettext_lazy as _, pgettext_lazy
|
||||
from django.utils import html
|
||||
|
@ -110,6 +112,28 @@ class BirthdateRestField(DateRestField):
|
|||
]
|
||||
|
||||
|
||||
class AddressAutocompleteInput(forms.Select):
|
||||
template_name = 'authentic2/widgets/address_autocomplete.html'
|
||||
|
||||
class Media:
|
||||
js = [
|
||||
settings.SELECT2_JS,
|
||||
'authentic2/js/address_autocomplete.js',
|
||||
]
|
||||
css = {
|
||||
'screen': [settings.SELECT2_CSS],
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.attrs['data-select2-url'] = reverse('a2-api-address-autocomplete')
|
||||
self.attrs['class'] = 'address-autocomplete'
|
||||
|
||||
|
||||
class AddressAutocompleteField(forms.CharField):
|
||||
widget = AddressAutocompleteInput
|
||||
|
||||
|
||||
@to_iter
|
||||
def get_title_choices():
|
||||
return app_settings.A2_ATTRIBUTE_KIND_TITLE_CHOICES or DEFAULT_TITLE_CHOICES
|
||||
|
@ -269,6 +293,11 @@ DEFAULT_ATTRIBUTE_KINDS = [
|
|||
'rest_framework_field_class': BirthdateRestField,
|
||||
'free_text_search': date_free_text_search,
|
||||
},
|
||||
{
|
||||
'label': _('address (autocomplete)'),
|
||||
'name': 'address_auto',
|
||||
'field_class': AddressAutocompleteField,
|
||||
},
|
||||
{
|
||||
'label': _('french postcode'),
|
||||
'name': 'fr_postcode',
|
||||
|
|
|
@ -248,7 +248,8 @@ span.select2-container {
|
|||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.ui-dialog-content span.select2-container {
|
||||
.ui-dialog-content span.select2-container,
|
||||
form .widget span.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -319,6 +319,8 @@ class UserDetailView(OtherActionsMixin, BaseDetailView):
|
|||
if not self.object.username and self.object.ou and not self.object.ou.show_username:
|
||||
fields.remove('username')
|
||||
for attribute in Attribute.objects.all():
|
||||
if attribute.name == 'address_autocomplete':
|
||||
continue
|
||||
fields.append(attribute.name)
|
||||
if self.request.user.is_superuser and \
|
||||
'is_superuser' not in self.fields:
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
$(function() {
|
||||
$('select.address-autocomplete').select2({
|
||||
ajax: {
|
||||
delay: 250,
|
||||
dataType: 'json',
|
||||
data: function(params) {
|
||||
return {q: params.term, page_limit: 10};
|
||||
},
|
||||
processResults: function (data, params) {
|
||||
return {results: data.data};
|
||||
},
|
||||
url: function (params) {
|
||||
return $(this).data('select2-url')
|
||||
}
|
||||
}
|
||||
}).on('select2:select', function(e) {
|
||||
var data = e.params.data;
|
||||
if (data) {
|
||||
var address = undefined;
|
||||
if (typeof data.address == "object") {
|
||||
address = data.address;
|
||||
} else {
|
||||
address = data;
|
||||
}
|
||||
var road = address.road || address.nom_rue;
|
||||
var house_number = address.house_number || address.numero;
|
||||
var city = address.city || address.nom_commune;
|
||||
var postcode = address.postcode || address.code_postal;
|
||||
var number_and_street = null;
|
||||
if (house_number && road) {
|
||||
number_and_street = house_number + ' ' + road;
|
||||
} else {
|
||||
number_and_street = road;
|
||||
}
|
||||
$('#id_address').val(number_and_street);
|
||||
$('#id_city').val(city);
|
||||
$('#id_zipcode').val(postcode);
|
||||
}
|
||||
});
|
||||
$('#id_address, #id_city, #id_zipcode').attr('readonly', 'readonly');
|
||||
$('#manual-address').on('change', function() {
|
||||
$('#id_address, #id_city, #id_zipcode').attr('readonly', this.checked ? null : 'readonly');
|
||||
});
|
||||
if ($('#id_address').val() || $('#id_city').val() || $('#id_zipcode').val()) {
|
||||
var data = {
|
||||
id: 1,
|
||||
text: ''
|
||||
}
|
||||
$.each(['#id_address', '#id_zipcode', '#id_city'], function(idx, value) {
|
||||
if ($(value).val()) {
|
||||
if (data.text) {
|
||||
data.text += ' ';
|
||||
}
|
||||
data.text += $(value).val();
|
||||
}
|
||||
})
|
||||
var newOption = new Option(data.text, data.id, false, false);
|
||||
$('select.address-autocomplete').append(newOption).trigger('change');
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% include "django/forms/widgets/select.html" %}
|
||||
<div>
|
||||
<label><input id="manual-address" type="checkbox">{% trans "Manually enter the address" %}</label>
|
||||
</div>
|
|
@ -133,7 +133,6 @@ class EditProfile(cbv.HookMixin, cbv.TemplateNamesMixin, UpdateView):
|
|||
|
||||
def get_form_kwargs(self, **kwargs):
|
||||
kwargs = super(EditProfile, self).get_form_kwargs(**kwargs)
|
||||
kwargs['prefix'] = 'edit-profile'
|
||||
kwargs['next_url'] = utils.select_next_url(self.request, reverse('account_management'))
|
||||
return kwargs
|
||||
|
||||
|
@ -141,7 +140,7 @@ class EditProfile(cbv.HookMixin, cbv.TemplateNamesMixin, UpdateView):
|
|||
return utils.select_next_url(
|
||||
self.request,
|
||||
default=reverse('account_management'),
|
||||
field_name='edit-profile-next_url',
|
||||
field_name='next_url',
|
||||
include_post=True)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import datetime
|
||||
import json
|
||||
import mock
|
||||
import pytest
|
||||
import random
|
||||
import uuid
|
||||
|
@ -36,6 +37,7 @@ from django.utils.http import urlencode
|
|||
|
||||
from django_rbac.models import SEARCH_OP
|
||||
from django_rbac.utils import get_role_model, get_ou_model
|
||||
from requests.models import Response
|
||||
|
||||
from authentic2.a2_rbac.models import Role
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
|
@ -1926,3 +1928,48 @@ def test_find_duplicates_birthdate(app, admin, settings):
|
|||
resp = app.get('/api/users/find_duplicates/', params=params)
|
||||
assert len(resp.json['data']) == 2
|
||||
assert resp.json['data'][0]['id'] == homonym.pk
|
||||
|
||||
|
||||
class MockedRequestResponse(mock.Mock):
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
|
||||
def test_api_address_autocomplete(app, admin, settings):
|
||||
app.authorization = ('Basic', (admin.username, admin.username))
|
||||
|
||||
settings.ADDRESS_AUTOCOMPLETE_URL = 'example.com'
|
||||
|
||||
params = {'q': '42 avenue'}
|
||||
with mock.patch('authentic2.api_views.requests.get') as requests_get:
|
||||
mock_resp = Response()
|
||||
mock_resp.status_code = 500
|
||||
requests_get.return_value = mock_resp
|
||||
resp = app.get('/api/address-autocomplete/', params=params)
|
||||
assert resp.json == {}
|
||||
assert requests_get.call_args_list[0][0][0] == 'example.com'
|
||||
assert requests_get.call_args_list[0][1]['params'] == {'q': ['42 avenue']}
|
||||
with mock.patch('authentic2.api_views.requests.get') as requests_get:
|
||||
mock_resp = Response()
|
||||
mock_resp.status_code = 404
|
||||
requests_get.return_value = mock_resp
|
||||
resp = app.get('/api/address-autocomplete/', params=params)
|
||||
assert resp.json == {}
|
||||
with mock.patch('authentic2.api_views.requests.get') as requests_get:
|
||||
requests_get.return_value = MockedRequestResponse(content=json.dumps({'data': {'foo': 'bar'}}))
|
||||
resp = app.get('/api/address-autocomplete/', params=params)
|
||||
assert resp.json == {'data': {'foo': 'bar'}}
|
||||
|
||||
settings.ADDRESS_AUTOCOMPLETE_URL = None
|
||||
with mock.patch('authentic2.api_views.requests.get') as requests_get:
|
||||
resp = app.get('/api/address-autocomplete/', params=params)
|
||||
assert resp.json == {}
|
||||
assert requests_get.call_args_list == []
|
||||
|
||||
del settings.ADDRESS_AUTOCOMPLETE_URL
|
||||
with mock.patch('authentic2.api_views.requests.get') as requests_get:
|
||||
resp = app.get('/api/address-autocomplete/', params=params)
|
||||
assert resp.json == {}
|
||||
assert requests_get.call_args_list == []
|
||||
|
|
|
@ -456,9 +456,9 @@ def test_profile_image(db, app, admin, mailoutbox):
|
|||
# verify we can clear the image
|
||||
response = app.get('/accounts/edit/')
|
||||
form = response.form
|
||||
form.set('edit-profile-first_name', 'John')
|
||||
form.set('edit-profile-last_name', 'Doe')
|
||||
form.set('edit-profile-cityscape_image-clear', True)
|
||||
form.set('first_name', 'John')
|
||||
form.set('last_name', 'Doe')
|
||||
form.set('cityscape_image-clear', True)
|
||||
response = form.submit()
|
||||
assert john().attributes.cityscape_image == None
|
||||
|
||||
|
@ -470,7 +470,7 @@ def test_profile_image(db, app, admin, mailoutbox):
|
|||
# verify 201x201 image is accepted and resized
|
||||
response = app.get('/accounts/edit/')
|
||||
form = response.form
|
||||
form.set('edit-profile-cityscape_image', Upload('tests/201x201.jpg'))
|
||||
form.set('cityscape_image', Upload('tests/201x201.jpg'))
|
||||
response = form.submit()
|
||||
with PIL.Image.open(os.path.join(settings.MEDIA_ROOT, john().attributes.cityscape_image.name)) as image:
|
||||
assert image.width == 200
|
||||
|
@ -480,7 +480,7 @@ def test_profile_image(db, app, admin, mailoutbox):
|
|||
# verify file input mentions image files
|
||||
response = app.get('/accounts/edit/')
|
||||
form = response.form
|
||||
assert form['edit-profile-cityscape_image'].attrs['accept'] == 'image/*'
|
||||
assert form['cityscape_image'].attrs['accept'] == 'image/*'
|
||||
|
||||
|
||||
def test_multiple_attribute_setter(db, app, simple_user):
|
||||
|
|
|
@ -47,10 +47,10 @@ def test_account_edit_view(app, simple_user):
|
|||
kind='boolean', user_visible=True, user_editable=True)
|
||||
|
||||
resp = old_resp = app.get(url, status=200)
|
||||
resp.form['edit-profile-phone'] = '1234'
|
||||
assert resp.form['edit-profile-phone'].attrs['type'] == 'tel'
|
||||
resp.form['edit-profile-title'] = 'Mrs'
|
||||
resp.form['edit-profile-agreement'] = False
|
||||
resp.form['phone'] = '1234'
|
||||
assert resp.form['phone'].attrs['type'] == 'tel'
|
||||
resp.form['title'] = 'Mrs'
|
||||
resp.form['agreement'] = False
|
||||
resp = resp.form.submit()
|
||||
# verify that missing next_url in POST is ok
|
||||
assert resp['Location'].endswith(reverse('account_management'))
|
||||
|
@ -70,12 +70,12 @@ def test_account_edit_view(app, simple_user):
|
|||
]
|
||||
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('edit-profile-phone', '0123456789')
|
||||
resp.form.set('phone', '0123456789')
|
||||
resp = resp.form.submit().follow()
|
||||
assert phone.get_value(simple_user) == '0123456789'
|
||||
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('edit-profile-phone', '9876543210')
|
||||
resp.form.set('phone', '9876543210')
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
assert phone.get_value(simple_user) == '0123456789'
|
||||
|
||||
|
@ -83,16 +83,16 @@ def test_account_edit_view(app, simple_user):
|
|||
title.set_value(simple_user, 'Mr', verified=True)
|
||||
agreement.set_value(simple_user, True, verified=True)
|
||||
resp = app.get(url, status=200)
|
||||
assert 'edit-profile-phone' not in resp.form.fields
|
||||
assert 'edit-profile-title' not in resp.form.fields
|
||||
assert 'edit-profile-agreement' not in resp.form.fields
|
||||
assert 'readonly' in resp.form['edit-profile-phone@disabled'].attrs
|
||||
assert resp.form['edit-profile-phone@disabled'].value == '0123456789'
|
||||
assert resp.form['edit-profile-title@disabled'].value == 'Mr'
|
||||
assert resp.form['edit-profile-agreement@disabled'].value == 'Yes'
|
||||
resp.form.set('edit-profile-phone@disabled', '1234')
|
||||
resp.form.set('edit-profile-title@disabled', 'Mrs')
|
||||
resp.form.set('edit-profile-agreement@disabled', 'False')
|
||||
assert 'phone' not in resp.form.fields
|
||||
assert 'title' not in resp.form.fields
|
||||
assert 'agreement' not in resp.form.fields
|
||||
assert 'readonly' in resp.form['phone@disabled'].attrs
|
||||
assert resp.form['phone@disabled'].value == '0123456789'
|
||||
assert resp.form['title@disabled'].value == 'Mr'
|
||||
assert resp.form['agreement@disabled'].value == 'Yes'
|
||||
resp.form.set('phone@disabled', '1234')
|
||||
resp.form.set('title@disabled', 'Mrs')
|
||||
resp.form.set('agreement@disabled', 'False')
|
||||
resp = resp.form.submit().follow()
|
||||
assert phone.get_value(simple_user) == '0123456789'
|
||||
assert title.get_value(simple_user) == 'Mr'
|
||||
|
@ -106,9 +106,9 @@ def test_account_edit_view(app, simple_user):
|
|||
phone.disabled = True
|
||||
phone.save()
|
||||
resp = app.get(url, status=200)
|
||||
assert 'edit-profile-phone@disabled' not in resp
|
||||
assert 'edit-profile-title@disabled' in resp
|
||||
assert 'edit-profile-agreement@disabled' in resp
|
||||
assert 'phone@disabled' not in resp
|
||||
assert 'title@disabled' in resp
|
||||
assert 'agreement@disabled' in resp
|
||||
assert phone.get_value(simple_user) == '0123456789'
|
||||
|
||||
|
||||
|
@ -122,13 +122,13 @@ def test_account_edit_next_url(app, simple_user, external_redirect_next_url, ass
|
|||
user_editable=True)
|
||||
|
||||
resp = app.get(url + '?next=%s' % external_redirect_next_url, status=200)
|
||||
resp.form.set('edit-profile-phone', '0123456789')
|
||||
resp.form.set('phone', '0123456789')
|
||||
resp = resp.form.submit()
|
||||
assert_external_redirect(resp, reverse('account_management'))
|
||||
assert attribute.get_value(simple_user) == '0123456789'
|
||||
|
||||
resp = app.get(url + '?next=%s' % external_redirect_next_url, status=200)
|
||||
resp.form.set('edit-profile-phone', '1234')
|
||||
resp.form.set('phone', '1234')
|
||||
resp = resp.form.submit('cancel')
|
||||
assert_external_redirect(resp, reverse('account_management'))
|
||||
assert attribute.get_value(simple_user) == '0123456789'
|
||||
|
@ -153,8 +153,8 @@ def test_account_edit_scopes(app, simple_user):
|
|||
scopes='address')
|
||||
|
||||
def get_fields(resp):
|
||||
return set(key.split('edit-profile-')[1]
|
||||
for key in resp.form.fields.keys() if key and key.startswith('edit-profile-'))
|
||||
return set(key for key in resp.form.fields.keys() if key and key not in ['csrfmiddlewaretoken', 'cancel'])
|
||||
|
||||
resp = app.get(url, status=200)
|
||||
assert get_fields(resp) == set(['first_name', 'last_name', 'phone', 'mobile', 'city', 'zipcode', 'next_url'])
|
||||
|
||||
|
@ -185,15 +185,15 @@ def test_account_edit_locked_title(app, simple_user):
|
|||
utils.login(app, simple_user)
|
||||
url = reverse('profile_edit')
|
||||
response = app.get(url, status=200)
|
||||
assert len(response.pyquery('input[type="radio"][name="edit-profile-title"]')) == 2
|
||||
assert len(response.pyquery('input[type="radio"][name="edit-profile-title"][readonly="true"]')) == 0
|
||||
assert len(response.pyquery('select[name="edit-profile-title"]')) == 0
|
||||
assert len(response.pyquery('input[type="radio"][name="title"]')) == 2
|
||||
assert len(response.pyquery('input[type="radio"][name="title"][readonly="true"]')) == 0
|
||||
assert len(response.pyquery('select[name="title"]')) == 0
|
||||
|
||||
simple_user.verified_attributes.title = 'Monsieur'
|
||||
|
||||
response = app.get(url, status=200)
|
||||
assert len(response.pyquery('input[type="radio"][name="edit-profile-title"]')) == 0
|
||||
assert len(response.pyquery('input[type="text"][name="edit-profile-title@disabled"][readonly]')) == 1
|
||||
assert len(response.pyquery('input[type="radio"][name="title"]')) == 0
|
||||
assert len(response.pyquery('input[type="text"][name="title@disabled"][readonly]')) == 1
|
||||
|
||||
|
||||
def test_account_view(app, simple_user, settings):
|
||||
|
|
|
@ -379,13 +379,13 @@ def test_attribute_model(app, db, settings, mailoutbox):
|
|||
assert u'Prénom' not in response.text
|
||||
|
||||
response = app.get(reverse('profile_edit'))
|
||||
assert 'edit-profile-profession' in response.form.fields
|
||||
assert 'edit-profile-prenom' not in response.form.fields
|
||||
assert 'edit-profile-nom' not in response.form.fields
|
||||
assert 'profession' in response.form.fields
|
||||
assert 'prenom' not in response.form.fields
|
||||
assert 'nom' not in response.form.fields
|
||||
|
||||
assert response.pyquery('[for=id_edit-profile-profession]')
|
||||
assert not response.pyquery('[for=id_edit-profile-profession].form-field-required')
|
||||
response.form.set('edit-profile-profession', 'pompier')
|
||||
assert response.pyquery('[for=id_profession]')
|
||||
assert not response.pyquery('[for=id_profession].form-field-required')
|
||||
response.form.set('profession', 'pompier')
|
||||
response = response.form.submit()
|
||||
assert urlparse(response['Location']).path == reverse('account_management')
|
||||
|
||||
|
|
|
@ -733,6 +733,19 @@ def test_manager_edit_user_email_verified(app, simple_user, superuser_or_admin):
|
|||
assert not user.email_verified
|
||||
|
||||
|
||||
def test_manager_edit_user_address_autocomplete(app, simple_user, superuser_or_admin):
|
||||
url = u'/manage/users/%s/edit/' % simple_user.pk
|
||||
login(app, superuser_or_admin, '/manage/')
|
||||
|
||||
Attribute.objects.create(
|
||||
name='address_autocomplete', label='Address (autocomplete)',
|
||||
kind='address_auto', user_visible=True, user_editable=True)
|
||||
|
||||
resp = app.get(url)
|
||||
assert resp.html.find('select', {'name': 'address_autocomplete'})
|
||||
assert resp.html.find('input', {'id': 'manual-address'})
|
||||
|
||||
|
||||
def test_manager_email_verified_column_user(app, simple_user, superuser_or_admin):
|
||||
login(app, superuser_or_admin, '/manage/')
|
||||
|
||||
|
@ -793,6 +806,16 @@ def test_manager_user_username_field(app, superuser, simple_user):
|
|||
assert resp.html.find('input', {'name': 'username'})
|
||||
|
||||
|
||||
def test_manager_user_address_autocomplete_field(app, superuser, simple_user):
|
||||
login(app, superuser, '/manage/')
|
||||
Attribute.objects.create(
|
||||
name='address_autocomplete', label='Address (autocomplete)',
|
||||
kind='address_auto', user_visible=True, user_editable=True)
|
||||
resp = app.get(reverse('a2-manager-user-detail', kwargs={'pk': simple_user.id}))
|
||||
assert not resp.html.find('select', {'name': 'address_autocomplete'})
|
||||
assert not resp.html.find('input', {'id': 'manual-address'})
|
||||
|
||||
|
||||
def test_manager_user_roles_visibility(app, simple_user, admin, ou1, ou2):
|
||||
Role = get_role_model()
|
||||
role1 = Role.objects.create(name='Role 1', slug='role1', ou=ou1)
|
||||
|
|
Loading…
Reference in New Issue