Image Widgets
=============
This is an infrastructure to create an image file widget that behaves
as much as possible like a normal text widget in formlib. Normally a
file widget loses its file data when a form is re-presented for
reasons of failing form validation. A ``hurry.file`` widget retains
the file, for example by storing it in a session.
In order to do this, we have a special way to store file data along with
its filename:
>>> import os
>>> from waeup.ikoba.image import IkobaImageFile
>>> testimage = os.path.join(os.path.dirname(__file__), 'sample.jpg')
>>> testimage2 = os.path.join(os.path.dirname(__file__), 'sample2.jpg')
>>> some_file = IkobaImageFile('foo.jpg', open(testimage, 'rb').read())
>>> some_file.filename
'foo.jpg'
>>> 'Created with GIMP' in some_file.data
True
We can provide a download widget. In this case, there's nothing
to download:
>>> from hurry.file.browser import DownloadWidget
>>> from hurry.file.schema import File
>>> from waeup.ikoba.image.schema import ImageFile
>>> from zope.publisher.browser import TestRequest
>>> field = ImageFile(__name__='foo', title=u'Foo')
>>> field = field.bind(None)
>>> request = TestRequest()
>>> widget = DownloadWidget(field, request)
>>> widget()
u'
Download not available
'
Even if there were data in the request, there'd be nothing to download:
>>> from zope.publisher.browser import FileUpload
>>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
>>> widget = DownloadWidget(field, request)
>>> widget()
u'
Download not available
'
Now set a value:
>>> widget.setRenderedValue(some_file)
>>> widget()
u'foo.jpg'
Instead of downloading, we can also use a thumbnail widget:
>>> from waeup.ikoba.image.browser import ThumbnailWidget
>>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
>>> widget = ThumbnailWidget(field, request)
>>> widget.setRenderedValue(some_file)
>>> widget()
u''
Now on to an edit widget. First the case in an add form with no
data already available, and no data in request:
>>> from waeup.ikoba.image.browser import EncodingImageFileWidget
>>> field = ImageFile(__name__='foo', title=u'Foo', required=False)
>>> field = field.bind(None)
>>> request = TestRequest()
>>> widget = EncodingImageFileWidget(field, request)
>>> def normalize(s):
... return u'\n '.join(filter(None, s.split(' ')))
>>> print normalize(widget())
Now let's try a situation where data is available in the request, but
it's an empty string for the file:
>>> request = TestRequest(form={'field.foo': u''})
>>> widget = EncodingImageFileWidget(field, request)
>>> def normalize(s):
... return '\n '.join(filter(None, s.split(' ')))
>>> print normalize(widget())
Now let's render again when there's already available data. What should show
up is an extra, hidden field which contains the file_id:
>>> widget.setRenderedValue(some_file)
>>> print normalize(widget())
...
...
(foo.jpg)
Now let's render again, this time with file data available in the request
instead. The same should happen:
>>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
...
...
(foo.jpg)
Now let's render again, this time not with file data available in the
request, but an id. Again, we should see the same:
>>> request = TestRequest(form={'field.foo.file_id':
... 'Zm9vLnR4dAp0aGUgY29udGVudHM='})
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
...
...
(foo.txt)
If there is both file data and an id, something else happens. First, let's
prepare some new file:
>>> another_file = IkobaImageFile('bar.txt', 'bar contents')
We happen to know, due to the implementation of
EncodingImageFileWidget, that the file_id is going to be
"Zm9vLnR4dAp0aGUgY29udGVudHM=". Let's make a request with the original
id, but a new file upload:
>>> request = TestRequest(form={'field.foo': FileUpload(another_file),
... 'field.foo.file_id':
... 'Zm9vLnR4dAp0aGUgY29udGVudHM='})
We expect the new file to be the one that's uploaded:
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
...
...
(bar.txt)
Support for File Retrievals
---------------------------
As :class:`waeup.ikoba.image.IkobaImageFile` objects support storing
image data by using external 'storages', also our widgets should do
so.
We create a simple IFileRetrieval utility and enable it:
>>> import hashlib
>>> from zope.component import provideUtility
>>> from hurry.file.interfaces import IFileRetrieval
>>> from StringIO import StringIO
>>> class MyFileRetrieval(object):
... storage = dict()
... def getFile(self, data):
... entry = self.storage.get(data, None)
... if entry is None:
... return None
... return StringIO(entry)
... def createFile(self, filename, f):
... contents = f.read()
... id_string = hashlib.md5(contents).hexdigest()
... result = IkobaImageFile(filename, id_string)
... self.storage[id_string] = contents
... return result
>>> retrieval = MyFileRetrieval()
>>> provideUtility(retrieval, IFileRetrieval)
With this utility in place we can post requests. When no data was
posted and the field also contains no data, we will get a simple
input as appropriate for add forms:
>>> field = ImageFile(__name__='foo', title=u'Foo', required=False)
>>> field = field.bind(None)
>>> request = TestRequest()
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
If the request contains empty data but it is only an empty string, the
result will be the same:
>>> request = TestRequest(form={'field.foo': u''})
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
We now want to simulate, that the field contains already data,
identified by some `file_id`. To do so, we first store the data in our
file retrieval and then create a IkobaImageFile object with that
file_id stored:
>>> from waeup.ikoba.image import createIkobaImageFile
>>> image = createIkobaImageFile(
... 'sample.jpg', open(testimage, 'rb'))
>>> file_id = image.data
>>> file_id # MD5 sum of the file contents
'9feac4265077922000aa8b88748e25be'
>>> import hashlib
>>> hashlib.md5(open(testimage, 'rb').read()).hexdigest()
'9feac4265077922000aa8b88748e25be'
The new file was stored by our utility, as createIkobaImageFile looks
up IFileRetrieval utilities and uses them:
>>> retrieval.storage.keys()
['9feac4265077922000aa8b88748e25be']
We set this image as value of the widget:
>>> widget.setRenderedValue(image)
>>> print normalize(widget())
(sample.jpg)
>>> retrieval.storage.keys()
['9feac4265077922000aa8b88748e25be']
The stored hidden value contains the filename and our file_id:
>>> "c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ==".decode(
... 'base64')
'sample.jpg\n9feac4265077922000aa8b88748e25be'
Now, we want the the widget rendered again but this time with the data
coming from the request. To do so, we have to create a FileUpload
object that wraps the real file:
>>> class UploadedFile(object):
... headers = {}
... def __init__(self, filename, f):
... self.filename = filename
... self.file = f
Now we can 'post' the request and will get the same result as above:
>>> upload = FileUpload(UploadedFile('sample.jpg', open(testimage, 'rb')))
>>> request = TestRequest(form={'field.foo': upload})
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
...
...
(sample.jpg)
Now let's render again, this time not with file data available in the
request, but an id. Again, we should see the same:
>>> request = TestRequest(form={
... 'field.foo.file_id':
... 'c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ=='})
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
...
...
(sample.jpg)
If there is both file data and an id, something else happens. First, let's
prepare some new file:
>>> upload = FileUpload(UploadedFile('sample2.jpg', open(testimage2, 'rb')))
We happen to know, due to the implementation of
EncodingImageFileWidget, that the file_id is going to be
"Zm9vLnR4dAp0aGUgY29udGVudHM=". Let's make a request with the original
id, but a new file upload:
>>> request = TestRequest(form={'field.foo': upload,
... 'field.foo.file_id':
... 'Z2FtcGxlLmpwZwr/2P/gAA=='})
We expect the new file to be the one that's uploaded:
>>> field = ImageFile(__name__='foo', title=u'Foo', required=False)
>>> field = field.bind(None)
>>> widget = EncodingImageFileWidget(field, request)
>>> print normalize(widget())
...
...
(sample2.jpg)
The value displayed above again means the filename and md5 sum of the
stored file:
>>> "c2FtcGxlMi5qcGcKZDk2MDkwZWRlMmRjODlkZDdkZWM5ZDU3MmFkNThjNzQ=".decode(
... 'base64')
'sample2.jpg\nd96090ede2dc89dd7dec9d572ad58c74'
where the md5 sum is in fact correct:
>>> import hashlib
>>> hashlib.md5(open(testimage2, 'rb').read()).hexdigest()
'd96090ede2dc89dd7dec9d572ad58c74'
Our file retrieval utility now contains two files:
>>> retrieval.storage.keys()
['9feac4265077922000aa8b88748e25be', 'd96090ede2dc89dd7dec9d572ad58c74']