Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Repository: plone.formwidget.namedfile
Branch: refs/heads/master Date: 2015-01-21T10:40:43+01:00 Author: Johannes Raggam (thet) <raggam-nl@adm.at> Commit: plone/plone.formwidget.namedfile@93ef0d7 Base64Converter for file/image widgets on ASCII fields Add Base64 data converter for NamedImage and NamedFile widgets on ASCII fields with base64 encoded data and filename. Now the NamedImage and NamedFile widgets can be used with ``zope.schema.ASCII`` fields. Files changed: M CHANGES.rst M plone/formwidget/namedfile/configure.zcml M plone/formwidget/namedfile/converter.py M plone/formwidget/namedfile/widget.py M plone/formwidget/namedfile/widget.rst diff --git a/CHANGES.rst b/CHANGES.rst index 820b874..5ebfd19 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ Changelog 1.0.12 (unreleased) ------------------- +- Add Base64 data converter for NamedImage and NamedFile widgets on ASCII + fields with base64 encoded data and filename. Now the NamedImage and + NamedFile widgets can be used with ``zope.schema.ASCII`` fields. + [thet] + - PEP 8. [thet] diff --git a/plone/formwidget/namedfile/configure.zcml b/plone/formwidget/namedfile/configure.zcml index de01907..bce3532 100644 --- a/plone/formwidget/namedfile/configure.zcml +++ b/plone/formwidget/namedfile/configure.zcml @@ -12,6 +12,7 @@ <i18n:registerTranslations directory="locales" /> <adapter factory=".converter.NamedDataConverter" /> + <adapter factory=".converter.Base64Converter" /> <adapter factory=".validator.NamedFileWidgetValidator" /> <class class=".widget.NamedFileWidget"> diff --git a/plone/formwidget/namedfile/converter.py b/plone/formwidget/namedfile/converter.py index 25a77ca..71395ca 100644 --- a/plone/formwidget/namedfile/converter.py +++ b/plone/formwidget/namedfile/converter.py @@ -1,9 +1,15 @@ from ZPublisher.HTTPRequest import FileUpload from plone.formwidget.namedfile.interfaces import INamedFileWidget -from plone.namedfile.interfaces import INamedField, INamed +from plone.formwidget.namedfile.interfaces import INamedImageWidget +from plone.namedfile.file import NamedFile +from plone.namedfile.file import NamedImage +from plone.namedfile.interfaces import INamed +from plone.namedfile.interfaces import INamedField from plone.namedfile.utils import safe_basename from z3c.form.converter import BaseDataConverter from zope.component import adapts +from zope.schema.interfaces import IASCII +import base64 class NamedDataConverter(BaseDataConverter): @@ -39,3 +45,67 @@ def toFieldValue(self, value): else: return self.field._type(data=str(value)) + + +def b64encode_file(filename, data): + # encode filename and data using the standard alphabet, so that ";" can be + # used as delimiter. + if isinstance(filename, unicode): + filename = filename.encode('utf-8') + filenameb64 = base64.standard_b64encode(filename or '') + datab64 = base64.standard_b64encode(data) + return "filenameb64:{};datab64:{}".format( + filenameb64, datab64 + ).encode('ascii') + + +def b64decode_file(value): + filename, data = value.split(';') + + filename = filename.split(':')[1] + filename = base64.standard_b64decode(filename) + filename = filename.decode('utf-8') + + data = data.split(':')[1] + data = base64.standard_b64decode(data) + + return filename, data + + +class Base64Converter(BaseDataConverter): + """Converts between ASCII fields with base64 encoded data and a filename + and INamedImage/INamedFile values. + """ + adapts(IASCII, INamedFileWidget) + + def toWidgetValue(self, value): + + if not isinstance(value, basestring): + return None + + filename, data = b64decode_file(value) + + if INamedImageWidget.providedBy(self.widget): + value = NamedImage(data=data, filename=filename) + else: + value = NamedFile(data=data, filename=filename) + return value + + def toFieldValue(self, value): + + filename = None + data = None + + if INamed.providedBy(value): + filename = value.filename + data = value.data + + elif isinstance(value, FileUpload): + filename = safe_basename(value.filename) + value.seek(0) + data = value.read() + + if not data: + return self.field.missing_value + + return b64encode_file(filename, data) diff --git a/plone/formwidget/namedfile/widget.py b/plone/formwidget/namedfile/widget.py index 204bf07..05854bb 100644 --- a/plone/formwidget/namedfile/widget.py +++ b/plone/formwidget/namedfile/widget.py @@ -4,8 +4,11 @@ from Products.Five.browser import BrowserView from Products.MimetypesRegistry.common import MimeTypeException from ZPublisher.HTTPRequest import FileUpload +from plone.formwidget.namedfile.converter import b64decode_file from plone.formwidget.namedfile.interfaces import INamedFileWidget from plone.formwidget.namedfile.interfaces import INamedImageWidget +from plone.namedfile.file import NamedFile +from plone.namedfile.file import NamedImage from plone.namedfile.interfaces import INamed from plone.namedfile.interfaces import INamedFileField from plone.namedfile.interfaces import INamedImage @@ -27,6 +30,7 @@ from zope.interface import implementsOnly from zope.publisher.interfaces import IPublishTraverse from zope.publisher.interfaces import NotFound +from zope.schema.interfaces import IASCII from zope.size import byteDisplay import urllib @@ -231,7 +235,6 @@ def publishTraverse(self, request, name): return self def __call__(self): - # TODO: Security check on form view/widget if self.context.ignoreContext: @@ -246,6 +249,16 @@ def __call__(self): dm = getMultiAdapter((content, field,), IDataManager) file_ = dm.get() + + if isinstance(file_, basestring) and IASCII.providedBy(field): + """Encoded data. + """ + filename, data = b64decode_file(file_) + if INamedImageWidget.providedBy(self.context): + file_ = NamedImage(data=data, filename=filename) + else: + file_ = NamedFile(data=data, filename=filename) + if file_ is None: raise NotFound(self, self.filename, self.request) diff --git a/plone/formwidget/namedfile/widget.rst b/plone/formwidget/namedfile/widget.rst index c38a84a..24c806b 100644 --- a/plone/formwidget/namedfile/widget.rst +++ b/plone/formwidget/namedfile/widget.rst @@ -427,6 +427,7 @@ stored in the field:: >>> image_widget.extract() is content.image_field True + Download view ------------- @@ -469,6 +470,7 @@ Any additional traversal will result in an error:: ... NotFound: ... 'daisy.txt' + The converter ------------- @@ -553,8 +555,150 @@ being returned:: >>> field_value is IContent['file_field'].missing_value True >>> field_value = image_converter.toFieldValue(FileUpload(aFieldStorage)) - >>> field_value is IContent['file_field'].missing_value + >>> field_value is IContent['image_field'].missing_value + True + + +The Base64Converter for ASCII fields +------------------------------------ + +There is another converter, which converts between a NamedFile or file upload +instance and base64 encoded data, which can be stored in a ASCII field:: + + >>> from zope import schema + >>> from zope.interface import implements, Interface + >>> class IASCIIContent(Interface): + ... file_field = schema.ASCII(title=u"File") + ... image_field = schema.ASCII(title=u"Image") + + >>> from plone.formwidget.namedfile.converter import Base64Converter + >>> provideAdapter(Base64Converter) + + >>> from zope.component import getMultiAdapter + >>> from z3c.form.interfaces import IDataConverter + + >>> ascii_file_converter = getMultiAdapter( + ... (IASCIIContent['file_field'], file_widget), + ... IDataConverter + ... ) + >>> ascii_image_converter = getMultiAdapter( + ... (IASCIIContent['image_field'], image_widget), + ... IDataConverter + ... ) + +A value of None or '' results in the field's missing_value being returned:: + + >>> ascii_file_converter.toFieldValue(u'') is IASCIIContent['file_field'].missing_value True + >>> ascii_file_converter.toFieldValue(None) is IASCIIContent['file_field'].missing_value + True + + >>> ascii_image_converter.toFieldValue(u'') is IASCIIContent['image_field'].missing_value + True + >>> ascii_image_converter.toFieldValue(None) is IASCIIContent['image_field'].missing_value + True + +A named file/image instance is returned as Base 64 encoded string in the +following form:: + + filenameb64:BASE64_ENCODED_FILENAME;data64:BASE64_ENCODED_DATA + +Like so:: + + >>> ascii_file_converter.toFieldValue( + ... NamedFile(data='testfile', filename=u'test.txt')) + 'filenameb64:dGVzdC50eHQ=;datab64:dGVzdGZpbGU=' + >>> ascii_image_converter.toFieldValue( + ... NamedImage(data='testimage', filename=u'test.png')) + 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl' + +A Base 64 encoded structure like descibed above is converted to the appropriate +type:: + + >>> afile = ascii_file_converter.toWidgetValue( + ... 'filenameb64:dGVzdC50eHQ=;datab64:dGVzdGZpbGU=') + >>> afile + <plone.namedfile.file.NamedFile object at ...> + >>> afile.data + 'testfile' + >>> afile.filename + u'test.txt' + + >>> aimage = ascii_image_converter.toWidgetValue( + ... 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl') + >>> aimage + <plone.namedfile.file.NamedImage object at ...> + >>> aimage.data + 'testimage' + >>> aimage.filename + u'test.png' + +Finally, some tests with image uploads converted to the field value. + +Convert a file upload to the Base 64 encoded field value and handle the +filename too:: + + + >>> myfile = cStringIO.StringIO('File upload contents.') + >>> # \xc3\xb8 is UTF-8 for a small letter o with slash + >>> aFieldStorage = FieldStorageStub(myfile, filename='rand\xc3\xb8m.txt', + ... headers={'Content-Type': 'text/x-dummy'}) + >>> ascii_file_converter.toFieldValue(FileUpload(aFieldStorage)) + 'filenameb64:cmFuZMO4bS50eHQ=;datab64:RmlsZSB1cGxvYWQgY29udGVudHMu' + +A zero-length, unnamed FileUpload results in the field's missing_value +being returned:: + + >>> myfile = cStringIO.StringIO('') + >>> aFieldStorage = FieldStorageStub(myfile, filename='', headers={'Content-Type': 'application/octet-stream'}) + >>> field_value = ascii_file_converter.toFieldValue(FileUpload(aFieldStorage)) + >>> field_value is IASCIIContent['file_field'].missing_value + True + >>> field_value = ascii_image_converter.toFieldValue(FileUpload(aFieldStorage)) + >>> field_value is IASCIIContent['image_field'].missing_value + True + + +The Download view on ASCII fields +--------------------------------- +:: + + >>> class ASCIIContent(object): + ... implements(IASCIIContent) + ... def __init__(self, file, image): + ... self.file_field = file + ... self.image_field = image + ... + ... def absolute_url(self): + ... return 'http://example.com/content2' + + >>> content = ASCIIContent( + ... NamedFile(data="testfile", filename=u"test.txt"), + ... NamedImage(data="testimage", filename=u"test.png")) + + >>> from z3c.form.widget import FieldWidget + + >>> ascii_file_widget = FieldWidget(IASCIIContent['file_field'], NamedFileWidget(TestRequest())) + >>> ascii_file_widget.context = content + + >>> ascii_image_widget = FieldWidget(IASCIIContent['image_field'], NamedImageWidget(TestRequest())) + >>> ascii_image_widget.context = content + + >>> request = TestRequest() + >>> view = Download(ascii_file_widget, request) + >>> view() + 'testfile' + + >>> request.response.getHeader('Content-Disposition') + "attachment; filename*=UTF-8''test.txt" + + >>> view = Download(ascii_image_widget, request) + >>> view() + 'testimage' + + >>> request.response.getHeader('Content-Disposition') + "attachment; filename*=UTF-8''test.png" + The validator ------------- Repository: plone.formwidget.namedfile Branch: refs/heads/master Date: 2015-01-22T16:29:39+01:00 Author: Jens W. Klein (jensens) <jk@kleinundpartner.at> Commit: plone/plone.formwidget.namedfile@c0f2ca7 Merge pull request #15 from plone/thet-encodedconverter Base64Converter for file/image widgets on ASCII fields Files changed: M CHANGES.rst M plone/formwidget/namedfile/configure.zcml M plone/formwidget/namedfile/converter.py M plone/formwidget/namedfile/widget.py M plone/formwidget/namedfile/widget.rst diff --git a/CHANGES.rst b/CHANGES.rst index 820b874..5ebfd19 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ Changelog 1.0.12 (unreleased) ------------------- +- Add Base64 data converter for NamedImage and NamedFile widgets on ASCII + fields with base64 encoded data and filename. Now the NamedImage and + NamedFile widgets can be used with ``zope.schema.ASCII`` fields. + [thet] + - PEP 8. [thet] diff --git a/plone/formwidget/namedfile/configure.zcml b/plone/formwidget/namedfile/configure.zcml index de01907..bce3532 100644 --- a/plone/formwidget/namedfile/configure.zcml +++ b/plone/formwidget/namedfile/configure.zcml @@ -12,6 +12,7 @@ <i18n:registerTranslations directory="locales" /> <adapter factory=".converter.NamedDataConverter" /> + <adapter factory=".converter.Base64Converter" /> <adapter factory=".validator.NamedFileWidgetValidator" /> <class class=".widget.NamedFileWidget"> diff --git a/plone/formwidget/namedfile/converter.py b/plone/formwidget/namedfile/converter.py index 25a77ca..71395ca 100644 --- a/plone/formwidget/namedfile/converter.py +++ b/plone/formwidget/namedfile/converter.py @@ -1,9 +1,15 @@ from ZPublisher.HTTPRequest import FileUpload from plone.formwidget.namedfile.interfaces import INamedFileWidget -from plone.namedfile.interfaces import INamedField, INamed +from plone.formwidget.namedfile.interfaces import INamedImageWidget +from plone.namedfile.file import NamedFile +from plone.namedfile.file import NamedImage +from plone.namedfile.interfaces import INamed +from plone.namedfile.interfaces import INamedField from plone.namedfile.utils import safe_basename from z3c.form.converter import BaseDataConverter from zope.component import adapts +from zope.schema.interfaces import IASCII +import base64 class NamedDataConverter(BaseDataConverter): @@ -39,3 +45,67 @@ def toFieldValue(self, value): else: return self.field._type(data=str(value)) + + +def b64encode_file(filename, data): + # encode filename and data using the standard alphabet, so that ";" can be + # used as delimiter. + if isinstance(filename, unicode): + filename = filename.encode('utf-8') + filenameb64 = base64.standard_b64encode(filename or '') + datab64 = base64.standard_b64encode(data) + return "filenameb64:{};datab64:{}".format( + filenameb64, datab64 + ).encode('ascii') + + +def b64decode_file(value): + filename, data = value.split(';') + + filename = filename.split(':')[1] + filename = base64.standard_b64decode(filename) + filename = filename.decode('utf-8') + + data = data.split(':')[1] + data = base64.standard_b64decode(data) + + return filename, data + + +class Base64Converter(BaseDataConverter): + """Converts between ASCII fields with base64 encoded data and a filename + and INamedImage/INamedFile values. + """ + adapts(IASCII, INamedFileWidget) + + def toWidgetValue(self, value): + + if not isinstance(value, basestring): + return None + + filename, data = b64decode_file(value) + + if INamedImageWidget.providedBy(self.widget): + value = NamedImage(data=data, filename=filename) + else: + value = NamedFile(data=data, filename=filename) + return value + + def toFieldValue(self, value): + + filename = None + data = None + + if INamed.providedBy(value): + filename = value.filename + data = value.data + + elif isinstance(value, FileUpload): + filename = safe_basename(value.filename) + value.seek(0) + data = value.read() + + if not data: + return self.field.missing_value + + return b64encode_file(filename, data) diff --git a/plone/formwidget/namedfile/widget.py b/plone/formwidget/namedfile/widget.py index 204bf07..05854bb 100644 --- a/plone/formwidget/namedfile/widget.py +++ b/plone/formwidget/namedfile/widget.py @@ -4,8 +4,11 @@ from Products.Five.browser import BrowserView from Products.MimetypesRegistry.common import MimeTypeException from ZPublisher.HTTPRequest import FileUpload +from plone.formwidget.namedfile.converter import b64decode_file from plone.formwidget.namedfile.interfaces import INamedFileWidget from plone.formwidget.namedfile.interfaces import INamedImageWidget +from plone.namedfile.file import NamedFile +from plone.namedfile.file import NamedImage from plone.namedfile.interfaces import INamed from plone.namedfile.interfaces import INamedFileField from plone.namedfile.interfaces import INamedImage @@ -27,6 +30,7 @@ from zope.interface import implementsOnly from zope.publisher.interfaces import IPublishTraverse from zope.publisher.interfaces import NotFound +from zope.schema.interfaces import IASCII from zope.size import byteDisplay import urllib @@ -231,7 +235,6 @@ def publishTraverse(self, request, name): return self def __call__(self): - # TODO: Security check on form view/widget if self.context.ignoreContext: @@ -246,6 +249,16 @@ def __call__(self): dm = getMultiAdapter((content, field,), IDataManager) file_ = dm.get() + + if isinstance(file_, basestring) and IASCII.providedBy(field): + """Encoded data. + """ + filename, data = b64decode_file(file_) + if INamedImageWidget.providedBy(self.context): + file_ = NamedImage(data=data, filename=filename) + else: + file_ = NamedFile(data=data, filename=filename) + if file_ is None: raise NotFound(self, self.filename, self.request) diff --git a/plone/formwidget/namedfile/widget.rst b/plone/formwidget/namedfile/widget.rst index c38a84a..24c806b 100644 --- a/plone/formwidget/namedfile/widget.rst +++ b/plone/formwidget/namedfile/widget.rst @@ -427,6 +427,7 @@ stored in the field:: >>> image_widget.extract() is content.image_field True + Download view ------------- @@ -469,6 +470,7 @@ Any additional traversal will result in an error:: ... NotFound: ... 'daisy.txt' + The converter ------------- @@ -553,8 +555,150 @@ being returned:: >>> field_value is IContent['file_field'].missing_value True >>> field_value = image_converter.toFieldValue(FileUpload(aFieldStorage)) - >>> field_value is IContent['file_field'].missing_value + >>> field_value is IContent['image_field'].missing_value + True + + +The Base64Converter for ASCII fields +------------------------------------ + +There is another converter, which converts between a NamedFile or file upload +instance and base64 encoded data, which can be stored in a ASCII field:: + + >>> from zope import schema + >>> from zope.interface import implements, Interface + >>> class IASCIIContent(Interface): + ... file_field = schema.ASCII(title=u"File") + ... image_field = schema.ASCII(title=u"Image") + + >>> from plone.formwidget.namedfile.converter import Base64Converter + >>> provideAdapter(Base64Converter) + + >>> from zope.component import getMultiAdapter + >>> from z3c.form.interfaces import IDataConverter + + >>> ascii_file_converter = getMultiAdapter( + ... (IASCIIContent['file_field'], file_widget), + ... IDataConverter + ... ) + >>> ascii_image_converter = getMultiAdapter( + ... (IASCIIContent['image_field'], image_widget), + ... IDataConverter + ... ) + +A value of None or '' results in the field's missing_value being returned:: + + >>> ascii_file_converter.toFieldValue(u'') is IASCIIContent['file_field'].missing_value True + >>> ascii_file_converter.toFieldValue(None) is IASCIIContent['file_field'].missing_value + True + + >>> ascii_image_converter.toFieldValue(u'') is IASCIIContent['image_field'].missing_value + True + >>> ascii_image_converter.toFieldValue(None) is IASCIIContent['image_field'].missing_value + True + +A named file/image instance is returned as Base 64 encoded string in the +following form:: + + filenameb64:BASE64_ENCODED_FILENAME;data64:BASE64_ENCODED_DATA + +Like so:: + + >>> ascii_file_converter.toFieldValue( + ... NamedFile(data='testfile', filename=u'test.txt')) + 'filenameb64:dGVzdC50eHQ=;datab64:dGVzdGZpbGU=' + >>> ascii_image_converter.toFieldValue( + ... NamedImage(data='testimage', filename=u'test.png')) + 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl' + +A Base 64 encoded structure like descibed above is converted to the appropriate +type:: + + >>> afile = ascii_file_converter.toWidgetValue( + ... 'filenameb64:dGVzdC50eHQ=;datab64:dGVzdGZpbGU=') + >>> afile + <plone.namedfile.file.NamedFile object at ...> + >>> afile.data + 'testfile' + >>> afile.filename + u'test.txt' + + >>> aimage = ascii_image_converter.toWidgetValue( + ... 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl') + >>> aimage + <plone.namedfile.file.NamedImage object at ...> + >>> aimage.data + 'testimage' + >>> aimage.filename + u'test.png' + +Finally, some tests with image uploads converted to the field value. + +Convert a file upload to the Base 64 encoded field value and handle the +filename too:: + + + >>> myfile = cStringIO.StringIO('File upload contents.') + >>> # \xc3\xb8 is UTF-8 for a small letter o with slash + >>> aFieldStorage = FieldStorageStub(myfile, filename='rand\xc3\xb8m.txt', + ... headers={'Content-Type': 'text/x-dummy'}) + >>> ascii_file_converter.toFieldValue(FileUpload(aFieldStorage)) + 'filenameb64:cmFuZMO4bS50eHQ=;datab64:RmlsZSB1cGxvYWQgY29udGVudHMu' + +A zero-length, unnamed FileUpload results in the field's missing_value +being returned:: + + >>> myfile = cStringIO.StringIO('') + >>> aFieldStorage = FieldStorageStub(myfile, filename='', headers={'Content-Type': 'application/octet-stream'}) + >>> field_value = ascii_file_converter.toFieldValue(FileUpload(aFieldStorage)) + >>> field_value is IASCIIContent['file_field'].missing_value + True + >>> field_value = ascii_image_converter.toFieldValue(FileUpload(aFieldStorage)) + >>> field_value is IASCIIContent['image_field'].missing_value + True + + +The Download view on ASCII fields +--------------------------------- +:: + + >>> class ASCIIContent(object): + ... implements(IASCIIContent) + ... def __init__(self, file, image): + ... self.file_field = file + ... self.image_field = image + ... + ... def absolute_url(self): + ... return 'http://example.com/content2' + + >>> content = ASCIIContent( + ... NamedFile(data="testfile", filename=u"test.txt"), + ... NamedImage(data="testimage", filename=u"test.png")) + + >>> from z3c.form.widget import FieldWidget + + >>> ascii_file_widget = FieldWidget(IASCIIContent['file_field'], NamedFileWidget(TestRequest())) + >>> ascii_file_widget.context = content + + >>> ascii_image_widget = FieldWidget(IASCIIContent['image_field'], NamedImageWidget(TestRequest())) + >>> ascii_image_widget.context = content + + >>> request = TestRequest() + >>> view = Download(ascii_file_widget, request) + >>> view() + 'testfile' + + >>> request.response.getHeader('Content-Disposition') + "attachment; filename*=UTF-8''test.txt" + + >>> view = Download(ascii_image_widget, request) + >>> view() + 'testimage' + + >>> request.response.getHeader('Content-Disposition') + "attachment; filename*=UTF-8''test.png" + The validator -------------
- Loading branch information