[IMP] renamed some stuff for clarity
[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, full=False):
216         """ Lists the templates customizing ``xml_id``. By default, only
217         returns optional templates (which can be toggled on and off), if
218         ``full=True`` returns all templates customizing ``xml_id``
219         """
220         imd = request.registry['ir.model.data']
221         view_model, view_theme_id = imd.get_object_reference(
222             request.cr, request.uid, 'website', 'theme')
223
224         user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
225         user_groups = set(user.groups_id)
226
227         view = request.registry["ir.ui.view"]
228         views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
229         done = set()
230         result = []
231         for v in views:
232             if not user_groups.issuperset(v.groups_id):
233                 continue
234             if full or v.inherit_option_id and v.inherit_option_id.id != view_theme_id:
235                 if v.inherit_option_id.id not in done:
236                     result.append({
237                         'name': v.inherit_option_id.name,
238                         'id': v.id,
239                         'xml_id': v.xml_id,
240                         'inherit_id': v.inherit_id.id,
241                         'header': True,
242                         'active': False
243                     })
244                     done.add(v.inherit_option_id.id)
245                 result.append({
246                     'name': v.name,
247                     'id': v.id,
248                     'xml_id': v.xml_id,
249                     'inherit_id': v.inherit_id.id,
250                     'header': False,
251                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (full and v.inherit_id.id)
252                 })
253         return result
254
255     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
256     def get_view_translations(self, xml_id, lang=None):
257         lang = lang or request.context.get('lang')
258         views = self.customize_template_get(xml_id, optional=False)
259         views_ids = [view.get('id') for view in views if view.get('active')]
260         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
261         irt = request.registry.get('ir.translation')
262         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
263
264     @http.route('/website/set_translations', type='json', auth='public', website=True)
265     def set_translations(self, data, lang):
266         irt = request.registry.get('ir.translation')
267         for view_id, trans in data.items():
268             view_id = int(view_id)
269             for t in trans:
270                 initial_content = t['initial_content'].strip()
271                 new_content = t['new_content'].strip()
272                 tid = t['translation_id']
273                 if not tid:
274                     old_trans = irt.search_read(
275                         request.cr, request.uid,
276                         [
277                             ('type', '=', 'view'),
278                             ('res_id', '=', view_id),
279                             ('lang', '=', lang),
280                             ('src', '=', initial_content),
281                         ])
282                     if old_trans:
283                         tid = old_trans[0]['id']
284                 if tid:
285                     vals = {'value': new_content}
286                     irt.write(request.cr, request.uid, [tid], vals)
287                 else:
288                     new_trans = {
289                         'name': 'website',
290                         'res_id': view_id,
291                         'lang': lang,
292                         'type': 'view',
293                         'source': initial_content,
294                         'value': new_content,
295                     }
296                     if t.get('gengo_translation'):
297                         new_trans['gengo_translation'] = t.get('gengo_translation')
298                         new_trans['gengo_comment'] = t.get('gengo_comment')
299                     irt.create(request.cr, request.uid, new_trans)
300         return True
301
302     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
303     def attach(self, func, upload=None, url=None):
304         Attachments = request.registry['ir.attachment']
305
306         website_url = message = None
307         if not upload:
308             website_url = url
309             name = url.split("/").pop()
310             attachment_id = Attachments.create(request.cr, request.uid, {
311                 'name':name,
312                 'type': 'url',
313                 'url': url,
314                 'res_model': 'ir.ui.view',
315             }, request.context)
316         else:
317             try:
318                 image_data = upload.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': upload.filename,
328                     'datas': image_data.encode('base64'),
329                     'datas_fname': upload.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                 website_url = attachment['website_url']
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(website_url), 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     #------------------------------------------------------
361     # Helpers
362     #------------------------------------------------------
363     @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
364     def kanban(self, **post):
365         return request.website.kanban_col(**post)
366
367     def placeholder(self, response):
368         return request.registry['website']._image_placeholder(response)
369
370     @http.route([
371         '/website/image',
372         '/website/image/<model>/<id>/<field>'
373         ], auth="public", website=True)
374     def website_image(self, model, id, field, max_width=None, max_height=None):
375         """ Fetches the requested field and ensures it does not go above
376         (max_width, max_height), resizing it if necessary.
377
378         If the record is not found or does not have the requested field,
379         returns a placeholder image via :meth:`~.placeholder`.
380
381         Sets and checks conditional response parameters:
382         * :mailheader:`ETag` is always set (and checked)
383         * :mailheader:`Last-Modified is set iif the record has a concurrency
384           field (``__last_update``)
385
386         The requested field is assumed to be base64-encoded image data in
387         all cases.
388         """
389         response = werkzeug.wrappers.Response()
390         return request.registry['website']._image(
391                     request.cr, request.uid, model, id, field, response)
392
393
394     #------------------------------------------------------
395     # Server actions
396     #------------------------------------------------------
397     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
398     def actions_server(self, path_or_xml_id_or_id, **post):
399         cr, uid, context = request.cr, request.uid, request.context
400         res, action_id, action = None, None, None
401         ServerActions = request.registry['ir.actions.server']
402
403         # find the action_id: either an xml_id, the path, or an ID
404         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
405             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)
406         if not action_id:
407             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
408             action_id = action_ids and action_ids[0] or None
409         if not action_id:
410             try:
411                 action_id = int(path_or_xml_id_or_id)
412             except ValueError:
413                 pass
414
415         # check it effectively exists
416         if action_id:
417             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
418             action_id = action_ids and action_ids[0] or None
419         # run it, return only if we got a Response object
420         if action_id:
421             action = ServerActions.browse(cr, uid, action_id, context=context)
422             if action.state == 'code' and action.website_published:
423                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
424                 if isinstance(action_res, Response):
425                     res = action_res
426         if res:
427             return res
428         return request.redirect('/')
429