import os
import re
import simplejson
+import sys
import time
import urllib2
import zlib
import openerp
import openerp.modules.registry
+from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound
from openerp.tools.translate import _
from openerp import http
-from openerp.http import request, serialize_exception as _serialize_exception, LazyResponse
+from openerp.http import request, serialize_exception as _serialize_exception
_logger = logging.getLogger(__name__)
-env = jinja2.Environment(
- loader=jinja2.PackageLoader('openerp.addons.web', "views"),
- autoescape=True
-)
+if hasattr(sys, 'frozen'):
+ # When running on compiled windows binary, we don't have access to package loader.
+ path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views'))
+ loader = jinja2.FileSystemLoader(path)
+else:
+ loader = jinja2.PackageLoader('openerp.addons.web', "views")
+
+env = jinja2.Environment(loader=loader, autoescape=True)
env.filters["json"] = simplejson.dumps
#----------------------------------------------------------
# OpenERP Web helpers
#----------------------------------------------------------
-def rjsmin(script):
- """ Minify js with a clever regex.
- Taken from http://opensource.perlig.de/rjsmin
- Apache License, Version 2.0 """
- def subber(match):
- """ Substitution callback """
- groups = match.groups()
- return (
- groups[0] or
- groups[1] or
- groups[2] or
- groups[3] or
- (groups[4] and '\n') or
- (groups[5] and ' ') or
- (groups[6] and ' ') or
- (groups[7] and ' ') or
- ''
- )
-
- result = re.sub(
- r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
- r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
- r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
- r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
- r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
- r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
- r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
- r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
- r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
- r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
- r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
- r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
- r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
- r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
- r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
- r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
- r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
- r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
- r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
- r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
- r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
- ).strip()
- return result
-
db_list = http.db_list
db_monodb = http.db_monodb
"""
return http.redirect_with_hash(*args, **kw)
+def abort_and_redirect(url):
+ r = request.httprequest
+ response = werkzeug.utils.redirect(url, 302)
+ response = r.app.get_response(r, response, explicit_session=False)
+ werkzeug.exceptions.abort(response)
+
def ensure_db(redirect='/web/database/selector'):
# This helper should be used in web client auth="none" routes
# if those routes needs a db to work with.
url_redirect += '?' + r.query_string
response = werkzeug.utils.redirect(url_redirect, 302)
request.session.db = db
- response = r.app.get_response(r, response, explicit_session=False)
- werkzeug.exceptions.abort(response)
- return
+ abort_and_redirect(url_redirect)
# if db not provided, use the session one
if not db and http.db_filter([request.session.db]):
# always switch the session to the computed db
if db != request.session.db:
request.session.logout()
+ abort_and_redirect(request.httprequest.url)
request.session.db = db
root.append(child)
return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
-def concat_files(file_list, reader=None, intersperse=""):
- """ Concatenates contents of all provided files
-
- :param list(str) file_list: list of files to check
- :param function reader: reading procedure for each file
- :param str intersperse: string to intersperse between file contents
- :returns: (concatenation_result, checksum)
- :rtype: (str, str)
- """
- checksum = hashlib.new('sha1')
- if not file_list:
- return '', checksum.hexdigest()
-
- if reader is None:
- def reader(f):
- import codecs
- with codecs.open(f, 'rb', "utf-8-sig") as fp:
- return fp.read().encode("utf-8")
-
- files_content = []
- for fname in file_list:
- contents = reader(fname)
- checksum.update(contents)
- files_content.append(contents)
-
- files_concat = intersperse.join(files_content)
- return files_concat, checksum.hexdigest()
-
-concat_js_cache = {}
-
-def concat_js(file_list):
- content, checksum = concat_files(file_list, intersperse=';')
- if checksum in concat_js_cache:
- content = concat_js_cache[checksum]
- else:
- content = rjsmin(content)
- concat_js_cache[checksum] = content
- return content, checksum
-
def fs2web(path):
"""convert FS path into web path"""
return '/'.join(path.split(os.path.sep))
r.append((None, pattern))
else:
for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
- # Hack for IE, who limit 288Ko, 4095 rules, 31 sheets
- # http://support.microsoft.com/kb/262161/en
- if pattern == "static/lib/bootstrap/css/bootstrap.css":
- if include_remotes:
- r.insert(0, (None, fs2web(path[len(addons_path):])))
- else:
- r.append((path, fs2web(path[len(addons_path):])))
+ r.append((path, fs2web(path[len(addons_path):])))
return r
-def manifest_list(extension, mods=None, db=None, debug=False):
+def manifest_list(extension, mods=None, db=None, debug=None):
""" list ressources to load specifying either:
mods: a comma separated string listing modules
db: a database name (return all installed modules in that database)
"""
+ if debug is not None:
+ _logger.warning("openerp.addons.web.main.manifest_list(): debug parameter is deprecated")
files = manifest_glob(extension, addons=mods, db=db, include_remotes=True)
- if not debug:
- path = '/web/webclient/' + extension
- if mods is not None:
- path += '?' + werkzeug.url_encode({'mods': mods})
- elif db:
- path += '?' + werkzeug.url_encode({'db': db})
-
- remotes = [wp for fp, wp in files if fp is None]
- return [path] + remotes
return [wp for _fp, wp in files]
def get_last_modified(files):
for f in files)
return datetime.datetime(1970, 1, 1)
-def make_conditional(response, last_modified=None, etag=None):
+def make_conditional(response, last_modified=None, etag=None, max_age=0):
""" Makes the provided response conditional based upon the request,
and mandates revalidation from clients
:rtype: werkzeug.wrappers.Response
"""
response.cache_control.must_revalidate = True
- response.cache_control.max_age = 0
+ response.cache_control.max_age = max_age
if last_modified:
response.last_modified = last_modified
if etag:
#----------------------------------------------------------
# OpenERP Web web Controllers
#----------------------------------------------------------
-
-# TODO: to remove once the database manager has been migrated server side
-# and `edi` + `pos` addons has been adapted to use render_bootstrap_template()
-html_template = """<!DOCTYPE html>
-<html style="height: 100%%">
- <head>
- <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <title>OpenERP</title>
- <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
- <link rel="stylesheet" href="/web/static/src/css/full.css" />
- %(css)s
- %(js)s
- <script type="text/javascript">
- $(function() {
- var s = new openerp.init(%(modules)s);
- %(init)s
- });
- </script>
- </head>
- <body>
- <!--[if lte IE 8]>
- <script src="//ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
- <script>CFInstall.check({mode: "overlay"});</script>
- <![endif]-->
- </body>
-</html>
-"""
-
-def render_bootstrap_template(db, template, values=None, debug=False, lazy=False, **kw):
- if request and request.debug:
- debug = True
- if values is None:
- values = {}
- values.update(kw)
- values['debug'] = debug
- values['current_db'] = db
- try:
- values['databases'] = http.db_list()
- except openerp.exceptions.AccessDenied:
- values['databases'] = None
-
- for res in ['js', 'css']:
- if res not in values:
- values[res] = manifest_list(res, db=db, debug=debug)
-
- if 'modules' not in values:
- values['modules'] = module_boot(db=db)
- values['modules'] = simplejson.dumps(values['modules'])
-
- def callback(template, values):
- registry = openerp.modules.registry.RegistryManager.get(db)
- with registry.cursor() as cr:
- view_obj = registry["ir.ui.view"]
- uid = request.uid or openerp.SUPERUSER_ID
- return view_obj.render(cr, uid, template, values)
- if lazy:
- return LazyResponse(callback, template=template, values=values)
- else:
- return callback(template, values)
-
class Home(http.Controller):
@http.route('/', type='http', auth="none")
if request.session.uid:
if kw.get('redirect'):
return werkzeug.utils.redirect(kw.get('redirect'), 303)
-
- html = render_bootstrap_template(request.session.db, "web.webclient_bootstrap")
- return request.make_response(html, {'Cache-Control': 'no-cache', 'Content-Type': 'text/html; charset=utf-8'})
+ return request.render('web.webclient_bootstrap')
else:
return login_redirect()
if request.httprequest.method == 'GET' and redirect and request.session.uid:
return http.redirect_with_hash(redirect)
+ if not request.uid:
+ request.uid = openerp.SUPERUSER_ID
+
values = request.params.copy()
if not redirect:
redirect = '/web?' + request.httprequest.query_string
values['redirect'] = redirect
+
+ try:
+ values['databases'] = http.db_list()
+ except openerp.exceptions.AccessDenied:
+ values['databases'] = None
+
if request.httprequest.method == 'POST':
+ old_uid = request.uid
uid = request.session.authenticate(request.session.db, request.params['login'], request.params['password'])
if uid is not False:
return http.redirect_with_hash(redirect)
+ request.uid = old_uid
values['error'] = "Wrong login/password"
- return render_bootstrap_template(request.session.db, 'web.login', values, lazy=True)
+ return request.render('web.login', values)
@http.route('/login', type='http', auth="none")
def login(self, db, login, key, redirect="/web", **kw):
return werkzeug.utils.redirect('/', 303)
return login_and_redirect(db, login, key, redirect_url=redirect)
-class WebClient(http.Controller):
-
- @http.route('/web/webclient/csslist', type='json', auth="none")
- def csslist(self, mods=None):
- return manifest_list('css', mods=mods)
-
- @http.route('/web/webclient/jslist', type='json', auth="none")
- def jslist(self, mods=None):
- return manifest_list('js', mods=mods)
-
- @http.route('/web/webclient/qweblist', type='json', auth="none")
- def qweblist(self, mods=None):
- return manifest_list('qweb', mods=mods)
-
- @http.route('/web/webclient/css', type='http', auth="none")
- def css(self, mods=None, db=None):
- files = list(manifest_glob('css', addons=mods, db=db))
- last_modified = get_last_modified(f[0] for f in files)
- if request.httprequest.if_modified_since and request.httprequest.if_modified_since >= last_modified:
- return werkzeug.wrappers.Response(status=304)
-
- file_map = dict(files)
-
- rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
- rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
-
- def reader(f):
- """read the a css file and absolutify all relative uris"""
- with open(f, 'rb') as fp:
- data = fp.read().decode('utf-8')
-
- path = file_map[f]
- web_dir = os.path.dirname(path)
-
- data = re.sub(
- rx_import,
- r"""@import \1%s/""" % (web_dir,),
- data,
- )
-
- data = re.sub(
- rx_url,
- r"url(\1%s/" % (web_dir,),
- data,
- )
- return data.encode('utf-8')
+ @http.route('/web/js/<xmlid>', type='http', auth="public")
+ def js_bundle(self, xmlid, **kw):
+ # manifest backward compatible mode, to be removed
+ values = {'manifest_list': manifest_list}
+ try:
+ assets_html = request.render(xmlid, lazy=False, qcontext=values)
+ except QWebTemplateNotFound:
+ return request.not_found()
+ bundle = AssetsBundle(xmlid, assets_html, debug=request.debug)
- content, checksum = concat_files((f[0] for f in files), reader)
+ response = request.make_response(
+ bundle.js(), [('Content-Type', 'application/javascript')])
- # move up all @import and @charset rules to the top
- matches = []
- def push(matchobj):
- matches.append(matchobj.group(0))
- return ''
+ # TODO: check that we don't do weird lazy overriding of __call__ which break body-removal
+ return make_conditional(
+ response, bundle.last_modified, bundle.checksum, max_age=60*60*24)
- content = re.sub(re.compile("(@charset.+;$)", re.M), push, content)
- content = re.sub(re.compile("(@import.+;$)", re.M), push, content)
+ @http.route('/web/css/<xmlid>', type='http', auth='public')
+ def css_bundle(self, xmlid, **kw):
+ values = {'manifest_list': manifest_list} # manifest backward compatible mode, to be removed
+ try:
+ assets_html = request.render(xmlid, lazy=False, qcontext=values)
+ except QWebTemplateNotFound:
+ return request.not_found()
+ bundle = AssetsBundle(xmlid, assets_html, debug=request.debug)
- matches.append(content)
- content = '\n'.join(matches)
+ response = request.make_response(
+ bundle.css(), [('Content-Type', 'text/css')])
return make_conditional(
- request.make_response(content, [('Content-Type', 'text/css')]),
- last_modified, checksum)
+ response, bundle.last_modified, bundle.checksum, max_age=60*60*24)
- @http.route('/web/webclient/js', type='http', auth="none")
- def js(self, mods=None, db=None):
- files = [f[0] for f in manifest_glob('js', addons=mods, db=db)]
- last_modified = get_last_modified(files)
- if request.httprequest.if_modified_since and request.httprequest.if_modified_since >= last_modified:
- return werkzeug.wrappers.Response(status=304)
+class WebClient(http.Controller):
- content, checksum = concat_js(files)
+ @http.route('/web/webclient/csslist', type='json', auth="none")
+ def csslist(self, mods=None):
+ return manifest_list('css', mods=mods)
- return make_conditional(
- request.make_response(content, [('Content-Type', 'application/javascript')]),
- last_modified, checksum)
+ @http.route('/web/webclient/jslist', type='json', auth="none")
+ def jslist(self, mods=None):
+ return manifest_list('js', mods=mods)
@http.route('/web/webclient/qweb', type='http', auth="none")
def qweb(self, mods=None, db=None):
def version_info(self):
return openerp.service.common.exp_version()
+ @http.route('/web/tests', type='http', auth="none")
+ def index(self, mod=None, **kwargs):
+ return request.render('web.qunit_suite')
+
class Proxy(http.Controller):
@http.route('/web/proxy/load', type='json', auth="none")
def manager(self, **kw):
# TODO: migrate the webclient's database manager to server side views
request.session.logout()
- js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list('js', debug=request.debug))
- css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list('css', debug=request.debug))
-
- r = html_template % {
- 'js': js,
- 'css': css,
+ return env.get_template("database_manager.html").render({
'modules': simplejson.dumps(module_boot()),
- 'init': """
- var wc = new s.web.WebClient(null, { action: 'database_manager' });
- wc.appendTo($(document.body));
- """
- }
- return r
+ })
@http.route('/web/database/get_list', type='json', auth="none")
def get_list(self):
return simplejson.dumps([[],[{'error': openerp.tools.ustr(e), 'title': _('Backup Database')}]])
@http.route('/web/database/restore', type='http', auth="none")
- def restore(self, db_file, restore_pwd, new_db):
+ def restore(self, db_file, restore_pwd, new_db, mode):
try:
+ copy = mode == 'copy'
data = base64.b64encode(db_file.read())
- request.session.proxy("db").restore(restore_pwd, new_db, data)
+ request.session.proxy("db").restore(restore_pwd, new_db, data, copy)
return ''
except openerp.exceptions.AccessDenied, e:
raise Exception("AccessDenied")
"""
s = request.session
Menus = s.model('ir.ui.menu')
- # If a menu action is defined use its domain to get the root menu items
- user_menu_id = s.model('res.users').read([s.uid], ['menu_id'],
- request.context)[0]['menu_id']
-
menu_domain = [('parent_id', '=', False)]
- if user_menu_id:
- domain_string = s.model('ir.actions.act_window').read(
- [user_menu_id[0]], ['domain'],request.context)[0]['domain']
- if domain_string:
- menu_domain = ast.literal_eval(domain_string)
return Menus.search(menu_domain, 0, False, False, request.context)
for k, v in self.fields_info(model, export_fields).iteritems())
class ExportFormat(object):
+ raw_data = False
+
@property
def content_type(self):
""" Provides the format's content type """
ids = ids or Model.search(domain, 0, False, False, context)
field_names = map(operator.itemgetter('name'), fields)
- import_data = Model.export_data(ids, field_names, context).get('datas',[])
+ import_data = Model.export_data(ids, field_names, self.raw_data, context=context).get('datas',[])
if import_compat:
columns_headers = field_names
return data
class ExcelExport(ExportFormat, http.Controller):
+ # Excel needs raw data to correctly handle numbers and date values
+ raw_data = True
@http.route('/web/export/xls', type='http', auth="user")
@serialize_exception
worksheet.write(0, i, fieldname)
worksheet.col(i).width = 8000 # around 220 pixels
- style = xlwt.easyxf('align: wrap yes')
+ base_style = xlwt.easyxf('align: wrap yes')
+ date_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD')
+ datetime_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD HH:mm:SS')
for row_index, row in enumerate(rows):
for cell_index, cell_value in enumerate(row):
+ cell_style = base_style
if isinstance(cell_value, basestring):
cell_value = re.sub("\r", " ", cell_value)
- if cell_value is False: cell_value = None
- worksheet.write(row_index + 1, cell_index, cell_value, style)
+ elif isinstance(cell_value, datetime.datetime):
+ cell_style = datetime_style
+ elif isinstance(cell_value, datetime.date):
+ cell_style = date_style
+ worksheet.write(row_index + 1, cell_index, cell_value, cell_style)
fp = StringIO()
workbook.save(fp)