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