eac1f163cf643e5733705f86df3a18ad1e128c07
[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.addons.web.http import request, LazyResponse
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         response = super(Website, self).web_login(*args, **kw)
49         if isinstance(response, LazyResponse):
50             values = dict(response.params['values'], disable_footer=True)
51             response = request.website.render(response.params['template'], values)
52         return response
53
54     @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
55     def page(self, page, **opt):
56         values = {
57             'path': page,
58         }
59         # allow shortcut for /page/<website_xml_id>
60         if '.' not in page:
61             page = 'website.%s' % page
62
63         try:
64             request.website.get_template(page)
65         except ValueError, e:
66             # page not found
67             if request.context['editable']:
68                 page = 'website.page_404'
69             else:
70                 return request.registry['ir.http']._handle_exception(e, 404)
71
72         return request.website.render(page, values)
73
74     @http.route(['/robots.txt'], type='http', auth="public", website=True)
75     def robots(self):
76         response = request.website.render('website.robots', {'url_root': request.httprequest.url_root})
77         response.mimetype = 'text/plain'
78         return response
79
80     @http.route('/sitemap', type='http', auth='public', website=True, multilang=True)
81     def sitemap(self):
82         return request.website.render('website.sitemap', {
83             'pages': request.website.enumerate_pages()
84         })
85
86     @http.route('/sitemap.xml', type='http', auth="public", website=True)
87     def sitemap_xml(self):
88         response = request.website.render('website.sitemap_xml', {
89             'pages': request.website.enumerate_pages()
90         })
91         response.headers['Content-Type'] = 'application/xml;charset=utf-8'
92         return response
93
94     #------------------------------------------------------
95     # Edit
96     #------------------------------------------------------
97     @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
98     def pagenew(self, path, noredirect=False):
99         xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
100         url = "/page/" + xml_id
101         if noredirect:
102             return werkzeug.wrappers.Response(url, mimetype='text/plain')
103         return werkzeug.utils.redirect(url)
104
105     @http.route('/website/theme_change', type='http', auth="user", website=True)
106     def theme_change(self, theme_id=False, **kwargs):
107         imd = request.registry['ir.model.data']
108         view = request.registry['ir.ui.view']
109
110         view_model, view_option_id = imd.get_object_reference(
111             request.cr, request.uid, 'website', 'theme')
112         views = view.search(
113             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
114             context=request.context)
115         view.write(request.cr, request.uid, views, {'inherit_id': False},
116                    context=request.context)
117
118         if theme_id:
119             module, xml_id = theme_id.split('.')
120             view_model, view_id = imd.get_object_reference(
121                 request.cr, request.uid, module, xml_id)
122             view.write(request.cr, request.uid, [view_id],
123                        {'inherit_id': view_option_id}, context=request.context)
124
125         return request.website.render('website.themes', {'theme_changed': True})
126
127     @http.route(['/website/snippets'], type='json', auth="public", website=True)
128     def snippets(self):
129         return request.website._render('website.snippets')
130
131     @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
132     def reset_template(self, templates, redirect='/'):
133         templates = request.httprequest.form.getlist('templates')
134         modules_to_update = []
135         for temp_id in templates:
136             view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
137             view.model_data_id.write({
138                 'noupdate': False
139             })
140             if view.model_data_id.module not in modules_to_update:
141                 modules_to_update.append(view.model_data_id.module)
142         module_obj = request.registry['ir.module.module']
143         module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
144         module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
145         return request.redirect(redirect)
146
147     @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
148     def customize_template_set(self, view_id):
149         view_obj = request.registry.get("ir.ui.view")
150         view = view_obj.browse(request.cr, request.uid, int(view_id),
151                                context=request.context)
152         if view.inherit_id:
153             value = False
154         else:
155             value = view.inherit_option_id and view.inherit_option_id.id or False
156         view_obj.write(request.cr, request.uid, [view_id], {
157             'inherit_id': value
158         }, context=request.context)
159         return True
160
161     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
162     def customize_template_get(self, xml_id, optional=True):
163         imd = request.registry['ir.model.data']
164         view_model, view_theme_id = imd.get_object_reference(
165             request.cr, request.uid, 'website', 'theme')
166
167         user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
168         group_ids = [g.id for g in user.groups_id]
169
170         view = request.registry.get("ir.ui.view")
171         views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
172         done = {}
173         result = []
174         for v in views:
175             if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
176                 continue
177             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
178                 if v.inherit_option_id.id not in done:
179                     result.append({
180                         'name': v.inherit_option_id.name,
181                         'id': v.id,
182                         'xml_id': v.xml_id,
183                         'inherit_id': v.inherit_id.id,
184                         'header': True,
185                         'active': False
186                     })
187                     done[v.inherit_option_id.id] = True
188                 result.append({
189                     'name': v.name,
190                     'id': v.id,
191                     'xml_id': v.xml_id,
192                     'inherit_id': v.inherit_id.id,
193                     'header': False,
194                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
195                 })
196         return result
197
198     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
199     def get_view_translations(self, xml_id, lang=None):
200         lang = lang or request.context.get('lang')
201         views = self.customize_template_get(xml_id, optional=False)
202         views_ids = [view.get('id') for view in views if view.get('active')]
203         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
204         irt = request.registry.get('ir.translation')
205         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
206
207     @http.route('/website/set_translations', type='json', auth='public', website=True)
208     def set_translations(self, data, lang):
209         irt = request.registry.get('ir.translation')
210         for view_id, trans in data.items():
211             view_id = int(view_id)
212             for t in trans:
213                 initial_content = t['initial_content'].strip()
214                 new_content = t['new_content'].strip()
215                 tid = t['translation_id']
216                 if not tid:
217                     old_trans = irt.search_read(
218                         request.cr, request.uid,
219                         [
220                             ('type', '=', 'view'),
221                             ('res_id', '=', view_id),
222                             ('lang', '=', lang),
223                             ('src', '=', initial_content),
224                         ])
225                     if old_trans:
226                         tid = old_trans[0]['id']
227                 if tid:
228                     vals = {'value': new_content}
229                     irt.write(request.cr, request.uid, [tid], vals)
230                 else:
231                     new_trans = {
232                         'name': 'website',
233                         'res_id': view_id,
234                         'lang': lang,
235                         'type': 'view',
236                         'source': initial_content,
237                         'value': new_content,
238                     }
239                     irt.create(request.cr, request.uid, new_trans)
240         return True
241
242     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
243     def attach(self, func, upload):
244
245         url = message = None
246         try:
247             image_data = upload.read()
248             image = Image.open(cStringIO.StringIO(image_data))
249             w, h = image.size
250             if w*h > 42e6: # Nokia Lumia 1020 photo resolution
251                 raise ValueError(
252                     u"Image size excessive, uploaded images must be smaller "
253                     u"than 42 million pixel")
254
255             attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
256                 'name': upload.filename,
257                 'datas': image_data.encode('base64'),
258                 'datas_fname': upload.filename,
259                 'res_model': 'ir.ui.view',
260             }, request.context)
261
262             url = website.urlplus('/website/image', {
263                 'model': 'ir.attachment',
264                 'id': attachment_id,
265                 'field': 'datas',
266                 'max_height': MAX_IMAGE_HEIGHT,
267                 'max_width': MAX_IMAGE_WIDTH,
268             })
269         except Exception, e:
270             logger.exception("Failed to upload image to attachment")
271             message = unicode(e)
272
273         return """<script type='text/javascript'>
274             window.parent['%s'](%s, %s);
275         </script>""" % (func, json.dumps(url), json.dumps(message))
276
277     @http.route(['/website/publish'], type='json', auth="public", website=True)
278     def publish(self, id, object):
279         _id = int(id)
280         _object = request.registry[object]
281         obj = _object.browse(request.cr, request.uid, _id)
282
283         values = {}
284         if 'website_published' in _object._all_columns:
285             values['website_published'] = not obj.website_published
286         if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
287             values['website_published_datetime'] = fields.datetime.now()
288         _object.write(request.cr, request.uid, [_id],
289                       values, context=request.context)
290
291         obj = _object.browse(request.cr, request.uid, _id)
292         return bool(obj.website_published)
293
294     #------------------------------------------------------
295     # Helpers
296     #------------------------------------------------------
297     @http.route(['/website/kanban/'], type='http', auth="public", methods=['POST'], website=True)
298     def kanban(self, **post):
299         return request.website.kanban_col(**post)
300
301     def placeholder(self, response):
302         # file_open may return a StringIO. StringIO can be closed but are
303         # not context managers in Python 2 though that is fixed in 3
304         with contextlib.closing(openerp.tools.misc.file_open(
305                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
306                 mode='rb')) as f:
307             response.data = f.read()
308             return response.make_conditional(request.httprequest)
309
310     @http.route([
311         '/website/image',
312         '/website/image/<model>/<id>/<field>'
313         ], auth="public", website=True)
314     def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
315         Model = request.registry[model]
316
317         response = werkzeug.wrappers.Response()
318
319         id = int(id)
320
321         ids = Model.search(request.cr, request.uid,
322                            [('id', '=', id)], context=request.context) \
323             or Model.search(request.cr, openerp.SUPERUSER_ID,
324                             [('id', '=', id), ('website_published', '=', True)], context=request.context)
325
326         if not ids:
327             return self.placeholder(response)
328
329         concurrency = '__last_update'
330         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
331                               [concurrency, field], context=request.context)
332
333         if concurrency in record:
334             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
335             try:
336                 response.last_modified = datetime.datetime.strptime(
337                     record[concurrency], server_format + '.%f')
338             except ValueError:
339                 # just in case we have a timestamp without microseconds
340                 response.last_modified = datetime.datetime.strptime(
341                     record[concurrency], server_format)
342
343         # Field does not exist on model or field set to False
344         if not record.get(field):
345             # FIXME: maybe a field which does not exist should be a 404?
346             return self.placeholder(response)
347
348         response.set_etag(hashlib.sha1(record[field]).hexdigest())
349         response.make_conditional(request.httprequest)
350
351         # conditional request match
352         if response.status_code == 304:
353             return response
354
355         data = record[field].decode('base64')
356         fit = int(max_width), int(max_height)
357
358         buf = cStringIO.StringIO(data)
359
360         image = Image.open(buf)
361         image.load()
362         response.mimetype = Image.MIME[image.format]
363
364         w, h = image.size
365         max_w, max_h = fit
366
367         if w < max_w and h < max_h:
368             response.data = data
369         else:
370             image.thumbnail(fit, Image.ANTIALIAS)
371             image.save(response.stream, image.format)
372             # invalidate content-length computed by make_conditional as writing
373             # to response.stream does not do it (as of werkzeug 0.9.3)
374             del response.headers['Content-Length']
375
376         return response
377
378     #------------------------------------------------------
379     # Server actions
380     #------------------------------------------------------
381     @http.route(['/website/action/<id_or_xml_id>'], type='http', auth="public", website=True)
382     def actions_server(self, id_or_xml_id, **post):
383         cr, uid, context = request.cr, request.uid, request.context
384         res, action_id, action = None, None, None
385         ServerActions = request.registry['ir.actions.server']
386
387         # find the action_id, either an int, an int into a basestring, or an xml_id
388         if isinstance(id_or_xml_id, basestring) and '.' in id_or_xml_id:
389             action_id = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, request.uid, id_or_xml_id, raise_if_not_found=False)
390         else:
391             try:
392                 action_id = int(id_or_xml_id)
393             except ValueError:
394                 pass
395         # check it effectively exists
396         if action_id:
397             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
398             action_id = action_ids and action_ids[0] or None
399         # run it, return only LazyResponse that are templates to be rendered
400         if action_id:
401             action = ServerActions.browse(cr, uid, action_id, context=context)
402             if action.state == 'code' and action.website_published:
403                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
404                 if isinstance(action_res, LazyResponse):
405                     res = action_res
406         if res:
407             return res
408         return request.redirect('/')