[IMP] website media editor: change template
[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 werkzeug
13 import werkzeug.exceptions
14 import werkzeug.utils
15 import werkzeug.wrappers
16 from PIL import Image
17
18 import openerp
19 from openerp.osv import fields
20 from openerp.addons.website.models import website
21 from openerp.addons.web import http
22 from openerp.http import request, Response
23
24 logger = logging.getLogger(__name__)
25
26 # Completely arbitrary limits
27 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
28
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         # TODO: can't we just put auth=public, ... in web client ?
49         return super(Website, self).web_login(*args, **kw)
50
51     @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
52     def page(self, page, **opt):
53         values = {
54             'path': page,
55         }
56         # allow shortcut for /page/<website_xml_id>
57         if '.' not in page:
58             page = 'website.%s' % page
59
60         try:
61             request.website.get_template(page)
62         except ValueError, e:
63             # page not found
64             if request.context['editable']:
65                 page = 'website.page_404'
66             else:
67                 return request.registry['ir.http']._handle_exception(e, 404)
68
69         return request.render(page, values)
70
71     @http.route(['/robots.txt'], type='http', auth="public", website=True)
72     def robots(self):
73         return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain')
74
75     @http.route('/sitemap', type='http', auth='public', website=True, multilang=True)
76     def sitemap(self):
77         return request.render('website.sitemap', {
78             'pages': request.website.enumerate_pages()
79         })
80
81     @http.route('/sitemap.xml', type='http', auth="public", website=True)
82     def sitemap_xml(self):
83         values = {
84             'pages': request.website.enumerate_pages()
85         }
86         headers = {
87             'Content-Type': 'application/xml;charset=utf-8',
88         }
89         return request.render('website.sitemap_xml', values, headers=headers)
90
91     #------------------------------------------------------
92     # Edit
93     #------------------------------------------------------
94     @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
95     def pagenew(self, path, noredirect=False, add_menu=None):
96         xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
97         if add_menu:
98             model, id  = request.registry["ir.model.data"].get_object_reference(request.cr, request.uid, 'website', 'main_menu')
99             request.registry['website.menu'].create(request.cr, request.uid, {
100                     'name': path,
101                     'url': "/page/" + xml_id,
102                     'parent_id': id,
103                 }, context=request.context)
104         url = "/page/" + xml_id
105         if noredirect:
106             return werkzeug.wrappers.Response(url, mimetype='text/plain')
107         return werkzeug.utils.redirect(url)
108
109     @http.route('/website/theme_change', type='http', auth="user", website=True)
110     def theme_change(self, theme_id=False, **kwargs):
111         imd = request.registry['ir.model.data']
112         view = request.registry['ir.ui.view']
113
114         view_model, view_option_id = imd.get_object_reference(
115             request.cr, request.uid, 'website', 'theme')
116         views = view.search(
117             request.cr, request.uid, [('inherit_id', '=', view_option_id)],
118             context=request.context)
119         view.write(request.cr, request.uid, views, {'inherit_id': False},
120                    context=request.context)
121
122         if theme_id:
123             module, xml_id = theme_id.split('.')
124             view_model, view_id = imd.get_object_reference(
125                 request.cr, request.uid, module, xml_id)
126             view.write(request.cr, request.uid, [view_id],
127                        {'inherit_id': view_option_id}, context=request.context)
128
129         return request.render('website.themes', {'theme_changed': True})
130
131     @http.route(['/website/snippets'], type='json', auth="public", website=True)
132     def snippets(self):
133         return request.website._render('website.snippets')
134
135     @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
136     def reset_template(self, templates, redirect='/'):
137         templates = request.httprequest.form.getlist('templates')
138         modules_to_update = []
139         for temp_id in templates:
140             view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
141             view.model_data_id.write({
142                 'noupdate': False
143             })
144             if view.model_data_id.module not in modules_to_update:
145                 modules_to_update.append(view.model_data_id.module)
146         module_obj = request.registry['ir.module.module']
147         module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
148         module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
149         return request.redirect(redirect)
150
151     @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
152     def customize_template_set(self, view_id):
153         view_obj = request.registry.get("ir.ui.view")
154         view = view_obj.browse(request.cr, request.uid, int(view_id),
155                                context=request.context)
156         if view.inherit_id:
157             value = False
158         else:
159             value = view.inherit_option_id and view.inherit_option_id.id or False
160         view_obj.write(request.cr, request.uid, [view_id], {
161             'inherit_id': value
162         }, context=request.context)
163         return True
164
165     @http.route('/website/customize_template_get', type='json', auth='user', website=True)
166     def customize_template_get(self, xml_id, optional=True):
167         imd = request.registry['ir.model.data']
168         view_model, view_theme_id = imd.get_object_reference(
169             request.cr, request.uid, 'website', 'theme')
170
171         user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
172         group_ids = [g.id for g in user.groups_id]
173
174         view = request.registry.get("ir.ui.view")
175         views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
176         done = {}
177         result = []
178         for v in views:
179             if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
180                 continue
181             if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
182                 if v.inherit_option_id.id not in done:
183                     result.append({
184                         'name': v.inherit_option_id.name,
185                         'id': v.id,
186                         'xml_id': v.xml_id,
187                         'inherit_id': v.inherit_id.id,
188                         'header': True,
189                         'active': False
190                     })
191                     done[v.inherit_option_id.id] = True
192                 result.append({
193                     'name': v.name,
194                     'id': v.id,
195                     'xml_id': v.xml_id,
196                     'inherit_id': v.inherit_id.id,
197                     'header': False,
198                     'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
199                 })
200         return result
201
202     @http.route('/website/get_view_translations', type='json', auth='public', website=True)
203     def get_view_translations(self, xml_id, lang=None):
204         lang = lang or request.context.get('lang')
205         views = self.customize_template_get(xml_id, optional=False)
206         views_ids = [view.get('id') for view in views if view.get('active')]
207         domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
208         irt = request.registry.get('ir.translation')
209         return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
210
211     @http.route('/website/set_translations', type='json', auth='public', website=True)
212     def set_translations(self, data, lang):
213         irt = request.registry.get('ir.translation')
214         for view_id, trans in data.items():
215             view_id = int(view_id)
216             for t in trans:
217                 initial_content = t['initial_content'].strip()
218                 new_content = t['new_content'].strip()
219                 tid = t['translation_id']
220                 if not tid:
221                     old_trans = irt.search_read(
222                         request.cr, request.uid,
223                         [
224                             ('type', '=', 'view'),
225                             ('res_id', '=', view_id),
226                             ('lang', '=', lang),
227                             ('src', '=', initial_content),
228                         ])
229                     if old_trans:
230                         tid = old_trans[0]['id']
231                 if tid:
232                     vals = {'value': new_content}
233                     irt.write(request.cr, request.uid, [tid], vals)
234                 else:
235                     new_trans = {
236                         'name': 'website',
237                         'res_id': view_id,
238                         'lang': lang,
239                         'type': 'view',
240                         'source': initial_content,
241                         'value': new_content,
242                     }
243                     irt.create(request.cr, request.uid, new_trans)
244         return True
245
246     @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
247     def attach(self, func, upload=None, url=None):
248         Attachments = request.registry['ir.attachment']
249
250         website_url = message = None
251         if not upload:
252             website_url = url
253             name = url.split("/").pop()
254             attachment_id = Attachments.create(request.cr, request.uid, {
255                 'name':name,
256                 'type': 'url',
257                 'url': url,
258                 'res_model': 'ir.ui.view',
259             }, request.context)
260         else:
261             try:
262                 image_data = upload.read()
263                 image = Image.open(cStringIO.StringIO(image_data))
264                 w, h = image.size
265                 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
266                     raise ValueError(
267                         u"Image size excessive, uploaded images must be smaller "
268                         u"than 42 million pixel")
269
270                 attachment_id = Attachments.create(request.cr, request.uid, {
271                     'name': upload.filename,
272                     'datas': image_data.encode('base64'),
273                     'datas_fname': upload.filename,
274                     'res_model': 'ir.ui.view',
275                 }, request.context)
276
277                 [attachment] = Attachments.read(
278                     request.cr, request.uid, [attachment_id], ['website_url'],
279                     context=request.context)
280                 website_url = attachment['website_url']
281             except Exception, e:
282                 logger.exception("Failed to upload image to attachment")
283                 message = unicode(e)
284
285         return """<script type='text/javascript'>
286             window.parent['%s'](%s, %s);
287         </script>""" % (func, json.dumps(website_url), json.dumps(message))
288
289     @http.route(['/website/publish'], type='json', auth="public", website=True)
290     def publish(self, id, object):
291         _id = int(id)
292         _object = request.registry[object]
293         obj = _object.browse(request.cr, request.uid, _id)
294
295         values = {}
296         if 'website_published' in _object._all_columns:
297             values['website_published'] = not obj.website_published
298         if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
299             values['website_published_datetime'] = fields.datetime.now()
300         _object.write(request.cr, request.uid, [_id],
301                       values, context=request.context)
302
303         obj = _object.browse(request.cr, request.uid, _id)
304         return bool(obj.website_published)
305
306     #------------------------------------------------------
307     # Helpers
308     #------------------------------------------------------
309     @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
310     def kanban(self, **post):
311         return request.website.kanban_col(**post)
312
313     def placeholder(self, response):
314         # file_open may return a StringIO. StringIO can be closed but are
315         # not context managers in Python 2 though that is fixed in 3
316         with contextlib.closing(openerp.tools.misc.file_open(
317                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
318                 mode='rb')) as f:
319             response.data = f.read()
320             return response.make_conditional(request.httprequest)
321
322     @http.route([
323         '/website/image',
324         '/website/image/<model>/<id>/<field>'
325         ], auth="public", website=True)
326     def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
327         """ Fetches the requested field and ensures it does not go above
328         (max_width, max_height), resizing it if necessary.
329
330         Resizing is bypassed if the object provides a $field_big, which will
331         be interpreted as a pre-resized version of the base field.
332
333         If the record is not found or does not have the requested field,
334         returns a placeholder image via :meth:`~.placeholder`.
335
336         Sets and checks conditional response parameters:
337         * :mailheader:`ETag` is always set (and checked)
338         * :mailheader:`Last-Modified is set iif the record has a concurrency
339           field (``__last_update``)
340
341         The requested field is assumed to be base64-encoded image data in
342         all cases.
343         """
344         Model = request.registry[model]
345
346         response = werkzeug.wrappers.Response()
347
348         id = int(id)
349
350         ids = Model.search(request.cr, request.uid,
351                            [('id', '=', id)], context=request.context) \
352            or Model.search(request.cr, openerp.SUPERUSER_ID,
353                            [('id', '=', id), ('website_published', '=', True)], context=request.context)
354
355         if not ids:
356             return self.placeholder(response)
357
358         presized = '%s_big' % field
359         concurrency = '__last_update'
360         [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
361                               [concurrency, field, presized],
362                               context=request.context)
363
364         if concurrency in record:
365             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
366             try:
367                 response.last_modified = datetime.datetime.strptime(
368                     record[concurrency], server_format + '.%f')
369             except ValueError:
370                 # just in case we have a timestamp without microseconds
371                 response.last_modified = datetime.datetime.strptime(
372                     record[concurrency], server_format)
373
374         # Field does not exist on model or field set to False
375         if not record.get(field):
376             # FIXME: maybe a field which does not exist should be a 404?
377             return self.placeholder(response)
378
379         response.set_etag(hashlib.sha1(record[field]).hexdigest())
380         response.make_conditional(request.httprequest)
381
382         # conditional request match
383         if response.status_code == 304:
384             return response
385
386         data = (record.get(presized) or record[field]).decode('base64')
387
388         image = Image.open(cStringIO.StringIO(data))
389         response.mimetype = Image.MIME[image.format]
390
391         # record provides a pre-resized version of the base field, use that
392         # directly
393         if record.get(presized):
394             response.set_data(data)
395             return response
396
397         fit = int(max_width), int(max_height)
398         w, h = image.size
399         max_w, max_h = fit
400
401         if w < max_w and h < max_h:
402             response.set_data(data)
403         else:
404             image.thumbnail(fit, Image.ANTIALIAS)
405             image.save(response.stream, image.format)
406             # invalidate content-length computed by make_conditional as
407             # writing to response.stream does not do it (as of werkzeug 0.9.3)
408             del response.headers['Content-Length']
409
410         return response
411
412     #------------------------------------------------------
413     # Server actions
414     #------------------------------------------------------
415     @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
416     def actions_server(self, path_or_xml_id_or_id, **post):
417         cr, uid, context = request.cr, request.uid, request.context
418         res, action_id, action = None, None, None
419         ServerActions = request.registry['ir.actions.server']
420
421         # find the action_id: either an xml_id, the path, or an ID
422         if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
423             action_id = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, request.uid, path_or_xml_id_or_id, raise_if_not_found=False)
424         if not action_id:
425             action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
426             action_id = action_ids and action_ids[0] or None
427         if not action_id:
428             try:
429                 action_id = int(path_or_xml_id_or_id)
430             except ValueError:
431                 pass
432
433         # check it effectively exists
434         if action_id:
435             action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
436             action_id = action_ids and action_ids[0] or None
437         # run it, return only if we got a Response object
438         if action_id:
439             action = ServerActions.browse(cr, uid, action_id, context=context)
440             if action.state == 'code' and action.website_published:
441                 action_res = ServerActions.run(cr, uid, [action_id], context=context)
442                 if isinstance(action_res, Response):
443                     res = action_res
444         if res:
445             return res
446         return request.redirect('/')
447
448 # vim:et: