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