Skip to content

Commit

Permalink
Merge pull request #15 from plone/thet-encodedconverter
Browse files Browse the repository at this point in the history
Base64Converter for file/image widgets on ASCII fields
  • Loading branch information
jensens committed Jan 22, 2015
2 parents 6411861 + 93ef0d7 commit c0f2ca7
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
1 change: 1 addition & 0 deletions plone/formwidget/namedfile/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<i18n:registerTranslations directory="locales" />

<adapter factory=".converter.NamedDataConverter" />
<adapter factory=".converter.Base64Converter" />
<adapter factory=".validator.NamedFileWidgetValidator" />

<class class=".widget.NamedFileWidget">
Expand Down
72 changes: 71 additions & 1 deletion plone/formwidget/namedfile/converter.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
15 changes: 14 additions & 1 deletion plone/formwidget/namedfile/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down
146 changes: 145 additions & 1 deletion plone/formwidget/namedfile/widget.rst
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ stored in the field::
>>> image_widget.extract() is content.image_field
True


Download view
-------------

Expand Down Expand Up @@ -469,6 +470,7 @@ Any additional traversal will result in an error::
...
NotFound: ... 'daisy.txt'


The converter
-------------

Expand Down Expand Up @@ -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
-------------
Expand Down

0 comments on commit c0f2ca7

Please sign in to comment.