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