1 # -*- coding: utf-8 -*-
11 from sys import maxint
16 import werkzeug.exceptions
18 import werkzeug.wrappers
22 from openerp.addons.website.models import website
23 from openerp.addons.web import http
24 from openerp.addons.web.http import request
26 logger = logging.getLogger(__name__)
29 def auth_method_public():
30 registry = openerp.modules.registry.RegistryManager.get(request.db)
31 if not request.session.uid:
32 request.uid = registry['website'].get_public_user().id
34 request.uid = request.session.uid
35 http.auth_methods['public'] = auth_method_public
38 # PIL images have a type flag, but no MIME. Reverse type flag to MIME.
39 PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
40 # Completely arbitrary limits
41 MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
42 class Website(openerp.addons.web.controllers.main.Home):
43 @website.route('/', type='http', auth="public", multilang=True)
44 def index(self, **kw):
45 return self.page("website.homepage")
47 @http.route('/admin', type='http', auth="none")
48 def admin(self, *args, **kw):
49 return super(Website, self).index(*args, **kw)
51 # FIXME: auth, if /pagenew known anybody can create new empty page
52 @website.route('/pagenew/<path:path>', type='http', auth="admin")
53 def pagenew(self, path, noredirect=NOPE):
55 # completely arbitrary max_length
56 idname = slugify.slugify(path, max_length=50)
58 request.cr.execute('SAVEPOINT pagenew')
59 imd = request.registry['ir.model.data']
60 view = request.registry['ir.ui.view']
61 view_model, view_id = imd.get_object_reference(
62 request.cr, request.uid, 'website', 'default_page')
63 newview_id = view.copy(
64 request.cr, request.uid, view_id, context=request.context)
65 newview = view.browse(
66 request.cr, request.uid, newview_id, context=request.context)
68 'arch': newview.arch.replace("website.default_page",
69 "%s.%s" % (module, idname)),
73 # Fuck it, we're doing it live
75 imd.create(request.cr, request.uid, {
78 'model': 'ir.ui.view',
81 }, context=request.context)
82 except psycopg2.IntegrityError:
83 logger.exception('Unable to create ir_model_data for page %s', path)
84 request.cr.execute('ROLLBACK TO SAVEPOINT pagenew')
85 return werkzeug.exceptions.InternalServerError()
87 request.cr.execute('RELEASE SAVEPOINT pagenew')
89 url = "/page/%s" % idname
90 if noredirect is not NOPE:
91 return werkzeug.wrappers.Response(url, mimetype='text/plain')
92 return werkzeug.utils.redirect(url)
94 @website.route('/website/theme_change', type='http', auth="admin")
95 def theme_change(self, theme_id=False, **kwargs):
96 imd = request.registry['ir.model.data']
97 view = request.registry['ir.ui.view']
99 view_model, view_option_id = imd.get_object_reference(
100 request.cr, request.uid, 'website', 'theme')
102 request.cr, request.uid, [('inherit_id', '=', view_option_id)],
103 context=request.context)
104 view.write(request.cr, request.uid, views, {'inherit_id': False},
105 context=request.context)
108 module, xml_id = theme_id.split('.')
109 view_model, view_id = imd.get_object_reference(
110 request.cr, request.uid, module, xml_id)
111 view.write(request.cr, request.uid, [view_id],
112 {'inherit_id': view_option_id}, context=request.context)
114 return request.website.render('website.themes', {'theme_changed': True})
116 @website.route(['/website/snippets'], type='json', auth="public")
118 return request.website.render('website.snippets')
120 @website.route('/page/<path:path>', type='http', auth="public", multilang=True)
121 def page(self, path, **kwargs):
126 module, xmlid = path.split('.', 1)
127 IMD = request.registry.get("ir.model.data")
128 obj = IMD.get_object_reference(request.cr, request.uid, module, xmlid)
129 values['main_object'] = request.registry[obj[0]].browse(request.cr, request.uid, obj[1])
133 html = request.website.render(path, values)
135 html = request.website.render('website.404', values)
138 @website.route('/website/customize_template_toggle', type='json', auth='admin') # FIXME: auth
139 def customize_template_set(self, view_id):
140 view_obj = request.registry.get("ir.ui.view")
141 view = view_obj.browse(request.cr, request.uid, int(view_id),
142 context=request.context)
146 value = view.inherit_option_id and view.inherit_option_id.id or False
147 view_obj.write(request.cr, request.uid, [view_id], {
149 }, context=request.context)
152 @website.route('/website/customize_template_get', type='json', auth='admin') # FIXME: auth
153 def customize_template_get(self, xml_id, optional=True):
154 imd = request.registry['ir.model.data']
155 view_model, view_theme_id = imd.get_object_reference(
156 request.cr, request.uid, 'website', 'theme')
158 view = request.registry.get("ir.ui.view")
159 views = view._views_get(request.cr, request.uid, xml_id, request.context)
163 if v.inherit_option_id and v.inherit_option_id.id != view_theme_id or not optional:
164 if v.inherit_option_id.id not in done:
166 'name': v.inherit_option_id.name,
171 done[v.inherit_option_id.id] = True
176 'active': (v.inherit_id.id == v.inherit_option_id.id) or (not optional and v.inherit_id.id)
180 @website.route('/website/get_view_translations', type='json', auth='admin')
181 def get_view_translations(self, xml_id, lang=None):
182 lang = lang or request.context.get('lang')
183 views = self.customize_template_get(xml_id, optional=False)
184 views_ids = [view.get('id') for view in views if view.get('active')]
185 domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
186 irt = request.registry.get('ir.translation')
187 return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context)
189 @website.route('/website/set_translations', type='json', auth='admin')
190 def set_translations(self, data, lang):
191 irt = request.registry.get('ir.translation')
192 for view_id, trans in data.items():
193 view_id = int(view_id)
195 initial_content = t['initial_content'].strip()
196 new_content = t['new_content'].strip()
197 tid = t['translation_id']
199 old_trans = irt.search_read(
200 request.cr, request.uid,
202 ('type', '=', 'view'),
203 ('res_id', '=', view_id),
205 ('src', '=', initial_content),
208 tid = old_trans[0]['id']
210 vals = {'value': new_content}
211 irt.write(request.cr, request.uid, [tid], vals)
218 'source': initial_content,
219 'value': new_content,
221 irt.create(request.cr, request.uid, new_trans)
222 irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate
225 # # FIXME: auth, anybody can upload an attachment if URL known/found
226 @website.route('/website/attach', type='http', auth='admin')
227 def attach(self, func, upload):
228 req = request.httprequest
229 if req.method != 'POST':
230 return werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
234 attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
235 'name': upload.filename,
236 'datas': base64.encodestring(upload.read()),
237 'datas_fname': upload.filename,
238 'res_model': 'ir.ui.view',
240 # FIXME: auth=user... no good.
241 url = '/website/attachment/%d' % attachment_id
243 logger.exception("Failed to upload image to attachment")
246 return """<script type='text/javascript'>
247 window.parent['%s'](%s, %s);
248 </script>""" % (func, json.dumps(url), json.dumps(message))
250 @website.route(['/website/publish'], type='json', auth="public")
251 def publish(self, id, object):
253 _object = request.registry[object]
255 obj = _object.browse(request.cr, request.uid, _id)
256 _object.write(request.cr, request.uid, [_id],
257 {'website_published': not obj.website_published},
258 context=request.context)
259 obj = _object.browse(request.cr, request.uid, _id)
260 return obj.website_published and True or False
262 @website.route(['/website/kanban/'], type='http', auth="public")
263 def kanban(self, **post):
264 return request.website.kanban_col(**post)
266 @website.route(['/robots.txt'], type='http', auth="public")
268 return request.website.render('website.robots', {'url_root': request.httprequest.url_root})
270 @website.route(['/sitemap.xml'], type='http', auth="public")
272 return request.website.render('website.sitemap', {'pages': request.website.list_pages()})
274 class Images(http.Controller):
275 @website.route('/website/image', auth="public")
276 def image(self, model, id, field):
277 Model = request.registry[model]
279 response = werkzeug.wrappers.Response()
283 ids = Model.search(request.cr, request.uid,
284 [('id', '=', id)], context=request.context)\
285 or Model.search(request.cr, openerp.SUPERUSER_ID,
286 [('id', '=', id), ('website_published', '=', True)], context=request.context)
289 # file_open may return a StringIO. StringIO can be closed but are
290 # not context managers in Python 2 though that is fixed in 3
291 with contextlib.closing(openerp.tools.misc.file_open(
292 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
294 response.set_data(f.read())
297 concurrency = '__last_update'
298 [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id],
299 [concurrency, field], context=request.context)
301 if concurrency in record:
302 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
304 response.last_modified = datetime.datetime.strptime(
305 record[concurrency], server_format + '.%f')
307 # just in case we have a timestamp without microseconds
308 response.last_modified = datetime.datetime.strptime(
309 record[concurrency], server_format)
310 # FIXME: no field in record?
311 if not field in record or not record[field]:
314 response.set_etag(hashlib.sha1(record[field]).hexdigest())
315 response.make_conditional(request.httprequest)
317 # conditional request match
318 if response.status_code == 304:
321 return self.set_image_data(response, record[field].decode('base64'))
324 # FIXME: delegate to image?
325 @website.route('/website/attachment/<int:id>', auth='admin')
326 def attachment(self, id):
327 attachment = request.registry['ir.attachment'].browse(
328 request.cr, request.uid, id, request.context)
330 return self.set_image_data(
331 werkzeug.wrappers.Response(),
332 attachment.datas.decode('base64'),
335 def set_image_data(self, response, data, fit=(maxint, maxint)):
336 """ Sets an inferred mime type on the response object, and puts the
337 provided image's data in it, possibly after resizing if requested
339 Returns the response object after setting its mime and content, so
340 the result of ``get_final_image`` can be returned directly.
342 buf = cStringIO.StringIO(data)
344 # FIXME: unknown format or not an image
345 image = Image.open(buf)
346 response.mimetype = PIL_MIME_MAPPING[image.format]
351 if w < max_w and h < max_h:
352 response.set_data(data)
355 image.thumbnail(fit, Image.ANTIALIAS)
356 image.save(response.stream, image.format)
360 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: