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):
251 image_data = upload.read()
252 image = Image.open(cStringIO.StringIO(image_data))
254 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
256 u"Image size excessive, uploaded images must be smaller "
257 u"than 42 million pixel")
259 attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
260 'name': upload.filename,
261 'datas': image_data.encode('base64'),
262 'datas_fname': upload.filename,
263 'res_model': 'ir.ui.view',
266 url = website.urlplus('/website/image', {
267 'model': 'ir.attachment',
270 'max_height': MAX_IMAGE_HEIGHT,
271 'max_width': MAX_IMAGE_WIDTH,
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 Model = request.registry[model]
321 response = werkzeug.wrappers.Response()
325 ids = Model.search(request.cr, request.uid,
326 [('id', '=', id)], context=request.context) \
327 or Model.search(request.cr, openerp.SUPERUSER_ID,
328 [('id', '=', id), ('website_published', '=', True)], context=request.context)
331 return self.placeholder(response)
333 concurrency = '__last_update'
334 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
335 [concurrency, field], context=request.context)
337 if concurrency in record:
338 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
340 response.last_modified = datetime.datetime.strptime(
341 record[concurrency], server_format + '.%f')
343 # just in case we have a timestamp without microseconds
344 response.last_modified = datetime.datetime.strptime(
345 record[concurrency], server_format)
347 # Field does not exist on model or field set to False
348 if not record.get(field):
349 # FIXME: maybe a field which does not exist should be a 404?
350 return self.placeholder(response)
352 response.set_etag(hashlib.sha1(record[field]).hexdigest())
353 response.make_conditional(request.httprequest)
355 # conditional request match
356 if response.status_code == 304:
359 data = record[field].decode('base64')
360 fit = int(max_width), int(max_height)
362 buf = cStringIO.StringIO(data)
364 image = Image.open(buf)
366 response.mimetype = Image.MIME[image.format]
371 if w < max_w and h < max_h:
374 image.thumbnail(fit, Image.ANTIALIAS)
375 image.save(response.stream, image.format)
376 # invalidate content-length computed by make_conditional as writing
377 # to response.stream does not do it (as of werkzeug 0.9.3)
378 del response.headers['Content-Length']
382 @http.route(['/website/current_user/'], type='json', auth="public", website=True)
383 def get_current_user(self, fields):
384 cr, uid, context = request.cr, request.uid, request.context
385 if request.registry["website"].get_public_user(cr, uid, context=context) != uid:
386 return request.registry["res.users"].read(cr, openerp.SUPERUSER_ID, [uid], fields, context=context)[0]
390 #------------------------------------------------------
392 #------------------------------------------------------
393 @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
394 def actions_server(self, path_or_xml_id_or_id, **post):
395 cr, uid, context = request.cr, request.uid, request.context
396 res, action_id, action = None, None, None
397 ServerActions = request.registry['ir.actions.server']
399 # find the action_id: either an xml_id, the path, or an ID
400 if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
401 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)
403 action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
404 action_id = action_ids and action_ids[0] or None
407 action_id = int(path_or_xml_id_or_id)
411 # check it effectively exists
413 action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
414 action_id = action_ids and action_ids[0] or None
415 # run it, return only if we got a Response object
417 action = ServerActions.browse(cr, uid, action_id, context=context)
418 if action.state == 'code' and action.website_published:
419 action_res = ServerActions.run(cr, uid, [action_id], context=context)
420 if isinstance(action_res, Response):
424 return request.redirect('/')