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