19df0de92a241edfc7061027e6ba7f72cd44819a
[odoo/odoo.git] / addons / website / controllers / main.py
1 # -*- coding: utf-8 -*-
2 import cStringIO
3 import contextlib
4 import hashlib
5 import json
6 import logging
7 import os
8 import datetime
9
10 from sys import maxint
11
12 import werkzeug
13 import werkzeug.exceptions
14 import werkzeug.utils
15 import werkzeug.wrappers
16 from PIL import Image
17
18 import openerp
19 from openerp.osv import fields
20 from openerp.addons.website.models import website
21 from openerp.addons.web import http
22 from openerp.http import request, Response
23
24 logger = logging.getLogger(__name__)
25
26 # Completely arbitrary limits
27 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
28
29
30 class Website(openerp.addons.web.controllers.main.Home):
31     #------------------------------------------------------
32     # View
33     #------------------------------------------------------
34     @http.route('/', type='http', auth="public", website=True, multilang=True)
35     def index(self, **kw):
36         try:
37             main_menu = request.registry['ir.model.data'].get_object(request.cr, request.uid, 'website', 'main_menu')
38             first_menu = main_menu.child_id and main_menu.child_id[0]
39             # Dont 302 loop on /
40             if first_menu and not ((first_menu.url == '/') or first_menu.url.startswith('/#') or first_menu.url.startswith('/?')):
41                 return request.redirect(first_menu.url)
42         except:
43             pass
44         return self.page("website.homepage")
45
46     @http.route(website=True, auth="public", multilang=True)
47     def web_login(self, *args, **kw):
48         # TODO: can't we just put auth=public, ... in web client ?
49         return super(Website, self).web_login(*args, **kw)
50
51     @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
52     def page(self, page, **opt):
53         values = {
54             'path': page,
55         }
56         # allow shortcut for /page/<website_xml_id>
57         if '.' not in page:
58             page = 'website.%s' % page
59
60         try:
61             request.website.get_template(page)
62         except ValueError, e:
63             # page not found
64             if request.context['editable']:
65                 page = 'website.page_404'
66             else:
67                 return request.registry['ir.http']._handle_exception(e, 404)
68
69         return request.render(page, values)
70
71     @http.route(['/robots.txt'], type='http', auth="public", website=True)
72     def robots(self):
73         return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain')
74
75     @http.route('/sitemap', type='http', auth='public', website=True, multilang=True)
76     def sitemap(self):
77         return request.render('website.sitemap', {
78             'pages': request.website.enumerate_pages()
79         })
80
81     @http.route('/sitemap.xml', type='http', auth="public", website=True)
82     def sitemap_xml(self):
83         values = {
84             'pages': request.website.enumerate_pages()
85         }
86         headers = {
87             'Content-Type': 'application/xml;charset=utf-8',
88         }
89         return request.render('website.sitemap_xml', values, headers=headers)
90
91     #------------------------------------------------------
92     # Edit
93     #------------------------------------------------------
94     @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
95     def pagenew(self, path, noredirect=False, add_menu=None):
96         xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
97         if add_menu:
98             model, id  = request.registry["ir.model.data"].get_object_reference(request.cr, request.uid, 'website', 'main_menu')
99             request.registry['website.menu'].create(request.cr, request.uid, {
100                     'name': path,
101                     'url': "/page/" + xml_id,
102                     'parent_id': id,
103                 }, context=request.context)
104         url = "/page/" + xml_id
105         if noredirect:
106             return werkzeug.wrappers.Response(url, mimetype='text/plain')
107         return werkzeug.utils.redirect(url)
108
109     @http.route('/website/theme_change', type='http', auth="user", website=True)
110     def theme_change(self, theme_id=False, **kwargs):
111         imd = request.registry['ir.model.data']
112         view = request.registry['ir.ui.view']
113
114         view_model, view_option_id = imd.get_object_reference(
115             request.cr, request.uid, 'website', 'theme')
116         views = view.search(
117             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
118             context=request.context)
119         view.write(request.cr, request.uid, views, {'inherit_id': False},
120                    context=request.context)
121
122         if theme_id:
123             module, xml_id = theme_id.split('.')
124             view_model, view_id = imd.get_object_reference(
125                 request.cr, request.uid, module, xml_id)
126             view.write(request.cr, request.uid, [view_id],
127                        {'inherit_id': view_option_id}, context=request.context)
128
129         return request.render('website.themes', {'theme_changed': True})
130
131     @http.route(['/website/snippets'], type='json', auth="public", website=True)
132     def snippets(self):
133         return request.website._render('website.snippets')
134
135     @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
136     def reset_template(self, templates, redirect='/'):
137         templates = request.httprequest.form.getlist('templates')
138         modules_to_update = []
139         for temp_id in templates:
140             view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
141             view.model_data_id.write({
142                 'noupdate': False
143             })
144             if view.model_data_id.module not in modules_to_update:
145                 modules_to_update.append(view.model_data_id.module)
146         module_obj = request.registry['ir.module.module']
147         module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
148         module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
149         return request.redirect(redirect)
150
151     @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
152     def customize_template_set(self, view_id):
153         view_obj = request.registry.get("ir.ui.view")
154         view = view_obj.browse(request.cr, request.uid, int(view_id),
155                                context=request.context)
156         if view.inherit_id:
157             value = False
158         else:
159             value = view.inherit_option_id and view.inherit_option_id.id or False
160         view_obj.write(request.cr, request.uid, [view_id], {
161             'inherit_id': value
162         }, context=request.context)
163         return True
164
165     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
166     def customize_template_get(self, xml_id, optional=True):
167         imd = request.registry['ir.model.data']
168         view_model, view_theme_id = imd.get_object_reference(
169             request.cr, request.uid, 'website', 'theme')
170
171         user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
172         group_ids = [g.id for g in user.groups_id]
173
174         view = request.registry.get("ir.ui.view")
175         views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
176         done = {}
177         result = []
178         for v in views:
179             if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
180                 continue
181             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
182                 if v.inherit_option_id.id not in done:
183                     result.append({
184                         'name': v.inherit_option_id.name,
185                         'id': v.id,
186                         'xml_id': v.xml_id,
187                         'inherit_id': v.inherit_id.id,
188                         'header': True,
189                         'active': False
190                     })
191                     done[v.inherit_option_id.id] = True
192                 result.append({
193                     'name': v.name,
194                     'id': v.id,
195                     'xml_id': v.xml_id,
196                     'inherit_id': v.inherit_id.id,
197                     'header': False,
198                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
199                 })
200         return result
201
202     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
203     def get_view_translations(self, xml_id, lang=None):
204         lang = lang or request.context.get('lang')
205         views = self.customize_template_get(xml_id, optional=False)
206         views_ids = [view.get('id') for view in views if view.get('active')]
207         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
208         irt = request.registry.get('ir.translation')
209         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
210
211     @http.route('/website/set_translations', type='json', auth='public', website=True)
212     def set_translations(self, data, lang):
213         irt = request.registry.get('ir.translation')
214         for view_id, trans in data.items():
215             view_id = int(view_id)
216             for t in trans:
217                 initial_content = t['initial_content'].strip()
218                 new_content = t['new_content'].strip()
219                 tid = t['translation_id']
220                 if not tid:
221                     old_trans = irt.search_read(
222                         request.cr, request.uid,
223                         [
224                             ('type', '=', 'view'),
225                             ('res_id', '=', view_id),
226                             ('lang', '=', lang),
227                             ('src', '=', initial_content),
228                         ])
229                     if old_trans:
230                         tid = old_trans[0]['id']
231                 if tid:
232                     vals = {'value': new_content}
233                     irt.write(request.cr, request.uid, [tid], vals)
234                 else:
235                     new_trans = {
236                         'name': 'website',
237                         'res_id': view_id,
238                         'lang': lang,
239                         'type': 'view',
240                         'source': initial_content,
241                         'value': new_content,
242                     }
243                     if t.get('gengo_translation'):
244                         new_trans['gengo_translation'] = t.get('gengo_translation')
245                         new_trans['gengo_comment'] = t.get('gengo_comment')
246                     irt.create(request.cr, request.uid, new_trans)
247         return True
248
249     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
250     def attach(self, func, upload=None, url=None):
251         Attachments = request.registry['ir.attachment']
252
253         website_url = message = None
254         if not upload:
255             website_url = url
256             name = url.split("/").pop()
257             attachment_id = Attachments.create(request.cr, request.uid, {
258                 'name':name,
259                 'type': 'url',
260                 'url': url,
261                 'res_model': 'ir.ui.view',
262             }, request.context)
263         else:
264             try:
265                 image_data = upload.read()
266                 image = Image.open(cStringIO.StringIO(image_data))
267                 w, h = image.size
268                 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
269                     raise ValueError(
270                         u"Image size excessive, uploaded images must be smaller "
271                         u"than 42 million pixel")
272
273                 attachment_id = Attachments.create(request.cr, request.uid, {
274                     'name': upload.filename,
275                     'datas': image_data.encode('base64'),
276                     'datas_fname': upload.filename,
277                     'res_model': 'ir.ui.view',
278                 }, request.context)
279
280                 [attachment] = Attachments.read(
281                     request.cr, request.uid, [attachment_id], ['website_url'],
282                     context=request.context)
283                 website_url = attachment['website_url']
284             except Exception, e:
285                 logger.exception("Failed to upload image to attachment")
286                 message = unicode(e)
287
288         return """<script type='text/javascript'>
289             window.parent['%s'](%s, %s);
290         </script>""" % (func, json.dumps(website_url), json.dumps(message))
291
292     @http.route(['/website/publish'], type='json', auth="public", website=True)
293     def publish(self, id, object):
294         _id = int(id)
295         _object = request.registry[object]
296         obj = _object.browse(request.cr, request.uid, _id)
297
298         values = {}
299         if 'website_published' in _object._all_columns:
300             values['website_published'] = not obj.website_published
301         _object.write(request.cr, request.uid, [_id],
302                       values, context=request.context)
303
304         obj = _object.browse(request.cr, request.uid, _id)
305         return bool(obj.website_published)
306
307     #------------------------------------------------------
308     # Helpers
309     #------------------------------------------------------
310     @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
311     def kanban(self, **post):
312         return request.website.kanban_col(**post)
313
314     def placeholder(self, response):
315         # file_open may return a StringIO. StringIO can be closed but are
316         # not context managers in Python 2 though that is fixed in 3
317         with contextlib.closing(openerp.tools.misc.file_open(
318                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
319                 mode='rb')) as f:
320             response.data = f.read()
321             return response.make_conditional(request.httprequest)
322
323     @http.route([
324         '/website/image',
325         '/website/image/<model>/<id>/<field>'
326         ], auth="public", website=True)
327     def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
328         """ Fetches the requested field and ensures it does not go above
329         (max_width, max_height), resizing it if necessary.
330
331         Resizing is bypassed if the object provides a $field_big, which will
332         be interpreted as a pre-resized version of the base field.
333
334         If the record is not found or does not have the requested field,
335         returns a placeholder image via :meth:`~.placeholder`.
336
337         Sets and checks conditional response parameters:
338         * :mailheader:`ETag` is always set (and checked)
339         * :mailheader:`Last-Modified is set iif the record has a concurrency
340           field (``__last_update``)
341
342         The requested field is assumed to be base64-encoded image data in
343         all cases.
344         """
345         Model = request.registry[model]
346
347         response = werkzeug.wrappers.Response()
348
349         id = int(id)
350
351         ids = Model.search(request.cr, request.uid,
352                            [('id', '=', id)], context=request.context)
353         if not ids and 'website_published' in Model._all_columns:
354             ids = Model.search(request.cr, openerp.SUPERUSER_ID,
355                                [('id', '=', id), ('website_published', '=', True)], context=request.context)
356
357         if not ids:
358             return self.placeholder(response)
359
360         presized = '%s_big' % field
361         concurrency = '__last_update'
362         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
363                               [concurrency, field, presized],
364                               context=request.context)
365
366         if concurrency in record:
367             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
368             try:
369                 response.last_modified = datetime.datetime.strptime(
370                     record[concurrency], server_format + '.%f')
371             except ValueError:
372                 # just in case we have a timestamp without microseconds
373                 response.last_modified = datetime.datetime.strptime(
374                     record[concurrency], server_format)
375
376         # Field does not exist on model or field set to False
377         if not record.get(field):
378             # FIXME: maybe a field which does not exist should be a 404?
379             return self.placeholder(response)
380
381         response.set_etag(hashlib.sha1(record[field]).hexdigest())
382         response.make_conditional(request.httprequest)
383
384         # conditional request match
385         if response.status_code == 304:
386             return response
387
388         data = (record.get(presized) or record[field]).decode('base64')
389
390         image = Image.open(cStringIO.StringIO(data))
391         response.mimetype = Image.MIME[image.format]
392
393         # record provides a pre-resized version of the base field, use that
394         # directly
395         if record.get(presized):
396             response.set_data(data)
397             return response
398
399         fit = int(max_width), int(max_height)
400         w, h = image.size
401         max_w, max_h = fit
402
403         if w < max_w and h < max_h:
404             response.set_data(data)
405         else:
406             image.thumbnail(fit, Image.ANTIALIAS)
407             image.save(response.stream, image.format)
408             # invalidate content-length computed by make_conditional as
409             # writing to response.stream does not do it (as of werkzeug 0.9.3)
410             del response.headers['Content-Length']
411
412         return response
413
414     #------------------------------------------------------
415     # Server actions
416     #------------------------------------------------------
417     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
418     def actions_server(self, path_or_xml_id_or_id, **post):
419         cr, uid, context = request.cr, request.uid, request.context
420         res, action_id, action = None, None, None
421         ServerActions = request.registry['ir.actions.server']
422
423         # find the action_id: either an xml_id, the path, or an ID
424         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
425             action_id = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, request.uid, path_or_xml_id_or_id, raise_if_not_found=False)
426         if not action_id:
427             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
428             action_id = action_ids and action_ids[0] or None
429         if not action_id:
430             try:
431                 action_id = int(path_or_xml_id_or_id)
432             except ValueError:
433                 pass
434
435         # check it effectively exists
436         if action_id:
437             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
438             action_id = action_ids and action_ids[0] or None
439         # run it, return only if we got a Response object
440         if action_id:
441             action = ServerActions.browse(cr, uid, action_id, context=context)
442             if action.state == 'code' and action.website_published:
443                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
444                 if isinstance(action_res, Response):
445                     res = action_res
446         if res:
447             return res
448         return request.redirect('/')
449
450 # vim:et: