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