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