1 # -*- coding: utf-8 -*-
12 from sys import maxint
16 import werkzeug.exceptions
18 import werkzeug.wrappers
22 from slugify import slugify
24 def slugify(s, max_length=None):
25 spaceless = re.sub(r'\s+', '-', s)
26 specialless = re.sub(r'[^-_a-z0-9]', '', spaceless)
27 return specialless[:max_length]
30 from openerp.osv import fields
31 from openerp.addons.website.models import website
32 from openerp.addons.web import http
33 from openerp.addons.web.http import request
35 logger = logging.getLogger(__name__)
38 def auth_method_public():
39 registry = openerp.modules.registry.RegistryManager.get(request.db)
40 if not request.session.uid:
41 request.uid = registry['website'].get_public_user().id
43 request.uid = request.session.uid
44 http.auth_methods['public'] = auth_method_public
47 # Completely arbitrary limits
48 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
49 class Website(openerp.addons.web.controllers.main.Home):
50 @website.route('/', type='http', auth="public", multilang=True)
51 def index(self, **kw):
52 # TODO: check if plain SQL is needed
53 menu = request.registry['website.menu']
54 root_domain = [('parent_id', '=', False)] # TODO: multiwebsite ('website_id', '=', request.website.id),
55 root_id = menu.search(request.cr, request.uid, root_domain, limit=1, context=request.context)[0]
56 first_menu = menu.search_read(
57 request.cr, request.uid, [('parent_id', '=', root_id)], ['url'],
58 limit=1, order='sequence', context=request.context)
60 first_menu = first_menu[0]['url']
61 if first_menu and first_menu != '/':
62 return request.redirect(first_menu)
64 return self.page("website.homepage")
66 @website.route('/pagenew/<path:path>', type='http', auth="user")
67 def pagenew(self, path, noredirect=NOPE):
69 # completely arbitrary max_length
70 idname = slugify(path, max_length=50)
72 request.cr.execute('SAVEPOINT pagenew')
73 imd = request.registry['ir.model.data']
74 view = request.registry['ir.ui.view']
75 view_model, view_id = imd.get_object_reference(
76 request.cr, request.uid, 'website', 'default_page')
77 newview_id = view.copy(
78 request.cr, request.uid, view_id, context=request.context)
79 newview = view.browse(
80 request.cr, request.uid, newview_id, context=request.context)
82 'arch': newview.arch.replace("website.default_page",
83 "%s.%s" % (module, idname)),
87 # Fuck it, we're doing it live
89 imd.create(request.cr, request.uid, {
92 'model': 'ir.ui.view',
95 }, context=request.context)
96 except psycopg2.IntegrityError:
97 logger.exception('Unable to create ir_model_data for page %s', path)
98 request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
99 return werkzeug.exceptions.InternalServerError()
101 request.cr.execute('RELEASE SAVEPOINT pagenew')
103 url = "/page/%s" % idname
104 if noredirect is not NOPE:
105 return werkzeug.wrappers.Response(url, mimetype='text/plain')
106 return werkzeug.utils.redirect(url)
108 @website.route('/website/theme_change', type='http', auth="admin")
109 def theme_change(self, theme_id=False, **kwargs):
110 imd = request.registry['ir.model.data']
111 view = request.registry['ir.ui.view']
113 view_model, view_option_id = imd.get_object_reference(
114 request.cr, request.uid, 'website', 'theme')
116 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
117 context=request.context)
118 view.write(request.cr, request.uid, views, {'inherit_id': False},
119 context=request.context)
122 module, xml_id = theme_id.split('.')
123 view_model, view_id = imd.get_object_reference(
124 request.cr, request.uid, module, xml_id)
125 view.write(request.cr, request.uid, [view_id],
126 {'inherit_id': view_option_id}, context=request.context)
128 return request.website.render('website.themes', {'theme_changed': True})
130 @website.route(['/website/snippets'], type='json', auth="public")
132 return request.website.render('website.snippets')
134 @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
135 def page(self, path, **kwargs):
140 html = request.website.render(path, values)
142 html = request.website.render('website.404', values)
145 @website.route('/website/customize_template_toggle', type='json', auth='user')
146 def customize_template_set(self, view_id):
147 view_obj = request.registry.get("ir.ui.view")
148 view = view_obj.browse(request.cr, request.uid, int(view_id),
149 context=request.context)
153 value = view.inherit_option_id and view.inherit_option_id.id or False
154 view_obj.write(request.cr, request.uid, [view_id], {
156 }, context=request.context)
159 @website.route('/website/customize_template_get', type='json', auth='user')
160 def customize_template_get(self, xml_id, optional=True):
161 imd = request.registry['ir.model.data']
162 view_model, view_theme_id = imd.get_object_reference(
163 request.cr, request.uid, 'website', 'theme')
165 view = request.registry.get("ir.ui.view")
166 views = view._views_get(request.cr, request.uid, xml_id, request.context)
170 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
171 if v.inherit_option_id.id not in done:
173 'name': v.inherit_option_id.name,
178 done[v.inherit_option_id.id] = True
183 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
187 @website.route('/website/get_view_translations', type='json', auth='admin')
188 def get_view_translations(self, xml_id, lang=None):
189 lang = lang or request.context.get('lang')
190 views = self.customize_template_get(xml_id, optional=False)
191 views_ids = [view.get('id') for view in views if view.get('active')]
192 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
193 irt = request.registry.get('ir.translation')
194 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
196 @website.route('/website/set_translations', type='json', auth='admin')
197 def set_translations(self, data, lang):
198 irt = request.registry.get('ir.translation')
199 for view_id, trans in data.items():
200 view_id = int(view_id)
202 initial_content = t['initial_content'].strip()
203 new_content = t['new_content'].strip()
204 tid = t['translation_id']
206 old_trans = irt.search_read(
207 request.cr, request.uid,
209 ('type', '=', 'view'),
210 ('res_id', '=', view_id),
212 ('src', '=', initial_content),
215 tid = old_trans[0]['id']
217 vals = {'value': new_content}
218 irt.write(request.cr, request.uid, [tid], vals)
225 'source': initial_content,
226 'value': new_content,
228 irt.create(request.cr, request.uid, new_trans)
231 @website.route('/website/attach', type='http', auth='user')
232 def attach(self, func, upload):
233 req = request.httprequest
234 if req.method != 'POST':
235 return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
239 attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
240 'name': upload.filename,
241 'datas': upload.read().encode('base64'),
242 'datas_fname': upload.filename,
243 'res_model': 'ir.ui.view',
246 url = website.urlplus('/website/image', {
247 'model': 'ir.attachment',
250 'max_height': MAX_IMAGE_HEIGHT,
251 'max_width': MAX_IMAGE_WIDTH,
254 logger.exception("Failed to upload image to attachment")
257 return """<script type='text/javascript'>
258 window.parent['%s'](%s, %s);
259 </script>""" % (func, json.dumps(url), json.dumps(message))
261 @website.route(['/website/publish'], type='json', auth="public")
262 def publish(self, id, object):
264 _object = request.registry[object]
265 obj = _object.browse(request.cr, request.uid, _id)
268 if 'website_published' in _object._all_columns:
269 values['website_published'] = not obj.website_published
270 if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
271 values['website_published_datetime'] = fields.datetime.now()
272 _object.write(request.cr, request.uid, [_id],
273 values, context=request.context)
275 obj = _object.browse(request.cr, request.uid, _id)
276 return obj.website_published and True or False
278 @website.route(['/website/kanban/'], type='http', auth="public")
279 def kanban(self, **post):
280 return request.website.kanban_col(**post)
282 @website.route(['/robots.txt'], type='http', auth="public")
284 return request.website.render('website.robots', {'url_root': request.httprequest.url_root})
286 @website.route('/sitemap', type='http', auth='public', multilang=True)
287 def sitemap(self, **kwargs):
288 return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
290 @website.route('/sitemap.xml', type='http', auth="public")
291 def sitemap_xml(self):
292 body = request.website.render('website.sitemap_xml', {
293 'pages': request.website.list_pages()
296 return request.make_response(body, [
297 ('Content-Type', 'application/xml;charset=utf-8')
301 class Images(http.Controller):
302 def placeholder(self, response):
303 # file_open may return a StringIO. StringIO can be closed but are
304 # not context managers in Python 2 though that is fixed in 3
305 with contextlib.closing(openerp.tools.misc.file_open(
306 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
308 response.set_data(f.read())
309 return response.make_conditional(request.httprequest)
311 @website.route('/website/image', auth="public")
312 def image(self, model, id, field, max_width=maxint, max_height=maxint):
313 Model = request.registry[model]
315 response = werkzeug.wrappers.Response()
319 ids = Model.search(request.cr, request.uid,
320 [('id', '=', id)], context=request.context) \
321 or Model.search(request.cr, openerp.SUPERUSER_ID,
322 [('id', '=', id), ('website_published', '=', True)], context=request.context)
325 return self.placeholder(response)
327 concurrency = '__last_update'
328 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
329 [concurrency, field], context=request.context)
331 if concurrency in record:
332 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
334 response.last_modified = datetime.datetime.strptime(
335 record[concurrency], server_format + '.%f')
337 # just in case we have a timestamp without microseconds
338 response.last_modified = datetime.datetime.strptime(
339 record[concurrency], server_format)
341 # Field does not exist on model or field set to False
342 if not record.get(field):
343 # FIXME: maybe a field which does not exist should be a 404?
344 return self.placeholder(response)
346 response.set_etag(hashlib.sha1(record[field]).hexdigest())
347 response.make_conditional(request.httprequest)
349 # conditional request match
350 if response.status_code == 304:
353 data = record[field].decode('base64')
354 fit = int(max_width), int(max_height)
356 buf = cStringIO.StringIO(data)
358 image = Image.open(buf)
360 response.mimetype = Image.MIME[image.format]
365 if w < max_w and h < max_h:
366 response.set_data(data)
368 image.thumbnail(fit, Image.ANTIALIAS)
369 image.save(response.stream, image.format)
370 # invalidate content-length computed by make_conditional as writing
371 # to response.stream does not do it (as of werkzeug 0.9.3)
372 del response.headers['Content-Length']
377 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: