0001-api-allow-changing-profile-image-52949.patch
src/authentic2/attribute_kinds.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import base64 |
|
18 |
import binascii |
|
17 | 19 |
import datetime |
18 | 20 |
import hashlib |
19 | 21 |
import os |
20 | 22 |
import re |
21 | 23 |
import string |
24 |
import urllib |
|
25 |
import uuid |
|
22 | 26 |
from itertools import chain |
23 | 27 | |
24 | 28 |
from django import forms |
25 | 29 |
from django.conf import settings |
26 | 30 |
from django.core.exceptions import ValidationError |
27 | 31 |
from django.core.files.storage import default_storage |
32 |
from django.core.files.uploadedfile import SimpleUploadedFile |
|
28 | 33 |
from django.core.validators import RegexValidator |
29 | 34 |
from django.db.models import query |
30 | 35 |
from django.urls import reverse, reverse_lazy |
... | ... | |
34 | 39 |
from django.utils.translation import ugettext_lazy as _ |
35 | 40 |
from gadjo.templatetags.gadjo import xstatic |
36 | 41 |
from rest_framework import serializers |
42 |
from rest_framework.exceptions import ValidationError |
|
37 | 43 |
from rest_framework.fields import empty |
38 | 44 | |
39 | 45 |
from . import app_settings |
... | ... | |
241 | 247 |
return None |
242 | 248 | |
243 | 249 | |
250 |
class Base64ImageField(serializers.ImageField): |
|
251 |
def to_internal_value(self, data): |
|
252 |
if data == '': |
|
253 |
return None |
|
254 | ||
255 |
if isinstance(data, str): |
|
256 |
if ';base64,' in data: |
|
257 |
data = data.split(';base64,')[1] |
|
258 |
try: |
|
259 |
decoded_file = base64.b64decode(data, validate=True) |
|
260 |
except binascii.Error: |
|
261 |
raise ValidationError(_('Invalid base64 encoding.')) |
|
262 | ||
263 |
data = SimpleUploadedFile(name=str(uuid.uuid4()), content=decoded_file) |
|
264 | ||
265 |
return super().to_internal_value(data) |
|
266 | ||
267 | ||
244 | 268 |
def date_free_text_search(term): |
245 | 269 |
for date_format in formats.get_format('DATE_INPUT_FORMATS'): |
246 | 270 |
try: |
... | ... | |
322 | 346 |
'field_class': fields.ProfileImageField, |
323 | 347 |
'serialize': profile_image_serialize, |
324 | 348 |
'deserialize': profile_image_deserialize, |
325 |
'rest_framework_field_class': serializers.FileField,
|
|
349 |
'rest_framework_field_class': Base64ImageField,
|
|
326 | 350 |
'rest_framework_field_kwargs': { |
327 |
'read_only': True, |
|
328 | 351 |
'use_url': True, |
352 |
'allow_empty_file': True, |
|
353 |
'_DjangoImageField': fields.ProfileImageField, |
|
329 | 354 |
}, |
330 | 355 |
'html_value': profile_image_html_value, |
331 | 356 |
'attributes_ng_serialize': profile_attributes_ng_serialize, |
tests/test_attribute_kinds.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import base64 |
|
17 | 18 |
import datetime |
18 | 19 |
import os |
19 | 20 | |
... | ... | |
440 | 441 |
form = response.form |
441 | 442 |
assert form['cityscape_image'].attrs['accept'] == 'image/*' |
442 | 443 | |
444 |
# clear image via API, by putting empty JSON |
|
445 |
response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': ''}) |
|
446 |
assert john().attributes.cityscape_image == None |
|
447 | ||
448 |
# put back first image via API, by putting base64 encoded JSON |
|
449 |
with open('tests/200x200.jpg', 'rb') as f: |
|
450 |
image = f.read() |
|
451 |
base64_image = base64.b64encode(image).decode() |
|
452 |
response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': base64_image}) |
|
453 |
assert john().attributes.cityscape_image |
|
454 |
profile_filename = john().attributes.cityscape_image.name |
|
455 |
assert profile_filename.endswith('.jpeg') |
|
456 | ||
457 |
# verify 201x201 image is accepted and resized by API |
|
458 |
with open('tests/201x201.jpg', 'rb') as f: |
|
459 |
image = f.read() |
|
460 |
base64_image = base64.b64encode(image).decode() |
|
461 |
response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': base64_image}) |
|
462 |
with PIL.Image.open(os.path.join(settings.MEDIA_ROOT, john().attributes.cityscape_image.name)) as image: |
|
463 |
assert image.width == 200 |
|
464 |
assert image.height == 200 |
|
465 |
assert john().attributes.cityscape_image.name != profile_filename |
|
466 | ||
467 |
# put back first image via API, by putting data URI representing a base64 encoded image using JSON |
|
468 |
data_uri = 'data:%s;base64,%s' % ('image/jpeg', base64_image) |
|
469 |
response = app.put_json('/api/users/%s/' % john().uuid, params={'cityscape_image': data_uri}) |
|
470 |
assert john().attributes.cityscape_image |
|
471 |
profile_filename = john().attributes.cityscape_image.name |
|
472 |
assert profile_filename.endswith('.jpeg') |
|
473 | ||
474 |
# bad request on invalid b64 |
|
475 |
response = app.put_json( |
|
476 |
'/api/users/%s/' % john().uuid, params={'cityscape_image': 'invalid_64'}, status=400 |
|
477 |
) |
|
478 | ||
479 |
# clear image via API, not using JSON |
|
480 |
response = app.put('/api/users/%s/' % john().uuid, params={'cityscape_image': ''}) |
|
481 |
assert john().attributes.cityscape_image == None |
|
482 | ||
483 |
# put back first image via API, not using JSON |
|
484 |
response = app.put( |
|
485 |
'/api/users/%s/' % john().uuid, params={'cityscape_image': Upload('tests/200x200.jpg')} |
|
486 |
) |
|
487 |
assert john().attributes.cityscape_image |
|
488 |
profile_filename = john().attributes.cityscape_image.name |
|
489 |
assert profile_filename.endswith('.jpeg') |
|
490 | ||
443 | 491 | |
444 | 492 |
def test_multiple_attribute_setter(db, app, simple_user): |
445 | 493 |
nicks = Attribute.objects.create( |
446 |
- |