011a31e00e390093d1758c793fefcb427e1766f2
[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'], 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                     irt.create(request.cr, request.uid, new_trans)
246         return True
247
248     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
249     def attach(self, func, upload):
250
251         url = message = None
252         try:
253             image_data = upload.read()
254             image = Image.open(cStringIO.StringIO(image_data))
255             w, h = image.size
256             if w*h > 42e6: # Nokia Lumia 1020 photo resolution
257                 raise ValueError(
258                     u"Image size excessive, uploaded images must be smaller "
259                     u"than 42 million pixel")
260
261             Attachments = request.registry['ir.attachment']
262             attachment_id = Attachments.create(request.cr, request.uid, {
263                 'name': upload.filename,
264                 'datas': image_data.encode('base64'),
265                 'datas_fname': upload.filename,
266                 'res_model': 'ir.ui.view',
267             }, request.context)
268
269             [attachment] = Attachments.read(
270                 request.cr, request.uid, [attachment_id], ['website_url'],
271                 context=request.context)
272             url = attachment['website_url']
273         except Exception, e:
274             logger.exception("Failed to upload image to attachment")
275             message = unicode(e)
276
277         return """<script type='text/javascript'>
278             window.parent['%s'](%s, %s);
279         </script>""" % (func, json.dumps(url), json.dumps(message))
280
281     @http.route(['/website/publish'], type='json', auth="public", website=True)
282     def publish(self, id, object):
283         _id = int(id)
284         _object = request.registry[object]
285         obj = _object.browse(request.cr, request.uid, _id)
286
287         values = {}
288         if 'website_published' in _object._all_columns:
289             values['website_published'] = not obj.website_published
290         if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
291             values['website_published_datetime'] = fields.datetime.now()
292         _object.write(request.cr, request.uid, [_id],
293                       values, context=request.context)
294
295         obj = _object.browse(request.cr, request.uid, _id)
296         return bool(obj.website_published)
297
298     #------------------------------------------------------
299     # Helpers
300     #------------------------------------------------------
301     @http.route(['/website/kanban/'], type='http', auth="public", methods=['POST'], website=True)
302     def kanban(self, **post):
303         return request.website.kanban_col(**post)
304
305     def placeholder(self, response):
306         # file_open may return a StringIO. StringIO can be closed but are
307         # not context managers in Python 2 though that is fixed in 3
308         with contextlib.closing(openerp.tools.misc.file_open(
309                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
310                 mode='rb')) as f:
311             response.data = f.read()
312             return response.make_conditional(request.httprequest)
313
314     @http.route([
315         '/website/image',
316         '/website/image/<model>/<id>/<field>'
317         ], auth="public", website=True)
318     def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
319         """ Fetches the requested field and ensures it does not go above
320         (max_width, max_height), resizing it if necessary.
321
322         Resizing is bypassed if the object provides a $field_big, which will
323         be interpreted as a pre-resized version of the base field.
324
325         If the record is not found or does not have the requested field,
326         returns a placeholder image via :meth:`~.placeholder`.
327
328         Sets and checks conditional response parameters:
329         * :mailheader:`ETag` is always set (and checked)
330         * :mailheader:`Last-Modified is set iif the record has a concurrency
331           field (``__last_update``)
332
333         The requested field is assumed to be base64-encoded image data in
334         all cases.
335         """
336         Model = request.registry[model]
337
338         response = werkzeug.wrappers.Response()
339
340         id = int(id)
341
342         ids = Model.search(request.cr, request.uid,
343                            [('id', '=', id)], context=request.context) \
344            or Model.search(request.cr, openerp.SUPERUSER_ID,
345                            [('id', '=', id), ('website_published', '=', True)], context=request.context)
346
347         if not ids:
348             return self.placeholder(response)
349
350         presized = '%s_big' % field
351         concurrency = '__last_update'
352         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
353                               [concurrency, field, presized],
354                               context=request.context)
355
356         if concurrency in record:
357             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
358             try:
359                 response.last_modified = datetime.datetime.strptime(
360                     record[concurrency], server_format + '.%f')
361             except ValueError:
362                 # just in case we have a timestamp without microseconds
363                 response.last_modified = datetime.datetime.strptime(
364                     record[concurrency], server_format)
365
366         # Field does not exist on model or field set to False
367         if not record.get(field):
368             # FIXME: maybe a field which does not exist should be a 404?
369             return self.placeholder(response)
370
371         response.set_etag(hashlib.sha1(record[field]).hexdigest())
372         response.make_conditional(request.httprequest)
373
374         # conditional request match
375         if response.status_code == 304:
376             return response
377
378         data = (record.get(presized) or record[field]).decode('base64')
379
380         image = Image.open(cStringIO.StringIO(data))
381         response.mimetype = Image.MIME[image.format]
382
383         # record provides a pre-resized version of the base field, use that
384         # directly
385         if record.get(presized):
386             response.set_data(data)
387             return response
388
389         fit = int(max_width), int(max_height)
390         w, h = image.size
391         max_w, max_h = fit
392
393         if w < max_w and h < max_h:
394             response.set_data(data)
395         else:
396             image.thumbnail(fit, Image.ANTIALIAS)
397             image.save(response.stream, image.format)
398             # invalidate content-length computed by make_conditional as
399             # writing to response.stream does not do it (as of werkzeug 0.9.3)
400             del response.headers['Content-Length']
401
402         return response
403
404     #------------------------------------------------------
405     # Server actions
406     #------------------------------------------------------
407     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
408     def actions_server(self, path_or_xml_id_or_id, **post):
409         cr, uid, context = request.cr, request.uid, request.context
410         res, action_id, action = None, None, None
411         ServerActions = request.registry['ir.actions.server']
412
413         # find the action_id: either an xml_id, the path, or an ID
414         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
415             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)
416         if not action_id:
417             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
418             action_id = action_ids and action_ids[0] or None
419         if not action_id:
420             try:
421                 action_id = int(path_or_xml_id_or_id)
422             except ValueError:
423                 pass
424
425         # check it effectively exists
426         if action_id:
427             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
428             action_id = action_ids and action_ids[0] or None
429         # run it, return only if we got a Response object
430         if action_id:
431             action = ServerActions.browse(cr, uid, action_id, context=context)
432             if action.state == 'code' and action.website_published:
433                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
434                 if isinstance(action_res, Response):
435                     res = action_res
436         if res:
437             return res
438         return request.redirect('/')
439
440 # vim:et: