1 # -*- coding: utf-8 -*-
10 from sys import maxint
13 import werkzeug.exceptions
15 import werkzeug.wrappers
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
24 logger = logging.getLogger(__name__)
26 # Completely arbitrary limits
27 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
30 class Website(openerp.addons.web.controllers.main.Home):
31 #------------------------------------------------------
33 #------------------------------------------------------
34 @http.route('/', type='http', auth="public", website=True, multilang=True)
35 def index(self, **kw):
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]
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)
44 return self.page("website.homepage")
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)
51 @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
52 def page(self, page, **opt):
56 # allow shortcut for /page/<website_xml_id>
58 page = 'website.%s' % page
61 request.website.get_template(page)
64 if request.context['editable']:
65 page = 'website.page_404'
67 return request.registry['ir.http']._handle_exception(e, 404)
69 return request.render(page, values)
71 @http.route(['/robots.txt'], type='http', auth="public", website=True)
73 return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain')
75 @http.route('/sitemap', type='http', auth='public', website=True, multilang=True)
77 return request.render('website.sitemap', {
78 'pages': request.website.enumerate_pages()
81 @http.route('/sitemap.xml', type='http', auth="public", website=True)
82 def sitemap_xml(self):
84 'pages': request.website.enumerate_pages()
87 'Content-Type': 'application/xml;charset=utf-8',
89 return request.render('website.sitemap_xml', values, headers=headers)
91 #------------------------------------------------------
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)
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, {
101 'url': "/page/" + xml_id,
103 }, context=request.context)
104 url = "/page/" + xml_id
106 return werkzeug.wrappers.Response(url, mimetype='text/plain')
107 return werkzeug.utils.redirect(url)
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']
114 view_model, view_option_id = imd.get_object_reference(
115 request.cr, request.uid, 'website', 'theme')
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)
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)
129 return request.render('website.themes', {'theme_changed': True})
131 @http.route(['/website/snippets'], type='json', auth="public", website=True)
133 return request.website._render('website.snippets')
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({
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)
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)
159 value = view.inherit_option_id and view.inherit_option_id.id or False
160 view_obj.write(request.cr, request.uid, [view_id], {
162 }, context=request.context)
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')
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]
174 view = request.registry.get("ir.ui.view")
175 views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
179 if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
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:
184 'name': v.inherit_option_id.name,
187 'inherit_id': v.inherit_id.id,
191 done[v.inherit_option_id.id] = True
196 'inherit_id': v.inherit_id.id,
198 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
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)
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)
217 initial_content = t['initial_content'].strip()
218 new_content = t['new_content'].strip()
219 tid = t['translation_id']
221 old_trans = irt.search_read(
222 request.cr, request.uid,
224 ('type', '=', 'view'),
225 ('res_id', '=', view_id),
227 ('src', '=', initial_content),
230 tid = old_trans[0]['id']
232 vals = {'value': new_content}
233 irt.write(request.cr, request.uid, [tid], vals)
240 'source': initial_content,
241 'value': new_content,
243 irt.create(request.cr, request.uid, new_trans)
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']
250 website_url = message = None
253 name = url.split("/").pop()
254 attachment_id = Attachments.create(request.cr, request.uid, {
258 'res_model': 'ir.ui.view',
262 image_data = upload.read()
263 image = Image.open(cStringIO.StringIO(image_data))
265 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
267 u"Image size excessive, uploaded images must be smaller "
268 u"than 42 million pixel")
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',
277 [attachment] = Attachments.read(
278 request.cr, request.uid, [attachment_id], ['website_url'],
279 context=request.context)
280 website_url = attachment['website_url']
282 logger.exception("Failed to upload image to attachment")
285 return """<script type='text/javascript'>
286 window.parent['%s'](%s, %s);
287 </script>""" % (func, json.dumps(website_url), json.dumps(message))
289 @http.route(['/website/publish'], type='json', auth="public", website=True)
290 def publish(self, id, object):
292 _object = request.registry[object]
293 obj = _object.browse(request.cr, request.uid, _id)
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)
303 obj = _object.browse(request.cr, request.uid, _id)
304 return bool(obj.website_published)
306 #------------------------------------------------------
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)
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'),
319 response.data = f.read()
320 return response.make_conditional(request.httprequest)
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.
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.
333 If the record is not found or does not have the requested field,
334 returns a placeholder image via :meth:`~.placeholder`.
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``)
341 The requested field is assumed to be base64-encoded image data in
344 Model = request.registry[model]
346 response = werkzeug.wrappers.Response()
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)
356 return self.placeholder(response)
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)
364 if concurrency in record:
365 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
367 response.last_modified = datetime.datetime.strptime(
368 record[concurrency], server_format + '.%f')
370 # just in case we have a timestamp without microseconds
371 response.last_modified = datetime.datetime.strptime(
372 record[concurrency], server_format)
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)
379 response.set_etag(hashlib.sha1(record[field]).hexdigest())
380 response.make_conditional(request.httprequest)
382 # conditional request match
383 if response.status_code == 304:
386 data = (record.get(presized) or record[field]).decode('base64')
388 image = Image.open(cStringIO.StringIO(data))
389 response.mimetype = Image.MIME[image.format]
391 # record provides a pre-resized version of the base field, use that
393 if record.get(presized):
394 response.set_data(data)
397 fit = int(max_width), int(max_height)
401 if w < max_w and h < max_h:
402 response.set_data(data)
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']
412 #------------------------------------------------------
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']
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)
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
429 action_id = int(path_or_xml_id_or_id)
433 # check it effectively exists
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
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):
446 return request.redirect('/')