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