1 # -*- coding: utf-8 -*-
11 from sys import maxint
15 import werkzeug.exceptions
17 import werkzeug.wrappers
21 from slugify import slugify
23 def slugify(s, max_length=None):
24 spaceless = re.sub(r'\s+', '-', s)
25 specialless = re.sub(r'[^-_a-z0-9]', '', spaceless)
26 return specialless[:max_length]
29 from openerp.osv import fields
30 from openerp.addons.website.models import website
31 from openerp.addons.web import http
32 from openerp.addons.web.http import request
34 logger = logging.getLogger(__name__)
37 def auth_method_public():
38 registry = openerp.modules.registry.RegistryManager.get(request.db)
39 if not request.session.uid:
40 request.uid = registry['website'].get_public_user(request.cr, openerp.SUPERUSER_ID, request.context).id
42 request.uid = request.session.uid
43 http.auth_methods['public'] = auth_method_public
46 # Completely arbitrary limits
47 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
48 class Website(openerp.addons.web.controllers.main.Home):
49 @website.route('/', type='http', auth="public", multilang=True)
50 def index(self, **kw):
51 # TODO: check if plain SQL is needed
52 menu = request.registry['website.menu']
53 root_domain = [('parent_id', '=', False)] # TODO: multiwebsite ('website_id', '=', request.website.id),
54 root_id = menu.search(request.cr, request.uid, root_domain, limit=1, context=request.context)[0]
55 first_menu = menu.search_read(
56 request.cr, request.uid, [('parent_id', '=', root_id)], ['url'],
57 limit=1, order='sequence', context=request.context)
59 first_menu = first_menu[0]['url']
60 if first_menu and first_menu != '/':
61 return request.redirect(first_menu)
63 return self.page("website.homepage")
65 @website.route('/pagenew/<path:path>', type='http', auth="user")
66 def pagenew(self, path, noredirect=NOPE):
68 # completely arbitrary max_length
69 idname = slugify(path, max_length=50)
71 request.cr.execute('SAVEPOINT pagenew')
72 imd = request.registry['ir.model.data']
73 view = request.registry['ir.ui.view']
74 view_model, view_id = imd.get_object_reference(
75 request.cr, request.uid, 'website', 'default_page')
76 newview_id = view.copy(
77 request.cr, request.uid, view_id, context=request.context)
78 newview = view.browse(
79 request.cr, request.uid, newview_id, context=request.context)
81 'arch': newview.arch.replace("website.default_page",
82 "%s.%s" % (module, idname)),
86 # Fuck it, we're doing it live
88 imd.create(request.cr, request.uid, {
91 'model': 'ir.ui.view',
94 }, context=request.context)
95 except psycopg2.IntegrityError:
96 logger.exception('Unable to create ir_model_data for page %s', path)
97 request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
98 return werkzeug.exceptions.InternalServerError()
100 request.cr.execute('RELEASE SAVEPOINT pagenew')
102 url = "/page/%s" % idname
103 if noredirect is not NOPE:
104 return werkzeug.wrappers.Response(url, mimetype='text/plain')
105 return werkzeug.utils.redirect(url)
107 @website.route('/website/theme_change', type='http', auth="admin")
108 def theme_change(self, theme_id=False, **kwargs):
109 imd = request.registry['ir.model.data']
110 view = request.registry['ir.ui.view']
112 view_model, view_option_id = imd.get_object_reference(
113 request.cr, request.uid, 'website', 'theme')
115 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
116 context=request.context)
117 view.write(request.cr, request.uid, views, {'inherit_id': False},
118 context=request.context)
121 module, xml_id = theme_id.split('.')
122 view_model, view_id = imd.get_object_reference(
123 request.cr, request.uid, module, xml_id)
124 view.write(request.cr, request.uid, [view_id],
125 {'inherit_id': view_option_id}, context=request.context)
127 return request.website.render('website.themes', {'theme_changed': True})
129 @website.route(['/website/snippets'], type='json', auth="public")
131 return request.website.render('website.snippets')
133 @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
134 def page(self, path, **kwargs):
139 return request.website.render(path, values)
141 @website.route('/website/customize_template_toggle', type='json', auth='user')
142 def customize_template_set(self, view_id):
143 view_obj = request.registry.get("ir.ui.view")
144 view = view_obj.browse(request.cr, request.uid, int(view_id),
145 context=request.context)
149 value = view.inherit_option_id and view.inherit_option_id.id or False
150 view_obj.write(request.cr, request.uid, [view_id], {
152 }, context=request.context)
155 @website.route('/website/customize_template_get', type='json', auth='user')
156 def customize_template_get(self, xml_id, optional=True):
157 imd = request.registry['ir.model.data']
158 view_model, view_theme_id = imd.get_object_reference(
159 request.cr, request.uid, 'website', 'theme')
161 view = request.registry.get("ir.ui.view")
162 views = view._views_get(request.cr, request.uid, xml_id, request.context)
166 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
167 if v.inherit_option_id.id not in done:
169 'name': v.inherit_option_id.name,
174 done[v.inherit_option_id.id] = True
179 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
183 @website.route('/website/get_view_translations', type='json', auth='admin')
184 def get_view_translations(self, xml_id, lang=None):
185 lang = lang or request.context.get('lang')
186 views = self.customize_template_get(xml_id, optional=False)
187 views_ids = [view.get('id') for view in views if view.get('active')]
188 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
189 irt = request.registry.get('ir.translation')
190 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
192 @website.route('/website/set_translations', type='json', auth='admin')
193 def set_translations(self, data, lang):
194 irt = request.registry.get('ir.translation')
195 for view_id, trans in data.items():
196 view_id = int(view_id)
198 initial_content = t['initial_content'].strip()
199 new_content = t['new_content'].strip()
200 tid = t['translation_id']
202 old_trans = irt.search_read(
203 request.cr, request.uid,
205 ('type', '=', 'view'),
206 ('res_id', '=', view_id),
208 ('src', '=', initial_content),
211 tid = old_trans[0]['id']
213 vals = {'value': new_content}
214 irt.write(request.cr, request.uid, [tid], vals)
221 'source': initial_content,
222 'value': new_content,
224 irt.create(request.cr, request.uid, new_trans)
227 @website.route('/website/attach', type='http', auth='user')
228 def attach(self, func, upload):
229 req = request.httprequest
230 if req.method != 'POST':
231 return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
235 attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
236 'name': upload.filename,
237 'datas': upload.read().encode('base64'),
238 'datas_fname': upload.filename,
239 'res_model': 'ir.ui.view',
242 url = website.urlplus('/website/image', {
243 'model': 'ir.attachment',
246 'max_height': MAX_IMAGE_HEIGHT,
247 'max_width': MAX_IMAGE_WIDTH,
250 logger.exception("Failed to upload image to attachment")
253 return """<script type='text/javascript'>
254 window.parent['%s'](%s, %s);
255 </script>""" % (func, json.dumps(url), json.dumps(message))
257 @website.route(['/website/publish'], type='json', auth="public")
258 def publish(self, id, object):
260 _object = request.registry[object]
261 obj = _object.browse(request.cr, request.uid, _id)
264 if 'website_published' in _object._all_columns:
265 values['website_published'] = not obj.website_published
266 if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
267 values['website_published_datetime'] = fields.datetime.now()
268 _object.write(request.cr, request.uid, [_id],
269 values, context=request.context)
271 obj = _object.browse(request.cr, request.uid, _id)
272 return obj.website_published and True or False
274 @website.route(['/website/kanban/'], type='http', auth="public")
275 def kanban(self, **post):
276 return request.website.kanban_col(**post)
278 @website.route(['/robots.txt'], type='http', auth="public")
280 body = request.website.render('website.robots', {'url_root': request.httprequest.url_root})
281 return request.make_response(body, headers=[('Content-Type', 'text/plain')])
283 @website.route('/sitemap', type='http', auth='public', multilang=True)
284 def sitemap(self, **kwargs):
285 return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
287 @website.route('/sitemap.xml', type='http', auth="public")
288 def sitemap_xml(self):
289 body = request.website.render('website.sitemap_xml', {
290 'pages': request.website.list_pages()
293 return request.make_response(body, [
294 ('Content-Type', 'application/xml;charset=utf-8')
298 class Images(http.Controller):
299 def placeholder(self, response):
300 # file_open may return a StringIO. StringIO can be closed but are
301 # not context managers in Python 2 though that is fixed in 3
302 with contextlib.closing(openerp.tools.misc.file_open(
303 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
305 response.set_data(f.read())
306 return response.make_conditional(request.httprequest)
308 @website.route('/website/image', auth="public")
309 def image(self, model, id, field, max_width=maxint, max_height=maxint):
310 Model = request.registry[model]
312 response = werkzeug.wrappers.Response()
316 ids = Model.search(request.cr, request.uid,
317 [('id', '=', id)], context=request.context) \
318 or Model.search(request.cr, openerp.SUPERUSER_ID,
319 [('id', '=', id), ('website_published', '=', True)], context=request.context)
322 return self.placeholder(response)
324 concurrency = '__last_update'
325 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
326 [concurrency, field], context=request.context)
328 if concurrency in record:
329 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
331 response.last_modified = datetime.datetime.strptime(
332 record[concurrency], server_format + '.%f')
334 # just in case we have a timestamp without microseconds
335 response.last_modified = datetime.datetime.strptime(
336 record[concurrency], server_format)
338 # Field does not exist on model or field set to False
339 if not record.get(field):
340 # FIXME: maybe a field which does not exist should be a 404?
341 return self.placeholder(response)
343 response.set_etag(hashlib.sha1(record[field]).hexdigest())
344 response.make_conditional(request.httprequest)
346 # conditional request match
347 if response.status_code == 304:
350 data = record[field].decode('base64')
351 fit = int(max_width), int(max_height)
353 buf = cStringIO.StringIO(data)
355 image = Image.open(buf)
357 response.mimetype = Image.MIME[image.format]
362 if w < max_w and h < max_h:
363 response.set_data(data)
365 image.thumbnail(fit, Image.ANTIALIAS)
366 image.save(response.stream, image.format)
367 # invalidate content-length computed by make_conditional as writing
368 # to response.stream does not do it (as of werkzeug 0.9.3)
369 del response.headers['Content-Length']
374 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: