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