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 @@
+
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
+
+ >>> afile.data
+ 'testfile'
+ >>> afile.filename
+ u'test.txt'
+
+ >>> aimage = ascii_image_converter.toWidgetValue(
+ ... 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl')
+ >>> aimage
+
+ >>> 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
-------------