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