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