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