1 # -*- coding: utf-8 -*-
11 from sys import maxint
14 import werkzeug.exceptions
16 import werkzeug.wrappers
20 from openerp.osv import fields
21 from openerp.addons.website.models import website
22 from openerp.addons.web import http
23 from openerp.http import request, Response
25 logger = logging.getLogger(__name__)
27 # Completely arbitrary limits
28 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
29 LOC_PER_SITEMAP = 45000
31 class Website(openerp.addons.web.controllers.main.Home):
32 #------------------------------------------------------
34 #------------------------------------------------------
35 @http.route('/', type='http', auth="public", website=True, multilang=True)
36 def index(self, **kw):
38 main_menu = request.registry['ir.model.data'].get_object(request.cr, request.uid, 'website', 'main_menu')
39 first_menu = main_menu.child_id and main_menu.child_id[0]
41 if first_menu and not ((first_menu.url == '/') or first_menu.url.startswith('/#') or first_menu.url.startswith('/?')):
42 return request.redirect(first_menu.url)
45 return self.page("website.homepage")
47 @http.route(website=True, auth="public", multilang=True)
48 def web_login(self, *args, **kw):
49 # TODO: can't we just put auth=public, ... in web client ?
50 return super(Website, self).web_login(*args, **kw)
52 @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
53 def page(self, page, **opt):
57 # allow shortcut for /page/<website_xml_id>
59 page = 'website.%s' % page
62 request.website.get_template(page)
65 if request.context['editable']:
66 page = 'website.page_404'
68 return request.registry['ir.http']._handle_exception(e, 404)
70 return request.render(page, values)
72 @http.route(['/robots.txt'], type='http', auth="public")
74 return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain')
76 @http.route('/sitemap.xml', type='http', auth="public", website=True)
77 def sitemap_xml_index(self):
78 pages = list(request.website.enumerate_pages())
79 if len(pages)<=LOC_PER_SITEMAP:
80 return self.__sitemap_xml(pages, 0)
81 # Sitemaps must be split in several smaller files with a sitemap index
83 'pages': range(len(pages)/LOC_PER_SITEMAP+1),
84 'url_root': request.httprequest.url_root
87 'Content-Type': 'application/xml;charset=utf-8',
89 return request.render('website.sitemap_index_xml', values, headers=headers)
91 @http.route('/sitemap-<int:page>.xml', type='http', auth="public", website=True)
92 def sitemap_xml(self, page):
93 pages = list(request.website.enumerate_pages())
94 return self.__sitemap_xml(pages, page)
96 def __sitemap_xml(self, pages, index=0):
98 'pages': pages[index*LOC_PER_SITEMAP:(index+1)*LOC_PER_SITEMAP],
99 'url_root': request.httprequest.url_root.rstrip('/')
102 'Content-Type': 'application/xml;charset=utf-8',
104 return request.render('website.sitemap_xml', values, headers=headers)
106 #------------------------------------------------------
108 #------------------------------------------------------
109 @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
110 def pagenew(self, path, noredirect=False, add_menu=None):
111 xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
113 model, id = request.registry["ir.model.data"].get_object_reference(request.cr, request.uid, 'website', 'main_menu')
114 request.registry['website.menu'].create(request.cr, request.uid, {
116 'url': "/page/" + xml_id,
118 }, context=request.context)
119 # Reverse action in order to allow shortcut for /page/<website_xml_id>
120 url = "/page/" + re.sub(r"^website\.", '', xml_id)
123 return werkzeug.wrappers.Response(url, mimetype='text/plain')
124 return werkzeug.utils.redirect(url)
126 @http.route('/website/theme_change', type='http', auth="user", website=True)
127 def theme_change(self, theme_id=False, **kwargs):
128 imd = request.registry['ir.model.data']
129 view = request.registry['ir.ui.view']
131 view_model, view_option_id = imd.get_object_reference(
132 request.cr, request.uid, 'website', 'theme')
134 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
135 context=request.context)
136 view.write(request.cr, request.uid, views, {'inherit_id': False},
137 context=request.context)
140 module, xml_id = theme_id.split('.')
141 view_model, view_id = imd.get_object_reference(
142 request.cr, request.uid, module, xml_id)
143 view.write(request.cr, request.uid, [view_id],
144 {'inherit_id': view_option_id}, context=request.context)
146 return request.render('website.themes', {'theme_changed': True})
148 @http.route(['/website/snippets'], type='json', auth="public", website=True)
150 return request.website._render('website.snippets')
152 @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
153 def reset_template(self, templates, redirect='/'):
154 templates = request.httprequest.form.getlist('templates')
155 modules_to_update = []
156 for temp_id in templates:
157 view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
158 view.model_data_id.write({
161 if view.model_data_id.module not in modules_to_update:
162 modules_to_update.append(view.model_data_id.module)
163 module_obj = request.registry['ir.module.module']
164 module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
165 module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
166 return request.redirect(redirect)
168 @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
169 def customize_template_set(self, view_id):
170 view_obj = request.registry.get("ir.ui.view")
171 view = view_obj.browse(request.cr, request.uid, int(view_id),
172 context=request.context)
176 value = view.inherit_option_id and view.inherit_option_id.id or False
177 view_obj.write(request.cr, request.uid, [view_id], {
179 }, context=request.context)
182 @http.route('/website/customize_template_get', type='json', auth='user', website=True)
183 def customize_template_get(self, xml_id, optional=True):
184 imd = request.registry['ir.model.data']
185 view_model, view_theme_id = imd.get_object_reference(
186 request.cr, request.uid, 'website', 'theme')
188 user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
189 group_ids = [g.id for g in user.groups_id]
191 view = request.registry.get("ir.ui.view")
192 views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
196 if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
198 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
199 if v.inherit_option_id.id not in done:
201 'name': v.inherit_option_id.name,
204 'inherit_id': v.inherit_id.id,
208 done[v.inherit_option_id.id] = True
213 'inherit_id': v.inherit_id.id,
215 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
219 @http.route('/website/get_view_translations', type='json', auth='public', website=True)
220 def get_view_translations(self, xml_id, lang=None):
221 lang = lang or request.context.get('lang')
222 views = self.customize_template_get(xml_id, optional=False)
223 views_ids = [view.get('id') for view in views if view.get('active')]
224 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
225 irt = request.registry.get('ir.translation')
226 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
228 @http.route('/website/set_translations', type='json', auth='public', website=True)
229 def set_translations(self, data, lang):
230 irt = request.registry.get('ir.translation')
231 for view_id, trans in data.items():
232 view_id = int(view_id)
234 initial_content = t['initial_content'].strip()
235 new_content = t['new_content'].strip()
236 tid = t['translation_id']
238 old_trans = irt.search_read(
239 request.cr, request.uid,
241 ('type', '=', 'view'),
242 ('res_id', '=', view_id),
244 ('src', '=', initial_content),
247 tid = old_trans[0]['id']
249 vals = {'value': new_content}
250 irt.write(request.cr, request.uid, [tid], vals)
257 'source': initial_content,
258 'value': new_content,
260 if t.get('gengo_translation'):
261 new_trans['gengo_translation'] = t.get('gengo_translation')
262 new_trans['gengo_comment'] = t.get('gengo_comment')
263 irt.create(request.cr, request.uid, new_trans)
266 @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
267 def attach(self, func, upload=None, url=None):
268 Attachments = request.registry['ir.attachment']
270 website_url = message = None
273 name = url.split("/").pop()
274 attachment_id = Attachments.create(request.cr, request.uid, {
278 'res_model': 'ir.ui.view',
282 image_data = upload.read()
283 image = Image.open(cStringIO.StringIO(image_data))
285 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
287 u"Image size excessive, uploaded images must be smaller "
288 u"than 42 million pixel")
290 attachment_id = Attachments.create(request.cr, request.uid, {
291 'name': upload.filename,
292 'datas': image_data.encode('base64'),
293 'datas_fname': upload.filename,
294 'res_model': 'ir.ui.view',
297 [attachment] = Attachments.read(
298 request.cr, request.uid, [attachment_id], ['website_url'],
299 context=request.context)
300 website_url = attachment['website_url']
302 logger.exception("Failed to upload image to attachment")
305 return """<script type='text/javascript'>
306 window.parent['%s'](%s, %s);
307 </script>""" % (func, json.dumps(website_url), json.dumps(message))
309 @http.route(['/website/publish'], type='json', auth="public", website=True)
310 def publish(self, id, object):
312 _object = request.registry[object]
313 obj = _object.browse(request.cr, request.uid, _id)
316 if 'website_published' in _object._all_columns:
317 values['website_published'] = not obj.website_published
318 _object.write(request.cr, request.uid, [_id],
319 values, context=request.context)
321 obj = _object.browse(request.cr, request.uid, _id)
322 return bool(obj.website_published)
324 #------------------------------------------------------
326 #------------------------------------------------------
327 @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
328 def kanban(self, **post):
329 return request.website.kanban_col(**post)
331 def placeholder(self, response):
332 # file_open may return a StringIO. StringIO can be closed but are
333 # not context managers in Python 2 though that is fixed in 3
334 with contextlib.closing(openerp.tools.misc.file_open(
335 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
337 response.data = f.read()
338 return response.make_conditional(request.httprequest)
342 '/website/image/<model>/<id>/<field>'
343 ], auth="public", website=True)
344 def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
345 """ Fetches the requested field and ensures it does not go above
346 (max_width, max_height), resizing it if necessary.
348 Resizing is bypassed if the object provides a $field_big, which will
349 be interpreted as a pre-resized version of the base field.
351 If the record is not found or does not have the requested field,
352 returns a placeholder image via :meth:`~.placeholder`.
354 Sets and checks conditional response parameters:
355 * :mailheader:`ETag` is always set (and checked)
356 * :mailheader:`Last-Modified is set iif the record has a concurrency
357 field (``__last_update``)
359 The requested field is assumed to be base64-encoded image data in
362 Model = request.registry[model]
364 response = werkzeug.wrappers.Response()
368 ids = Model.search(request.cr, request.uid,
369 [('id', '=', id)], context=request.context)
370 if not ids and 'website_published' in Model._all_columns:
371 ids = Model.search(request.cr, openerp.SUPERUSER_ID,
372 [('id', '=', id), ('website_published', '=', True)], context=request.context)
375 return self.placeholder(response)
377 presized = '%s_big' % field
378 concurrency = '__last_update'
379 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
380 [concurrency, field, presized],
381 context=request.context)
383 if concurrency in record:
384 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
386 response.last_modified = datetime.datetime.strptime(
387 record[concurrency], server_format + '.%f')
389 # just in case we have a timestamp without microseconds
390 response.last_modified = datetime.datetime.strptime(
391 record[concurrency], server_format)
393 # Field does not exist on model or field set to False
394 if not record.get(field):
395 # FIXME: maybe a field which does not exist should be a 404?
396 return self.placeholder(response)
398 response.set_etag(hashlib.sha1(record[field]).hexdigest())
399 response.make_conditional(request.httprequest)
401 # conditional request match
402 if response.status_code == 304:
405 data = (record.get(presized) or record[field]).decode('base64')
407 image = Image.open(cStringIO.StringIO(data))
408 response.mimetype = Image.MIME[image.format]
410 # record provides a pre-resized version of the base field, use that
412 if record.get(presized):
416 fit = int(max_width), int(max_height)
420 if w < max_w and h < max_h:
423 image.thumbnail(fit, Image.ANTIALIAS)
424 image.save(response.stream, image.format)
425 # invalidate content-length computed by make_conditional as
426 # writing to response.stream does not do it (as of werkzeug 0.9.3)
427 del response.headers['Content-Length']
431 #------------------------------------------------------
433 #------------------------------------------------------
434 @http.route('/website/action/<path_or_xml_id_or_id>', type='http', auth="public", website=True)
435 def actions_server(self, path_or_xml_id_or_id, **post):
436 cr, uid, context = request.cr, request.uid, request.context
437 res, action_id, action = None, None, None
438 ServerActions = request.registry['ir.actions.server']
440 # find the action_id: either an xml_id, the path, or an ID
441 if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
442 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)
444 action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context)
445 action_id = action_ids and action_ids[0] or None
448 action_id = int(path_or_xml_id_or_id)
452 # check it effectively exists
454 action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
455 action_id = action_ids and action_ids[0] or None
456 # run it, return only if we got a Response object
458 action = ServerActions.browse(cr, uid, action_id, context=context)
459 if action.state == 'code' and action.website_published:
460 action_res = ServerActions.run(cr, uid, [action_id], context=context)
461 if isinstance(action_res, Response):
465 return request.redirect('/')