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