[IMP] website: create share snippet. TODO: controller to send mail or mailto href
[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             attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
260                 'name': upload.filename,
261                 'datas': image_data.encode('base64'),
262                 'datas_fname': upload.filename,
263                 'res_model': 'ir.ui.view',
264             }, request.context)
265
266             url = website.urlplus('/website/image', {
267                 'model': 'ir.attachment',
268                 'id': attachment_id,
269                 'field': 'datas',
270                 'max_height': MAX_IMAGE_HEIGHT,
271                 'max_width': MAX_IMAGE_WIDTH,
272             })
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         Model = request.registry[model]
320
321         response = werkzeug.wrappers.Response()
322
323         id = int(id)
324
325         ids = Model.search(request.cr, request.uid,
326                            [('id', '=', id)], context=request.context) \
327             or Model.search(request.cr, openerp.SUPERUSER_ID,
328                             [('id', '=', id), ('website_published', '=', True)], context=request.context)
329
330         if not ids:
331             return self.placeholder(response)
332
333         concurrency = '__last_update'
334         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
335                               [concurrency, field], context=request.context)
336
337         if concurrency in record:
338             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
339             try:
340                 response.last_modified = datetime.datetime.strptime(
341                     record[concurrency], server_format + '.%f')
342             except ValueError:
343                 # just in case we have a timestamp without microseconds
344                 response.last_modified = datetime.datetime.strptime(
345                     record[concurrency], server_format)
346
347         # Field does not exist on model or field set to False
348         if not record.get(field):
349             # FIXME: maybe a field which does not exist should be a 404?
350             return self.placeholder(response)
351
352         response.set_etag(hashlib.sha1(record[field]).hexdigest())
353         response.make_conditional(request.httprequest)
354
355         # conditional request match
356         if response.status_code == 304:
357             return response
358
359         data = record[field].decode('base64')
360         fit = int(max_width), int(max_height)
361
362         buf = cStringIO.StringIO(data)
363
364         image = Image.open(buf)
365         image.load()
366         response.mimetype = Image.MIME[image.format]
367
368         w, h = image.size
369         max_w, max_h = fit
370
371         if w < max_w and h < max_h:
372             response.data = data
373         else:
374             image.thumbnail(fit, Image.ANTIALIAS)
375             image.save(response.stream, image.format)
376             # invalidate content-length computed by make_conditional as writing
377             # to response.stream does not do it (as of werkzeug 0.9.3)
378             del response.headers['Content-Length']
379
380         return response
381     
382     @http.route(['/website/current_user/'], type='json', auth="public", website=True)
383     def get_current_user(self, fields):
384         cr, uid, context = request.cr, request.uid, request.context
385         if request.registry["website"].get_public_user(cr, uid, context=context) != uid:
386             return request.registry["res.users"].read(cr, openerp.SUPERUSER_ID, [uid], fields, context=context)[0]
387         else:
388             return None
389
390     #------------------------------------------------------
391     # Server actions
392     #------------------------------------------------------
393     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
394     def actions_server(self, path_or_xml_id_or_id, **post):
395         cr, uid, context = request.cr, request.uid, request.context
396         res, action_id, action = None, None, None
397         ServerActions = request.registry['ir.actions.server']
398
399         # find the action_id: either an xml_id, the path, or an ID
400         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
401             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)
402         if not action_id:
403             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
404             action_id = action_ids and action_ids[0] or None
405         if not action_id:
406             try:
407                 action_id = int(path_or_xml_id_or_id)
408             except ValueError:
409                 pass
410
411         # check it effectively exists
412         if action_id:
413             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
414             action_id = action_ids and action_ids[0] or None
415         # run it, return only if we got a Response object
416         if action_id:
417             action = ServerActions.browse(cr, uid, action_id, context=context)
418             if action.state == 'code' and action.website_published:
419                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
420                 if isinstance(action_res, Response):
421                     res = action_res
422         if res:
423             return res
424         return request.redirect('/')
425
426 # vim:et: