36050d63c6fe786dc71d425211c61b63a35e6879
[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             if view.page:
195                 continue
196             view.model_data_id.write({
197                 'noupdate': False
198             })
199             if view.model_data_id.module not in modules_to_update:
200                 modules_to_update.append(view.model_data_id.module)
201
202         if modules_to_update:
203             module_obj = request.registry['ir.module.module']
204             module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
205             if module_ids:
206                 module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
207         return request.redirect(redirect)
208
209     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
210     def customize_template_get(self, xml_id, full=False, bundles=False):
211         """ Lists the templates customizing ``xml_id``. By default, only
212         returns optional templates (which can be toggled on and off), if
213         ``full=True`` returns all templates customizing ``xml_id``
214         ``bundles=True`` returns also the asset bundles
215         """
216         imd = request.registry['ir.model.data']
217         view_model, view_theme_id = imd.get_object_reference(
218             request.cr, request.uid, 'website', 'theme')
219
220         user = request.registry['res.users']\
221             .browse(request.cr, request.uid, request.uid, request.context)
222         user_groups = set(user.groups_id)
223
224         views = request.registry["ir.ui.view"]\
225             ._views_get(request.cr, request.uid, xml_id, bundles=bundles, context=request.context)
226         done = set()
227         result = []
228         for v in views:
229             if not user_groups.issuperset(v.groups_id):
230                 continue
231             if full or (v.application != 'always' and v.inherit_id.id != view_theme_id):
232                 if v.inherit_id not in done:
233                     result.append({
234                         'name': v.inherit_id.name,
235                         'id': v.id,
236                         'xml_id': v.xml_id,
237                         'inherit_id': v.inherit_id.id,
238                         'header': True,
239                         'active': False
240                     })
241                     done.add(v.inherit_id)
242                 result.append({
243                     'name': v.name,
244                     'id': v.id,
245                     'xml_id': v.xml_id,
246                     'inherit_id': v.inherit_id.id,
247                     'header': False,
248                     'active': v.application in ('always', 'enabled'),
249                 })
250         return result
251
252     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
253     def get_view_translations(self, xml_id, lang=None):
254         lang = lang or request.context.get('lang')
255         views = self.customize_template_get(xml_id, full=True)
256         views_ids = [view.get('id') for view in views if view.get('active')]
257         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
258         irt = request.registry.get('ir.translation')
259         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
260
261     @http.route('/website/set_translations', type='json', auth='public', website=True)
262     def set_translations(self, data, lang):
263         irt = request.registry.get('ir.translation')
264         for view_id, trans in data.items():
265             view_id = int(view_id)
266             for t in trans:
267                 initial_content = t['initial_content'].strip()
268                 new_content = t['new_content'].strip()
269                 tid = t['translation_id']
270                 if not tid:
271                     old_trans = irt.search_read(
272                         request.cr, request.uid,
273                         [
274                             ('type', '=', 'view'),
275                             ('res_id', '=', view_id),
276                             ('lang', '=', lang),
277                             ('src', '=', initial_content),
278                         ])
279                     if old_trans:
280                         tid = old_trans[0]['id']
281                 if tid:
282                     vals = {'value': new_content}
283                     irt.write(request.cr, request.uid, [tid], vals)
284                 else:
285                     new_trans = {
286                         'name': 'website',
287                         'res_id': view_id,
288                         'lang': lang,
289                         'type': 'view',
290                         'source': initial_content,
291                         'value': new_content,
292                     }
293                     if t.get('gengo_translation'):
294                         new_trans['gengo_translation'] = t.get('gengo_translation')
295                         new_trans['gengo_comment'] = t.get('gengo_comment')
296                     irt.create(request.cr, request.uid, new_trans)
297         return True
298
299     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
300     def attach(self, func, upload=None, url=None):
301         Attachments = request.registry['ir.attachment']
302
303         website_url = message = None
304         if not upload:
305             website_url = url
306             name = url.split("/").pop()
307             attachment_id = Attachments.create(request.cr, request.uid, {
308                 'name':name,
309                 'type': 'url',
310                 'url': url,
311                 'res_model': 'ir.ui.view',
312             }, request.context)
313         else:
314             try:
315                 image_data = upload.read()
316                 image = Image.open(cStringIO.StringIO(image_data))
317                 w, h = image.size
318                 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
319                     raise ValueError(
320                         u"Image size excessive, uploaded images must be smaller "
321                         u"than 42 million pixel")
322
323                 attachment_id = Attachments.create(request.cr, request.uid, {
324                     'name': upload.filename,
325                     'datas': image_data.encode('base64'),
326                     'datas_fname': upload.filename,
327                     'res_model': 'ir.ui.view',
328                 }, request.context)
329
330                 [attachment] = Attachments.read(
331                     request.cr, request.uid, [attachment_id], ['website_url'],
332                     context=request.context)
333                 website_url = attachment['website_url']
334             except Exception, e:
335                 logger.exception("Failed to upload image to attachment")
336                 message = unicode(e)
337
338         return """<script type='text/javascript'>
339             window.parent['%s'](%s, %s);
340         </script>""" % (func, json.dumps(website_url), json.dumps(message))
341
342     @http.route(['/website/publish'], type='json', auth="public", website=True)
343     def publish(self, id, object):
344         _id = int(id)
345         _object = request.registry[object]
346         obj = _object.browse(request.cr, request.uid, _id)
347
348         values = {}
349         if 'website_published' in _object._all_columns:
350             values['website_published'] = not obj.website_published
351         _object.write(request.cr, request.uid, [_id],
352                       values, context=request.context)
353
354         obj = _object.browse(request.cr, request.uid, _id)
355         return bool(obj.website_published)
356
357     #------------------------------------------------------
358     # Helpers
359     #------------------------------------------------------
360     @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
361     def kanban(self, **post):
362         return request.website.kanban_col(**post)
363
364     def placeholder(self, response):
365         return request.registry['website']._image_placeholder(response)
366
367     @http.route([
368         '/website/image',
369         '/website/image/<model>/<id>/<field>'
370         ], auth="public", website=True)
371     def website_image(self, model, id, field, max_width=None, max_height=None):
372         """ Fetches the requested field and ensures it does not go above
373         (max_width, max_height), resizing it if necessary.
374
375         If the record is not found or does not have the requested field,
376         returns a placeholder image via :meth:`~.placeholder`.
377
378         Sets and checks conditional response parameters:
379         * :mailheader:`ETag` is always set (and checked)
380         * :mailheader:`Last-Modified is set iif the record has a concurrency
381           field (``__last_update``)
382
383         The requested field is assumed to be base64-encoded image data in
384         all cases.
385         """
386         response = werkzeug.wrappers.Response()
387         return request.registry['website']._image(
388                     request.cr, request.uid, model, id, field, response, max_width, max_height)
389
390
391     #------------------------------------------------------
392     # Server actions
393     #------------------------------------------------------
394     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
395     def actions_server(self, path_or_xml_id_or_id, **post):
396         cr, uid, context = request.cr, request.uid, request.context
397         res, action_id, action = None, None, None
398         ServerActions = request.registry['ir.actions.server']
399
400         # find the action_id: either an xml_id, the path, or an ID
401         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
402             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)
403         if not action_id:
404             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
405             action_id = action_ids and action_ids[0] or None
406         if not action_id:
407             try:
408                 action_id = int(path_or_xml_id_or_id)
409             except ValueError:
410                 pass
411
412         # check it effectively exists
413         if action_id:
414             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
415             action_id = action_ids and action_ids[0] or None
416         # run it, return only if we got a Response object
417         if action_id:
418             action = ServerActions.browse(cr, uid, action_id, context=context)
419             if action.state == 'code' and action.website_published:
420                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
421                 if isinstance(action_res, werkzeug.wrappers.Response):
422                     res = action_res
423         if res:
424             return res
425         return request.redirect('/')
426