8c3814735c7f1bf65b3fc192900020085c9a262d
[odoo/odoo.git] / addons / website / controllers / main.py
1 # -*- coding: utf-8 -*-
2 import base64
3 import cStringIO
4 import hashlib
5 import json
6 import logging
7 import os
8
9 import psycopg2
10 import werkzeug
11 import werkzeug.exceptions
12 import werkzeug.utils
13 import werkzeug.wrappers
14 from PIL import Image
15
16 import openerp
17 from openerp.addons.website import website
18 from openerp.addons.web import http
19 from openerp.addons.web.http import request
20
21 logger = logging.getLogger(__name__)
22
23
24 def auth_method_public():
25     registry = openerp.modules.registry.RegistryManager.get(request.db)
26     if not request.session.uid:
27         request.uid = registry['website'].get_public_user().id
28     else:
29         request.uid = request.session.uid
30 http.auth_methods['public'] = auth_method_public
31
32 NOPE = object()
33 # PIL images have a type flag, but no MIME. Reverse type flag to MIME.
34 PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
35 # Completely arbitrary limits
36 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
37 class Website(openerp.addons.web.controllers.main.Home):
38     @website.route('/', type='http', auth="admin")
39     def index(self, **kw):
40         return self.page("website.homepage")
41
42     @http.route('/admin', type='http', auth="none")
43     def admin(self, *args, **kw):
44         return super(Website, self).index(*args, **kw)
45
46      # FIXME: auth, if /pagenew known anybody can create new empty page
47     @website.route('/pagenew/<path:path>', type='http', auth="admin")
48     def pagenew(self, path, noredirect=NOPE):
49         if '.' in path:
50             module, idname = path.split('.', 1)
51         else:
52             module = 'website'
53             idname = path
54         path = "%s.%s" % (module, idname)
55
56         request.cr.execute('SAVEPOINT pagenew')
57         imd = request.registry['ir.model.data']
58         view = request.registry['ir.ui.view']
59         view_model, view_id = imd.get_object_reference(
60             request.cr, request.uid, 'website', 'default_page')
61         newview_id = view.copy(
62             request.cr, request.uid, view_id, context=request.context)
63         newview = view.browse(
64             request.cr, request.uid, newview_id, context=request.context)
65         newview.write({
66             'arch': newview.arch.replace("website.default_page", path),
67             'name': "page/%s" % path,
68             'page': True,
69         })
70         # Fuck it, we're doing it live
71         try:
72             imd.create(request.cr, request.uid, {
73                 'name': idname,
74                 'module': module,
75                 'model': 'ir.ui.view',
76                 'res_id': newview_id,
77                 'noupdate': True
78             }, context=request.context)
79         except psycopg2.IntegrityError:
80             request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
81         else:
82             request.cr.execute('RELEASE SAVEPOINT pagenew')
83         url = "/page/%s" % path
84         if noredirect is not NOPE:
85             return werkzeug.wrappers.Response(url, mimetype='text/plain')
86         return werkzeug.utils.redirect(url)
87
88     @website.route('/website/theme_change', type='http', auth="admin")
89     def theme_change(self, theme_id=False, **kwargs):
90         imd = request.registry['ir.model.data']
91         view = request.registry['ir.ui.view']
92
93         view_model, view_option_id = imd.get_object_reference(
94             request.cr, request.uid, 'website', 'theme')
95         views = view.search(
96             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
97             context=request.context)
98         view.write(request.cr, request.uid, views, {'inherit_id': False},
99                    context=request.context)
100
101         if theme_id:
102             module, xml_id = theme_id.split('.')
103             view_model, view_id = imd.get_object_reference(
104                 request.cr, request.uid, module, xml_id)
105             view.write(request.cr, request.uid, [view_id],
106                        {'inherit_id': view_option_id}, context=request.context)
107
108         return request.website.render('website.themes', {'theme_changed': True})
109
110     @website.route('/page/<path:path>', type='http', auth="admin")
111     def page(self, path, **kwargs):
112         values = {
113             'path': path,
114         }
115         try:
116             html = request.website.render(path, values)
117         except ValueError:
118             html = request.website.render('website.404', values)
119         return html
120
121     @website.route('/website/customize_template_toggle', type='json', auth='admin') # FIXME: auth
122     def customize_template_set(self, view_id):
123         view_obj = request.registry.get("ir.ui.view")
124         view = view_obj.browse(request.cr, request.uid, int(view_id),
125                                context=request.context)
126         if view.inherit_id:
127             value = False
128         else:
129             value = view.inherit_option_id and view.inherit_option_id.id or False
130         view_obj.write(request.cr, request.uid, [view_id], {
131             'inherit_id': value
132         }, context=request.context)
133         return True
134
135     @website.route('/website/customize_template_get', type='json', auth='admin') # FIXME: auth
136     def customize_template_get(self, xml_id, optional=True):
137         imd = request.registry['ir.model.data']
138         view_model, view_theme_id = imd.get_object_reference(
139             request.cr, request.uid, 'website', 'theme')
140
141         view = request.registry.get("ir.ui.view")
142         views = view._views_get(request.cr, request.uid, xml_id, request.context)
143         done = {}
144         result = []
145         for v in views:
146             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
147                 if v.inherit_option_id.id not in done:
148                     result.append({
149                         'name': v.inherit_option_id.name,
150                         'id': v.id,
151                         'header': True,
152                         'active': False
153                     })
154                     done[v.inherit_option_id.id] = True
155                 result.append({
156                     'name': v.name,
157                     'id': v.id,
158                     'header': False,
159                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
160                 })
161         return result
162
163     @website.route('/website/get_view_translations', type='json', auth='admin')
164     def get_view_translations(self, xml_id, optional=False):
165         view = request.registry.get("ir.ui.view")
166         views = view._views_get(request.cr, request.uid, xml_id, request.context)
167         return []
168
169     @website.route('/website/set_translations', type='json', auth='admin')
170     def set_translations(self, data, lang):
171         irt = request.registry.get('ir.translation')
172         for view_id, trans in data.items():
173             view_id = int(view_id)
174             for t in trans:
175                 initial_content = t['initial_content'].strip()
176                 new_content = t['new_content'].strip()
177                 old_trans = irt.search_read(
178                     request.cr, request.uid,
179                     [
180                         ('type', '=', 'view'),
181                         ('res_id', '=', view_id),
182                         ('lang', '=', lang),
183                         ('src', '=', initial_content),
184                     ])
185                 if old_trans:
186                     vals = {'value': new_content}
187                     irt.write(request.cr, request.uid, [old_trans[0]['id']], vals)
188                 else:
189                     new_trans = {
190                         'name': 'website',
191                         'res_id': view_id,
192                         'lang': lang,
193                         'type': 'view',
194                         'source': initial_content,
195                         'value': new_content,
196                     }
197                     irt.create(request.cr, request.uid, new_trans)
198         irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate
199         return True
200
201     #  # FIXME: auth, anybody can upload an attachment if URL known/found
202     @website.route('/website/attach', type='http', auth='admin')
203     def attach(self, func, upload):
204         req = request.httprequest
205         if req.method != 'POST':
206             return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
207
208         url = message = None
209         try:
210             attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
211                 'name': upload.filename,
212                 'datas': base64.encodestring(upload.read()),
213                 'datas_fname': upload.filename,
214                 'res_model': 'ir.ui.view',
215             }, request.context)
216             # FIXME: auth=user... no good.
217             url = '/website/attachment/%d' % attachment_id
218         except Exception, e:
219             logger.exception("Failed to upload image to attachment")
220             message = str(e)
221
222         return """<script type='text/javascript'>
223             window.parent['%s'](%s, %s);
224         </script>""" % (func, json.dumps(url), json.dumps(message))
225
226     @website.route('/website/attachment/<int:id>', type='http', auth="admin")
227     def attachment(self, id):
228         # TODO: provide actual thumbnails?
229         # FIXME: can't use Binary.image because auth=user and website attachments need to be public
230         attachment = request.registry['ir.attachment'].browse(
231             request.cr, request.uid, id, request.context)
232
233         buf = cStringIO.StringIO(base64.decodestring(attachment.datas))
234
235         image = Image.open(buf)
236         mime = PIL_MIME_MAPPING[image.format]
237
238         w, h = image.size
239         resized = w > MAX_IMAGE_WIDTH or h > MAX_IMAGE_HEIGHT
240
241         # If saving unnecessary, just send the image buffer, don't go through
242         # Image.save() (especially as it breaks animated gifs)
243         if not resized:
244             buf.seek(0)
245             return werkzeug.wrappers.Response(buf, status=200, mimetype=mime)
246
247         image.thumbnail(IMAGE_LIMITS, Image.ANTIALIAS)
248         response = werkzeug.wrappers.Response(status=200, mimetype=mime)
249         image.save(response.stream, image.format)
250         return response
251
252     @website.route('/website/image', type='http', auth="public")
253     def image(self, model, id, field, **kw):
254         last_update = '__last_update'
255         Model = request.registry[model]
256         headers = [('Content-Type', 'image/png')]
257         etag = request.httprequest.headers.get('If-None-Match')
258         hashed_session = hashlib.md5(request.session_id).hexdigest()
259         retag = hashed_session
260         try:
261             if etag:
262                 date = Model.read(request.cr, request.uid, [id], [last_update], request.context)[0].get(last_update)
263                 if hashlib.md5(date).hexdigest() == etag:
264                     return werkzeug.wrappers.Response(status=304)
265
266             res = Model.read(request.cr, request.uid, [id], [last_update, field], request.context)[0]
267             retag = hashlib.md5(res.get(last_update)).hexdigest()
268             image_base64 = res.get(field)
269
270             if kw.get('resize'):
271                 resize = kw.get('resize').split(',')
272                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
273                     width = int(resize[0])
274                     height = int(resize[1])
275                     # resize maximum 500*500
276                     if width > 500:
277                         width = 500
278                     if height > 500:
279                         height = 500
280                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
281
282             image_data = base64.b64decode(image_base64)
283         except Exception:
284             image_data = open(os.path.join(http.addons_manifest['web']['addons_path'], 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
285
286         headers.append(('ETag', retag))
287         headers.append(('Content-Length', len(image_data)))
288         try:
289             ncache = int(kw.get('cache'))
290             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
291         except:
292             pass
293         return request.make_response(image_data, headers)
294
295     @website.route(['/website/publish/'], type='http', auth="public")
296     def publish(self, **post):
297         _id = int(post['id'])
298         _object = request.registry[post['object']]
299
300         obj = _object.browse(request.cr, request.uid, _id)
301         _object.write(request.cr, request.uid, [_id],
302                       {'website_published': not obj.website_published},
303                       context=request.context)
304         obj = _object.browse(request.cr, request.uid, _id)
305
306         return obj.website_published and "1" or "0"
307
308     @website.route(['/website/kanban/'], type='http', auth="public")
309     def kanban(self, **post):
310         return request.website.kanban_col(**post)
311
312 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: