From 50cf7ca5280518c0f152569b033cb62d095fd13e Mon Sep 17 00:00:00 2001 From: vargi Date: Wed, 16 Jul 2014 19:11:06 +0300 Subject: [PATCH 1/3] Base64ImageField and tests are implemented --- rest_framework/fields.py | 49 +++++++++++++++++++++++++++ rest_framework/tests/test_fields.py | 52 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6caae9242..489b9cda2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -5,19 +5,25 @@ They are very similar to Django's form fields. """ from __future__ import unicode_literals +import base64 import copy import datetime +import imghdr import inspect import re +import uuid import warnings from decimal import Decimal, DecimalException from django import forms + +from django.core.files.base import ContentFile from django.core import validators from django.core.exceptions import ValidationError from django.conf import settings from django.db.models.fields import BLANK_CHOICE_DASH from django.http import QueryDict from django.forms import widgets + from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from django.utils.datastructures import SortedDict @@ -1038,3 +1044,46 @@ class SerializerMethodField(Field): def field_to_native(self, obj, field_name): value = getattr(self.parent, self.method_name)(obj) return self.to_native(value) + + +DEFAULT_CONTENT_TYPE = "application/octet-stream" +ALLOWED_IMAGE_TYPES = ( + "jpeg", + "jpg", + "png", + "gif" + ) + + +class Base64ImageField(ImageField): + """ + A django-rest-framework field for handling image-uploads through raw post data. + It uses base64 for en-/decoding the contents of the file. + """ + def from_native(self, base64_data): + # Check if this is a base64 string + if isinstance(base64_data, basestring): + # Try to decode the file. Return validation error if it fails. + try: + decoded_file = base64.b64decode(base64_data) + except TypeError: + raise ValidationError(_("Please upload a valid image.")) + # Generate file name: + file_name = str(uuid.uuid4())[:12] # 12 characters are more than enough. + # Get the file name extension: + file_extension = self.get_file_extension(file_name, decoded_file) + if file_extension not in ALLOWED_IMAGE_TYPES: + raise ValidationError(_("The type of the image couldn't been determined.")) + complete_file_name = file_name + "." + file_extension + data = ContentFile(decoded_file, name=complete_file_name) + return super(Base64ImageField, self).from_native(data) + raise ValidationError(_('This is not an base64 string')) + + def to_native(self, value): + # Return url including domain name. + return "" + + def get_file_extension(self, filename, decoded_file): + extension = imghdr.what(filename, decoded_file) + extension = "jpg" if extension == "jpeg" else extension + return extension \ No newline at end of file diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index b04b947f2..f0690e965 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -1002,3 +1002,55 @@ class BooleanField(TestCase): bool_field = serializers.BooleanField(required=True) self.assertFalse(BooleanRequiredSerializer(data={}).is_valid()) + + +class UploadedBase64Image(object): + def __init__(self, file=None, created=None): + self.file = file + self.created = created or datetime.datetime.now() + + +class UploadedBase64ImageSerializer(serializers.Serializer): + file = serializers.Base64ImageField() + created = serializers.DateTimeField() + + def restore_object(self, attrs, instance=None): + if instance: + instance.file = attrs['file'] + instance.created = attrs['created'] + return instance + return UploadedBase64Image(**attrs) + + +class Base64ImageSerializerTests(TestCase): + + def test_create(self): + now = datetime.datetime.now() + file = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + serializer = UploadedBase64ImageSerializer(data={'created': now, 'file': file}) + uploaded_image = UploadedBase64Image(file=file, created=now) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.object.created, uploaded_image.created) + self.assertFalse(serializer.object is uploaded_image) + + + def test_creation_failure(self): + """ + Passing file=None should result in an ValidationError + """ + errmsg = 'This field is required.' + now = datetime.datetime.now() + serializer = UploadedBase64ImageSerializer(data={'created': now}) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {'file': [errmsg]}) + + def test_validation_error_with_non_file(self): + """ + Passing non-base64 should raise a validation error. + """ + now = datetime.datetime.now() + errmsg = "Please upload a valid image." + + serializer = UploadedBase64ImageSerializer(data={'created': now, 'file': 'abc'}) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {'file': [errmsg]}) \ No newline at end of file From 92566cce565399d84f17b539e2aa4433530490c0 Mon Sep 17 00:00:00 2001 From: vargi Date: Thu, 17 Jul 2014 16:06:15 +0300 Subject: [PATCH 2/3] documentation for Base64ImageField and test for blank case --- docs/api-guide/fields.md | 27 ++++++++++++++++++++++ env/share/man/man1/ipcluster.1.gz | Bin 0 -> 350 bytes env/share/man/man1/ipcontroller.1.gz | Bin 0 -> 353 bytes env/share/man/man1/ipengine.1.gz | Bin 0 -> 343 bytes env/share/man/man1/ipython.1.gz | Bin 0 -> 1039 bytes rest_framework/fields.py | 5 ++++- rest_framework/tests/test_fields.py | 32 +++++++++++++++------------ 7 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 env/share/man/man1/ipcluster.1.gz create mode 100644 env/share/man/man1/ipcontroller.1.gz create mode 100644 env/share/man/man1/ipengine.1.gz create mode 100644 env/share/man/man1/ipython.1.gz diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 58dbf977e..86775d061 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -299,6 +299,7 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is Signature and validation is the same as with `FileField`. + --- **Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads. @@ -306,6 +307,32 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. --- + +## Base64ImageField + +An image representation for Base64ImageField + +Intherited by `ImageField` + +**Signature:** `Base64ImageField()` + + - It takes a base64 image as a string. + - a base64 image: `data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7` + - Base64ImageField accepts only the part after base64, `R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7` + + **Example:** + + #serializer + class UploadedBase64ImageSerializer(serializers.Serializer): + file = serializers.Base64ImageField(required=False) + created = serializers.DateTimeField() + + #use the serializer + file = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + serializer = UploadedBase64ImageSerializer(data={'created': now, 'file': file}) + + + # Custom fields If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects. diff --git a/env/share/man/man1/ipcluster.1.gz b/env/share/man/man1/ipcluster.1.gz new file mode 100644 index 0000000000000000000000000000000000000000..70cc7ff4af723688799d947e6052edb520a15749 GIT binary patch literal 350 zcmV-k0ipgMiwFoX{Craa18Hz$Y;|*VWpXYt0Bup-Zh|lvz3)?;aHC6*h;N{iMHVN7 z;>9v{QM5{v(vng%zWplxE?e?7X}_OyzH=Jn3&iQ`a!vCnLkRD^Qyk&g$7CE%Jb33I z0bO7<{fJ1hn~7#4yVi1pVuaQ*W5IAs?X+=lCtGQSh8ZR$m)PjK>8w!u9*2Iel9a}j zkYI)fSlgAZuV!jXf-J@tbYq29Ext)VHjn6Q7N>cftO&iZsyo#){a@OEkT+MTwc!xT z_0;SP4HuO@!fJFaf2`u8Z5mOepIP-=aLhR{U<_oFBN&RHFhA8hADOC16L06Lhh;Q#;t literal 0 HcmV?d00001 diff --git a/env/share/man/man1/ipcontroller.1.gz b/env/share/man/man1/ipcontroller.1.gz new file mode 100644 index 0000000000000000000000000000000000000000..212303ed4205f26f1bcdcc5a0c431c1cf6df0b25 GIT binary patch literal 353 zcmV-n0iOOJiwFoX{Craa18Hz$Z*FvQZ)|L3axO6dZBff^!Y~lL`zuztK&227{(wg+ zQqd%e(?dmiFt89yjvd7gh`;X=Xa%&=TC!*D*_m-rEFsS3X;S3rY8B-Wg7?;GgmCP` zWE@UB@GivyzJz4@9*BYg;g>nSeiPnLDJ9KYa;yt^{XlO($2vpRso6bpn zU_BlGOwx?UoCPzur({7M!x~KEq|p|>uD2HvpXYH_#A(7F1oYGG-|?=(UkO{)Sx3z5 zl^V5a5u~O&wYZD`s>+a=>lfl)a$D;_QE1pu`vz(Ol>WATre>vaxWIuKG#FhgwMOXyFbv=ZDZ1ctmBI8nuH0MIEqjwGm|JE&>1m3eT(} literal 0 HcmV?d00001 diff --git a/env/share/man/man1/ipengine.1.gz b/env/share/man/man1/ipengine.1.gz new file mode 100644 index 0000000000000000000000000000000000000000..d661879f3682c8ce797f4f210aa43a5088654dc4 GIT binary patch literal 343 zcmV-d0jT~TiwFoX{Craa18Hz&Zf9w3WiBxQWl`O3gCG>X=P6FSX_u&}eS>y&tIMWF z+{H3?LEFwK=9Tpgk%=Ne3r}u2!V3NJ+C_iVoF5cu*60;@NSnpkszfi4wND?RqEF0WP4CNx$-1Ba7`3n z0tGZpVew82Kf`6^-NzSClc(@{-fHEH_OmmO%}XHPA@RnHLW|FOT~ze(1DbsamW$59 pNjQF+MhGnv%K3#m*qr|iYK2FXx@%E6sFAfX`32D5BCP%a002QGqK^Oo literal 0 HcmV?d00001 diff --git a/env/share/man/man1/ipython.1.gz b/env/share/man/man1/ipython.1.gz new file mode 100644 index 0000000000000000000000000000000000000000..badbb5591ef9f81efa4f9212ba6f976f012717a8 GIT binary patch literal 1039 zcmV+q1n~PGiwFoX{Craa18H!1bZBpGE-?U&Rbg-2HW2;JUvUTqIPGM49yV+UiUMxp zASxEev%CVqJfxI#ve-(bKvMDa?{_36+iCM*h5^aEyLa#2-O=&x0{++M4*Lj}_lxWF z3Z`$TAhjw>_r_v^xE0!1Xu0MMTGSD&7Wc~tOr^TIfDD1xHRQZC-aExy$>Q8SU0!dK zht+w+eENF?4e#moFay(N26HQv^a#pUs80X}xb`{J0gEdzh!iO=e|r=8=ll(ew|9`N zAGdc8E11K@$FA;S{%Zt3|1_Upc*u2)+z?3drK3`8B@7hYQt}EVK2tW0&%y|~C!2?F z`5t2z8fsDvg4d+sxmG5EQn{ECHOd^Xyi+WzQO`mcp4A9d-%b;O=pjCcLs}yxjwFYx3zM=HOn(5I> zc&IIq2_e(SzXk`>!aMo?%t8J*2W(o~d)L6hsWpf?H7(|%6!}rgQ$u|Pd#CeLI0z}y zyOF~%B~T$Rjug}M%uJ)lb^n^eOx`P(}=yMru8vR7fj(5HvZw zv#O!_%Yn48inO~Fl8F0WsLs+WEn3k_-iVy3>Jzzn3a zm?`x&fD+K9%J5!SqHOPH$|U`z1@ zmb|MiBe);#9`2X3c&ts7a7ymdk-E;E$|#8cqw46wmtH1EBs2sIc;^!t=BX0+jZy4SV4;q9r;R}uYa zS*}l3H&^2zutJ?0KBR=4Za<5YFkDy&3zN4`Av$jR$p9P*bzIqr+ibvi8Ym4iEdC<< zCOzsvf*-W77A0i8FJZLY(7#o!+J;sEw#XZJS6SPdtJ!Q%I&@h~t(Xmj$LIT){RePY JU6%?7000GL`&Iw| literal 0 HcmV?d00001 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 489b9cda2..111b005d8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1062,6 +1062,9 @@ class Base64ImageField(ImageField): """ def from_native(self, base64_data): # Check if this is a base64 string + if base64_data in validators.EMPTY_VALUES: + return None + if isinstance(base64_data, basestring): # Try to decode the file. Return validation error if it fails. try: @@ -1081,7 +1084,7 @@ class Base64ImageField(ImageField): def to_native(self, value): # Return url including domain name. - return "" + return value.name def get_file_extension(self, filename, decoded_file): extension = imghdr.what(filename, decoded_file) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index f0690e965..54949d271 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -1011,7 +1011,7 @@ class UploadedBase64Image(object): class UploadedBase64ImageSerializer(serializers.Serializer): - file = serializers.Base64ImageField() + file = serializers.Base64ImageField(required=False) created = serializers.DateTimeField() def restore_object(self, attrs, instance=None): @@ -1025,6 +1025,9 @@ class UploadedBase64ImageSerializer(serializers.Serializer): class Base64ImageSerializerTests(TestCase): def test_create(self): + """ + Test for creating Base64 image in the server side + """ now = datetime.datetime.now() file = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' serializer = UploadedBase64ImageSerializer(data={'created': now, 'file': file}) @@ -1033,24 +1036,25 @@ class Base64ImageSerializerTests(TestCase): self.assertEqual(serializer.object.created, uploaded_image.created) self.assertFalse(serializer.object is uploaded_image) - - def test_creation_failure(self): - """ - Passing file=None should result in an ValidationError - """ - errmsg = 'This field is required.' - now = datetime.datetime.now() - serializer = UploadedBase64ImageSerializer(data={'created': now}) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'file': [errmsg]}) - def test_validation_error_with_non_file(self): """ Passing non-base64 should raise a validation error. """ now = datetime.datetime.now() errmsg = "Please upload a valid image." - serializer = UploadedBase64ImageSerializer(data={'created': now, 'file': 'abc'}) self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'file': [errmsg]}) \ No newline at end of file + self.assertEqual(serializer.errors, {'file': [errmsg]}) + + + def test_remove_with_empty_string(self): + """ + Passing empty string as data should cause image to be removed + """ + now = datetime.datetime.now() + file = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + uploaded_image = UploadedBase64Image(file=file, created=now) + serializer = UploadedBase64ImageSerializer(instance=uploaded_image, data={'created': now, 'file': ''}) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.object.created, uploaded_image.created) + self.assertIsNone(serializer.object.file) \ No newline at end of file From ffdf2ffb19fb75013e03fae0c26a7aa9d2b7cdd5 Mon Sep 17 00:00:00 2001 From: vargi Date: Thu, 17 Jul 2014 16:43:52 +0300 Subject: [PATCH 3/3] type fixes --- docs/api-guide/fields.md | 1 - rest_framework/fields.py | 2 +- rest_framework/tests/test_fields.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 86775d061..4626df19b 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -299,7 +299,6 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is Signature and validation is the same as with `FileField`. - --- **Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 111b005d8..26bbe6912 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1089,4 +1089,4 @@ class Base64ImageField(ImageField): def get_file_extension(self, filename, decoded_file): extension = imghdr.what(filename, decoded_file) extension = "jpg" if extension == "jpeg" else extension - return extension \ No newline at end of file + return extension diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 54949d271..14d8ca26a 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -1057,4 +1057,4 @@ class Base64ImageSerializerTests(TestCase): serializer = UploadedBase64ImageSerializer(instance=uploaded_image, data={'created': now, 'file': ''}) self.assertTrue(serializer.is_valid()) self.assertEqual(serializer.object.created, uploaded_image.created) - self.assertIsNone(serializer.object.file) \ No newline at end of file + self.assertIsNone(serializer.object.file)