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):
39 main_menu = request.registry['ir.model.data'].get_object(request.cr, request.uid, 'website', 'main_menu')
40 first_menu = main_menu.child_id and main_menu.child_id[0]
42 if not (first_menu.startswith(('/page/', '/?', '/#')) or (first_menu=='/')):
43 return request.redirect(first_menu.url)
44 if first_menu.startswith('/page/'):
48 return self.page(page)
50 @http.route(website=True, auth="public", multilang=True)
51 def web_login(self, *args, **kw):
52 # TODO: can't we just put auth=public, ... in web client ?
53 return super(Website, self).web_login(*args, **kw)
55 @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
56 def page(self, page, **opt):
60 # allow shortcut for /page/<website_xml_id>
62 page = 'website.%s' % page
65 request.website.get_template(page)
68 if request.website.is_publisher():
69 page = 'website.page_404'
71 return request.registry['ir.http']._handle_exception(e, 404)
73 return request.render(page, values)
75 @http.route(['/robots.txt'], type='http', auth="public")
77 return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain')
79 @http.route('/sitemap.xml', type='http', auth="public", website=True)
80 def sitemap_xml_index(self):
81 pages = list(request.website.enumerate_pages())
82 if len(pages)<=LOC_PER_SITEMAP:
83 return self.__sitemap_xml(pages, 0)
84 # Sitemaps must be split in several smaller files with a sitemap index
86 'pages': range(len(pages)/LOC_PER_SITEMAP+1),
87 'url_root': request.httprequest.url_root
90 'Content-Type': 'application/xml;charset=utf-8',
92 return request.render('website.sitemap_index_xml', values, headers=headers)
94 @http.route('/sitemap-<int:page>.xml', type='http', auth="public", website=True)
95 def sitemap_xml(self, page):
96 pages = list(request.website.enumerate_pages())
97 return self.__sitemap_xml(pages, page)
99 def __sitemap_xml(self, pages, index=0):
101 'pages': pages[index*LOC_PER_SITEMAP:(index+1)*LOC_PER_SITEMAP],
102 'url_root': request.httprequest.url_root.rstrip('/')
105 'Content-Type': 'application/xml;charset=utf-8',
107 return request.render('website.sitemap_xml', values, headers=headers)
109 #------------------------------------------------------
111 #------------------------------------------------------
112 @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
113 def pagenew(self, path, noredirect=False, add_menu=None):
114 xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
116 model, id = request.registry["ir.model.data"].get_object_reference(request.cr, request.uid, 'website', 'main_menu')
117 request.registry['website.menu'].create(request.cr, request.uid, {
119 'url': "/page/" + xml_id,
121 }, context=request.context)
122 # Reverse action in order to allow shortcut for /page/<website_xml_id>
123 url = "/page/" + re.sub(r"^website\.", '', xml_id)
126 return werkzeug.wrappers.Response(url, mimetype='text/plain')
127 return werkzeug.utils.redirect(url)
129 @http.route('/website/theme_change', type='http', auth="user", website=True)
130 def theme_change(self, theme_id=False, **kwargs):
131 imd = request.registry['ir.model.data']
132 view = request.registry['ir.ui.view']
134 view_model, view_option_id = imd.get_object_reference(
135 request.cr, request.uid, 'website', 'theme')
137 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
138 context=request.context)
139 view.write(request.cr, request.uid, views, {'inherit_id': False},
140 context=request.context)
143 module, xml_id = theme_id.split('.')
144 view_model, view_id = imd.get_object_reference(
145 request.cr, request.uid, module, xml_id)
146 view.write(request.cr, request.uid, [view_id],
147 {'inherit_id': view_option_id}, context=request.context)
149 return request.render('website.themes', {'theme_changed': True})
151 @http.route(['/website/snippets'], type='json', auth="public", website=True)
153 return request.website._render('website.snippets')
155 @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
156 def reset_template(self, templates, redirect='/'):
157 templates = request.httprequest.form.getlist('templates')
158 modules_to_update = []
159 for temp_id in templates:
160 view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
161 view.model_data_id.write({
164 if view.model_data_id.module not in modules_to_update:
165 modules_to_update.append(view.model_data_id.module)
166 module_obj = request.registry['ir.module.module']
167 module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
168 module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
169 return request.redirect(redirect)
171 @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
172 def customize_template_set(self, view_id):
173 view_obj = request.registry.get("ir.ui.view")
174 view = view_obj.browse(request.cr, request.uid, int(view_id),
175 context=request.context)
179 value = view.inherit_option_id and view.inherit_option_id.id or False
180 view_obj.write(request.cr, request.uid, [view_id], {
182 }, context=request.context)
185 @http.route('/website/customize_template_get', type='json', auth='user', website=True)
186 def customize_template_get(self, xml_id, optional=True):
187 imd = request.registry['ir.model.data']
188 view_model, view_theme_id = imd.get_object_reference(
189 request.cr, request.uid, 'website', 'theme')
191 user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
192 group_ids = [g.id for g in user.groups_id]
194 view = request.registry.get("ir.ui.view")
195 views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
199 if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
201 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
202 if v.inherit_option_id.id not in done:
204 'name': v.inherit_option_id.name,
207 'inherit_id': v.inherit_id.id,
211 done[v.inherit_option_id.id] = True
216 'inherit_id': v.inherit_id.id,
218 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
222 @http.route('/website/get_view_translations', type='json', auth='public', website=True)
223 def get_view_translations(self, xml_id, lang=None):
224 lang = lang or request.context.get('lang')
225 views = self.customize_template_get(xml_id, optional=False)
226 views_ids = [view.get('id') for view in views if view.get('active')]
227 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
228 irt = request.registry.get('ir.translation')
229 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value','state','gengo_translation'], context=request.context)
231 @http.route('/website/set_translations', type='json', auth='public', website=True)
232 def set_translations(self, data, lang):
233 irt = request.registry.get('ir.translation')
234 for view_id, trans in data.items():
235 view_id = int(view_id)
237 initial_content = t['initial_content'].strip()
238 new_content = t['new_content'].strip()
239 tid = t['translation_id']
241 old_trans = irt.search_read(
242 request.cr, request.uid,
244 ('type', '=', 'view'),
245 ('res_id', '=', view_id),
247 ('src', '=', initial_content),
250 tid = old_trans[0]['id']
252 vals = {'value': new_content}
253 irt.write(request.cr, request.uid, [tid], vals)
260 'source': initial_content,
261 'value': new_content,
263 if t.get('gengo_translation'):
264 new_trans['gengo_translation'] = t.get('gengo_translation')
265 new_trans['gengo_comment'] = t.get('gengo_comment')
266 irt.create(request.cr, request.uid, new_trans)
269 @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
270 def attach(self, func, upload=None, url=None):
271 Attachments = request.registry['ir.attachment']
273 website_url = message = None
276 name = url.split("/").pop()
277 attachment_id = Attachments.create(request.cr, request.uid, {
281 'res_model': 'ir.ui.view',
285 image_data = upload.read()
286 image = Image.open(cStringIO.StringIO(image_data))
288 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
290 u"Image size excessive, uploaded images must be smaller "
291 u"than 42 million pixel")
293 attachment_id = Attachments.create(request.cr, request.uid, {
294 'name': upload.filename,
295 'datas': image_data.encode('base64'),
296 'datas_fname': upload.filename,
297 'res_model': 'ir.ui.view',
300 [attachment] = Attachments.read(
301 request.cr, request.uid, [attachment_id], ['website_url'],
302 context=request.context)
303 website_url = attachment['website_url']
305 logger.exception("Failed to upload image to attachment")
308 return """<script type='text/javascript'>
309 window.parent['%s'](%s, %s);
310 </script>""" % (func, json.dumps(website_url), json.dumps(message))
312 @http.route(['/website/publish'], type='json', auth="public", website=True)
313 def publish(self, id, object):
315 _object = request.registry[object]
316 obj = _object.browse(request.cr, request.uid, _id)
319 if 'website_published' in _object._all_columns:
320 values['website_published'] = not obj.website_published
321 _object.write(request.cr, request.uid, [_id],
322 values, context=request.context)
324 obj = _object.browse(request.cr, request.uid, _id)
325 return bool(obj.website_published)
327 #------------------------------------------------------
329 #------------------------------------------------------
330 @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
331 def kanban(self, **post):
332 return request.website.kanban_col(**post)
334 def placeholder(self, response):
335 # file_open may return a StringIO. StringIO can be closed but are
336 # not context managers in Python 2 though that is fixed in 3
337 with contextlib.closing(openerp.tools.misc.file_open(
338 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
340 response.data = f.read()
341 return response.make_conditional(request.httprequest)
345 '/website/image/<model>/<id>/<field>'
346 ], auth="public", website=True)
347 def website_image(self, model, id, field, max_width=None, max_height=None):
348 """ Fetches the requested field and ensures it does not go above
349 (max_width, max_height), resizing it if necessary.
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 (``write_date``)
359 The requested field is assumed to be base64-encoded image data in
363 response = werkzeug.wrappers.Response()
364 concurrency = 'write_date'
366 [record] = request.registry[model].read(request.cr, openerp.SUPERUSER_ID, [id],
367 [concurrency, field],
368 context=request.context)
370 return self.placeholder(response)
372 if concurrency in record:
373 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
375 response.last_modified = datetime.datetime.strptime(
376 record[concurrency], server_format + '.%f')
378 # just in case we have a timestamp without microseconds
379 response.last_modified = datetime.datetime.strptime(
380 record[concurrency], server_format)
382 # Field does not exist on model or field set to False
383 if not record.get(field):
384 # FIXME: maybe a field which does not exist should be a 404?
385 return self.placeholder(response)
387 response.set_etag(hashlib.sha1(record[field]).hexdigest())
388 response.make_conditional(request.httprequest)
390 # conditional request match
391 if response.status_code == 304:
394 data = record[field].decode('base64')
395 if (not max_width) and (not max_height):
399 image = Image.open(cStringIO.StringIO(data))
400 response.mimetype = Image.MIME[image.format]
403 max_w, max_h = int(max_width), int(max_height)
404 if w < max_w and h < max_h:
407 image.thumbnail((max_w, max_h), Image.ANTIALIAS)
408 image.save(response.stream, image.format)
409 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('/')