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