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