[IMP] extract & simplify image serving, deduplicate a bit between image fields and...
authorXavier Morel <xmo@openerp.com>
Wed, 2 Oct 2013 09:59:17 +0000 (11:59 +0200)
committerXavier Morel <xmo@openerp.com>
Wed, 2 Oct 2013 09:59:17 +0000 (11:59 +0200)
bzr revid: xmo@openerp.com-20131002095917-coux4ikqfthqhlo7

addons/website/controllers/main.py

index 1bdeddb..f7854a9 100644 (file)
@@ -1,10 +1,14 @@
 # -*- coding: utf-8 -*-
 import base64
 import cStringIO
+import contextlib
 import hashlib
 import json
 import logging
 import os
+import datetime
+
+from sys import maxint
 
 import psycopg2
 import werkzeug
@@ -230,79 +234,6 @@ class Website(openerp.addons.web.controllers.main.Home):
             window.parent['%s'](%s, %s);
         </script>""" % (func, json.dumps(url), json.dumps(message))
 
-    @website.route('/website/attachment/<int:id>', type='http', auth="admin")
-    def attachment(self, id):
-        # TODO: provide actual thumbnails?
-        # FIXME: can't use Binary.image because auth=user and website attachments need to be public
-        attachment = request.registry['ir.attachment'].browse(
-            request.cr, request.uid, id, request.context)
-
-        buf = cStringIO.StringIO(base64.decodestring(attachment.datas))
-
-        image = Image.open(buf)
-        mime = PIL_MIME_MAPPING[image.format]
-
-        w, h = image.size
-        resized = w > MAX_IMAGE_WIDTH or h > MAX_IMAGE_HEIGHT
-
-        # If saving unnecessary, just send the image buffer, don't go through
-        # Image.save() (especially as it breaks animated gifs)
-        if not resized:
-            buf.seek(0)
-            return werkzeug.wrappers.Response(buf, status=200, mimetype=mime)
-
-        image.thumbnail(IMAGE_LIMITS, Image.ANTIALIAS)
-        response = werkzeug.wrappers.Response(status=200, mimetype=mime)
-        image.save(response.stream, image.format)
-        return response
-
-    @website.route('/website/image', type='http', auth="public")
-    def image(self, model, id, field, **kw):
-        last_update = '__last_update'
-        Model = request.registry[model]
-        headers = [('Content-Type', 'image/png')]
-        etag = request.httprequest.headers.get('If-None-Match')
-        hashed_session = hashlib.md5(request.session_id).hexdigest()
-        retag = hashed_session
-        try:
-            id = int(id)
-            ids = Model.search(request.cr, request.uid, [('id', '=', id)], context=request.context)
-            if not ids:
-                id = Model.search(request.cr, openerp.SUPERUSER_ID, [('id', '=', id), ('website_published', '=', True)], context=request.context)[0]
-            if etag:
-                date = Model.read(request.cr, openerp.SUPERUSER_ID, [id], [last_update], request.context)[0].get(last_update)
-                if hashlib.md5(date).hexdigest() == etag:
-                    return werkzeug.wrappers.Response(status=304)
-
-            res = Model.read(request.cr, openerp.SUPERUSER_ID, [id], [last_update, field], context=request.context)[0]
-            retag = hashlib.md5(res.get(last_update)).hexdigest()
-            image_base64 = res.get(field)
-
-            if kw.get('resize'):
-                resize = kw.get('resize').split(',')
-                if len(resize) == 2 and int(resize[0]) and int(resize[1]):
-                    width = int(resize[0])
-                    height = int(resize[1])
-                    # resize maximum 500*500
-                    if width > 500:
-                        width = 500
-                    if height > 500:
-                        height = 500
-                    image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
-
-            image_data = base64.b64decode(image_base64)
-        except Exception:
-            image_data = open(os.path.join(http.addons_manifest['web']['addons_path'], 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
-
-        headers.append(('ETag', retag))
-        headers.append(('Content-Length', len(image_data)))
-        try:
-            ncache = int(kw.get('cache'))
-            headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
-        except:
-            pass
-        return request.make_response(image_data, headers)
-
     @website.route(['/website/publish'], type='json', auth="public")
     def publish(self, id, object):
         _id = int(id)
@@ -327,4 +258,87 @@ class Website(openerp.addons.web.controllers.main.Home):
     def sitemap(self):
         return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
 
+class Images(http.Controller):
+    @website.route('/website/image', auth="public")
+    def image(self, model, id, field):
+        Model = request.registry[model]
+
+        response = werkzeug.wrappers.Response()
+
+        id = int(id)
+
+        ids = Model.search(request.cr, request.uid,
+                           [('id', '=', id)], context=request.context)\
+            or Model.search(request.cr, openerp.SUPERUSER_ID,
+                            [('id', '=', id), ('website_published', '=', True)], context=request.context)
+
+        if not ids:
+            # file_open may return a StringIO. StringIO can be closed but are
+            # not context managers in Python 2 though that is fixed in 3
+            with contextlib.closing(openerp.tools.misc.file_open(
+                    os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
+                    mode='rb')) as f:
+                response.set_data(f.read())
+                return response
+
+        concurrency = '__last_update'
+        [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
+                              [concurrency, field], context=request.context)
+
+        if concurrency in record:
+            server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+            try:
+                response.last_modified = datetime.datetime.strptime(
+                    record[concurrency], server_format + '.%f')
+            except ValueError:
+                # just in case we have a timestamp without microseconds
+                response.last_modified = datetime.datetime.strptime(
+                    record[concurrency], server_format)
+        # FIXME: no field in record?
+        response.set_etag(hashlib.sha1(record[field]).hexdigest())
+        response.make_conditional(request.httprequest)
+
+        # conditional request match
+        if response.status_code == 304:
+            return response
+
+        return self.set_image_data(response, record[field].decode('base64'))
+
+    # FIXME: auth
+    # FIXME: delegate to image?
+    @website.route('/website/attachment/<int:id>', auth='admin')
+    def attachment(self, id):
+        attachment = request.registry['ir.attachment'].browse(
+            request.cr, request.uid, id, request.context)
+
+        return self.set_image_data(
+            werkzeug.wrappers.Response(),
+            attachment.datas.decode('base64'),
+            fit=IMAGE_LIMITS,)
+
+    def set_image_data(self, response, data, fit=(maxint, maxint)):
+        """ Sets an inferred mime type on the response object, and puts the
+        provided image's data in it, possibly after resizing if requested
+
+        Returns the response object after setting its mime and content, so
+        the result of ``get_final_image`` can be returned directly.
+        """
+        buf = cStringIO.StringIO(data)
+
+        # FIXME: unknown format or not an image
+        image = Image.open(buf)
+        response.mimetype = PIL_MIME_MAPPING[image.format]
+
+        w, h = image.size
+        max_w, max_h = fit
+
+        if w < max_w and h < max_h:
+            response.set_data(data)
+            return response
+
+        image.thumbnail(fit, Image.ANTIALIAS)
+        image.save(response.stream, image.format)
+        return response
+
+
 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: