620ec543b54914ac7e6a621beab94d1b04045122
[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 werkzeug
15 import werkzeug.exceptions
16 import werkzeug.utils
17 import werkzeug.wrappers
18 from PIL import Image
19
20 import openerp
21 from openerp.addons.website.models import website
22 from openerp.addons.web import http
23 from openerp.addons.web.http import request
24
25 logger = logging.getLogger(__name__)
26
27
28 def auth_method_public():
29     registry = openerp.modules.registry.RegistryManager.get(request.db)
30     if not request.session.uid:
31         request.uid = registry['website'].get_public_user().id
32     else:
33         request.uid = request.session.uid
34 http.auth_methods['public'] = auth_method_public
35
36 NOPE = object()
37 # PIL images have a type flag, but no MIME. Reverse type flag to MIME.
38 PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
39 # Completely arbitrary limits
40 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
41 class Website(openerp.addons.web.controllers.main.Home):
42     @website.route('/', type='http', auth="public", multilang=True)
43     def index(self, **kw):
44         return self.page("website.homepage")
45
46     @http.route('/admin', type='http', auth="none")
47     def admin(self, *args, **kw):
48         return super(Website, self).index(*args, **kw)
49
50      # FIXME: auth, if /pagenew known anybody can create new empty page
51     @website.route('/pagenew/<path:path>', type='http', auth="admin")
52     def pagenew(self, path, noredirect=NOPE):
53         if '.' in path:
54             module, idname = path.split('.', 1)
55         else:
56             module = 'website'
57             idname = path
58         xid = "%s.%s" % (module, idname)
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", xid),
71             'name': "page/%s" % path,
72             'page': True,
73         })
74         # Fuck it, we're doing it live
75         try:
76             imd.create(request.cr, request.uid, {
77                 'name': idname,
78                 'module': module,
79                 'model': 'ir.ui.view',
80                 'res_id': newview_id,
81                 'noupdate': True
82             }, context=request.context)
83         except psycopg2.IntegrityError:
84             request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
85         else:
86             request.cr.execute('RELEASE SAVEPOINT pagenew')
87         url = "/page/%s" % path
88         if noredirect is not NOPE:
89             return werkzeug.wrappers.Response(url, mimetype='text/plain')
90         return werkzeug.utils.redirect(url)
91
92     @website.route('/website/theme_change', type='http', auth="admin")
93     def theme_change(self, theme_id=False, **kwargs):
94         imd = request.registry['ir.model.data']
95         view = request.registry['ir.ui.view']
96
97         view_model, view_option_id = imd.get_object_reference(
98             request.cr, request.uid, 'website', 'theme')
99         views = view.search(
100             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
101             context=request.context)
102         view.write(request.cr, request.uid, views, {'inherit_id': False},
103                    context=request.context)
104
105         if theme_id:
106             module, xml_id = theme_id.split('.')
107             view_model, view_id = imd.get_object_reference(
108                 request.cr, request.uid, module, xml_id)
109             view.write(request.cr, request.uid, [view_id],
110                        {'inherit_id': view_option_id}, context=request.context)
111
112         return request.website.render('website.themes', {'theme_changed': True})
113
114     @website.route(['/website/snippets'], type='json', auth="public")
115     def snippets(self):
116         return request.website.render('website.snippets')
117
118     @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
119     def page(self, path, **kwargs):
120         values = {
121             'path': path,
122         }
123         try:
124             html = request.website.render(path, values)
125         except ValueError:
126             html = request.website.render('website.404', values)
127         return html
128
129     @website.route('/website/customize_template_toggle', type='json', auth='admin') # FIXME: auth
130     def customize_template_set(self, view_id):
131         view_obj = request.registry.get("ir.ui.view")
132         view = view_obj.browse(request.cr, request.uid, int(view_id),
133                                context=request.context)
134         if view.inherit_id:
135             value = False
136         else:
137             value = view.inherit_option_id and view.inherit_option_id.id or False
138         view_obj.write(request.cr, request.uid, [view_id], {
139             'inherit_id': value
140         }, context=request.context)
141         return True
142
143     @website.route('/website/customize_template_get', type='json', auth='admin') # FIXME: auth
144     def customize_template_get(self, xml_id, optional=True):
145         imd = request.registry['ir.model.data']
146         view_model, view_theme_id = imd.get_object_reference(
147             request.cr, request.uid, 'website', 'theme')
148
149         view = request.registry.get("ir.ui.view")
150         views = view._views_get(request.cr, request.uid, xml_id, request.context)
151         done = {}
152         result = []
153         for v in views:
154             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
155                 if v.inherit_option_id.id not in done:
156                     result.append({
157                         'name': v.inherit_option_id.name,
158                         'id': v.id,
159                         'header': True,
160                         'active': False
161                     })
162                     done[v.inherit_option_id.id] = True
163                 result.append({
164                     'name': v.name,
165                     'id': v.id,
166                     'header': False,
167                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
168                 })
169         return result
170
171     @website.route('/website/get_view_translations', type='json', auth='admin')
172     def get_view_translations(self, xml_id, lang=None):
173         lang = lang or request.context.get('lang')
174         views = self.customize_template_get(xml_id, optional=False)
175         views_ids = [view.get('id') for view in views if view.get('active')]
176         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
177         irt = request.registry.get('ir.translation')
178         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
179
180     @website.route('/website/set_translations', type='json', auth='admin')
181     def set_translations(self, data, lang):
182         irt = request.registry.get('ir.translation')
183         for view_id, trans in data.items():
184             view_id = int(view_id)
185             for t in trans:
186                 initial_content = t['initial_content'].strip()
187                 new_content = t['new_content'].strip()
188                 tid = t['translation_id']
189                 if not tid:
190                     old_trans = irt.search_read(
191                         request.cr, request.uid,
192                         [
193                             ('type', '=', 'view'),
194                             ('res_id', '=', view_id),
195                             ('lang', '=', lang),
196                             ('src', '=', initial_content),
197                         ])
198                     if old_trans:
199                         tid = old_trans[0]['id']
200                 if tid:
201                     vals = {'value': new_content}
202                     irt.write(request.cr, request.uid, [tid], vals)
203                 else:
204                     new_trans = {
205                         'name': 'website',
206                         'res_id': view_id,
207                         'lang': lang,
208                         'type': 'view',
209                         'source': initial_content,
210                         'value': new_content,
211                     }
212                     irt.create(request.cr, request.uid, new_trans)
213         irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate
214         return True
215
216     #  # FIXME: auth, anybody can upload an attachment if URL known/found
217     @website.route('/website/attach', type='http', auth='admin')
218     def attach(self, func, upload):
219         req = request.httprequest
220         if req.method != 'POST':
221             return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
222
223         url = message = None
224         try:
225             attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
226                 'name': upload.filename,
227                 'datas': base64.encodestring(upload.read()),
228                 'datas_fname': upload.filename,
229                 'res_model': 'ir.ui.view',
230             }, request.context)
231             # FIXME: auth=user... no good.
232             url = '/website/attachment/%d' % attachment_id
233         except Exception, e:
234             logger.exception("Failed to upload image to attachment")
235             message = str(e)
236
237         return """<script type='text/javascript'>
238             window.parent['%s'](%s, %s);
239         </script>""" % (func, json.dumps(url), json.dumps(message))
240
241     @website.route(['/website/publish'], type='json', auth="public")
242     def publish(self, id, object):
243         _id = int(id)
244         _object = request.registry[object]
245
246         obj = _object.browse(request.cr, request.uid, _id)
247         _object.write(request.cr, request.uid, [_id],
248                       {'website_published': not obj.website_published},
249                       context=request.context)
250         obj = _object.browse(request.cr, request.uid, _id)
251         return obj.website_published and True or False
252
253     @website.route(['/website/kanban/'], type='http', auth="public")
254     def kanban(self, **post):
255         return request.website.kanban_col(**post)
256
257     @website.route(['/robots.txt'], type='http', auth="public")
258     def robots(self):
259         return request.website.render('website.robots', {'url_root': request.httprequest.url_root})
260
261     @website.route(['/sitemap.xml'], type='http', auth="public")
262     def sitemap(self):
263         return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
264
265 class Images(http.Controller):
266     @website.route('/website/image', auth="public")
267     def image(self, model, id, field):
268         Model = request.registry[model]
269
270         response = werkzeug.wrappers.Response()
271
272         id = int(id)
273
274         ids = Model.search(request.cr, request.uid,
275                            [('id', '=', id)], context=request.context)\
276             or Model.search(request.cr, openerp.SUPERUSER_ID,
277                             [('id', '=', id), ('website_published', '=', True)], context=request.context)
278
279         if not ids:
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
287
288         concurrency = '__last_update'
289         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
290                               [concurrency, field], context=request.context)
291
292         if concurrency in record:
293             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
294             try:
295                 response.last_modified = datetime.datetime.strptime(
296                     record[concurrency], server_format + '.%f')
297             except ValueError:
298                 # just in case we have a timestamp without microseconds
299                 response.last_modified = datetime.datetime.strptime(
300                     record[concurrency], server_format)
301         # FIXME: no field in record?
302         response.set_etag(hashlib.sha1(record[field]).hexdigest())
303         response.make_conditional(request.httprequest)
304
305         # conditional request match
306         if response.status_code == 304:
307             return response
308
309         return self.set_image_data(response, record[field].decode('base64'))
310
311     # FIXME: auth
312     # FIXME: delegate to image?
313     @website.route('/website/attachment/<int:id>', auth='admin')
314     def attachment(self, id):
315         attachment = request.registry['ir.attachment'].browse(
316             request.cr, request.uid, id, request.context)
317
318         return self.set_image_data(
319             werkzeug.wrappers.Response(),
320             attachment.datas.decode('base64'),
321             fit=IMAGE_LIMITS,)
322
323     def set_image_data(self, response, data, fit=(maxint, maxint)):
324         """ Sets an inferred mime type on the response object, and puts the
325         provided image's data in it, possibly after resizing if requested
326
327         Returns the response object after setting its mime and content, so
328         the result of ``get_final_image`` can be returned directly.
329         """
330         buf = cStringIO.StringIO(data)
331
332         # FIXME: unknown format or not an image
333         image = Image.open(buf)
334         response.mimetype = PIL_MIME_MAPPING[image.format]
335
336         w, h = image.size
337         max_w, max_h = fit
338
339         if w < max_w and h < max_h:
340             response.set_data(data)
341             return response
342
343         image.thumbnail(fit, Image.ANTIALIAS)
344         image.save(response.stream, image.format)
345         return response
346
347
348 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: