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