[WIP] Multi website
[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:], code=301)
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     @http.route('/website/info', type='http', auth="public", website=True)
145     def website_info(self):
146         try:
147             request.website.get_template('website.info').name
148         except Exception, e:
149             return request.registry['ir.http']._handle_exception(e, 404)
150         irm = request.env()['ir.module.module'].sudo()
151         apps = irm.search([('state','=','installed'),('application','=',True)])
152         modules = irm.search([('state','=','installed'),('application','=',False)])
153         values = {
154             'apps': apps,
155             'modules': modules,
156             'version': openerp.service.common.exp_version()
157         }
158         return request.render('website.info', values)
159
160     #------------------------------------------------------
161     # Edit
162     #------------------------------------------------------
163     @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
164     def pagenew(self, path, noredirect=False, add_menu=None):
165         xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
166         if add_menu:
167             current = request.website
168             new_menu_id = current.menu_id.id
169             request.registry['website.menu'].create(request.cr, request.uid, {
170                     'name': path,
171                     'url': "/page/" + xml_id,
172                     'parent_id': new_menu_id,
173                     'website_id': current.id,
174                 }, context=request.context)
175         # Reverse action in order to allow shortcut for /page/<website_xml_id>
176         url = "/page/" + re.sub(r"^website\.", '', xml_id)
177
178         if noredirect:
179             return werkzeug.wrappers.Response(url, mimetype='text/plain')
180         return werkzeug.utils.redirect(url)
181
182     @http.route(['/website/snippets'], type='json', auth="public", website=True)
183     def snippets(self):
184         return request.website._render('website.snippets')
185
186     @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
187     def reset_template(self, templates, redirect='/'):
188         templates = request.httprequest.form.getlist('templates')
189         modules_to_update = []
190         for temp_id in templates:
191             view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
192             if view.page:
193                 continue
194             view.model_data_id.write({
195                 'noupdate': False
196             })
197             if view.model_data_id.module not in modules_to_update:
198                 modules_to_update.append(view.model_data_id.module)
199
200         if modules_to_update:
201             module_obj = request.registry['ir.module.module']
202             module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
203             if module_ids:
204                 module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
205         return request.redirect(redirect)
206
207     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
208     def customize_template_get(self, xml_id, full=False, bundles=False):
209         """ Lists the templates customizing ``xml_id``. By default, only
210         returns optional templates (which can be toggled on and off), if
211         ``full=True`` returns all templates customizing ``xml_id``
212         ``bundles=True`` returns also the asset bundles
213         """
214         imd = request.registry['ir.model.data']
215         view_model, view_theme_id = imd.get_object_reference(
216             request.cr, request.uid, 'website', 'theme')
217
218         user = request.registry['res.users']\
219             .browse(request.cr, request.uid, request.uid, request.context)
220         user_groups = set(user.groups_id)
221
222         views = request.registry["ir.ui.view"]\
223             ._views_get(request.cr, request.uid, xml_id, bundles=bundles, context=dict(request.context or {}, active_test=False))
224         done = set()
225         result = []
226         for v in views:
227             if not user_groups.issuperset(v.groups_id):
228                 continue
229             if full or (v.customize_show and v.inherit_id.id != view_theme_id):
230                 if v.inherit_id not in done:
231                     result.append({
232                         'name': v.inherit_id.name,
233                         'id': v.id,
234                         'xml_id': v.xml_id,
235                         'inherit_id': v.inherit_id.id,
236                         'header': True,
237                         'active': False
238                     })
239                     done.add(v.inherit_id)
240                 result.append({
241                     'name': v.name,
242                     'id': v.id,
243                     'xml_id': v.xml_id,
244                     'inherit_id': v.inherit_id.id,
245                     'header': False,
246                     'active': v.active,
247                 })
248         return result
249
250     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
251     def get_view_translations(self, xml_id, lang=None):
252         lang = lang or request.context.get('lang')
253         views = self.customize_template_get(xml_id, full=True)
254         views_ids = [view.get('id') for view in views if view.get('active')]
255         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
256         irt = request.registry.get('ir.translation')
257         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
258
259     @http.route('/website/set_translations', type='json', auth='public', website=True)
260     def set_translations(self, data, lang):
261         irt = request.registry.get('ir.translation')
262         for view_id, trans in data.items():
263             view_id = int(view_id)
264             for t in trans:
265                 initial_content = t['initial_content'].strip()
266                 new_content = t['new_content'].strip()
267                 tid = t['translation_id']
268                 if not tid:
269                     old_trans = irt.search_read(
270                         request.cr, request.uid,
271                         [
272                             ('type', '=', 'view'),
273                             ('res_id', '=', view_id),
274                             ('lang', '=', lang),
275                             ('src', '=', initial_content),
276                         ])
277                     if old_trans:
278                         tid = old_trans[0]['id']
279                 if tid:
280                     vals = {'value': new_content}
281                     irt.write(request.cr, request.uid, [tid], vals)
282                 else:
283                     new_trans = {
284                         'name': 'website',
285                         'res_id': view_id,
286                         'lang': lang,
287                         'type': 'view',
288                         'source': initial_content,
289                         'value': new_content,
290                     }
291                     if t.get('gengo_translation'):
292                         new_trans['gengo_translation'] = t.get('gengo_translation')
293                         new_trans['gengo_comment'] = t.get('gengo_comment')
294                     irt.create(request.cr, request.uid, new_trans)
295         return True
296
297     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
298     def attach(self, func, upload=None, url=None):
299         # the upload argument doesn't allow us to access the files if more than
300         # one file is uploaded, as upload references the first file
301         # therefore we have to recover the files from the request object
302         Attachments = request.registry['ir.attachment']  # registry for the attachment table
303
304         uploads = []
305         message = None
306         if not upload: # no image provided, storing the link and the image name
307             uploads.append({'website_url': url})
308             name = url.split("/").pop()                       # recover filename
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:                                                  # images provided
316             try:
317                 for c_file in request.httprequest.files.getlist('upload'):
318                     image_data = c_file.read()
319                     image = Image.open(cStringIO.StringIO(image_data))
320                     w, h = image.size
321                     if w*h > 42e6: # Nokia Lumia 1020 photo resolution
322                         raise ValueError(
323                             u"Image size excessive, uploaded images must be smaller "
324                             u"than 42 million pixel")
325     
326                     attachment_id = Attachments.create(request.cr, request.uid, {
327                         'name': c_file.filename,
328                         'datas': image_data.encode('base64'),
329                         'datas_fname': c_file.filename,
330                         'res_model': 'ir.ui.view',
331                     }, request.context)
332     
333                     [attachment] = Attachments.read(
334                         request.cr, request.uid, [attachment_id], ['website_url'],
335                         context=request.context)
336                     uploads.append(attachment)
337             except Exception, e:
338                 logger.exception("Failed to upload image to attachment")
339                 message = unicode(e)
340
341         return """<script type='text/javascript'>
342             window.parent['%s'](%s, %s);
343         </script>""" % (func, json.dumps(uploads), json.dumps(message))
344
345     @http.route(['/website/publish'], type='json', auth="public", website=True)
346     def publish(self, id, object):
347         _id = int(id)
348         _object = request.registry[object]
349         obj = _object.browse(request.cr, request.uid, _id)
350
351         values = {}
352         if 'website_published' in _object._all_columns:
353             values['website_published'] = not obj.website_published
354         _object.write(request.cr, request.uid, [_id],
355                       values, context=request.context)
356
357         obj = _object.browse(request.cr, request.uid, _id)
358         return bool(obj.website_published)
359
360     @http.route(['/website/seo_suggest/<keywords>'], type='http', auth="public", website=True)
361     def seo_suggest(self, keywords):
362         url = "http://google.com/complete/search"
363         try:
364             req = urllib2.Request("%s?%s" % (url, werkzeug.url_encode({
365                 'ie': 'utf8', 'oe': 'utf8', 'output': 'toolbar', 'q': keywords})))
366             request = urllib2.urlopen(req)
367         except (urllib2.HTTPError, urllib2.URLError):
368             return []
369         xmlroot = ET.fromstring(request.read())
370         return json.dumps([sugg[0].attrib['data'] for sugg in xmlroot if len(sugg) and sugg[0].attrib['data']])
371
372     #------------------------------------------------------
373     # Themes
374     #------------------------------------------------------
375
376     def get_view_ids(self, xml_ids):
377         ids = []
378         imd = request.registry['ir.model.data']
379         for xml_id in xml_ids:
380             if "." in xml_id:
381                 xml = xml_id.split(".")
382                 view_model, id = imd.get_object_reference(request.cr, request.uid, xml[0], xml[1])
383             else:
384                 id = int(xml_id)
385             ids.append(id)
386         return ids
387
388     @http.route(['/website/theme_customize_get'], type='json', auth="public", website=True)
389     def theme_customize_get(self, xml_ids):
390         view = request.registry["ir.ui.view"]
391         enable = []
392         disable = []
393         ids = self.get_view_ids(xml_ids)
394         context = dict(request.context or {}, active_test=True)
395         for v in view.browse(request.cr, request.uid, ids, context=context):
396             if v.active:
397                 enable.append(v.xml_id)
398             else:
399                 disable.append(v.xml_id)
400         return [enable, disable]
401
402     @http.route(['/website/theme_customize'], type='json', auth="public", website=True)
403     def theme_customize(self, enable, disable):
404         """ enable or Disable lists of ``xml_id`` of the inherit templates
405         """
406         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
407         view = pool["ir.ui.view"]
408         context = dict(request.context or {}, active_test=True)
409
410         def set_active(ids, active):
411             if ids:
412                 view.write(cr, uid, self.get_view_ids(ids), {'active': active}, context=context)
413
414         set_active(disable, False)
415         set_active(enable, True)
416
417         return True
418
419     @http.route(['/website/theme_customize_reload'], type='http', auth="public", website=True)
420     def theme_customize_reload(self, href, enable, disable):
421         self.theme_customize(enable and enable.split(",") or [],disable and disable.split(",") or [])
422         return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true"))
423
424     #------------------------------------------------------
425     # Helpers
426     #------------------------------------------------------
427     @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
428     def kanban(self, **post):
429         return request.website.kanban_col(**post)
430
431     def placeholder(self, response):
432         return request.registry['website']._image_placeholder(response)
433
434     @http.route([
435         '/website/image',
436         '/website/image/<xmlid>',
437         '/website/image/<xmlid>/<field>',
438         '/website/image/<model>/<id>/<field>',
439         '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
440         ], auth="public", website=True)
441     def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
442         """ Fetches the requested field and ensures it does not go above
443         (max_width, max_height), resizing it if necessary.
444
445         If the record is not found or does not have the requested field,
446         returns a placeholder image via :meth:`~.placeholder`.
447
448         Sets and checks conditional response parameters:
449         * :mailheader:`ETag` is always set (and checked)
450         * :mailheader:`Last-Modified is set iif the record has a concurrency
451           field (``__last_update``)
452
453         The requested field is assumed to be base64-encoded image data in
454         all cases.
455
456         xmlid can be used to load the image. But the field image must by base64-encoded
457         """
458         if xmlid and "." in xmlid:
459             xmlid = xmlid.split(".", 1)
460             try:
461                 model, id = request.registry['ir.model.data'].get_object_reference(request.cr, request.uid, xmlid[0], xmlid[1])
462             except:
463                 raise werkzeug.exceptions.NotFound()
464             if model == 'ir.attachment':
465                 field = "datas"
466
467         if not model or not id or not field:
468             raise werkzeug.exceptions.NotFound()
469
470         try:
471             response = werkzeug.wrappers.Response()
472             return request.registry['website']._image(
473                 request.cr, request.uid, model, id, field, response, max_width, max_height)
474         except Exception:
475             logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)",
476                              field, model, id, max_width, max_height)
477             response = werkzeug.wrappers.Response()
478             return self.placeholder(response)
479
480     #------------------------------------------------------
481     # Server actions
482     #------------------------------------------------------
483     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
484     def actions_server(self, path_or_xml_id_or_id, **post):
485         cr, uid, context = request.cr, request.uid, request.context
486         res, action_id, action = None, None, None
487         ServerActions = request.registry['ir.actions.server']
488
489         # find the action_id: either an xml_id, the path, or an ID
490         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
491             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)
492         if not action_id:
493             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
494             action_id = action_ids and action_ids[0] or None
495         if not action_id:
496             try:
497                 action_id = int(path_or_xml_id_or_id)
498             except ValueError:
499                 pass
500
501         # check it effectively exists
502         if action_id:
503             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
504             action_id = action_ids and action_ids[0] or None
505         # run it, return only if we got a Response object
506         if action_id:
507             action = ServerActions.browse(cr, uid, action_id, context=context)
508             if action.state == 'code' and action.website_published:
509                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
510                 if isinstance(action_res, werkzeug.wrappers.Response):
511                     res = action_res
512         if res:
513             return res
514         return request.redirect('/')
515