support avatar picture in user profile (#26022)
This commit is contained in:
parent
e71b65b0cc
commit
a5d652ce81
|
@ -25,7 +25,8 @@ Depends: ${misc:Depends}, ${python:Depends},
|
|||
python-markdown (>= 2.1),
|
||||
python-ldap (>= 2.4),
|
||||
python-six (>= 1.0),
|
||||
python-django-filters (>= 1)
|
||||
python-django-filters (>= 1),
|
||||
python-pil
|
||||
Provides: ${python:Provides}
|
||||
Recommends: python-ldap
|
||||
Suggests: python-raven
|
||||
|
|
|
@ -27,7 +27,8 @@ Depends: ${misc:Depends}, ${python:Depends},
|
|||
python-jwcrypto (>= 0.3.1),
|
||||
python-cryptography (>= 1.3.4),
|
||||
python-django-filters (>= 1),
|
||||
python-django-filters (<< 2)
|
||||
python-django-filters (<< 2),
|
||||
python-pil
|
||||
Provides: ${python:Provides}
|
||||
Recommends: python-ldap
|
||||
Suggests: python-raven
|
||||
|
|
1
setup.py
1
setup.py
|
@ -131,6 +131,7 @@ setup(name="authentic2",
|
|||
'XStatic-jQuery',
|
||||
'XStatic-jquery-ui<1.12',
|
||||
'xstatic-select2',
|
||||
'pillow',
|
||||
],
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
|
|
|
@ -145,6 +145,7 @@ default_settings = dict(
|
|||
A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'),
|
||||
A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None),
|
||||
A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'),
|
||||
A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE=Setting(default=200, definition='Max width and height for a profile image'),
|
||||
A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'),
|
||||
A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'),
|
||||
A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import re
|
||||
import string
|
||||
import datetime
|
||||
import io
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from itertools import chain
|
||||
|
||||
|
@ -9,14 +12,17 @@ from django.core.exceptions import ValidationError
|
|||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import ugettext_lazy as _, pgettext_lazy
|
||||
from django.utils.functional import allow_lazy
|
||||
from django.utils import html
|
||||
from django.template.defaultfilters import capfirst
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .decorators import to_iter
|
||||
from .plugins import collect_from_plugins
|
||||
from . import app_settings
|
||||
from .forms import widgets
|
||||
from .forms import widgets, fields
|
||||
|
||||
capfirst = allow_lazy(capfirst, unicode)
|
||||
|
||||
|
@ -100,6 +106,54 @@ class FrPostcodeDRFField(serializers.CharField):
|
|||
default_validators = [validate_fr_postcode]
|
||||
|
||||
|
||||
class ProfileImageFile(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return default_storage.url(self.name)
|
||||
|
||||
|
||||
def profile_image_serialize(uploadedfile):
|
||||
if not uploadedfile:
|
||||
return ''
|
||||
if hasattr(uploadedfile, 'url'):
|
||||
return uploadedfile.name
|
||||
h_computation = hashlib.md5()
|
||||
for chunk in uploadedfile.chunks():
|
||||
h_computation.update(chunk)
|
||||
hexdigest = h_computation.hexdigest()
|
||||
stored_file = default_storage.save(
|
||||
os.path.join('profile-image', hexdigest),
|
||||
uploadedfile)
|
||||
return stored_file
|
||||
|
||||
|
||||
def profile_image_deserialize(name):
|
||||
if name:
|
||||
return ProfileImageFile(name)
|
||||
return None
|
||||
|
||||
|
||||
def profile_image_html_value(attribute, value):
|
||||
if value:
|
||||
fragment = u'<a href="%s"><img class="%s" src="%s"/></a>' % (
|
||||
value.url, attribute.name, value.url)
|
||||
return html.mark_safe(fragment)
|
||||
return ''
|
||||
|
||||
|
||||
def profile_attributes_ng_serialize(ctx, value):
|
||||
if value and getattr(value, 'url', None):
|
||||
request = ctx.get('request')
|
||||
if request:
|
||||
return request.build_absolute_uri(value.url)
|
||||
else:
|
||||
return value.url
|
||||
return None
|
||||
|
||||
|
||||
DEFAULT_ALLOW_BLANK = True
|
||||
DEFAULT_MAX_LENGTH = 256
|
||||
|
||||
|
@ -160,6 +214,20 @@ DEFAULT_ATTRIBUTE_KINDS = [
|
|||
'field_class': PhoneNumberField,
|
||||
'rest_framework_field_class': PhoneNumberDRFField,
|
||||
},
|
||||
{
|
||||
'label': _('profile image'),
|
||||
'name': 'profile_image',
|
||||
'field_class': fields.ProfileImageField,
|
||||
'serialize': profile_image_serialize,
|
||||
'deserialize': profile_image_deserialize,
|
||||
'rest_framework_field_class': serializers.FileField,
|
||||
'rest_framework_field_kwargs': {
|
||||
'read_only': True,
|
||||
'use_url': True,
|
||||
},
|
||||
'html_value': profile_image_html_value,
|
||||
'attributes_ng_serialize': profile_attributes_ng_serialize,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ def get_attribute_names(instance, ctx):
|
|||
|
||||
|
||||
def get_dependencies(instance, ctx):
|
||||
return ('user',)
|
||||
return ('user', 'request')
|
||||
|
||||
|
||||
def get_attributes(instance, ctx):
|
||||
|
@ -68,8 +68,11 @@ def get_attributes(instance, ctx):
|
|||
if user.ou:
|
||||
for attr in ('uuid', 'slug', 'name'):
|
||||
ctx['django_user_ou_' + attr] = getattr(user.ou, attr)
|
||||
for av in AttributeValue.objects.with_owner(user):
|
||||
ctx['django_user_' + str(av.attribute.name)] = av.to_python()
|
||||
for av in AttributeValue.objects.with_owner(user).select_related('attribute'):
|
||||
serialize = av.attribute.get_kind().get('attributes_ng_serialize', lambda a, b: b)
|
||||
value = av.to_python()
|
||||
serialized = serialize(ctx, value)
|
||||
ctx['django_user_' + str(av.attribute.name)] = serialized
|
||||
ctx['django_user_' + str(av.attribute.name) + ':verified'] = av.verified
|
||||
ctx['django_user_groups'] = [group for group in user.groups.all()]
|
||||
ctx['django_user_group_names'] = [unicode(group) for group in user.groups.all()]
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
from django.forms import CharField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import warnings
|
||||
import io
|
||||
|
||||
from django.forms import CharField, FileField, ValidationError
|
||||
from django.forms.fields import FILE_INPUT_CONTRADICTION
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.files import File
|
||||
|
||||
from authentic2 import app_settings
|
||||
from authentic2.passwords import password_help_text, validate_password
|
||||
from authentic2.forms.widgets import PasswordInput, NewPasswordInput, CheckPasswordInput
|
||||
from authentic2.forms.widgets import (PasswordInput, NewPasswordInput,
|
||||
CheckPasswordInput, ProfileImageInput)
|
||||
|
||||
import PIL.Image
|
||||
|
||||
|
||||
class PasswordField(CharField):
|
||||
|
@ -33,3 +42,46 @@ class CheckPasswordField(CharField):
|
|||
}
|
||||
super(CheckPasswordField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ProfileImageField(FileField):
|
||||
widget = ProfileImageInput
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault(
|
||||
'help_text',
|
||||
_('Image must be JPG or PNG of size less '
|
||||
'than {max_size}x{max_size} pixels').format(max_size=self.max_size))
|
||||
super(ProfileImageField, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def max_size(self):
|
||||
return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE
|
||||
|
||||
def clean(self, data, initial=None):
|
||||
if data is FILE_INPUT_CONTRADICTION or data is False or data is None:
|
||||
return super(ProfileImageField, self).clean(data, initial=initial)
|
||||
# we have a file
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
image = PIL.Image.open(io.BytesIO(data.read()))
|
||||
except (IOError, PIL.Image.DecompressionBombWarning):
|
||||
raise ValidationError(_('The image is not valid'))
|
||||
width, height = image.size
|
||||
max_size = app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE
|
||||
if width > max_size or height > max_size:
|
||||
raise ValidationError(_('The image is bigger than {max_size}x{max_size} pixels')
|
||||
.format(max_size=self.max_size))
|
||||
new_data = self.file_from_image(image, data.name)
|
||||
return super(ProfileImageField, self).clean(new_data, initial=initial)
|
||||
|
||||
def file_from_image(self, image, name=None):
|
||||
output = io.BytesIO()
|
||||
if image.mode != 'RGB':
|
||||
image = image.convert('RGB')
|
||||
image.save(
|
||||
output,
|
||||
format='JPEG',
|
||||
quality=99,
|
||||
optimize=1)
|
||||
output.seek(0)
|
||||
return File(output, name=name)
|
||||
|
|
|
@ -11,7 +11,9 @@ import json
|
|||
import re
|
||||
import uuid
|
||||
|
||||
from django.forms.widgets import DateTimeInput, DateInput, TimeInput
|
||||
import django
|
||||
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \
|
||||
ClearableFileInput
|
||||
from django.forms.widgets import PasswordInput as BasePasswordInput
|
||||
from django.utils.formats import get_language, get_format
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -246,3 +248,13 @@ class CheckPasswordInput(PasswordInput):
|
|||
json.dumps(_id),
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
class ProfileImageInput(ClearableFileInput):
|
||||
if django.VERSION < (1, 9):
|
||||
template_with_initial = (
|
||||
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> '
|
||||
'%(clear_template)s<br />%(input_text)s: %(input)s'
|
||||
)
|
||||
else:
|
||||
template_name = "authentic2/profile_image_input.html"
|
||||
|
|
|
@ -22,6 +22,8 @@ SECRET_KEY = 'please-change-me-with-a-very-long-random-string'
|
|||
DEBUG = False
|
||||
DEBUG_DB = False
|
||||
MEDIA = 'media'
|
||||
MEDIA_ROOT = 'media'
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = []
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<form enctype="multipart/form-data" method="post">
|
||||
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
{% if form.instance and form.instance.id %}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}"><img src="{{ widget.value.url }}"/></a>{% if not widget.required %}
|
||||
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" />
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br />
|
||||
{{ widget.input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
|
|
@ -15,7 +15,8 @@
|
|||
|
||||
<h2>{{ view.title }}</h2>
|
||||
|
||||
<form method="post">
|
||||
<form enctype="multipart/form-data" method="post">
|
||||
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="submit-button">{% trans 'Submit' %}</button>
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.conf.urls import url, include
|
|||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.views import serve
|
||||
from django.views.static import serve as media_serve
|
||||
|
||||
from . import app_settings, plugins, views
|
||||
|
||||
|
@ -44,6 +45,10 @@ if settings.DEBUG:
|
|||
urlpatterns += [
|
||||
url(r'^static/(?P<path>.*)$', serve)
|
||||
]
|
||||
urlpatterns += [
|
||||
url(r'^media/(?P<path>.*)$', media_serve, {
|
||||
'document_root': settings.MEDIA_ROOT})
|
||||
]
|
||||
|
||||
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
|
|
|
@ -444,11 +444,13 @@ class ProfileView(cbv.TemplateNamesMixin, TemplateView):
|
|||
if attribute:
|
||||
if not attribute.user_visible:
|
||||
continue
|
||||
html_value = attribute.get_kind().get('html_value', lambda a, b: b)
|
||||
qs = models.AttributeValue.objects.with_owner(request.user)
|
||||
qs = qs.filter(attribute=attribute)
|
||||
qs = qs.select_related()
|
||||
value = [at_value.to_python() for at_value in qs]
|
||||
value = filter(None, value)
|
||||
value = [html_value(attribute, at_value) for at_value in value]
|
||||
if not title:
|
||||
title = unicode(attribute)
|
||||
else:
|
||||
|
|
|
@ -161,14 +161,17 @@ def normalize_claim_values(values):
|
|||
return values_list
|
||||
|
||||
|
||||
def create_user_info(client, user, scope_set, id_token=False):
|
||||
def create_user_info(request, client, user, scope_set, id_token=False):
|
||||
'''Create user info dictionnary'''
|
||||
user_info = {
|
||||
'sub': make_sub(client, user)
|
||||
}
|
||||
attributes = get_attributes({
|
||||
'user': user, 'request': None, 'service': client,
|
||||
'__wanted_attributes': client.get_wanted_attributes()})
|
||||
'user': user,
|
||||
'request': request,
|
||||
'service': client,
|
||||
'__wanted_attributes': client.get_wanted_attributes(),
|
||||
})
|
||||
for claim in client.oidcclaim_set.filter(name__isnull=False):
|
||||
if not set(claim.get_scopes()).intersection(scope_set):
|
||||
continue
|
||||
|
|
|
@ -279,7 +279,11 @@ def authorize(request, *args, **kwargs):
|
|||
acr = '0'
|
||||
if nonce is not None and last_auth.get('nonce') == nonce:
|
||||
acr = '1'
|
||||
id_token = utils.create_user_info(client, request.user, scopes, id_token=True)
|
||||
id_token = utils.create_user_info(request,
|
||||
client,
|
||||
request.user,
|
||||
scopes,
|
||||
id_token=True)
|
||||
id_token.update({
|
||||
'iss': utils.get_issuer(request),
|
||||
'aud': client.client_id,
|
||||
|
@ -386,7 +390,12 @@ def token(request, *args, **kwargs):
|
|||
oidc_code.nonce):
|
||||
acr = '1'
|
||||
# prefill id_token with user info
|
||||
id_token = utils.create_user_info(client, oidc_code.user, oidc_code.scope_set(), id_token=True)
|
||||
id_token = utils.create_user_info(
|
||||
request,
|
||||
client,
|
||||
oidc_code.user,
|
||||
oidc_code.scope_set(),
|
||||
id_token=True)
|
||||
id_token.update({
|
||||
'iss': utils.get_issuer(request),
|
||||
'sub': utils.make_sub(client, oidc_code.user),
|
||||
|
@ -430,7 +439,9 @@ def user_info(request, *args, **kwargs):
|
|||
access_token = authenticate_access_token(request)
|
||||
if access_token is None:
|
||||
return HttpResponse('unauthenticated', status=401)
|
||||
user_info = utils.create_user_info(access_token.client, access_token.user,
|
||||
user_info = utils.create_user_info(request,
|
||||
access_token.client,
|
||||
access_token.user,
|
||||
access_token.scope_set())
|
||||
return HttpResponse(json.dumps(user_info), content_type='application/json')
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 317 B |
Binary file not shown.
After Width: | Height: | Size: 330 B |
|
@ -339,3 +339,8 @@ def french_translation():
|
|||
activate('fr')
|
||||
yield
|
||||
deactivate()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def media(settings, tmpdir):
|
||||
settings.MEDIA_ROOT = str(tmpdir.mkdir('media'))
|
||||
|
|
|
@ -5,6 +5,7 @@ from authentic2.custom_user.models import User
|
|||
from authentic2.models import Attribute
|
||||
|
||||
from utils import get_link_from_mail
|
||||
from webtest import Upload
|
||||
|
||||
|
||||
def test_string(db, app, admin, mailoutbox):
|
||||
|
@ -369,3 +370,66 @@ def test_birthdate_api(db, app, admin, mailoutbox, freezer):
|
|||
app.post_json('/api/users/', params=payload)
|
||||
assert qs.get().attributes.birthdate == datetime.date(1900, 1, 1)
|
||||
qs.delete()
|
||||
|
||||
|
||||
def test_profile_image(db, app, admin, mailoutbox, media):
|
||||
Attribute.objects.create(name='cityscape_image', label='cityscape', kind='profile_image',
|
||||
asked_on_registration=True, required=False,
|
||||
user_visible=True, user_editable=True)
|
||||
|
||||
def john():
|
||||
return User.objects.get(first_name='John')
|
||||
|
||||
response = app.get('/accounts/register/')
|
||||
form = response.form
|
||||
form.set('email', 'john.doe@example.com')
|
||||
response = form.submit().follow()
|
||||
assert 'john.doe@example.com' in response
|
||||
url = get_link_from_mail(mailoutbox[0])
|
||||
response = app.get(url)
|
||||
|
||||
# verify empty file is refused
|
||||
form = response.form
|
||||
form.set('first_name', 'John')
|
||||
form.set('last_name', 'Doe')
|
||||
form.set('cityscape_image', Upload('/dev/null'))
|
||||
form.set('password1', '12345abcdA')
|
||||
form.set('password2', '12345abcdA')
|
||||
response = form.submit()
|
||||
assert response.pyquery.find('.form-field-error #id_cityscape_image')
|
||||
|
||||
# verify 201x201 image is refused
|
||||
form = response.form
|
||||
form.set('cityscape_image', Upload('tests/201x201.jpg'))
|
||||
form.set('password1', '12345abcdA')
|
||||
form.set('password2', '12345abcdA')
|
||||
response = form.submit()
|
||||
assert response.pyquery.find('.form-field-error #id_cityscape_image')
|
||||
|
||||
# verify 200x200 image is accepted
|
||||
form = response.form
|
||||
form.set('cityscape_image', Upload('tests/200x200.jpg'))
|
||||
form.set('password1', '12345abcdA')
|
||||
form.set('password2', '12345abcdA')
|
||||
response = form.submit()
|
||||
assert john().attributes.cityscape_image
|
||||
|
||||
# verify API serves absolute URL for profile images
|
||||
app.authorization = ('Basic', (admin.username, admin.username))
|
||||
response = app.get('/api/users/%s/' % john().uuid)
|
||||
assert response.json['cityscape_image'] == 'http://testserver/media/%s' % john().attributes.cityscape_image.name
|
||||
app.authorization = None
|
||||
|
||||
# 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)
|
||||
response = form.submit()
|
||||
assert john().attributes.cityscape_image == None
|
||||
|
||||
# verify API serves absolute URL for profile images
|
||||
app.authorization = ('Basic', (admin.username, admin.username))
|
||||
response = app.get('/api/users/%s/' % john().uuid)
|
||||
assert response.json['cityscape_image'] is None
|
||||
|
|
|
@ -11,6 +11,7 @@ from jwcrypto.jwk import JWKSet, JWK
|
|||
import utils
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.files import File
|
||||
from django.db import connection
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.utils.timezone import now
|
||||
|
@ -19,6 +20,7 @@ from django.contrib.auth import get_user_model
|
|||
|
||||
User = get_user_model()
|
||||
|
||||
from authentic2.models import Attribute
|
||||
from authentic2_idp_oidc.models import OIDCClient, OIDCAuthorization, OIDCCode, OIDCAccessToken, OIDCClaim
|
||||
from authentic2_idp_oidc.utils import make_sub
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
|
@ -98,7 +100,16 @@ OIDC_CLIENT_PARAMS = [
|
|||
|
||||
|
||||
@pytest.fixture(params=OIDC_CLIENT_PARAMS)
|
||||
def oidc_client(request, superuser, app):
|
||||
def oidc_client(request, superuser, app, simple_user, media):
|
||||
Attribute.objects.create(
|
||||
name='cityscape_image',
|
||||
label='cityscape',
|
||||
kind='profile_image',
|
||||
asked_on_registration=True,
|
||||
required=False,
|
||||
user_visible=True,
|
||||
user_editable=True)
|
||||
|
||||
url = reverse('admin:authentic2_idp_oidc_oidcclient_add')
|
||||
assert OIDCClient.objects.count() == 0
|
||||
response = utils.login(app, superuser, path=url)
|
||||
|
@ -256,11 +267,19 @@ def test_authorization_code_sso(login_first, oidc_settings, oidc_client, simple_
|
|||
# when adding extra attributes
|
||||
OIDCClaim.objects.create(client=oidc_client, name='ou', value='django_user_ou_name', scopes='profile')
|
||||
OIDCClaim.objects.create(client=oidc_client, name='roles', value='a2_role_names', scopes='profile, role')
|
||||
OIDCClaim.objects.create(client=oidc_client,
|
||||
name='cityscape_image',
|
||||
value='django_user_cityscape_image',
|
||||
scopes='profile')
|
||||
simple_user.roles.add(get_role_model().objects.create(
|
||||
name='Whatever', slug='whatever', ou=get_default_ou()))
|
||||
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
|
||||
assert response.json['ou'] == simple_user.ou.name
|
||||
assert response.json['roles'][0] == 'Whatever'
|
||||
assert response.json.get('cityscape_image') is None
|
||||
simple_user.attributes.cityscape_image = File(open('tests/200x200.jpg'))
|
||||
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
|
||||
assert response.json['cityscape_image'].startswith('http://testserver/media/profile-image/')
|
||||
|
||||
# check against a user without username
|
||||
simple_user.username = None
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
import datetime
|
||||
import base64
|
||||
import unittest
|
||||
|
@ -9,6 +10,7 @@ from django.test import Client
|
|||
from django.test.utils import override_settings
|
||||
from django.contrib.auth import get_user_model, REDIRECT_FIELD_NAME
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.files import File
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from authentic2.saml import models as saml_models
|
||||
|
@ -85,6 +87,10 @@ class SamlBaseTestCase(Authentic2TestCase):
|
|||
self.code_attribute = Attribute.objects.create(kind='string', name='code', label='Code')
|
||||
self.mobile_attribute = Attribute.objects.create(kind='string', name='mobile',
|
||||
label='Mobile')
|
||||
self.avatar_attribute = Attribute.objects.create(
|
||||
kind='profile_image',
|
||||
name='avatar',
|
||||
label='Avatar')
|
||||
self.user = get_user_model().objects.create(
|
||||
email=self.email,
|
||||
username=self.username,
|
||||
|
@ -92,6 +98,7 @@ class SamlBaseTestCase(Authentic2TestCase):
|
|||
last_name=self.last_name)
|
||||
self.code_attribute.set_value(self.user, '1234', verified=True)
|
||||
self.mobile_attribute.set_value(self.user, '5678', verified=True)
|
||||
self.avatar_attribute.set_value(self.user, File(open('tests/200x200.jpg')))
|
||||
self.user.set_password(self.password)
|
||||
self.user.save()
|
||||
self.default_ou = OrganizationalUnit.objects.get()
|
||||
|
@ -154,6 +161,11 @@ class SamlBaseTestCase(Authentic2TestCase):
|
|||
name='verified_attributes',
|
||||
friendly_name='Verified attributes',
|
||||
attribute_name='@verified_attributes@')
|
||||
self.liberty_provider.attributes.create(
|
||||
name_format='basic',
|
||||
name='avatar',
|
||||
friendly_name='Avatar',
|
||||
attribute_name='django_user_avatar')
|
||||
self.role_authorized = Role.objects.create(name='PC Delta', slug='pc-delta')
|
||||
self.liberty_provider.unauthorized_url = 'https://whatever.com/loser/'
|
||||
self.liberty_provider.save()
|
||||
|
@ -406,6 +418,13 @@ class SamlSSOTestCase(SamlBaseTestCase):
|
|||
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='mobile']/"
|
||||
"saml:AttributeValue", '5678'),
|
||||
|
||||
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/"
|
||||
"@NameFormat", lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC),
|
||||
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/"
|
||||
"@FriendlyName", 'Avatar'),
|
||||
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/"
|
||||
"saml:AttributeValue", re.compile('^http://testserver/media/profile-image/.*$')),
|
||||
|
||||
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='verified_attributes']/"
|
||||
"@NameFormat", lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC),
|
||||
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='verified_attributes']/"
|
||||
|
|
|
@ -121,6 +121,11 @@ class Authentic2TestCase(TestCase):
|
|||
self.assertEqual(set(values), content)
|
||||
elif isinstance(content, list):
|
||||
self.assertEqual(values, content)
|
||||
elif hasattr(content, 'pattern'):
|
||||
for value in values:
|
||||
self.assertRegexpMatches(
|
||||
value, content,
|
||||
msg='xpath %s does not match regexp %s' % (xpath, content.pattern))
|
||||
else:
|
||||
raise NotImplementedError('comparing xpath result to type %s: %r is not '
|
||||
'implemented' % (type(content), content))
|
||||
|
|
Loading…
Reference in New Issue