[FIX] Some fixes for base_import_module
[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             model, id  = request.registry["ir.model.data"].get_object_reference(request.cr, request.uid, 'website', 'main_menu')
168             request.registry['website.menu'].create(request.cr, request.uid, {
169                     'name': path,
170                     'url': "/page/" + xml_id,
171                     'parent_id': id,
172                 }, context=request.context)
173         # Reverse action in order to allow shortcut for /page/<website_xml_id>
174         url = "/page/" + re.sub(r"^website\.", '', xml_id)
175
176         if noredirect:
177             return werkzeug.wrappers.Response(url, mimetype='text/plain')
178         return werkzeug.utils.redirect(url)
179
180     @http.route('/website/theme_change', type='http', auth="user", website=True)
181     def theme_change(self, theme_id=False, **kwargs):
182         imd = request.registry['ir.model.data']
183         Views = request.registry['ir.ui.view']
184
185         _, theme_template_id = imd.get_object_reference(
186             request.cr, request.uid, 'website', 'theme')
187         views = Views.search(request.cr, request.uid, [
188             ('inherit_id', '=', theme_template_id),
189         ], context=request.context)
190         Views.write(request.cr, request.uid, views, {
191             'active': False,
192         }, context=dict(request.context or {}, active_test=True))
193
194         if theme_id:
195             module, xml_id = theme_id.split('.')
196             _, view_id = imd.get_object_reference(
197                 request.cr, request.uid, module, xml_id)
198             Views.write(request.cr, request.uid, [view_id], {
199                 'active': True
200             }, context=dict(request.context or {}, active_test=True))
201
202         return request.render('website.themes', {'theme_changed': True})
203
204     @http.route(['/website/snippets'], type='json', auth="public", website=True)
205     def snippets(self):
206         return request.website._render('website.snippets')
207
208     @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
209     def reset_template(self, templates, redirect='/'):
210         templates = request.httprequest.form.getlist('templates')
211         modules_to_update = []
212         for temp_id in templates:
213             view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
214             if view.page:
215                 continue
216             view.model_data_id.write({
217                 'noupdate': False
218             })
219             if view.model_data_id.module not in modules_to_update:
220                 modules_to_update.append(view.model_data_id.module)
221
222         if modules_to_update:
223             module_obj = request.registry['ir.module.module']
224             module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
225             if module_ids:
226                 module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
227         return request.redirect(redirect)
228
229     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
230     def customize_template_get(self, xml_id, full=False):
231         """ Lists the templates customizing ``xml_id``. By default, only
232         returns optional templates (which can be toggled on and off), if
233         ``full=True`` returns all templates customizing ``xml_id``
234         """
235         imd = request.registry['ir.model.data']
236         view_model, view_theme_id = imd.get_object_reference(
237             request.cr, request.uid, 'website', 'theme')
238
239         user = request.registry['res.users']\
240             .browse(request.cr, request.uid, request.uid, request.context)
241         user_groups = set(user.groups_id)
242
243         views = request.registry["ir.ui.view"]\
244             ._views_get(request.cr, request.uid, xml_id, context=dict(request.context or {}, active_test=False))
245         done = set()
246         result = []
247         for v in views:
248             if not user_groups.issuperset(v.groups_id):
249                 continue
250             if full or (v.customize_show and v.inherit_id.id != view_theme_id):
251                 if v.inherit_id not in done:
252                     result.append({
253                         'name': v.inherit_id.name,
254                         'id': v.id,
255                         'xml_id': v.xml_id,
256                         'inherit_id': v.inherit_id.id,
257                         'header': True,
258                         'active': False
259                     })
260                     done.add(v.inherit_id)
261                 result.append({
262                     'name': v.name,
263                     'id': v.id,
264                     'xml_id': v.xml_id,
265                     'inherit_id': v.inherit_id.id,
266                     'header': False,
267                     'active': v.active,
268                 })
269         return result
270
271     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
272     def get_view_translations(self, xml_id, lang=None):
273         lang = lang or request.context.get('lang')
274         views = self.customize_template_get(xml_id, full=True)
275         views_ids = [view.get('id') for view in views if view.get('active')]
276         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
277         irt = request.registry.get('ir.translation')
278         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
279
280     @http.route('/website/set_translations', type='json', auth='public', website=True)
281     def set_translations(self, data, lang):
282         irt = request.registry.get('ir.translation')
283         for view_id, trans in data.items():
284             view_id = int(view_id)
285             for t in trans:
286                 initial_content = t['initial_content'].strip()
287                 new_content = t['new_content'].strip()
288                 tid = t['translation_id']
289                 if not tid:
290                     old_trans = irt.search_read(
291                         request.cr, request.uid,
292                         [
293                             ('type', '=', 'view'),
294                             ('res_id', '=', view_id),
295                             ('lang', '=', lang),
296                             ('src', '=', initial_content),
297                         ])
298                     if old_trans:
299                         tid = old_trans[0]['id']
300                 if tid:
301                     vals = {'value': new_content}
302                     irt.write(request.cr, request.uid, [tid], vals)
303                 else:
304                     new_trans = {
305                         'name': 'website',
306                         'res_id': view_id,
307                         'lang': lang,
308                         'type': 'view',
309                         'source': initial_content,
310                         'value': new_content,
311                     }
312                     if t.get('gengo_translation'):
313                         new_trans['gengo_translation'] = t.get('gengo_translation')
314                         new_trans['gengo_comment'] = t.get('gengo_comment')
315                     irt.create(request.cr, request.uid, new_trans)
316         return True
317
318     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
319     def attach(self, func, upload=None, url=None):
320         Attachments = request.registry['ir.attachment']
321
322         website_url = message = None
323         if not upload:
324             website_url = url
325             name = url.split("/").pop()
326             attachment_id = Attachments.create(request.cr, request.uid, {
327                 'name':name,
328                 'type': 'url',
329                 'url': url,
330                 'res_model': 'ir.ui.view',
331             }, request.context)
332         else:
333             try:
334                 image_data = upload.read()
335                 image = Image.open(cStringIO.StringIO(image_data))
336                 w, h = image.size
337                 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
338                     raise ValueError(
339                         u"Image size excessive, uploaded images must be smaller "
340                         u"than 42 million pixel")
341
342                 attachment_id = Attachments.create(request.cr, request.uid, {
343                     'name': upload.filename,
344                     'datas': image_data.encode('base64'),
345                     'datas_fname': upload.filename,
346                     'res_model': 'ir.ui.view',
347                 }, request.context)
348
349                 [attachment] = Attachments.read(
350                     request.cr, request.uid, [attachment_id], ['website_url'],
351                     context=request.context)
352                 website_url = attachment['website_url']
353             except Exception, e:
354                 logger.exception("Failed to upload image to attachment")
355                 message = unicode(e)
356
357         return """<script type='text/javascript'>
358             window.parent['%s'](%s, %s);
359         </script>""" % (func, json.dumps(website_url), json.dumps(message))
360
361     @http.route(['/website/publish'], type='json', auth="public", website=True)
362     def publish(self, id, object):
363         _id = int(id)
364         _object = request.registry[object]
365         obj = _object.browse(request.cr, request.uid, _id)
366
367         values = {}
368         if 'website_published' in _object._all_columns:
369             values['website_published'] = not obj.website_published
370         _object.write(request.cr, request.uid, [_id],
371                       values, context=request.context)
372
373         obj = _object.browse(request.cr, request.uid, _id)
374         return bool(obj.website_published)
375
376     @http.route(['/website/seo_suggest/<keywords>'], type='http', auth="public", website=True)
377     def seo_suggest(self, keywords):
378         url = "http://google.com/complete/search"
379         try:
380             req = urllib2.Request("%s?%s" % (url, werkzeug.url_encode({
381                 'ie': 'utf8', 'oe': 'utf8', 'output': 'toolbar', 'q': keywords})))
382             request = urllib2.urlopen(req)
383         except (urllib2.HTTPError, urllib2.URLError):
384             return []
385         xmlroot = ET.fromstring(request.read())
386         return json.dumps([sugg[0].attrib['data'] for sugg in xmlroot if len(sugg) and sugg[0].attrib['data']])
387
388     #------------------------------------------------------
389     # Helpers
390     #------------------------------------------------------
391     @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
392     def kanban(self, **post):
393         return request.website.kanban_col(**post)
394
395     def placeholder(self, response):
396         return request.registry['website']._image_placeholder(response)
397
398     @http.route([
399         '/website/image',
400         '/website/image/<model>/<id>/<field>',
401         '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
402         ], auth="public", website=True)
403     def website_image(self, model, id, field, max_width=None, max_height=None):
404         """ Fetches the requested field and ensures it does not go above
405         (max_width, max_height), resizing it if necessary.
406
407         If the record is not found or does not have the requested field,
408         returns a placeholder image via :meth:`~.placeholder`.
409
410         Sets and checks conditional response parameters:
411         * :mailheader:`ETag` is always set (and checked)
412         * :mailheader:`Last-Modified is set iif the record has a concurrency
413           field (``__last_update``)
414
415         The requested field is assumed to be base64-encoded image data in
416         all cases.
417         """
418         try:
419             response = werkzeug.wrappers.Response()
420             return request.registry['website']._image(
421                 request.cr, request.uid, model, id, field, response, max_width, max_height)
422         except Exception:
423             logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)",
424                              field, model, id, max_width, max_height)
425             response = werkzeug.wrappers.Response()
426             return self.placeholder(response)
427
428     #------------------------------------------------------
429     # Server actions
430     #------------------------------------------------------
431     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
432     def actions_server(self, path_or_xml_id_or_id, **post):
433         cr, uid, context = request.cr, request.uid, request.context
434         res, action_id, action = None, None, None
435         ServerActions = request.registry['ir.actions.server']
436
437         # find the action_id: either an xml_id, the path, or an ID
438         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
439             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)
440         if not action_id:
441             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
442             action_id = action_ids and action_ids[0] or None
443         if not action_id:
444             try:
445                 action_id = int(path_or_xml_id_or_id)
446             except ValueError:
447                 pass
448
449         # check it effectively exists
450         if action_id:
451             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
452             action_id = action_ids and action_ids[0] or None
453         # run it, return only if we got a Response object
454         if action_id:
455             action = ServerActions.browse(cr, uid, action_id, context=context)
456             if action.state == 'code' and action.website_published:
457                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
458                 if isinstance(action_res, werkzeug.wrappers.Response):
459                     res = action_res
460         if res:
461             return res
462         return request.redirect('/')
463