869d51d85c73ad3f748f2628fd1a0c0ad9d61463
[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
11 from sys import maxint
12
13 import psycopg2
14 import slugify
15 import werkzeug
16 import werkzeug.exceptions
17 import werkzeug.utils
18 import werkzeug.wrappers
19 from PIL import Image
20
21 import openerp
22 from openerp.addons.website.models import website
23 from openerp.addons.web import http
24 from openerp.addons.web.http import request
25
26 logger = logging.getLogger(__name__)
27
28
29 def auth_method_public():
30     registry = openerp.modules.registry.RegistryManager.get(request.db)
31     if not request.session.uid:
32         request.uid = registry['website'].get_public_user().id
33     else:
34         request.uid = request.session.uid
35 http.auth_methods['public'] = auth_method_public
36
37 NOPE = object()
38 # PIL images have a type flag, but no MIME. Reverse type flag to MIME.
39 PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
40 # Completely arbitrary limits
41 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
42 class Website(openerp.addons.web.controllers.main.Home):
43     @website.route('/', type='http', auth="public", multilang=True)
44     def index(self, **kw):
45         return self.page("website.homepage")
46
47     @http.route('/admin', type='http', auth="none")
48     def admin(self, *args, **kw):
49         return super(Website, self).index(*args, **kw)
50
51      # FIXME: auth, if /pagenew known anybody can create new empty page
52     @website.route('/pagenew/<path:path>', type='http', auth="admin")
53     def pagenew(self, path, noredirect=NOPE):
54         module = 'website'
55         # completely arbitrary max_length
56         idname = slugify.slugify(path, max_length=50)
57
58         request.cr.execute('SAVEPOINT pagenew')
59         imd = request.registry['ir.model.data']
60         view = request.registry['ir.ui.view']
61         view_model, view_id = imd.get_object_reference(
62             request.cr, request.uid, 'website', 'default_page')
63         newview_id = view.copy(
64             request.cr, request.uid, view_id, context=request.context)
65         newview = view.browse(
66             request.cr, request.uid, newview_id, context=request.context)
67         newview.write({
68             'arch': newview.arch.replace("website.default_page",
69                                          "%s.%s" % (module, idname)),
70             'name': path,
71             'page': True,
72         })
73         # Fuck it, we're doing it live
74         try:
75             imd.create(request.cr, request.uid, {
76                 'name': idname,
77                 'module': module,
78                 'model': 'ir.ui.view',
79                 'res_id': newview_id,
80                 'noupdate': True
81             }, context=request.context)
82         except psycopg2.IntegrityError:
83             logger.exception('Unable to create ir_model_data for page %s', path)
84             request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
85             return werkzeug.exceptions.InternalServerError()
86         else:
87             request.cr.execute('RELEASE SAVEPOINT pagenew')
88
89         url = "/page/%s" % idname
90         if noredirect is not NOPE:
91             return werkzeug.wrappers.Response(url, mimetype='text/plain')
92         return werkzeug.utils.redirect(url)
93
94     @website.route('/website/theme_change', type='http', auth="admin")
95     def theme_change(self, theme_id=False, **kwargs):
96         imd = request.registry['ir.model.data']
97         view = request.registry['ir.ui.view']
98
99         view_model, view_option_id = imd.get_object_reference(
100             request.cr, request.uid, 'website', 'theme')
101         views = view.search(
102             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
103             context=request.context)
104         view.write(request.cr, request.uid, views, {'inherit_id': False},
105                    context=request.context)
106
107         if theme_id:
108             module, xml_id = theme_id.split('.')
109             view_model, view_id = imd.get_object_reference(
110                 request.cr, request.uid, module, xml_id)
111             view.write(request.cr, request.uid, [view_id],
112                        {'inherit_id': view_option_id}, context=request.context)
113
114         return request.website.render('website.themes', {'theme_changed': True})
115
116     @website.route(['/website/snippets'], type='json', auth="public")
117     def snippets(self):
118         return request.website.render('website.snippets')
119
120     @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
121     def page(self, path, **kwargs):
122         values = {
123             'path': path,
124         }
125         try:
126             module, xmlid = path.split('.', 1)
127             IMD = request.registry.get("ir.model.data")
128             obj = IMD.get_object_reference(request.cr, request.uid, module, xmlid)
129             values['main_object'] = request.registry[obj[0]].browse(request.cr, request.uid, obj[1])
130         except Exception:
131             pass
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
255         obj = _object.browse(request.cr, request.uid, _id)
256         _object.write(request.cr, request.uid, [_id],
257                       {'website_published': not obj.website_published},
258                       context=request.context)
259         obj = _object.browse(request.cr, request.uid, _id)
260         return obj.website_published and True or False
261
262     @website.route(['/website/kanban/'], type='http', auth="public")
263     def kanban(self, **post):
264         return request.website.kanban_col(**post)
265
266     @website.route(['/robots.txt'], type='http', auth="public")
267     def robots(self):
268         return request.website.render('website.robots', {'url_root': request.httprequest.url_root})
269
270     @website.route(['/sitemap.xml'], type='http', auth="public")
271     def sitemap(self):
272         return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
273
274 class Images(http.Controller):
275     @website.route('/website/image', auth="public")
276     def image(self, model, id, field):
277         Model = request.registry[model]
278
279         response = werkzeug.wrappers.Response()
280
281         id = int(id)
282
283         ids = Model.search(request.cr, request.uid,
284                            [('id', '=', id)], context=request.context)\
285             or Model.search(request.cr, openerp.SUPERUSER_ID,
286                             [('id', '=', id), ('website_published', '=', True)], context=request.context)
287
288         if not ids:
289             # file_open may return a StringIO. StringIO can be closed but are
290             # not context managers in Python 2 though that is fixed in 3
291             with contextlib.closing(openerp.tools.misc.file_open(
292                     os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
293                     mode='rb')) as f:
294                 response.set_data(f.read())
295                 return response
296
297         concurrency = '__last_update'
298         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
299                               [concurrency, field], context=request.context)
300
301         if concurrency in record:
302             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
303             try:
304                 response.last_modified = datetime.datetime.strptime(
305                     record[concurrency], server_format + '.%f')
306             except ValueError:
307                 # just in case we have a timestamp without microseconds
308                 response.last_modified = datetime.datetime.strptime(
309                     record[concurrency], server_format)
310         # FIXME: no field in record?
311         if not field in record or not record[field]:
312             return response
313
314         response.set_etag(hashlib.sha1(record[field]).hexdigest())
315         response.make_conditional(request.httprequest)
316
317         # conditional request match
318         if response.status_code == 304:
319             return response
320
321         return self.set_image_data(response, record[field].decode('base64'))
322
323     # FIXME: auth
324     # FIXME: delegate to image?
325     @website.route('/website/attachment/<int:id>', auth='admin')
326     def attachment(self, id):
327         attachment = request.registry['ir.attachment'].browse(
328             request.cr, request.uid, id, request.context)
329
330         return self.set_image_data(
331             werkzeug.wrappers.Response(),
332             attachment.datas.decode('base64'),
333             fit=IMAGE_LIMITS,)
334
335     def set_image_data(self, response, data, fit=(maxint, maxint)):
336         """ Sets an inferred mime type on the response object, and puts the
337         provided image's data in it, possibly after resizing if requested
338
339         Returns the response object after setting its mime and content, so
340         the result of ``get_final_image`` can be returned directly.
341         """
342         buf = cStringIO.StringIO(data)
343
344         # FIXME: unknown format or not an image
345         image = Image.open(buf)
346         response.mimetype = PIL_MIME_MAPPING[image.format]
347
348         w, h = image.size
349         max_w, max_h = fit
350
351         if w < max_w and h < max_h:
352             response.set_data(data)
353             return response
354
355         image.thumbnail(fit, Image.ANTIALIAS)
356         image.save(response.stream, image.format)
357         return response
358
359
360 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: