From 07d80b7945876ff946f1b91696686c3d03fd856d Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 19 Jul 2021 15:37:17 +0200 Subject: [PATCH] api: allow changing profile image (#52949) --- src/authentic2/attribute_kinds.py | 29 +++++++++++++++++-- tests/test_attribute_kinds.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/authentic2/attribute_kinds.py b/src/authentic2/attribute_kinds.py index 2987fa22..4c4efee1 100644 --- a/src/authentic2/attribute_kinds.py +++ b/src/authentic2/attribute_kinds.py @@ -14,17 +14,22 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import base64 +import binascii import datetime import hashlib import os import re import string +import urllib +import uuid from itertools import chain from django import forms from django.conf import settings from django.core.exceptions import ValidationError from django.core.files.storage import default_storage +from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import RegexValidator from django.db.models import query from django.urls import reverse, reverse_lazy @@ -34,6 +39,7 @@ from django.utils.translation import pgettext_lazy from django.utils.translation import ugettext_lazy as _ from gadjo.templatetags.gadjo import xstatic from rest_framework import serializers +from rest_framework.exceptions import ValidationError from rest_framework.fields import empty from . import app_settings @@ -241,6 +247,24 @@ def profile_attributes_ng_serialize(ctx, value): return None +class Base64ImageField(serializers.ImageField): + def to_internal_value(self, data): + if data == '': + return None + + if isinstance(data, str): + if ';base64,' in data: + data = data.split(';base64,')[1] + try: + decoded_file = base64.b64decode(data, validate=True) + except binascii.Error: + raise ValidationError(_('Invalid base64 encoding.')) + + data = SimpleUploadedFile(name=str(uuid.uuid4()), content=decoded_file) + + return super().to_internal_value(data) + + def date_free_text_search(term): for date_format in formats.get_format('DATE_INPUT_FORMATS'): try: @@ -322,10 +346,11 @@ DEFAULT_ATTRIBUTE_KINDS = [ 'field_class': fields.ProfileImageField, 'serialize': profile_image_serialize, 'deserialize': profile_image_deserialize, - 'rest_framework_field_class': serializers.FileField, + 'rest_framework_field_class': Base64ImageField, 'rest_framework_field_kwargs': { - 'read_only': True, 'use_url': True, + 'allow_empty_file': True, + '_DjangoImageField': fields.ProfileImageField, }, 'html_value': profile_image_html_value, 'attributes_ng_serialize': profile_attributes_ng_serialize, diff --git a/tests/test_attribute_kinds.py b/tests/test_attribute_kinds.py index 43d6527e..e04a60ec 100644 --- a/tests/test_attribute_kinds.py +++ b/tests/test_attribute_kinds.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import base64 import datetime import os @@ -440,6 +441,53 @@ def test_profile_image(db, app, admin, mailoutbox): form = response.form assert form['cityscape_image'].attrs['accept'] == 'image/*' + # clear image via API, by putting empty JSON + response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': ''}) + assert john().attributes.cityscape_image == None + + # put back first image via API, by putting base64 encoded JSON + with open('tests/200x200.jpg', 'rb') as f: + image = f.read() + base64_image = base64.b64encode(image).decode() + response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': base64_image}) + assert john().attributes.cityscape_image + profile_filename = john().attributes.cityscape_image.name + assert profile_filename.endswith('.jpeg') + + # verify 201x201 image is accepted and resized by API + with open('tests/201x201.jpg', 'rb') as f: + image = f.read() + base64_image = base64.b64encode(image).decode() + response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': base64_image}) + with PIL.Image.open(os.path.join(settings.MEDIA_ROOT, john().attributes.cityscape_image.name)) as image: + assert image.width == 200 + assert image.height == 200 + assert john().attributes.cityscape_image.name != profile_filename + + # put back first image via API, by putting data URI representing a base64 encoded image using JSON + data_uri = 'data:%s;base64,%s' % ('image/jpeg', base64_image) + response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': data_uri}) + assert john().attributes.cityscape_image + profile_filename = john().attributes.cityscape_image.name + assert profile_filename.endswith('.jpeg') + + # bad request on invalid b64 + response = app.put_json( + '/api/users/%s/' % john().uuid, params={'cityscape_image': 'invalid_64'}, status=400 + ) + + # clear image via API, not using JSON + response = app.put('/api/users/%s/' % john().uuid, params={'cityscape_image': ''}) + assert john().attributes.cityscape_image == None + + # put back first image via API, not using JSON + response = app.put( + '/api/users/%s/' % john().uuid, params={'cityscape_image': Upload('tests/200x200.jpg')} + ) + assert john().attributes.cityscape_image + profile_filename = john().attributes.cityscape_image.name + assert profile_filename.endswith('.jpeg') + def test_multiple_attribute_setter(db, app, simple_user): nicks = Attribute.objects.create( -- 2.20.1