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.addons.web.http import request, LazyResponse
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 response = super(Website, self).web_login(*args, **kw)
49 if isinstance(response, LazyResponse):
50 values = dict(response.params['values'], disable_footer=True)
51 response = request.website.render(response.params['template'], values)
54 @http.route('/page/<page:page>', type='http', auth="public", website=True, multilang=True)
55 def page(self, page, **opt):
59 # allow shortcut for /page/<website_xml_id>
61 page = 'website.%s' % page
64 request.website.get_template(page)
67 if request.context['editable']:
68 page = 'website.page_404'
70 return request.registry['ir.http']._handle_exception(e, 404)
72 return request.website.render(page, values)
74 @http.route(['/robots.txt'], type='http', auth="public", website=True)
76 response = request.website.render('website.robots', {'url_root': request.httprequest.url_root})
77 response.mimetype = 'text/plain'
80 @http.route('/sitemap', type='http', auth='public', website=True, multilang=True)
82 return request.website.render('website.sitemap', {
83 'pages': request.website.enumerate_pages()
86 @http.route('/sitemap.xml', type='http', auth="public", website=True)
87 def sitemap_xml(self):
88 response = request.website.render('website.sitemap_xml', {
89 'pages': request.website.enumerate_pages()
91 response.headers['Content-Type'] = 'application/xml;charset=utf-8'
94 #------------------------------------------------------
96 #------------------------------------------------------
97 @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
98 def pagenew(self, path, noredirect=False):
99 xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
100 url = "/page/" + xml_id
102 return werkzeug.wrappers.Response(url, mimetype='text/plain')
103 return werkzeug.utils.redirect(url)
105 @http.route('/website/theme_change', type='http', auth="user", website=True)
106 def theme_change(self, theme_id=False, **kwargs):
107 imd = request.registry['ir.model.data']
108 view = request.registry['ir.ui.view']
110 view_model, view_option_id = imd.get_object_reference(
111 request.cr, request.uid, 'website', 'theme')
113 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
114 context=request.context)
115 view.write(request.cr, request.uid, views, {'inherit_id': False},
116 context=request.context)
119 module, xml_id = theme_id.split('.')
120 view_model, view_id = imd.get_object_reference(
121 request.cr, request.uid, module, xml_id)
122 view.write(request.cr, request.uid, [view_id],
123 {'inherit_id': view_option_id}, context=request.context)
125 return request.website.render('website.themes', {'theme_changed': True})
127 @http.route(['/website/snippets'], type='json', auth="public", website=True)
129 return request.website._render('website.snippets')
131 @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
132 def reset_template(self, templates, redirect='/'):
133 templates = request.httprequest.form.getlist('templates')
134 modules_to_update = []
135 for temp_id in templates:
136 view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context)
137 view.model_data_id.write({
140 if view.model_data_id.module not in modules_to_update:
141 modules_to_update.append(view.model_data_id.module)
142 module_obj = request.registry['ir.module.module']
143 module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context)
144 module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context)
145 return request.redirect(redirect)
147 @http.route('/website/customize_template_toggle', type='json', auth='user', website=True)
148 def customize_template_set(self, view_id):
149 view_obj = request.registry.get("ir.ui.view")
150 view = view_obj.browse(request.cr, request.uid, int(view_id),
151 context=request.context)
155 value = view.inherit_option_id and view.inherit_option_id.id or False
156 view_obj.write(request.cr, request.uid, [view_id], {
158 }, context=request.context)
161 @http.route('/website/customize_template_get', type='json', auth='user', website=True)
162 def customize_template_get(self, xml_id, optional=True):
163 imd = request.registry['ir.model.data']
164 view_model, view_theme_id = imd.get_object_reference(
165 request.cr, request.uid, 'website', 'theme')
167 user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context)
168 group_ids = [g.id for g in user.groups_id]
170 view = request.registry.get("ir.ui.view")
171 views = view._views_get(request.cr, request.uid, xml_id, context=request.context)
175 if v.groups_id and [g for g in v.groups_id if g.id not in group_ids]:
177 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
178 if v.inherit_option_id.id not in done:
180 'name': v.inherit_option_id.name,
183 'inherit_id': v.inherit_id.id,
187 done[v.inherit_option_id.id] = True
192 'inherit_id': v.inherit_id.id,
194 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
198 @http.route('/website/get_view_translations', type='json', auth='public', website=True)
199 def get_view_translations(self, xml_id, lang=None):
200 lang = lang or request.context.get('lang')
201 views = self.customize_template_get(xml_id, optional=False)
202 views_ids = [view.get('id') for view in views if view.get('active')]
203 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
204 irt = request.registry.get('ir.translation')
205 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
207 @http.route('/website/set_translations', type='json', auth='public', website=True)
208 def set_translations(self, data, lang):
209 irt = request.registry.get('ir.translation')
210 for view_id, trans in data.items():
211 view_id = int(view_id)
213 initial_content = t['initial_content'].strip()
214 new_content = t['new_content'].strip()
215 tid = t['translation_id']
217 old_trans = irt.search_read(
218 request.cr, request.uid,
220 ('type', '=', 'view'),
221 ('res_id', '=', view_id),
223 ('src', '=', initial_content),
226 tid = old_trans[0]['id']
228 vals = {'value': new_content}
229 irt.write(request.cr, request.uid, [tid], vals)
236 'source': initial_content,
237 'value': new_content,
239 irt.create(request.cr, request.uid, new_trans)
242 @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
243 def attach(self, func, upload):
247 image_data = upload.read()
248 image = Image.open(cStringIO.StringIO(image_data))
250 if w*h > 42e6: # Nokia Lumia 1020 photo resolution
252 u"Image size excessive, uploaded images must be smaller "
253 u"than 42 million pixel")
255 attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
256 'name': upload.filename,
257 'datas': image_data.encode('base64'),
258 'datas_fname': upload.filename,
259 'res_model': 'ir.ui.view',
262 url = website.urlplus('/website/image', {
263 'model': 'ir.attachment',
266 'max_height': MAX_IMAGE_HEIGHT,
267 'max_width': MAX_IMAGE_WIDTH,
270 logger.exception("Failed to upload image to attachment")
273 return """<script type='text/javascript'>
274 window.parent['%s'](%s, %s);
275 </script>""" % (func, json.dumps(url), json.dumps(message))
277 @http.route(['/website/publish'], type='json', auth="public", website=True)
278 def publish(self, id, object):
280 _object = request.registry[object]
281 obj = _object.browse(request.cr, request.uid, _id)
284 if 'website_published' in _object._all_columns:
285 values['website_published'] = not obj.website_published
286 if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
287 values['website_published_datetime'] = fields.datetime.now()
288 _object.write(request.cr, request.uid, [_id],
289 values, context=request.context)
291 obj = _object.browse(request.cr, request.uid, _id)
292 return bool(obj.website_published)
294 #------------------------------------------------------
296 #------------------------------------------------------
297 @http.route(['/website/kanban/'], type='http', auth="public", methods=['POST'], website=True)
298 def kanban(self, **post):
299 return request.website.kanban_col(**post)
301 def placeholder(self, response):
302 # file_open may return a StringIO. StringIO can be closed but are
303 # not context managers in Python 2 though that is fixed in 3
304 with contextlib.closing(openerp.tools.misc.file_open(
305 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
307 response.data = f.read()
308 return response.make_conditional(request.httprequest)
312 '/website/image/<model>/<id>/<field>'
313 ], auth="public", website=True)
314 def website_image(self, model, id, field, max_width=maxint, max_height=maxint):
315 Model = request.registry[model]
317 response = werkzeug.wrappers.Response()
321 ids = Model.search(request.cr, request.uid,
322 [('id', '=', id)], context=request.context) \
323 or Model.search(request.cr, openerp.SUPERUSER_ID,
324 [('id', '=', id), ('website_published', '=', True)], context=request.context)
327 return self.placeholder(response)
329 concurrency = '__last_update'
330 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
331 [concurrency, field], context=request.context)
333 if concurrency in record:
334 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
336 response.last_modified = datetime.datetime.strptime(
337 record[concurrency], server_format + '.%f')
339 # just in case we have a timestamp without microseconds
340 response.last_modified = datetime.datetime.strptime(
341 record[concurrency], server_format)
343 # Field does not exist on model or field set to False
344 if not record.get(field):
345 # FIXME: maybe a field which does not exist should be a 404?
346 return self.placeholder(response)
348 response.set_etag(hashlib.sha1(record[field]).hexdigest())
349 response.make_conditional(request.httprequest)
351 # conditional request match
352 if response.status_code == 304:
355 data = record[field].decode('base64')
356 fit = int(max_width), int(max_height)
358 buf = cStringIO.StringIO(data)
360 image = Image.open(buf)
362 response.mimetype = Image.MIME[image.format]
367 if w < max_w and h < max_h:
370 image.thumbnail(fit, Image.ANTIALIAS)
371 image.save(response.stream, image.format)
372 # invalidate content-length computed by make_conditional as writing
373 # to response.stream does not do it (as of werkzeug 0.9.3)
374 del response.headers['Content-Length']
378 #------------------------------------------------------
380 #------------------------------------------------------
381 @http.route(['/website/action/<id_or_xml_id>'], type='http', auth="public", website=True)
382 def actions_server(self, id_or_xml_id, **post):
383 cr, uid, context = request.cr, request.uid, request.context
384 res, action_id, action = None, None, None
385 ServerActions = request.registry['ir.actions.server']
387 # find the action_id, either an int, an int into a basestring, or an xml_id
388 if isinstance(id_or_xml_id, basestring) and '.' in id_or_xml_id:
389 action_id = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, request.uid, id_or_xml_id, raise_if_not_found=False)
392 action_id = int(id_or_xml_id)
395 # check it effectively exists
397 action_ids = ServerActions.exists(cr, uid, [action_id], context=context)
398 action_id = action_ids and action_ids[0] or None
399 # run it, return only LazyResponse that are templates to be rendered
401 action = ServerActions.browse(cr, uid, action_id, context=context)
402 if action.state == 'code' and action.website_published:
403 action_res = ServerActions.run(cr, uid, [action_id], context=context)
404 if isinstance(action_res, LazyResponse):
408 return request.redirect('/')