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 return self.page("website.homepage")
54 # FIXME: auth, if /pagenew known anybody can create new empty page
55 @website.route('/pagenew/<path:path>', type='http', auth="admin")
56 def pagenew(self, path, noredirect=NOPE):
58 # completely arbitrary max_length
59 idname = slugify(path, max_length=50)
61 request.cr.execute('SAVEPOINT pagenew')
62 imd = request.registry['ir.model.data']
63 view = request.registry['ir.ui.view']
64 view_model, view_id = imd.get_object_reference(
65 request.cr, request.uid, 'website', 'default_page')
66 newview_id = view.copy(
67 request.cr, request.uid, view_id, context=request.context)
68 newview = view.browse(
69 request.cr, request.uid, newview_id, context=request.context)
71 'arch': newview.arch.replace("website.default_page",
72 "%s.%s" % (module, idname)),
76 # Fuck it, we're doing it live
78 imd.create(request.cr, request.uid, {
81 'model': 'ir.ui.view',
84 }, context=request.context)
85 except psycopg2.IntegrityError:
86 logger.exception('Unable to create ir_model_data for page %s', path)
87 request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
88 return werkzeug.exceptions.InternalServerError()
90 request.cr.execute('RELEASE SAVEPOINT pagenew')
92 url = "/page/%s" % idname
93 if noredirect is not NOPE:
94 return werkzeug.wrappers.Response(url, mimetype='text/plain')
95 return werkzeug.utils.redirect(url)
97 @website.route('/website/theme_change', type='http', auth="admin")
98 def theme_change(self, theme_id=False, **kwargs):
99 imd = request.registry['ir.model.data']
100 view = request.registry['ir.ui.view']
102 view_model, view_option_id = imd.get_object_reference(
103 request.cr, request.uid, 'website', 'theme')
105 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
106 context=request.context)
107 view.write(request.cr, request.uid, views, {'inherit_id': False},
108 context=request.context)
111 module, xml_id = theme_id.split('.')
112 view_model, view_id = imd.get_object_reference(
113 request.cr, request.uid, module, xml_id)
114 view.write(request.cr, request.uid, [view_id],
115 {'inherit_id': view_option_id}, context=request.context)
117 return request.website.render('website.themes', {'theme_changed': True})
119 @website.route(['/website/snippets'], type='json', auth="public")
121 return request.website.render('website.snippets')
123 @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
124 def page(self, path, **kwargs):
129 html = request.website.render(path, values)
131 html = request.website.render('website.404', values)
134 @website.route('/website/customize_template_toggle', type='json', auth='admin') # FIXME: auth
135 def customize_template_set(self, view_id):
136 view_obj = request.registry.get("ir.ui.view")
137 view = view_obj.browse(request.cr, request.uid, int(view_id),
138 context=request.context)
142 value = view.inherit_option_id and view.inherit_option_id.id or False
143 view_obj.write(request.cr, request.uid, [view_id], {
145 }, context=request.context)
148 @website.route('/website/customize_template_get', type='json', auth='admin') # FIXME: auth
149 def customize_template_get(self, xml_id, optional=True):
150 imd = request.registry['ir.model.data']
151 view_model, view_theme_id = imd.get_object_reference(
152 request.cr, request.uid, 'website', 'theme')
154 view = request.registry.get("ir.ui.view")
155 views = view._views_get(request.cr, request.uid, xml_id, request.context)
159 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
160 if v.inherit_option_id.id not in done:
162 'name': v.inherit_option_id.name,
167 done[v.inherit_option_id.id] = True
172 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
176 @website.route('/website/get_view_translations', type='json', auth='admin')
177 def get_view_translations(self, xml_id, lang=None):
178 lang = lang or request.context.get('lang')
179 views = self.customize_template_get(xml_id, optional=False)
180 views_ids = [view.get('id') for view in views if view.get('active')]
181 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
182 irt = request.registry.get('ir.translation')
183 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
185 @website.route('/website/set_translations', type='json', auth='admin')
186 def set_translations(self, data, lang):
187 irt = request.registry.get('ir.translation')
188 for view_id, trans in data.items():
189 view_id = int(view_id)
191 initial_content = t['initial_content'].strip()
192 new_content = t['new_content'].strip()
193 tid = t['translation_id']
195 old_trans = irt.search_read(
196 request.cr, request.uid,
198 ('type', '=', 'view'),
199 ('res_id', '=', view_id),
201 ('src', '=', initial_content),
204 tid = old_trans[0]['id']
206 vals = {'value': new_content}
207 irt.write(request.cr, request.uid, [tid], vals)
214 'source': initial_content,
215 'value': new_content,
217 irt.create(request.cr, request.uid, new_trans)
218 irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate
221 @website.route('/website/attach', type='http', auth='user')
222 def attach(self, func, upload):
223 req = request.httprequest
224 if req.method != 'POST':
225 return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
229 attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
230 'name': upload.filename,
231 'datas': upload.read().encode('base64'),
232 'datas_fname': upload.filename,
233 'res_model': 'ir.ui.view',
236 url = website.urlplus('/website/image', {
237 'model': 'ir.attachment',
240 'max_height': MAX_IMAGE_HEIGHT,
241 'max_width': MAX_IMAGE_WIDTH,
244 logger.exception("Failed to upload image to attachment")
247 return """<script type='text/javascript'>
248 window.parent['%s'](%s, %s);
249 </script>""" % (func, json.dumps(url), json.dumps(message))
251 @website.route(['/website/publish'], type='json', auth="public")
252 def publish(self, id, object):
254 _object = request.registry[object]
255 obj = _object.browse(request.cr, request.uid, _id)
258 if 'website_published' in _object._all_columns:
259 values['website_published'] = not obj.website_published
260 if 'website_published_datetime' in _object._all_columns and values.get('website_published'):
261 values['website_published_datetime'] = fields.datetime.now()
262 _object.write(request.cr, request.uid, [_id],
263 values, context=request.context)
265 obj = _object.browse(request.cr, request.uid, _id)
266 return obj.website_published and True or False
268 @website.route(['/website/kanban/'], type='http', auth="public")
269 def kanban(self, **post):
270 return request.website.kanban_col(**post)
272 @website.route(['/robots.txt'], type='http', auth="public")
274 return request.website.render('website.robots', {'url_root': request.httprequest.url_root})
276 @website.route(['/sitemap.xml'], type='http', auth="public")
278 return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
280 class Images(http.Controller):
281 def placeholder(self, response):
282 # file_open may return a StringIO. StringIO can be closed but are
283 # not context managers in Python 2 though that is fixed in 3
284 with contextlib.closing(openerp.tools.misc.file_open(
285 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
287 response.set_data(f.read())
288 return response.make_conditional(request.httprequest)
290 @website.route('/website/image', auth="public")
291 def image(self, model, id, field, max_width=maxint, max_height=maxint):
292 Model = request.registry[model]
294 response = werkzeug.wrappers.Response()
298 ids = Model.search(request.cr, request.uid,
299 [('id', '=', id)], context=request.context) \
300 or Model.search(request.cr, openerp.SUPERUSER_ID,
301 [('id', '=', id), ('website_published', '=', True)], context=request.context)
304 return self.placeholder(response)
306 concurrency = '__last_update'
307 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
308 [concurrency, field], context=request.context)
310 if concurrency in record:
311 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
313 response.last_modified = datetime.datetime.strptime(
314 record[concurrency], server_format + '.%f')
316 # just in case we have a timestamp without microseconds
317 response.last_modified = datetime.datetime.strptime(
318 record[concurrency], server_format)
320 # Field does not exist on model or field set to False
321 if not record.get(field):
322 # FIXME: maybe a field which does not exist should be a 404?
323 return self.placeholder(response)
325 response.set_etag(hashlib.sha1(record[field]).hexdigest())
326 response.make_conditional(request.httprequest)
328 # conditional request match
329 if response.status_code == 304:
332 return self.set_image_data(
333 response, record[field].decode('base64'),
334 fit=(int(max_width), int(max_height)))
336 def set_image_data(self, response, data, fit=(maxint, maxint)):
337 """ Sets an inferred mime type on the response object, and puts the
338 provided image's data in it, possibly after resizing if requested
340 Returns the response object after setting its mime and content, so
341 the result of ``get_final_image`` can be returned directly.
343 buf = cStringIO.StringIO(data)
345 # FIXME: unknown format or not an image
346 image = Image.open(buf)
347 response.mimetype = Image.MIME[image.format]
352 if w < max_w and h < max_h:
353 response.set_data(data)
355 image.thumbnail(fit, Image.ANTIALIAS)
356 image.save(response.stream, image.format)
357 # invalidate content-length computed by make_conditional as writing
358 # to response.stream does not do it (as of werkzeug 0.9.3)
359 del response.headers['Content-Length']
364 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: