[IMP] website: add a checkbox on new page dialog to add page in menu. Fix: The new...
[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': path,
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             view.model_data_id.write({
145                 'noupdate': False
146             })
147             if view.model_data_id.module not in modules_to_update:
148                 modules_to_update.append(view.model_data_id.module)
149         module_obj = request.registry['ir.module.module']
150         module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
151         module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
152         return request.redirect(redirect)
153
154     @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
155     def customize_template_set(self, view_id):
156         view_obj = request.registry.get("ir.ui.view")
157         view = view_obj.browse(request.cr, request.uid, int(view_id),
158                                context=request.context)
159         if view.inherit_id:
160             value = False
161         else:
162             value = view.inherit_option_id and view.inherit_option_id.id or False
163         view_obj.write(request.cr, request.uid, [view_id], {
164             'inherit_id': value
165         }, context=request.context)
166         return True
167
168     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
169     def customize_template_get(self, xml_id, optional=True):
170         imd = request.registry['ir.model.data']
171         view_model, view_theme_id = imd.get_object_reference(
172             request.cr, request.uid, 'website', 'theme')
173
174         user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
175         group_ids = [g.id for g in user.groups_id]
176
177         view = request.registry.get("ir.ui.view")
178         views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
179         done = {}
180         result = []
181         for v in views:
182             if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
183                 continue
184             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
185                 if v.inherit_option_id.id not in done:
186                     result.append({
187                         'name': v.inherit_option_id.name,
188                         'id': v.id,
189                         'xml_id': v.xml_id,
190                         'inherit_id': v.inherit_id.id,
191                         'header': True,
192                         'active': False
193                     })
194                     done[v.inherit_option_id.id] = True
195                 result.append({
196                     'name': v.name,
197                     'id': v.id,
198                     'xml_id': v.xml_id,
199                     'inherit_id': v.inherit_id.id,
200                     'header': False,
201                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
202                 })
203         return result
204
205     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
206     def get_view_translations(self, xml_id, lang=None):
207         lang = lang or request.context.get('lang')
208         views = self.customize_template_get(xml_id, optional=False)
209         views_ids = [view.get('id') for view in views if view.get('active')]
210         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
211         irt = request.registry.get('ir.translation')
212         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
213
214     @http.route('/website/set_translations', type='json', auth='public', website=True)
215     def set_translations(self, data, lang):
216         irt = request.registry.get('ir.translation')
217         for view_id, trans in data.items():
218             view_id = int(view_id)
219             for t in trans:
220                 initial_content = t['initial_content'].strip()
221                 new_content = t['new_content'].strip()
222                 tid = t['translation_id']
223                 if not tid:
224                     old_trans = irt.search_read(
225                         request.cr, request.uid,
226                         [
227                             ('type', '=', 'view'),
228                             ('res_id', '=', view_id),
229                             ('lang', '=', lang),
230                             ('src', '=', initial_content),
231                         ])
232                     if old_trans:
233                         tid = old_trans[0]['id']
234                 if tid:
235                     vals = {'value': new_content}
236                     irt.write(request.cr, request.uid, [tid], vals)
237                 else:
238                     new_trans = {
239                         'name': 'website',
240                         'res_id': view_id,
241                         'lang': lang,
242                         'type': 'view',
243                         'source': initial_content,
244                         'value': new_content,
245                     }
246                     irt.create(request.cr, request.uid, new_trans)
247         return True
248
249     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
250     def attach(self, func, upload):
251
252         url = message = None
253         try:
254             image_data = upload.read()
255             image = Image.open(cStringIO.StringIO(image_data))
256             w, h = image.size
257             if w*h > 42e6: # Nokia Lumia 1020 photo resolution
258                 raise ValueError(
259                     u"Image size excessive, uploaded images must be smaller "
260                     u"than 42 million pixel")
261
262             attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
263                 'name': upload.filename,
264                 'datas': image_data.encode('base64'),
265                 'datas_fname': upload.filename,
266                 'res_model': 'ir.ui.view',
267             }, request.context)
268
269             url = website.urlplus('/website/image', {
270                 'model': 'ir.attachment',
271                 'id': attachment_id,
272                 'field': 'datas',
273                 'max_height': MAX_IMAGE_HEIGHT,
274                 'max_width': MAX_IMAGE_WIDTH,
275             })
276         except Exception, e:
277             logger.exception("Failed to upload image to attachment")
278             message = unicode(e)
279
280         return """<script type='text/javascript'>
281             window.parent['%s'](%s, %s);
282         </script>""" % (func, json.dumps(url), json.dumps(message))
283
284     @http.route(['/website/publish'], type='json', auth="public", website=True)
285     def publish(self, id, object):
286         _id = int(id)
287         _object = request.registry[object]
288         obj = _object.browse(request.cr, request.uid, _id)
289
290         values = {}
291         if 'website_published' in _object._all_columns:
292             values['website_published'] = not obj.website_published
293         if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
294             values['website_published_datetime'] = fields.datetime.now()
295         _object.write(request.cr, request.uid, [_id],
296                       values, context=request.context)
297
298         obj = _object.browse(request.cr, request.uid, _id)
299         return bool(obj.website_published)
300
301     #------------------------------------------------------
302     # Helpers
303     #------------------------------------------------------
304     @http.route(['/website/kanban/'], type='http', auth="public", methods=['POST'], website=True)
305     def kanban(self, **post):
306         return request.website.kanban_col(**post)
307
308     def placeholder(self, response):
309         # file_open may return a StringIO. StringIO can be closed but are
310         # not context managers in Python 2 though that is fixed in 3
311         with contextlib.closing(openerp.tools.misc.file_open(
312                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
313                 mode='rb')) as f:
314             response.data = f.read()
315             return response.make_conditional(request.httprequest)
316
317     @http.route([
318         '/website/image',
319         '/website/image/<model>/<id>/<field>'
320         ], auth="public", website=True)
321     def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
322         Model = request.registry[model]
323
324         response = werkzeug.wrappers.Response()
325
326         id = int(id)
327
328         ids = Model.search(request.cr, request.uid,
329                            [('id', '=', id)], context=request.context) \
330             or Model.search(request.cr, openerp.SUPERUSER_ID,
331                             [('id', '=', id), ('website_published', '=', True)], context=request.context)
332
333         if not ids:
334             return self.placeholder(response)
335
336         concurrency = '__last_update'
337         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
338                               [concurrency, field], context=request.context)
339
340         if concurrency in record:
341             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
342             try:
343                 response.last_modified = datetime.datetime.strptime(
344                     record[concurrency], server_format + '.%f')
345             except ValueError:
346                 # just in case we have a timestamp without microseconds
347                 response.last_modified = datetime.datetime.strptime(
348                     record[concurrency], server_format)
349
350         # Field does not exist on model or field set to False
351         if not record.get(field):
352             # FIXME: maybe a field which does not exist should be a 404?
353             return self.placeholder(response)
354
355         response.set_etag(hashlib.sha1(record[field]).hexdigest())
356         response.make_conditional(request.httprequest)
357
358         # conditional request match
359         if response.status_code == 304:
360             return response
361
362         data = record[field].decode('base64')
363         fit = int(max_width), int(max_height)
364
365         buf = cStringIO.StringIO(data)
366
367         image = Image.open(buf)
368         image.load()
369         response.mimetype = Image.MIME[image.format]
370
371         w, h = image.size
372         max_w, max_h = fit
373
374         if w < max_w and h < max_h:
375             response.data = data
376         else:
377             image.thumbnail(fit, Image.ANTIALIAS)
378             image.save(response.stream, image.format)
379             # invalidate content-length computed by make_conditional as writing
380             # to response.stream does not do it (as of werkzeug 0.9.3)
381             del response.headers['Content-Length']
382
383         return response
384
385
386 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: