[FIX] main_objet is now always set in qweb rendering context
[odoo/odoo.git] / addons / website / controllers / main.py
1 # -*- coding: utf-8 -*-
2 import base64
3 import cStringIO
4 import contextlib
5 import hashlib
6 import json
7 import logging
8 import os
9 import datetime
10 import re
11
12 from sys import maxint
13
14 import psycopg2
15 import werkzeug
16 import werkzeug.exceptions
17 import werkzeug.utils
18 import werkzeug.wrappers
19 from PIL import Image
20
21 try:
22     from slugify import slugify
23 except ImportError:
24     def slugify(s, max_length=None):
25         spaceless = re.sub(r'\s+', '-', s)
26         specialless = re.sub(r'[^-_a-z0-9]', '', spaceless)
27         return specialless[:max_length]
28
29 import openerp
30 from openerp.osv import fields
31 from openerp.addons.website.models import website
32 from openerp.addons.web import http
33 from openerp.addons.web.http import request
34
35 logger = logging.getLogger(__name__)
36
37
38 def auth_method_public():
39     registry = openerp.modules.registry.RegistryManager.get(request.db)
40     if not request.session.uid:
41         request.uid = registry['website'].get_public_user().id
42     else:
43         request.uid = request.session.uid
44 http.auth_methods['public'] = auth_method_public
45
46 NOPE = object()
47 # Completely arbitrary limits
48 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
49 class Website(openerp.addons.web.controllers.main.Home):
50     @website.route('/', type='http', auth="public", multilang=True)
51     def index(self, **kw):
52         return self.page("website.homepage")
53
54     @http.route('/admin', type='http', auth="none")
55     def admin(self, *args, **kw):
56         return super(Website, self).index(*args, **kw)
57
58      # FIXME: auth, if /pagenew known anybody can create new empty page
59     @website.route('/pagenew/<path:path>', type='http', auth="admin")
60     def pagenew(self, path, noredirect=NOPE):
61         module = 'website'
62         # completely arbitrary max_length
63         idname = slugify(path, max_length=50)
64
65         request.cr.execute('SAVEPOINT pagenew')
66         imd = request.registry['ir.model.data']
67         view = request.registry['ir.ui.view']
68         view_model, view_id = imd.get_object_reference(
69             request.cr, request.uid, 'website', 'default_page')
70         newview_id = view.copy(
71             request.cr, request.uid, view_id, context=request.context)
72         newview = view.browse(
73             request.cr, request.uid, newview_id, context=request.context)
74         newview.write({
75             'arch': newview.arch.replace("website.default_page",
76                                          "%s.%s" % (module, idname)),
77             'name': path,
78             'page': True,
79         })
80         # Fuck it, we're doing it live
81         try:
82             imd.create(request.cr, request.uid, {
83                 'name': idname,
84                 'module': module,
85                 'model': 'ir.ui.view',
86                 'res_id': newview_id,
87                 'noupdate': True
88             }, context=request.context)
89         except psycopg2.IntegrityError:
90             logger.exception('Unable to create ir_model_data for page %s', path)
91             request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
92             return werkzeug.exceptions.InternalServerError()
93         else:
94             request.cr.execute('RELEASE SAVEPOINT pagenew')
95
96         url = "/page/%s" % idname
97         if noredirect is not NOPE:
98             return werkzeug.wrappers.Response(url, mimetype='text/plain')
99         return werkzeug.utils.redirect(url)
100
101     @website.route('/website/theme_change', type='http', auth="admin")
102     def theme_change(self, theme_id=False, **kwargs):
103         imd = request.registry['ir.model.data']
104         view = request.registry['ir.ui.view']
105
106         view_model, view_option_id = imd.get_object_reference(
107             request.cr, request.uid, 'website', 'theme')
108         views = view.search(
109             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
110             context=request.context)
111         view.write(request.cr, request.uid, views, {'inherit_id': False},
112                    context=request.context)
113
114         if theme_id:
115             module, xml_id = theme_id.split('.')
116             view_model, view_id = imd.get_object_reference(
117                 request.cr, request.uid, module, xml_id)
118             view.write(request.cr, request.uid, [view_id],
119                        {'inherit_id': view_option_id}, context=request.context)
120
121         return request.website.render('website.themes', {'theme_changed': True})
122
123     @website.route(['/website/snippets'], type='json', auth="public")
124     def snippets(self):
125         return request.website.render('website.snippets')
126
127     @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
128     def page(self, path, **kwargs):
129         values = {
130             'path': path,
131         }
132         try:
133             html = request.website.render(path, values)
134         except ValueError:
135             html = request.website.render('website.404', values)
136         return html
137
138     @website.route('/website/customize_template_toggle', type='json', auth='admin') # FIXME: auth
139     def customize_template_set(self, view_id):
140         view_obj = request.registry.get("ir.ui.view")
141         view = view_obj.browse(request.cr, request.uid, int(view_id),
142                                context=request.context)
143         if view.inherit_id:
144             value = False
145         else:
146             value = view.inherit_option_id and view.inherit_option_id.id or False
147         view_obj.write(request.cr, request.uid, [view_id], {
148             'inherit_id': value
149         }, context=request.context)
150         return True
151
152     @website.route('/website/customize_template_get', type='json', auth='admin') # FIXME: auth
153     def customize_template_get(self, xml_id, optional=True):
154         imd = request.registry['ir.model.data']
155         view_model, view_theme_id = imd.get_object_reference(
156             request.cr, request.uid, 'website', 'theme')
157
158         view = request.registry.get("ir.ui.view")
159         views = view._views_get(request.cr, request.uid, xml_id, request.context)
160         done = {}
161         result = []
162         for v in views:
163             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
164                 if v.inherit_option_id.id not in done:
165                     result.append({
166                         'name': v.inherit_option_id.name,
167                         'id': v.id,
168                         'header': True,
169                         'active': False
170                     })
171                     done[v.inherit_option_id.id] = True
172                 result.append({
173                     'name': v.name,
174                     'id': v.id,
175                     'header': False,
176                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
177                 })
178         return result
179
180     @website.route('/website/get_view_translations', type='json', auth='admin')
181     def get_view_translations(self, xml_id, lang=None):
182         lang = lang or request.context.get('lang')
183         views = self.customize_template_get(xml_id, optional=False)
184         views_ids = [view.get('id') for view in views if view.get('active')]
185         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
186         irt = request.registry.get('ir.translation')
187         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
188
189     @website.route('/website/set_translations', type='json', auth='admin')
190     def set_translations(self, data, lang):
191         irt = request.registry.get('ir.translation')
192         for view_id, trans in data.items():
193             view_id = int(view_id)
194             for t in trans:
195                 initial_content = t['initial_content'].strip()
196                 new_content = t['new_content'].strip()
197                 tid = t['translation_id']
198                 if not tid:
199                     old_trans = irt.search_read(
200                         request.cr, request.uid,
201                         [
202                             ('type', '=', 'view'),
203                             ('res_id', '=', view_id),
204                             ('lang', '=', lang),
205                             ('src', '=', initial_content),
206                         ])
207                     if old_trans:
208                         tid = old_trans[0]['id']
209                 if tid:
210                     vals = {'value': new_content}
211                     irt.write(request.cr, request.uid, [tid], vals)
212                 else:
213                     new_trans = {
214                         'name': 'website',
215                         'res_id': view_id,
216                         'lang': lang,
217                         'type': 'view',
218                         'source': initial_content,
219                         'value': new_content,
220                     }
221                     irt.create(request.cr, request.uid, new_trans)
222         irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate
223         return True
224
225     #  # FIXME: auth, anybody can upload an attachment if URL known/found
226     @website.route('/website/attach', type='http', auth='admin')
227     def attach(self, func, upload):
228         req = request.httprequest
229         if req.method != 'POST':
230             return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
231
232         url = message = None
233         try:
234             attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
235                 'name': upload.filename,
236                 'datas': base64.encodestring(upload.read()),
237                 'datas_fname': upload.filename,
238                 'res_model': 'ir.ui.view',
239             }, request.context)
240             # FIXME: auth=user... no good.
241             url = '/website/attachment/%d' % attachment_id
242         except Exception, e:
243             logger.exception("Failed to upload image to attachment")
244             message = str(e)
245
246         return """<script type='text/javascript'>
247             window.parent['%s'](%s, %s);
248         </script>""" % (func, json.dumps(url), json.dumps(message))
249
250     @website.route(['/website/publish'], type='json', auth="public")
251     def publish(self, id, object):
252         _id = int(id)
253         _object = request.registry[object]
254         obj = _object.browse(request.cr, request.uid, _id)
255
256         values = {}
257         if 'website_published' in _object._all_columns:
258             values['website_published'] = not obj.website_published
259         if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
260             values['website_published_datetime'] = fields.datetime.now()
261         _object.write(request.cr, request.uid, [_id],
262                       values, context=request.context)
263
264         obj = _object.browse(request.cr, request.uid, _id)
265         return obj.website_published and True or False
266
267     @website.route(['/website/kanban/'], type='http', auth="public")
268     def kanban(self, **post):
269         return request.website.kanban_col(**post)
270
271     @website.route(['/robots.txt'], type='http', auth="public")
272     def robots(self):
273         return request.website.render('website.robots', {'url_root': request.httprequest.url_root})
274
275     @website.route(['/sitemap.xml'], type='http', auth="public")
276     def sitemap(self):
277         return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
278
279 class Images(http.Controller):
280     def placeholder(self, response):
281         # file_open may return a StringIO. StringIO can be closed but are
282         # not context managers in Python 2 though that is fixed in 3
283         with contextlib.closing(openerp.tools.misc.file_open(
284                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
285                 mode='rb')) as f:
286             response.set_data(f.read())
287             return response.make_conditional(request.httprequest)
288
289     @website.route('/website/image', auth="public")
290     def image(self, model, id, field):
291         Model = request.registry[model]
292
293         response = werkzeug.wrappers.Response()
294
295         id = int(id)
296
297         ids = Model.search(request.cr, request.uid,
298                            [('id', '=', id)], context=request.context)\
299             or Model.search(request.cr, openerp.SUPERUSER_ID,
300                             [('id', '=', id), ('website_published', '=', True)], context=request.context)
301
302         if not ids:
303             return self.placeholder(response)
304
305         concurrency = '__last_update'
306         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
307                               [concurrency, field], context=request.context)
308
309         if concurrency in record:
310             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
311             try:
312                 response.last_modified = datetime.datetime.strptime(
313                     record[concurrency], server_format + '.%f')
314             except ValueError:
315                 # just in case we have a timestamp without microseconds
316                 response.last_modified = datetime.datetime.strptime(
317                     record[concurrency], server_format)
318
319         if not record.get(field):
320             # Field does not exist on model or field set to False
321             # FIXME: maybe a field which does not exist should be a 404?
322             return self.placeholder(response)
323
324         response.set_etag(hashlib.sha1(record[field]).hexdigest())
325         response.make_conditional(request.httprequest)
326
327         # conditional request match
328         if response.status_code == 304:
329             return response
330
331         return self.set_image_data(response, record[field].decode('base64'))
332
333     # FIXME: auth
334     # FIXME: delegate to image?
335     @website.route('/website/attachment/<int:id>', auth='admin')
336     def attachment(self, id):
337         attachment = request.registry['ir.attachment'].browse(
338             request.cr, request.uid, id, request.context)
339
340         return self.set_image_data(
341             werkzeug.wrappers.Response(),
342             attachment.datas.decode('base64'),
343             fit=IMAGE_LIMITS,)
344
345     def set_image_data(self, response, data, fit=(maxint, maxint)):
346         """ Sets an inferred mime type on the response object, and puts the
347         provided image's data in it, possibly after resizing if requested
348
349         Returns the response object after setting its mime and content, so
350         the result of ``get_final_image`` can be returned directly.
351         """
352         buf = cStringIO.StringIO(data)
353
354         # FIXME: unknown format or not an image
355         image = Image.open(buf)
356         response.mimetype = Image.MIME[image.format]
357
358         w, h = image.size
359         max_w, max_h = fit
360
361         if w < max_w and h < max_h:
362             response.set_data(data)
363             return response
364
365         image.thumbnail(fit, Image.ANTIALIAS)
366         image.save(response.stream, image.format)
367         return response
368
369
370 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: