1 | Image Widgets |
---|
2 | ============= |
---|
3 | |
---|
4 | This is an infrastructure to create an image file widget that behaves |
---|
5 | as much as possible like a normal text widget in formlib. Normally a |
---|
6 | file widget loses its file data when a form is re-presented for |
---|
7 | reasons of failing form validation. A ``hurry.file`` widget retains |
---|
8 | the file, for example by storing it in a session. |
---|
9 | |
---|
10 | In order to do this, we have a special way to store file data along with |
---|
11 | its filename: |
---|
12 | |
---|
13 | >>> import os |
---|
14 | >>> from waeup.ikoba.image import IkobaImageFile |
---|
15 | >>> testimage = os.path.join(os.path.dirname(__file__), 'sample.jpg') |
---|
16 | >>> testimage2 = os.path.join(os.path.dirname(__file__), 'sample2.jpg') |
---|
17 | >>> some_file = IkobaImageFile('foo.jpg', open(testimage, 'rb').read()) |
---|
18 | >>> some_file.filename |
---|
19 | 'foo.jpg' |
---|
20 | >>> 'Created with GIMP' in some_file.data |
---|
21 | True |
---|
22 | |
---|
23 | We can provide a download widget. In this case, there's nothing |
---|
24 | to download: |
---|
25 | |
---|
26 | >>> from hurry.file.browser import DownloadWidget |
---|
27 | >>> from hurry.file.schema import File |
---|
28 | >>> from waeup.ikoba.image.schema import ImageFile |
---|
29 | >>> from zope.publisher.browser import TestRequest |
---|
30 | >>> field = ImageFile(__name__='foo', title=u'Foo') |
---|
31 | >>> field = field.bind(None) |
---|
32 | >>> request = TestRequest() |
---|
33 | >>> widget = DownloadWidget(field, request) |
---|
34 | >>> widget() |
---|
35 | u'<div>Download not available</div>' |
---|
36 | |
---|
37 | Even if there were data in the request, there'd be nothing to download: |
---|
38 | |
---|
39 | >>> from zope.publisher.browser import FileUpload |
---|
40 | >>> request = TestRequest(form={'field.foo': FileUpload(some_file)}) |
---|
41 | >>> widget = DownloadWidget(field, request) |
---|
42 | >>> widget() |
---|
43 | u'<div>Download not available</div>' |
---|
44 | |
---|
45 | Now set a value: |
---|
46 | |
---|
47 | >>> widget.setRenderedValue(some_file) |
---|
48 | >>> widget() |
---|
49 | u'<a href="foo.jpg">foo.jpg</a>' |
---|
50 | |
---|
51 | Instead of downloading, we can also use a thumbnail widget: |
---|
52 | |
---|
53 | >>> from waeup.ikoba.image.browser import ThumbnailWidget |
---|
54 | >>> request = TestRequest(form={'field.foo': FileUpload(some_file)}) |
---|
55 | >>> widget = ThumbnailWidget(field, request) |
---|
56 | >>> widget.setRenderedValue(some_file) |
---|
57 | >>> widget() |
---|
58 | u'<img src="foo.jpg" />' |
---|
59 | |
---|
60 | Now on to an edit widget. First the case in an add form with no |
---|
61 | data already available, and no data in request: |
---|
62 | |
---|
63 | >>> from waeup.ikoba.image.browser import EncodingImageFileWidget |
---|
64 | >>> field = ImageFile(__name__='foo', title=u'Foo', required=False) |
---|
65 | >>> field = field.bind(None) |
---|
66 | >>> request = TestRequest() |
---|
67 | >>> widget = EncodingImageFileWidget(field, request) |
---|
68 | |
---|
69 | >>> def normalize(s): |
---|
70 | ... return u'\n '.join(filter(None, s.split(' '))) |
---|
71 | |
---|
72 | >>> print normalize(widget()) |
---|
73 | <input |
---|
74 | class="fileType" |
---|
75 | id="field.foo" |
---|
76 | name="field.foo" |
---|
77 | size="20" |
---|
78 | type="file" |
---|
79 | /> |
---|
80 | |
---|
81 | Now let's try a situation where data is available in the request, but |
---|
82 | it's an empty string for the file: |
---|
83 | |
---|
84 | >>> request = TestRequest(form={'field.foo': u''}) |
---|
85 | >>> widget = EncodingImageFileWidget(field, request) |
---|
86 | |
---|
87 | >>> def normalize(s): |
---|
88 | ... return '\n '.join(filter(None, s.split(' '))) |
---|
89 | |
---|
90 | >>> print normalize(widget()) |
---|
91 | <input |
---|
92 | class="fileType" |
---|
93 | id="field.foo" |
---|
94 | name="field.foo" |
---|
95 | size="20" |
---|
96 | type="file" |
---|
97 | /> |
---|
98 | |
---|
99 | Now let's render again when there's already available data. What should show |
---|
100 | up is an extra, hidden field which contains the file_id: |
---|
101 | |
---|
102 | >>> widget.setRenderedValue(some_file) |
---|
103 | >>> print normalize(widget()) |
---|
104 | <img... />... |
---|
105 | ...<input |
---|
106 | class="fileType" |
---|
107 | id="field.foo" |
---|
108 | name="field.foo" |
---|
109 | size="20" |
---|
110 | type="file" |
---|
111 | /> |
---|
112 | (foo.jpg)<input |
---|
113 | class="hiddenType" |
---|
114 | id="field.foo.file_id" |
---|
115 | name="field.foo.file_id" |
---|
116 | type="hidden" |
---|
117 | value="Zm9vLmpw...=" |
---|
118 | /> |
---|
119 | |
---|
120 | Now let's render again, this time with file data available in the request |
---|
121 | instead. The same should happen: |
---|
122 | |
---|
123 | >>> request = TestRequest(form={'field.foo': FileUpload(some_file)}) |
---|
124 | >>> widget = EncodingImageFileWidget(field, request) |
---|
125 | >>> print normalize(widget()) |
---|
126 | <img... />... |
---|
127 | ...<input |
---|
128 | class="fileType" |
---|
129 | id="field.foo" |
---|
130 | name="field.foo" |
---|
131 | size="20" |
---|
132 | type="file" |
---|
133 | /> |
---|
134 | (foo.jpg)<input |
---|
135 | class="hiddenType" |
---|
136 | id="field.foo.file_id" |
---|
137 | name="field.foo.file_id" |
---|
138 | type="hidden" |
---|
139 | value="Zm9vLmpwZ...=" |
---|
140 | /> |
---|
141 | |
---|
142 | Now let's render again, this time not with file data available in the |
---|
143 | request, but an id. Again, we should see the same: |
---|
144 | |
---|
145 | >>> request = TestRequest(form={'field.foo.file_id': |
---|
146 | ... 'Zm9vLnR4dAp0aGUgY29udGVudHM='}) |
---|
147 | >>> widget = EncodingImageFileWidget(field, request) |
---|
148 | >>> print normalize(widget()) |
---|
149 | <img... />... |
---|
150 | ...<input |
---|
151 | class="fileType" |
---|
152 | id="field.foo" |
---|
153 | name="field.foo" |
---|
154 | size="20" |
---|
155 | type="file" |
---|
156 | /> |
---|
157 | (foo.txt)<input |
---|
158 | class="hiddenType" |
---|
159 | id="field.foo.file_id" |
---|
160 | name="field.foo.file_id" |
---|
161 | type="hidden" |
---|
162 | value="Zm9vLnR4dAp0aGUgY29udGVudHM=" |
---|
163 | /> |
---|
164 | |
---|
165 | If there is both file data and an id, something else happens. First, let's |
---|
166 | prepare some new file: |
---|
167 | |
---|
168 | >>> another_file = IkobaImageFile('bar.txt', 'bar contents') |
---|
169 | |
---|
170 | We happen to know, due to the implementation of |
---|
171 | EncodingImageFileWidget, that the file_id is going to be |
---|
172 | "Zm9vLnR4dAp0aGUgY29udGVudHM=". Let's make a request with the original |
---|
173 | id, but a new file upload: |
---|
174 | |
---|
175 | >>> request = TestRequest(form={'field.foo': FileUpload(another_file), |
---|
176 | ... 'field.foo.file_id': |
---|
177 | ... 'Zm9vLnR4dAp0aGUgY29udGVudHM='}) |
---|
178 | |
---|
179 | We expect the new file to be the one that's uploaded: |
---|
180 | |
---|
181 | >>> widget = EncodingImageFileWidget(field, request) |
---|
182 | >>> print normalize(widget()) |
---|
183 | <img... />... |
---|
184 | ...<input |
---|
185 | class="fileType" |
---|
186 | id="field.foo" |
---|
187 | name="field.foo" |
---|
188 | size="20" |
---|
189 | type="file" |
---|
190 | /> |
---|
191 | (bar.txt)<input |
---|
192 | class="hiddenType" |
---|
193 | id="field.foo.file_id" |
---|
194 | name="field.foo.file_id" |
---|
195 | type="hidden" |
---|
196 | value="YmFyLnR4dApiYXIgY29udGVudHM=" |
---|
197 | /> |
---|
198 | |
---|
199 | Support for File Retrievals |
---|
200 | --------------------------- |
---|
201 | |
---|
202 | As :class:`waeup.ikoba.image.IkobaImageFile` objects support storing |
---|
203 | image data by using external 'storages', also our widgets should do |
---|
204 | so. |
---|
205 | |
---|
206 | We create a simple IFileRetrieval utility and enable it: |
---|
207 | |
---|
208 | >>> import hashlib |
---|
209 | >>> from zope.component import provideUtility |
---|
210 | >>> from hurry.file.interfaces import IFileRetrieval |
---|
211 | >>> from StringIO import StringIO |
---|
212 | >>> class MyFileRetrieval(object): |
---|
213 | ... storage = dict() |
---|
214 | ... def getFile(self, data): |
---|
215 | ... entry = self.storage.get(data, None) |
---|
216 | ... if entry is None: |
---|
217 | ... return None |
---|
218 | ... return StringIO(entry) |
---|
219 | ... def createFile(self, filename, f): |
---|
220 | ... contents = f.read() |
---|
221 | ... id_string = hashlib.md5(contents).hexdigest() |
---|
222 | ... result = IkobaImageFile(filename, id_string) |
---|
223 | ... self.storage[id_string] = contents |
---|
224 | ... return result |
---|
225 | |
---|
226 | >>> retrieval = MyFileRetrieval() |
---|
227 | >>> provideUtility(retrieval, IFileRetrieval) |
---|
228 | |
---|
229 | With this utility in place we can post requests. When no data was |
---|
230 | posted and the field also contains no data, we will get a simple |
---|
231 | input as appropriate for add forms: |
---|
232 | |
---|
233 | >>> field = ImageFile(__name__='foo', title=u'Foo', required=False) |
---|
234 | >>> field = field.bind(None) |
---|
235 | >>> request = TestRequest() |
---|
236 | >>> widget = EncodingImageFileWidget(field, request) |
---|
237 | >>> print normalize(widget()) |
---|
238 | <input |
---|
239 | class="fileType" |
---|
240 | id="field.foo" |
---|
241 | name="field.foo" |
---|
242 | size="20" |
---|
243 | type="file" |
---|
244 | /> |
---|
245 | |
---|
246 | If the request contains empty data but it is only an empty string, the |
---|
247 | result will be the same: |
---|
248 | |
---|
249 | >>> request = TestRequest(form={'field.foo': u''}) |
---|
250 | >>> widget = EncodingImageFileWidget(field, request) |
---|
251 | >>> print normalize(widget()) |
---|
252 | <input |
---|
253 | class="fileType" |
---|
254 | id="field.foo" |
---|
255 | name="field.foo" |
---|
256 | size="20" |
---|
257 | type="file" |
---|
258 | /> |
---|
259 | |
---|
260 | We now want to simulate, that the field contains already data, |
---|
261 | identified by some `file_id`. To do so, we first store the data in our |
---|
262 | file retrieval and then create a IkobaImageFile object with that |
---|
263 | file_id stored: |
---|
264 | |
---|
265 | >>> from waeup.ikoba.image import createIkobaImageFile |
---|
266 | >>> image = createIkobaImageFile( |
---|
267 | ... 'sample.jpg', open(testimage, 'rb')) |
---|
268 | >>> file_id = image.data |
---|
269 | >>> file_id # MD5 sum of the file contents |
---|
270 | '9feac4265077922000aa8b88748e25be' |
---|
271 | |
---|
272 | >>> import hashlib |
---|
273 | >>> hashlib.md5(open(testimage, 'rb').read()).hexdigest() |
---|
274 | '9feac4265077922000aa8b88748e25be' |
---|
275 | |
---|
276 | The new file was stored by our utility, as createIkobaImageFile looks |
---|
277 | up IFileRetrieval utilities and uses them: |
---|
278 | |
---|
279 | >>> retrieval.storage.keys() |
---|
280 | ['9feac4265077922000aa8b88748e25be'] |
---|
281 | |
---|
282 | We set this image as value of the widget: |
---|
283 | |
---|
284 | >>> widget.setRenderedValue(image) |
---|
285 | >>> print normalize(widget()) |
---|
286 | <img |
---|
287 | src="sample.jpg" |
---|
288 | /><br |
---|
289 | /><input |
---|
290 | class="fileType" |
---|
291 | id="field.foo" |
---|
292 | name="field.foo" |
---|
293 | size="20" |
---|
294 | type="file" |
---|
295 | /> |
---|
296 | (sample.jpg)<input |
---|
297 | class="hiddenType" |
---|
298 | id="field.foo.file_id" |
---|
299 | name="field.foo.file_id" |
---|
300 | type="hidden" |
---|
301 | value="c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ==" |
---|
302 | /> |
---|
303 | |
---|
304 | >>> retrieval.storage.keys() |
---|
305 | ['9feac4265077922000aa8b88748e25be'] |
---|
306 | |
---|
307 | The stored hidden value contains the filename and our file_id: |
---|
308 | |
---|
309 | >>> "c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ==".decode( |
---|
310 | ... 'base64') |
---|
311 | 'sample.jpg\n9feac4265077922000aa8b88748e25be' |
---|
312 | |
---|
313 | Now, we want the the widget rendered again but this time with the data |
---|
314 | coming from the request. To do so, we have to create a FileUpload |
---|
315 | object that wraps the real file: |
---|
316 | |
---|
317 | >>> class UploadedFile(object): |
---|
318 | ... headers = {} |
---|
319 | ... def __init__(self, filename, f): |
---|
320 | ... self.filename = filename |
---|
321 | ... self.file = f |
---|
322 | |
---|
323 | Now we can 'post' the request and will get the same result as above: |
---|
324 | |
---|
325 | >>> upload = FileUpload(UploadedFile('sample.jpg', open(testimage, 'rb'))) |
---|
326 | >>> request = TestRequest(form={'field.foo': upload}) |
---|
327 | >>> widget = EncodingImageFileWidget(field, request) |
---|
328 | >>> print normalize(widget()) |
---|
329 | <img... />... |
---|
330 | ...<input |
---|
331 | class="fileType" |
---|
332 | id="field.foo" |
---|
333 | name="field.foo" |
---|
334 | size="20" |
---|
335 | type="file" |
---|
336 | /> |
---|
337 | (sample.jpg)<input |
---|
338 | class="hiddenType" |
---|
339 | id="field.foo.file_id" |
---|
340 | name="field.foo.file_id" |
---|
341 | type="hidden" |
---|
342 | value="c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ==" |
---|
343 | /> |
---|
344 | |
---|
345 | Now let's render again, this time not with file data available in the |
---|
346 | request, but an id. Again, we should see the same: |
---|
347 | |
---|
348 | >>> request = TestRequest(form={ |
---|
349 | ... 'field.foo.file_id': |
---|
350 | ... 'c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ=='}) |
---|
351 | >>> widget = EncodingImageFileWidget(field, request) |
---|
352 | >>> print normalize(widget()) |
---|
353 | <img... />... |
---|
354 | ...<input |
---|
355 | class="fileType" |
---|
356 | id="field.foo" |
---|
357 | name="field.foo" |
---|
358 | size="20" |
---|
359 | type="file" |
---|
360 | /> |
---|
361 | (sample.jpg)<input |
---|
362 | class="hiddenType" |
---|
363 | id="field.foo.file_id" |
---|
364 | name="field.foo.file_id" |
---|
365 | type="hidden" |
---|
366 | value="c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ==" |
---|
367 | /> |
---|
368 | |
---|
369 | If there is both file data and an id, something else happens. First, let's |
---|
370 | prepare some new file: |
---|
371 | |
---|
372 | >>> upload = FileUpload(UploadedFile('sample2.jpg', open(testimage2, 'rb'))) |
---|
373 | |
---|
374 | We happen to know, due to the implementation of |
---|
375 | EncodingImageFileWidget, that the file_id is going to be |
---|
376 | "Zm9vLnR4dAp0aGUgY29udGVudHM=". Let's make a request with the original |
---|
377 | id, but a new file upload: |
---|
378 | |
---|
379 | >>> request = TestRequest(form={'field.foo': upload, |
---|
380 | ... 'field.foo.file_id': |
---|
381 | ... 'Z2FtcGxlLmpwZwr/2P/gAA=='}) |
---|
382 | |
---|
383 | We expect the new file to be the one that's uploaded: |
---|
384 | |
---|
385 | >>> field = ImageFile(__name__='foo', title=u'Foo', required=False) |
---|
386 | >>> field = field.bind(None) |
---|
387 | >>> widget = EncodingImageFileWidget(field, request) |
---|
388 | >>> print normalize(widget()) |
---|
389 | <img... />... |
---|
390 | ...<input |
---|
391 | class="fileType" |
---|
392 | id="field.foo" |
---|
393 | name="field.foo" |
---|
394 | size="20" |
---|
395 | type="file" |
---|
396 | /> |
---|
397 | (sample2.jpg)<input |
---|
398 | class="hiddenType" |
---|
399 | id="field.foo.file_id" |
---|
400 | name="field.foo.file_id" |
---|
401 | type="hidden" |
---|
402 | value="c2FtcGxlMi5qcGcKZDk2MDkwZWRlMmRjODlkZDdkZWM5ZDU3MmFkNThjNzQ=" |
---|
403 | /> |
---|
404 | |
---|
405 | The value displayed above again means the filename and md5 sum of the |
---|
406 | stored file: |
---|
407 | |
---|
408 | >>> "c2FtcGxlMi5qcGcKZDk2MDkwZWRlMmRjODlkZDdkZWM5ZDU3MmFkNThjNzQ=".decode( |
---|
409 | ... 'base64') |
---|
410 | 'sample2.jpg\nd96090ede2dc89dd7dec9d572ad58c74' |
---|
411 | |
---|
412 | where the md5 sum is in fact correct: |
---|
413 | |
---|
414 | >>> import hashlib |
---|
415 | >>> hashlib.md5(open(testimage2, 'rb').read()).hexdigest() |
---|
416 | 'd96090ede2dc89dd7dec9d572ad58c74' |
---|
417 | |
---|
418 | Our file retrieval utility now contains two files: |
---|
419 | |
---|
420 | >>> retrieval.storage.keys() |
---|
421 | ['9feac4265077922000aa8b88748e25be', 'd96090ede2dc89dd7dec9d572ad58c74'] |
---|