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