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