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