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