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','gengo_translation'], 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 new_trans['gengo_comment'] = t.get('gengo_comment')
246 irt.create(request.cr, request.uid, new_trans)
249 @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
250 def attach(self, func, upload=None, url=None):
251 Attachments = request.registry['ir.attachment']
253 website_url = message = None
256 name = url.split("/").pop()
257 attachment_id = Attachments.create(request.cr, request.uid, {
261 'res_model': 'ir.ui.view',
265 image_data = upload.read()
266 image = Image.open(cStringIO.StringIO(image_data))
268 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
270 u"Image size excessive, uploaded images must be smaller "
271 u"than 42 million pixel")
273 attachment_id = Attachments.create(request.cr, request.uid, {
274 'name': upload.filename,
275 'datas': image_data.encode('base64'),
276 'datas_fname': upload.filename,
277 'res_model': 'ir.ui.view',
280 [attachment] = Attachments.read(
281 request.cr, request.uid, [attachment_id], ['website_url'],
282 context=request.context)
283 website_url = attachment['website_url']
285 logger.exception("Failed to upload image to attachment")
288 return """<script type='text/javascript'>
289 window.parent['%s'](%s, %s);
290 </script>""" % (func, json.dumps(website_url), json.dumps(message))
292 @http.route(['/website/publish'], type='json', auth="public", website=True)
293 def publish(self, id, object):
295 _object = request.registry[object]
296 obj = _object.browse(request.cr, request.uid, _id)
299 if 'website_published' in _object._all_columns:
300 values['website_published'] = not obj.website_published
301 _object.write(request.cr, request.uid, [_id],
302 values, context=request.context)
304 obj = _object.browse(request.cr, request.uid, _id)
305 return bool(obj.website_published)
307 #------------------------------------------------------
309 #------------------------------------------------------
310 @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
311 def kanban(self, **post):
312 return request.website.kanban_col(**post)
314 def placeholder(self, response):
315 # file_open may return a StringIO. StringIO can be closed but are
316 # not context managers in Python 2 though that is fixed in 3
317 with contextlib.closing(openerp.tools.misc.file_open(
318 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
320 response.data = f.read()
321 return response.make_conditional(request.httprequest)
325 '/website/image/<model>/<id>/<field>'
326 ], auth="public", website=True)
327 def website_image(self, model, id, field, max_width=None, max_height=None):
328 """ Fetches the requested field and ensures it does not go above
329 (max_width, max_height), resizing it if necessary.
331 Resizing is bypassed if the object provides a $field_big, which will
332 be interpreted as a pre-resized version of the base field.
334 If the record is not found or does not have the requested field,
335 returns a placeholder image via :meth:`~.placeholder`.
337 Sets and checks conditional response parameters:
338 * :mailheader:`ETag` is always set (and checked)
339 * :mailheader:`Last-Modified is set iif the record has a concurrency
340 field (``__last_update``)
342 The requested field is assumed to be base64-encoded image data in
346 response = werkzeug.wrappers.Response()
347 concurrency = 'write_date'
349 [record] = request.registry[model].read(request.cr, openerp.SUPERUSER_ID, [id],
350 [concurrency, field],
351 context=request.context)
353 return self.placeholder(response)
355 if concurrency in record:
356 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
358 response.last_modified = datetime.datetime.strptime(
359 record[concurrency], server_format + '.%f')
361 # just in case we have a timestamp without microseconds
362 response.last_modified = datetime.datetime.strptime(
363 record[concurrency], server_format)
365 # Field does not exist on model or field set to False
366 if not record.get(field):
367 # FIXME: maybe a field which does not exist should be a 404?
368 return self.placeholder(response)
370 response.set_etag(hashlib.sha1(record[field]).hexdigest())
371 response.make_conditional(request.httprequest)
373 # conditional request match
374 if response.status_code == 304:
377 data = record[field].decode('base64')
378 if (not max_width) and (not max_height):
379 response.set_data(data)
382 image = Image.open(cStringIO.StringIO(data))
383 response.mimetype = Image.MIME[image.format]
385 max_w, max_h = int(max_width), int(max_height)
386 if w < max_w and h < max_h:
387 response.set_data(data)
389 image.thumbnail((max_w, max_h), Image.ANTIALIAS)
390 image.save(response.stream, image.format)
391 del response.headers['Content-Length']
394 #------------------------------------------------------
396 #------------------------------------------------------
397 @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
398 def actions_server(self, path_or_xml_id_or_id, **post):
399 cr, uid, context = request.cr, request.uid, request.context
400 res, action_id, action = None, None, None
401 ServerActions = request.registry['ir.actions.server']
403 # find the action_id: either an xml_id, the path, or an ID
404 if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
405 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)
407 action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
408 action_id = action_ids and action_ids[0] or None
411 action_id = int(path_or_xml_id_or_id)
415 # check it effectively exists
417 action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
418 action_id = action_ids and action_ids[0] or None
419 # run it, return only if we got a Response object
421 action = ServerActions.browse(cr, uid, action_id, context=context)
422 if action.state == 'code' and action.website_published:
423 action_res = ServerActions.run(cr, uid, [action_id], context=context)
424 if isinstance(action_res, Response):
428 return request.redirect('/')