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','state'], 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 if t.get('gengo_translation'):
244 new_trans['gengo_translation'] = t.get('gengo_translation')
245 irt.create(request.cr, request.uid, new_trans)
248 @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
249 def attach(self, func, upload):
253 image_data = upload.read()
254 image = Image.open(cStringIO.StringIO(image_data))
256 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
258 u"Image size excessive, uploaded images must be smaller "
259 u"than 42 million pixel")
261 Attachments = request.registry['ir.attachment']
262 attachment_id = Attachments.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',
269 [attachment] = Attachments.read(
270 request.cr, request.uid, [attachment_id], ['website_url'],
271 context=request.context)
272 url = attachment['website_url']
274 logger.exception("Failed to upload image to attachment")
277 return """<script type='text/javascript'>
278 window.parent['%s'](%s, %s);
279 </script>""" % (func, json.dumps(url), json.dumps(message))
281 @http.route(['/website/publish'], type='json', auth="public", website=True)
282 def publish(self, id, object):
284 _object = request.registry[object]
285 obj = _object.browse(request.cr, request.uid, _id)
288 if 'website_published' in _object._all_columns:
289 values['website_published'] = not obj.website_published
290 if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
291 values['website_published_datetime'] = fields.datetime.now()
292 _object.write(request.cr, request.uid, [_id],
293 values, context=request.context)
295 obj = _object.browse(request.cr, request.uid, _id)
296 return bool(obj.website_published)
298 #------------------------------------------------------
300 #------------------------------------------------------
301 @http.route(['/website/kanban/'], type='http', auth="public", methods=['POST'], website=True)
302 def kanban(self, **post):
303 return request.website.kanban_col(**post)
305 def placeholder(self, response):
306 # file_open may return a StringIO. StringIO can be closed but are
307 # not context managers in Python 2 though that is fixed in 3
308 with contextlib.closing(openerp.tools.misc.file_open(
309 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
311 response.data = f.read()
312 return response.make_conditional(request.httprequest)
316 '/website/image/<model>/<id>/<field>'
317 ], auth="public", website=True)
318 def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
319 """ Fetches the requested field and ensures it does not go above
320 (max_width, max_height), resizing it if necessary.
322 Resizing is bypassed if the object provides a $field_big, which will
323 be interpreted as a pre-resized version of the base field.
325 If the record is not found or does not have the requested field,
326 returns a placeholder image via :meth:`~.placeholder`.
328 Sets and checks conditional response parameters:
329 * :mailheader:`ETag` is always set (and checked)
330 * :mailheader:`Last-Modified is set iif the record has a concurrency
331 field (``__last_update``)
333 The requested field is assumed to be base64-encoded image data in
336 Model = request.registry[model]
338 response = werkzeug.wrappers.Response()
342 ids = Model.search(request.cr, request.uid,
343 [('id', '=', id)], context=request.context) \
344 or Model.search(request.cr, openerp.SUPERUSER_ID,
345 [('id', '=', id), ('website_published', '=', True)], context=request.context)
348 return self.placeholder(response)
350 presized = '%s_big' % field
351 concurrency = '__last_update'
352 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
353 [concurrency, field, presized],
354 context=request.context)
356 if concurrency in record:
357 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
359 response.last_modified = datetime.datetime.strptime(
360 record[concurrency], server_format + '.%f')
362 # just in case we have a timestamp without microseconds
363 response.last_modified = datetime.datetime.strptime(
364 record[concurrency], server_format)
366 # Field does not exist on model or field set to False
367 if not record.get(field):
368 # FIXME: maybe a field which does not exist should be a 404?
369 return self.placeholder(response)
371 response.set_etag(hashlib.sha1(record[field]).hexdigest())
372 response.make_conditional(request.httprequest)
374 # conditional request match
375 if response.status_code == 304:
378 data = (record.get(presized) or record[field]).decode('base64')
380 image = Image.open(cStringIO.StringIO(data))
381 response.mimetype = Image.MIME[image.format]
383 # record provides a pre-resized version of the base field, use that
385 if record.get(presized):
386 response.set_data(data)
389 fit = int(max_width), int(max_height)
393 if w < max_w and h < max_h:
394 response.set_data(data)
396 image.thumbnail(fit, Image.ANTIALIAS)
397 image.save(response.stream, image.format)
398 # invalidate content-length computed by make_conditional as
399 # writing to response.stream does not do it (as of werkzeug 0.9.3)
400 del response.headers['Content-Length']
404 #------------------------------------------------------
406 #------------------------------------------------------
407 @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
408 def actions_server(self, path_or_xml_id_or_id, **post):
409 cr, uid, context = request.cr, request.uid, request.context
410 res, action_id, action = None, None, None
411 ServerActions = request.registry['ir.actions.server']
413 # find the action_id: either an xml_id, the path, or an ID
414 if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
415 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)
417 action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
418 action_id = action_ids and action_ids[0] or None
421 action_id = int(path_or_xml_id_or_id)
425 # check it effectively exists
427 action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
428 action_id = action_ids and action_ids[0] or None
429 # run it, return only if we got a Response object
431 action = ServerActions.browse(cr, uid, action_id, context=context)
432 if action.state == 'code' and action.website_published:
433 action_res = ServerActions.run(cr, uid, [action_id], context=context)
434 if isinstance(action_res, Response):
438 return request.redirect('/')