diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py
index bc6bc141863..36dd21901a9 100644
--- a/addons/base/controllers/main.py
+++ b/addons/base/controllers/main.py
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
-import base64
-import glob, os
-import pprint
+import base64, glob, os, re
from xml.etree import ElementTree
from cStringIO import StringIO
@@ -56,6 +54,20 @@ class Xml2Json:
# OpenERP Web base Controllers
#----------------------------------------------------------
+class Database(openerpweb.Controller):
+ _cp_path = "/base/database"
+
+ @openerpweb.jsonrequest
+ def get_databases_list(self, req):
+ proxy = req.session.proxy("db")
+ dbs = proxy.list()
+ h = req.httprequest.headers['Host'].split(':')[0]
+ d = h.split('.')[0]
+ r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d)
+ print "h,d",h,d,r
+ dbs = [i for i in dbs if re.match(r,i)]
+ return {"db_list": dbs}
+
class Session(openerpweb.Controller):
_cp_path = "/base/session"
@@ -104,13 +116,6 @@ class Session(openerpweb.Controller):
return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
req.session.eval_context(req.context))
- @openerpweb.jsonrequest
- def get_databases_list(self, req):
- proxy = req.session.proxy("db")
- dbs = proxy.list()
-
- return {"db_list": dbs}
-
@openerpweb.jsonrequest
def modules(self, req):
return {"modules": [name
@@ -125,14 +130,14 @@ class Session(openerpweb.Controller):
def jslist(self, req, mods='base'):
return {'files': self.manifest_glob(mods.split(','), 'js')}
- def css(self, req, mods='base,base_hello'):
+ def css(self, req, mods='base'):
files = self.manifest_glob(mods.split(','), 'css')
concat = self.concat_files(files)[0]
# TODO request set the Date of last modif and Etag
return concat
css.exposed = True
- def js(self, req, mods='base,base_hello'):
+ def js(self, req, mods='base'):
files = self.manifest_glob(mods.split(','), 'js')
concat = self.concat_files(files)[0]
# TODO request set the Date of last modif and Etag
@@ -171,8 +176,9 @@ class Session(openerpweb.Controller):
a list of fields to group by, potentially empty (in which case
no group by should be performed)
"""
- context = req.session.eval_context(openerpweb.nonliterals.CompoundContext(*contexts))
- domain = req.session.eval_domain(openerpweb.nonliterals.CompoundDomain(*(domains or [])), context)
+ context, domain = eval_context_and_domain(req.session,
+ openerpweb.nonliterals.CompoundContext(*(contexts or [])),
+ openerpweb.nonliterals.CompoundDomain(*(domains or [])))
group_by_sequence = []
for candidate in (group_by_seq or []):
@@ -230,40 +236,77 @@ class Session(openerpweb.Controller):
if not saved_actions:
return None
return saved_actions["actions"].get(key)
-
+
def eval_context_and_domain(session, context, domain=None):
e_context = session.eval_context(context)
- e_domain = session.eval_domain(domain or [], e_context)
+ # should we give the evaluated context as an evaluation context to the domain?
+ e_domain = session.eval_domain(domain or [])
+
+ return e_context, e_domain
- return (e_context, e_domain)
-
def load_actions_from_ir_values(req, key, key2, models, meta, context):
- context['bin_size'] = False # Possible upstream bug. Antony says not to loose time on this.
Values = req.session.model('ir.values')
actions = Values.get(key, key2, models, meta, context)
- for _, _, action in actions:
- clean_action(action, req.session)
-
- return actions
+ return [(id, name, clean_action(action, req.session))
+ for id, name, action in actions]
def clean_action(action, session):
+ if action['type'] != 'ir.actions.act_window':
+ return action
# values come from the server, we can just eval them
- if isinstance(action['context'], basestring):
+ if isinstance(action.get('context', None), basestring):
action['context'] = eval(
action['context'],
session.evaluation_context()) or {}
- if isinstance(action['domain'], basestring):
+ if isinstance(action.get('domain', None), basestring):
action['domain'] = eval(
action['domain'],
session.evaluation_context(
action['context'])) or []
- if not action.has_key('flags'):
+ if 'flags' not in action:
# Set empty flags dictionary for web client.
action['flags'] = dict()
return fix_view_modes(action)
+def generate_views(action):
+ """
+ While the server generates a sequence called "views" computing dependencies
+ between a bunch of stuff for views coming directly from the database
+ (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
+ to return custom view dictionaries generated on the fly.
+
+ In that case, there is no ``views`` key available on the action.
+
+ Since the web client relies on ``action['views']``, generate it here from
+ ``view_mode`` and ``view_id``.
+
+ Currently handles two different cases:
+
+ * no view_id, multiple view_mode
+ * single view_id, single view_mode
+
+ :param dict action: action descriptor dictionary to generate a views key for
+ """
+ view_id = action.get('view_id', False)
+ if isinstance(view_id, (list, tuple)):
+ view_id = view_id[0]
+
+ # providing at least one view mode is a requirement, not an option
+ view_modes = action['view_mode'].split(',')
+
+ if len(view_modes) > 1:
+ if view_id:
+ raise ValueError('Non-db action dictionaries should provide '
+ 'either multiple view modes or a single view '
+ 'mode and an optional view id.\n\n Got view '
+ 'modes %r and view id %r for action %r' % (
+ view_modes, view_id, action))
+ action['views'] = [(False, mode) for mode in view_modes]
+ return
+ action['views'] = [(view_id, view_modes[0])]
+
def fix_view_modes(action):
""" For historical reasons, OpenERP has weird dealings in relation to
view_mode and the view_type attribute (on window actions):
@@ -282,16 +325,17 @@ def fix_view_modes(action):
:param dict action: an action descriptor
:returns: nothing, the action is modified in place
"""
- if action.pop('view_type') != 'form':
- return
+ if 'views' not in action:
+ generate_views(action)
+
+ if action.pop('view_type') != 'form':
+ return action
- action['view_mode'] = ','.join(
- mode if mode != 'tree' else 'list'
- for mode in action['view_mode'].split(','))
action['views'] = [
[id, mode if mode != 'tree' else 'list']
for id, mode in action['views']
]
+
return action
class Menu(openerpweb.Controller):
@@ -352,9 +396,10 @@ class DataSet(openerpweb.Controller):
req.session.eval_context(req.context))}
@openerpweb.jsonrequest
- def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
- return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
- def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
+ def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
+ return self.do_search_read(request, model, fields, offset, limit, domain, sort)
+ def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
+ , sort=None):
""" Performs a search() followed by a read() (if needed) using the
provided search criteria
@@ -367,22 +412,33 @@ class DataSet(openerpweb.Controller):
:param int limit: the maximum number of records to return
:param list domain: the search domain for the query
:param list sort: sorting directives
- :returns: a list of result records
+ :returns: A structure (dict) with two keys: ids (all the ids matching
+ the (domain, context) pair) and records (paginated records
+ matching fields selection set)
:rtype: list
"""
Model = request.session.model(model)
- context, domain = eval_context_and_domain(request.session, request.context, domain)
-
- ids = Model.search(domain, offset or 0, limit or False,
- sort or False, context)
+ context, domain = eval_context_and_domain(
+ request.session, request.context, domain)
+ ids = Model.search(domain, 0, False, sort or False, context)
+ # need to fill the dataset with all ids for the (domain, context) pair,
+ # so search un-paginated and paginate manually before reading
+ paginated_ids = ids[offset:(offset + limit if limit else None)]
if fields and fields == ['id']:
# shortcut read if we only want the ids
- return map(lambda id: {'id': id}, ids)
+ return {
+ 'ids': ids,
+ 'records': map(lambda id: {'id': id}, paginated_ids)
+ }
+
+ records = Model.read(paginated_ids, fields or False, context)
+ records.sort(key=lambda obj: ids.index(obj['id']))
+ return {
+ 'ids': ids,
+ 'records': records
+ }
- reads = Model.read(ids, fields or False, context)
- reads.sort(key=lambda obj: ids.index(obj['id']))
- return reads
@openerpweb.jsonrequest
def get(self, request, model, ids, fields=False):
@@ -435,23 +491,31 @@ class DataSet(openerpweb.Controller):
return {'result': r}
@openerpweb.jsonrequest
- def unlink(self, request, model, ids=[]):
+ def unlink(self, request, model, ids=()):
Model = request.session.model(model)
return Model.unlink(ids, request.session.eval_context(request.context))
- @openerpweb.jsonrequest
- def call(self, req, model, method, args, domain_id=None, context_id=None):
+ def call_common(self, req, model, method, args, domain_id=None, context_id=None):
domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
- c, d = eval_context_and_domain(req.session, context, domain);
- if(domain_id and len(args) - 1 >= domain_id):
+ c, d = eval_context_and_domain(req.session, context, domain)
+ if domain_id and len(args) - 1 >= domain_id:
args[domain_id] = d
- if(context_id and len(args) - 1 >= context_id):
+ if context_id and len(args) - 1 >= context_id:
args[context_id] = c
-
- m = req.session.model(model)
- r = getattr(m, method)(*args)
- return {'result': r}
+
+ return getattr(req.session.model(model), method)(*args)
+
+ @openerpweb.jsonrequest
+ def call(self, req, model, method, args, domain_id=None, context_id=None):
+ return self.call_common(req, model, method, args, domain_id, context_id)
+
+ @openerpweb.jsonrequest
+ def call_button(self, req, model, method, args, domain_id=None, context_id=None):
+ action = self.call_common(req, model, method, args, domain_id, context_id)
+ if isinstance(action, dict) and action.get('type') != '':
+ return {'result': clean_action(action, req.session)}
+ return {'result': False}
@openerpweb.jsonrequest
def exec_workflow(self, req, model, id, signal):
@@ -460,20 +524,19 @@ class DataSet(openerpweb.Controller):
@openerpweb.jsonrequest
def default_get(self, req, model, fields):
- m = req.session.model(model)
- r = m.default_get(fields, req.session.eval_context(req.context))
- return {'result': r}
+ Model = req.session.model(model)
+ return Model.default_get(fields, req.session.eval_context(req.context))
class DataGroup(openerpweb.Controller):
_cp_path = "/base/group"
@openerpweb.jsonrequest
- def read(self, request, model, group_by_fields, domain=None):
+ def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
Model = request.session.model(model)
context, domain = eval_context_and_domain(request.session, request.context, domain)
return Model.read_group(
- domain or [], False, group_by_fields, 0, False,
- dict(context, group_by=group_by_fields))
+ domain or [], fields, group_by_fields, 0, False,
+ dict(context, group_by=group_by_fields), sort or False)
class View(openerpweb.Controller):
_cp_path = "/base/view"
@@ -486,18 +549,35 @@ class View(openerpweb.Controller):
# todo fme?: check that we should pass the evaluated context here
self.process_view(request.session, fvg, context, transform)
return fvg
-
+
def process_view(self, session, fvg, context, transform):
+ # depending on how it feels, xmlrpclib.ServerProxy can translate
+ # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
+ # enjoy unicode strings which can not be trivially converted to
+ # strings, and it blows up during parsing.
+
+ # So ensure we fix this retardation by converting view xml back to
+ # bit strings.
+ if isinstance(fvg['arch'], unicode):
+ arch = fvg['arch'].encode('utf-8')
+ else:
+ arch = fvg['arch']
+
if transform:
evaluation_context = session.evaluation_context(context or {})
- xml = self.transform_view(fvg['arch'], session, evaluation_context)
+ xml = self.transform_view(arch, session, evaluation_context)
else:
- xml = ElementTree.fromstring(fvg['arch'])
+ xml = ElementTree.fromstring(arch)
fvg['arch'] = Xml2Json.convert_element(xml)
- for field in fvg['fields'].values():
- if field.has_key('views') and field['views']:
- for view in field["views"].values():
+
+ for field in fvg['fields'].itervalues():
+ if field.get('views'):
+ for view in field["views"].itervalues():
self.process_view(session, view, None, transform)
+ if field.get('domain'):
+ field["domain"] = self.parse_domain(field["domain"], session)
+ if field.get('context'):
+ field["context"] = self.parse_context(field["context"], session)
@openerpweb.jsonrequest
def add_custom(self, request, view_id, arch):
@@ -523,36 +603,6 @@ class View(openerpweb.Controller):
return {'result': True}
return {'result': False}
- def normalize_attrs(self, elem, context):
- """ Normalize @attrs, @invisible, @required, @readonly and @states, so
- the client only has to deal with @attrs.
-
- See `the discoveries pad `_ for
- the rationale.
-
- :param elem: the current view node (Python object)
- :type elem: xml.etree.ElementTree.Element
- :param dict context: evaluation context
- """
- # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
- attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
- if 'states' in elem.attrib:
- attrs.setdefault('invisible', [])\
- .append(('state', 'not in', elem.attrib.pop('states').split(',')))
- if attrs:
- elem.set('attrs', simplejson.dumps(attrs))
- for a in ['invisible', 'readonly', 'required']:
- if a in elem.attrib:
- # In the XML we trust
- avalue = bool(eval(elem.get(a, 'False'),
- {'context': context or {}}))
- if not avalue:
- del elem.attrib[a]
- else:
- elem.attrib[a] = '1'
- if a == 'invisible' and 'attrs' in elem.attrib:
- del elem.attrib['attrs']
-
def transform_view(self, view_string, session, context=None):
# transform nodes on the fly via iterparse, instead of
# doing it statically on the parsing result
@@ -562,31 +612,41 @@ class View(openerpweb.Controller):
if event == "start":
if root is None:
root = elem
- self.normalize_attrs(elem, context)
self.parse_domains_and_contexts(elem, session)
return root
- def parse_domain(self, elem, attr_name, session):
- """ Parses an attribute of the provided name as a domain, transforms it
+ def parse_domain(self, domain, session):
+ """ Parses an arbitrary string containing a domain, transforms it
to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
- :param elem: the node being parsed
- :type param: xml.etree.ElementTree.Element
- :param str attr_name: the name of the attribute which should be parsed
+ :param domain: the domain to parse, if the domain is not a string it is assumed to
+ be a literal domain and is returned as-is
:param session: Current OpenERP session
:type session: openerpweb.openerpweb.OpenERPSession
"""
- domain = elem.get(attr_name, '').strip()
- if domain:
- try:
- elem.set(
- attr_name,
- openerpweb.ast.literal_eval(
- domain))
- except ValueError:
- # not a literal
- elem.set(attr_name,
- openerpweb.nonliterals.Domain(session, domain))
+ if not isinstance(domain, (str, unicode)):
+ return domain
+ try:
+ return openerpweb.ast.literal_eval(domain)
+ except ValueError:
+ # not a literal
+ return openerpweb.nonliterals.Domain(session, domain)
+
+ def parse_context(self, context, session):
+ """ Parses an arbitrary string containing a context, transforms it
+ to either a literal context or a :class:`openerpweb.nonliterals.Context`
+
+ :param context: the context to parse, if the context is not a string it is assumed to
+ be a literal domain and is returned as-is
+ :param session: Current OpenERP session
+ :type session: openerpweb.openerpweb.OpenERPSession
+ """
+ if not isinstance(context, (str, unicode)):
+ return context
+ try:
+ return openerpweb.ast.literal_eval(context)
+ except ValueError:
+ return openerpweb.nonliterals.Context(session, context)
def parse_domains_and_contexts(self, elem, session):
""" Converts domains and contexts from the view into Python objects,
@@ -599,17 +659,14 @@ class View(openerpweb.Controller):
non-literal objects
:type session: openerpweb.openerpweb.OpenERPSession
"""
- self.parse_domain(elem, 'domain', session)
- self.parse_domain(elem, 'filter_domain', session)
- context_string = elem.get('context', '').strip()
- if context_string:
- try:
- elem.set('context',
- openerpweb.ast.literal_eval(context_string))
- except ValueError:
- elem.set('context',
- openerpweb.nonliterals.Context(
- session, context_string))
+ for el in ['domain', 'filter_domain']:
+ domain = elem.get(el, '').strip()
+ if domain:
+ elem.set(el, self.parse_domain(domain, session))
+ for el in ['context', 'default_get']:
+ context_string = elem.get(el, '').strip()
+ if context_string:
+ elem.set(el, self.parse_context(context_string, session))
class FormView(View):
_cp_path = "/base/formview"
@@ -657,6 +714,12 @@ class SearchView(View):
def fields_get(self, req, model):
Model = req.session.model(model)
fields = Model.fields_get(False, req.session.eval_context(req.context))
+ for field in fields.values():
+ # shouldn't convert the views too?
+ if field.get('domain'):
+ field["domain"] = self.parse_domain(field["domain"], req.session)
+ if field.get('context'):
+ field["context"] = self.parse_domain(field["context"], req.session)
return {'fields': fields}
class Binary(openerpweb.Controller):
@@ -701,7 +764,7 @@ class Binary(openerpweb.Controller):
for key, val in cherrypy.request.headers.iteritems():
headers[key.lower()] = val
size = int(headers.get('content-length', 0))
- # TODO: might be usefull to have a configuration flag for max-lenght file uploads
+ # TODO: might be useful to have a configuration flag for max-length file uploads
try:
out = """
+
+If you run into trouble, please look at the included example code to see how
+to best implement this
\ No newline at end of file
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/codebase/thirdparty/excanvas/excanvas.js b/addons/base_graph/static/lib/dhtmlxGraph/codebase/thirdparty/excanvas/excanvas.js
new file mode 100644
index 00000000000..3fe46d3336f
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/codebase/thirdparty/excanvas/excanvas.js
@@ -0,0 +1,927 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+// different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+// width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+// Quirks mode will draw the canvas using border-box. Either change your
+// doctype to HTML5
+// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+// or use Box Sizing Behavior from WebFX
+// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+ // alias some functions to make (compiled) code shorter
+ var m = Math;
+ var mr = m.round;
+ var ms = m.sin;
+ var mc = m.cos;
+ var abs = m.abs;
+ var sqrt = m.sqrt;
+
+ // this is used for sub pixel precision
+ var Z = 10;
+ var Z2 = Z / 2;
+
+ /**
+ * This funtion is assigned to the
+
+
+
+
+
+
+
+
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/01_dhtmlxgrid.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/01_dhtmlxgrid.html
new file mode 100644
index 00000000000..698da15d9fd
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/01_dhtmlxgrid.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+ Integration with grid
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/02_dhtmlxgrid_group.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/02_dhtmlxgrid_group.html
new file mode 100644
index 00000000000..d911e209922
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/02_dhtmlxgrid_group.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+ Integration with Grid and Grouping
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/03_windows.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/03_windows.html
new file mode 100644
index 00000000000..822e621d9ca
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/09_integration/03_windows.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+ Integration with dhtmlxWindows
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/common/config.php b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/config.php
new file mode 100644
index 00000000000..3695ac3905a
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/config.php
@@ -0,0 +1,13 @@
+
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/common/data.php b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/data.php
new file mode 100644
index 00000000000..ac8f5116a9d
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/data.php
@@ -0,0 +1,16 @@
+";
+
+for ($i=1; $i <= 100; $i++) {
+ $sales = rand(100,1000);
+ $year = rand(1996,2009);
+ $company = "Company ".rand(1,3);
+ $str.="";
+}
+
+$str .= "";
+
+file_put_contents("stat.xml",$str);
+
+?>
\ No newline at end of file
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/common/sales.xml b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/sales.xml
new file mode 100644
index 00000000000..03c153cb1e5
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/sales.xml
@@ -0,0 +1,46 @@
+
+
+ 2.9
+ 2000
+
+
+ 3.5
+ 2001
+
+
+ 3.1
+ 2002
+
+
+ 4.2
+ 2003
+
+
+ 4.5
+ 2004
+
+
+ 7.4
+ 2005
+
+
+ 9.6
+ 2006
+
+
+ 9
+ 2007
+
+
+ 7.3
+ 2008
+
+
+ 5.8
+ 2009
+
+
+ 7.7
+ 2010
+
+
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/common/stat.xml b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/stat.xml
new file mode 100644
index 00000000000..824ff6f6829
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/common/stat.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/readme.txt b/addons/base_graph/static/lib/dhtmlxGraph/samples/readme.txt
new file mode 100644
index 00000000000..c1f4b60567c
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/readme.txt
@@ -0,0 +1,7 @@
+Next samples
+ 08_integration\01_dhtmlxgrid.html
+ 08_integration\02_dhtmlxgrid_group.html
+ 08_integration\03_layout.html
+requires external components ( dhtmlxgrid and dhtmlxwindow )
+
+Its recommended to run samples through any kind of webserver.
diff --git a/addons/base_graph/static/lib/dhtmlxGraph/sources/dhtmlxchart.js b/addons/base_graph/static/lib/dhtmlxGraph/sources/dhtmlxchart.js
new file mode 100644
index 00000000000..c0c4838a0c0
--- /dev/null
+++ b/addons/base_graph/static/lib/dhtmlxGraph/sources/dhtmlxchart.js
@@ -0,0 +1,3499 @@
+/*
+2010 July 02
+*/
+
+
+
+/* DHX DEPEND FROM FILE 'assert.js'*/
+
+
+if (!window.dhtmlx)
+ dhtmlx={};
+
+//check some rule, show message as error if rule is not correct
+dhtmlx.assert = function(test, message){
+ if (!test) dhtmlx.error(message);
+};
+dhtmlx.assert_enabled=function(){ return false; };
+
+//register names of event, which can be triggered by the object
+dhtmlx.assert_event = function(obj, evs){
+ if (!obj._event_check){
+ obj._event_check = {};
+ obj._event_check_size = {};
+ }
+
+ for (var a in evs){
+ obj._event_check[a.toLowerCase()]=evs[a];
+ var count=-1; for (var t in evs[a]) count++;
+ obj._event_check_size[a.toLowerCase()]=count;
+ }
+};
+dhtmlx.assert_method_info=function(obj, name, descr, rules){
+ var args = [];
+ for (var i=0; i < rules.length; i++) {
+ args.push(rules[i][0]+" : "+rules[i][1]+"\n "+rules[i][2].describe()+(rules[i][3]?"; optional":""));
+ }
+ return obj.name+"."+name+"\n"+descr+"\n Arguments:\n - "+args.join("\n - ");
+};
+dhtmlx.assert_method = function(obj, config){
+ for (var key in config)
+ dhtmlx.assert_method_process(obj, key, config[key].descr, config[key].args, (config[key].min||99), config[key].skip);
+};
+dhtmlx.assert_method_process = function (obj, name, descr, rules, min, skip){
+ var old = obj[name];
+ if (!skip)
+ obj[name] = function(){
+ if (arguments.length != rules.length && arguments.length < min)
+ dhtmlx.log("warn","Incorrect count of parameters\n"+obj[name].describe()+"\n\nExpecting "+rules.length+" but have only "+arguments.length);
+ else
+ for (var i=0; i= 0) return true;
+ return false;
+ };
+ dhtmlx.assert_rule_dimension.describe=function(){
+ return "{Integer} value must be a positive number";
+ };
+
+ dhtmlx.assert_rule_number=function(check){
+ if (typeof check == "number") return true;
+ return false;
+ };
+ dhtmlx.assert_rule_number.describe=function(){
+ return "{Integer} value must be a number";
+ };
+
+ dhtmlx.assert_rule_function=function(check){
+ if (typeof check == "function") return true;
+ return false;
+ };
+ dhtmlx.assert_rule_function.describe=function(){
+ return "{Function} value must be a custom function";
+ };
+
+ dhtmlx.assert_rule_any=function(check){
+ return true;
+ };
+ dhtmlx.assert_rule_any.describe=function(){
+ return "Any value";
+ };
+
+ dhtmlx.assert_rule_mix=function(a,b){
+ var t = function(check){
+ if (a(check)||b(check)) return true;
+ return false;
+ };
+ t.describe = function(){
+ return a.describe();
+ };
+ return t;
+ };
+
+}
+
+
+/* DHX DEPEND FROM FILE 'dhtmlx.js'*/
+
+
+/*DHX:Depend assert.js*/
+
+/*
+ Common helpers
+*/
+dhtmlx.version="3.0";
+dhtmlx.codebase="./";
+
+//coding helpers
+
+//copies methods and properties from source to the target
+dhtmlx.extend = function(target, source){
+ for (var method in source)
+ target[method] = source[method];
+
+ //applying asserts
+ if (dhtmlx.assert_enabled() && source._assert){
+ target._assert();
+ target._assert=null;
+ }
+
+ //if source object has init code - call init against target
+ if (source._init)
+ target._init();
+
+ return target;
+};
+//creates function with specified "this" pointer
+dhtmlx.bind=function(functor, object){
+ return function(){ return functor.apply(object,arguments); };
+};
+
+//loads module from external js file
+dhtmlx.require=function(module){
+ if (!dhtmlx._modules[module]){
+ dhtmlx.assert(dhtmlx.ajax,"load module is required");
+
+ //load and exec the required module
+ dhtmlx.exec( dhtmlx.ajax().sync().get(dhtmlx.codebase+module).responseText );
+ dhtmlx._modules[module]=true;
+ }
+};
+dhtmlx._modules = {}; //hash of already loaded modules
+
+//evaluate javascript code in the global scoope
+dhtmlx.exec=function(code){
+ if (window.execScript) //special handling for IE
+ window.execScript(code);
+ else window.eval(code);
+};
+
+/*
+ creates method in the target object which will transfer call to the source object
+ if event parameter was provided , each call of method will generate onBefore and onAfter events
+*/
+dhtmlx.methodPush=function(object,method,event){
+ return function(){
+ var res = false;
+ //if (!event || this.callEvent("onBefore"+event,arguments)){ //not used anymore, probably can be removed
+ res=object[method].apply(object,arguments);
+ // if (event) this.callEvent("onAfter"+event,arguments);
+ //}
+ return res; //result of wrapped method
+ };
+};
+//check === undefined
+dhtmlx.isNotDefined=function(a){
+ return typeof a == "undefined";
+};
+//delay call to after-render time
+dhtmlx.delay=function(method, obj, params){
+ setTimeout(function(){
+ var ret = method.apply(obj,params);
+ method = obj = params = null;
+ return ret;
+ },1);
+};
+
+//common helpers
+
+//generates unique ID (unique per window, nog GUID)
+dhtmlx.uid = function(){
+ if (!this._seed) this._seed=(new Date).valueOf(); //init seed with timestemp
+ this._seed++;
+ return this._seed;
+};
+//resolve ID as html object
+dhtmlx.toNode = function(node){
+ if (typeof node == "string") return document.getElementById(node);
+ return node;
+};
+//adds extra methods for the array
+dhtmlx.toArray = function(array){
+ return dhtmlx.extend((array||[]),dhtmlx.PowerArray);
+};
+//resolve function name
+dhtmlx.toFunctor=function(str){
+ return (typeof(str)=="string") ? eval(str) : str;
+};
+
+//dom helpers
+
+//hash of attached events
+dhtmlx._events = {};
+//attach event to the DOM element
+dhtmlx.event=function(node,event,handler,master){
+ node = dhtmlx.toNode(node);
+
+ var id = dhtmlx.uid();
+ dhtmlx._events[id]=[node,event,handler]; //store event info, for detaching
+
+ if (master)
+ handler=dhtmlx.bind(handler,master);
+
+ //use IE's of FF's way of event's attaching
+ if (node.addEventListener)
+ node.addEventListener(event, handler, false);
+ else if (node.attachEvent)
+ node.attachEvent("on"+event, handler);
+
+ return id; //return id of newly created event, can be used in eventRemove
+};
+
+//remove previously attached event
+dhtmlx.eventRemove=function(id){
+
+ if (!id) return;
+ dhtmlx.assert(this._events[id],"Removing non-existing event");
+
+ var ev = dhtmlx._events[id];
+ //browser specific event removing
+ if (ev[0].removeEventListener)
+ ev[0].removeEventListener(ev[1],ev[2],false);
+ else if (ev[0].detachEvent)
+ ev[0].detachEvent("on"+ev[1],ev[2]);
+
+ delete this._events[id]; //delete all traces
+};
+
+
+//debugger helpers
+//anything starting from error or log will be removed during code compression
+
+//add message in the log
+dhtmlx.log = function(type,message,details){
+ if (window.console && console.log){
+ type=type.toLowerCase();
+ if (window.console[type])
+ window.console[type](message);
+ else
+ window.console.log(type +": "+message);
+ if (details)
+ window.console.log(details);
+ }
+};
+//register rendering time from call point
+dhtmlx.log_full_time = function(name){
+ dhtmlx._start_time_log = new Date();
+ dhtmlx.log("Info","Timing start ["+name+"]");
+ window.setTimeout(function(){
+ var time = new Date();
+ dhtmlx.log("Info","Timing end ["+name+"]:"+(time.valueOf()-dhtmlx._start_time_log.valueOf())/1000+"s");
+ },1);
+};
+//register execution time from call point
+dhtmlx.log_time = function(name){
+ var fname = "_start_time_log"+name;
+ if (!dhtmlx[fname]){
+ dhtmlx[fname] = new Date();
+ dhtmlx.log("Info","Timing start ["+name+"]");
+ } else {
+ var time = new Date();
+ dhtmlx.log("Info","Timing end ["+name+"]:"+(time.valueOf()-dhtmlx[fname].valueOf())/1000+"s");
+ dhtmlx[fname] = null;
+ }
+};
+//log message with type=error
+dhtmlx.error = function(message,details){
+ dhtmlx.log("Error",message,details);
+};
+//event system
+dhtmlx.EventSystem={
+ _init:function(){
+ this._events = {}; //hash of event handlers, name => handler
+ this._handlers = {}; //hash of event handlers, ID => handler
+ this._map = {};
+ },
+ //temporary block event triggering
+ block : function(){
+ this._events._block = true;
+ },
+ //re-enable event triggering
+ unblock : function(){
+ this._events._block = false;
+ },
+ mapEvent:function(map){
+ dhtmlx.extend(this._map, map);
+ },
+ //trigger event
+ callEvent:function(type,params){
+ if (this._events._block) return true;
+
+ type = type.toLowerCase();
+ dhtmlx.assert_event_call(this, type, params);
+
+ var event_stack =this._events[type.toLowerCase()]; //all events for provided name
+ var return_value = true;
+
+ if (dhtmlx.debug) //can slowdown a lot
+ dhtmlx.log("info","["+this.name+"] event:"+type,params);
+
+ if (event_stack)
+ for(var i=0; i=0) this.splice(pos,(len||1));
+ },
+ //find element in collection and remove it
+ remove:function(value){
+ this.removeAt(this.find(value));
+ },
+ //add element to collection at specific position
+ insertAt:function(data,pos){
+ if (!pos && pos!==0) //add to the end by default
+ this.push(data);
+ else {
+ var b = this.splice(pos,(this.length-pos));
+ this[pos] = data;
+ this.push.apply(this,b); //reconstruct array without loosing this pointer
+ }
+ },
+ //return index of element, -1 if it doesn't exists
+ find:function(data){
+ for (i=0; i0){
+ str=this._toHex[number%16]+str;
+ number=Math.floor(number/16);
+ }
+ while (str.length ");
+ },
+ addSector:function(id,alpha0,alpha1,x,y,R,ky){
+ var points = [];
+ points.push(x);
+ points.push(Math.floor(y*ky));
+ for(var i = alpha0; i < alpha1; i+=Math.PI/18){
+ points.push(Math.floor(x+R*Math.cos(i)));
+ points.push(Math.floor((y+R*Math.sin(i))*ky));
+ }
+ points.push(Math.floor(x+R*Math.cos(alpha1)));
+ points.push(Math.floor((y+R*Math.sin(alpha1))*ky));
+ points.push(x);
+ points.push(Math.floor(y*ky));
+
+ return this.addPoly(id,points);
+ },
+ render:function(obj){
+ var d = dhtmlx.html.create("DIV");
+ d.style.cssText="position:absolute; width:100%; height:100%; top:0px; left:0px;";
+ obj.appendChild(d);
+ var src = dhtmlx._isIE?"":"src='data:image/gif;base64,R0lGODlhEgASAIAAAP///////yH5BAUUAAEALAAAAAASABIAAAIPjI+py+0Po5y02ouz3pwXADs='";
+ d.innerHTML="";
+
+ obj._htmlmap = d; //for clearing routine
+
+ this._map = [];
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'chart_graph.js'*/
+
+
+/*DHX:Depend map.js*/
+
+if (!dhtmlx.chart) dhtmlx.chart = {};
+
+dhtmlx.chart.line = {
+
+ /**
+ * renders a graphic
+ * @param: ctx - canvas object
+ * @param: data - object those need to be displayed
+ * @param: width - the width of the container
+ * @param: height - the height of the container
+ */
+ pvt_render_line:function(ctx, data, width, height){
+ var maxValue,minValue;
+ /*necessary if maxValue - minValue < 0*/
+ var valueFactor;
+ /*maxValue - minValue*/
+ var relValue;
+
+
+ /*necessary to add events and tooltips*/
+ var map = new dhtmlx.ui.Map(this._id);
+
+ /*scales*/
+ var yax = !!this._settings.yAxis;
+ var xax = !!this._settings.xAxis;
+
+ if(yax&&(typeof this._settings.yAxis.end!="undefied")&&(typeof this._settings.yAxis.start!="undefied")&&this._settings.yAxis.step){
+ maxValue = parseFloat(this._settings.yAxis.end);
+ minValue = parseFloat(this._settings.yAxis.start);
+ }
+ else{
+ maxValue = this.max(this._settings.value);
+ minValue = this.min(this._settings.value);
+ }
+
+ /*a vertical scale*/
+ this._drawYAxis(ctx,data,width,height,minValue,maxValue);
+
+ /*necessary for automatic scale*/
+ if(yax){
+ maxValue = parseFloat(this._settings.yAxis.end);
+ minValue = parseFloat(this._settings.yAxis.start);
+ }
+
+ var topPadding = parseInt(this._settings.padding.top,10);
+ var leftPadding = parseInt(this._settings.padding.left,10);
+ var bottomPadding = parseInt(this._settings.padding.bottom,10);
+ var rightPadding = parseInt(this._settings.padding.right,10);
+
+ /*available height*/
+ var total_height = height-topPadding-bottomPadding;
+
+
+ /*unit calculation (y_position = value*unit)*/
+ var relativeValues = this._getRelativeValue(minValue,maxValue);
+ relValue = relativeValues[0];
+ valueFactor = relativeValues[1];
+ var unit = total_height/relValue;
+
+ if(!yax){
+ /*defines start value for better representation of small values*/
+ var startValue = (unit>10?unit:10);
+ unit = (total_height-startValue)/relValue;
+ }
+
+ /*a space available for a single item*/
+ var cellWidth = Math.round((width - leftPadding - rightPadding)/data.length);
+
+ /*the value that defines the map area position*/
+ var areaPos = Math.floor(cellWidth/2);
+
+ /*drawing all items*/
+ if (data.length) {
+ /*gets the vertical coordinate of an item*/
+ var _graph_point = function(data){
+ /*the real value of an object*/
+ var value = this._settings.value(data);
+ /*a relative value*/
+ var v = (parseFloat(value) - minValue)*valueFactor;
+ if(!yax)
+ v += startValue/unit;
+ /*a vertical coordinate*/
+ var y = height - Math.floor(unit*v) - bottomPadding;
+ /*the limit of the minimum value is the minimum visible value*/
+ if(v<0)
+ y = height - topPadding;
+ /*the limit of the maximum value*/
+ if(value > maxValue)
+ y = topPadding;
+ /*the limit of the minimum value*/
+ if(value < minValue)
+ y = height - bottomPadding;
+ return y;
+ };
+ /*the vertical position of the first item*/
+ var y1 = _graph_point.call(this,data[0]);
+ /*drawing the previous item and the line between to items*/
+ for(var i=1; i <= data.length;i ++){
+
+
+ /*horizontal positions of the previous and current items (0.5 - the fix for line width)*/
+ var x1 = Math.floor(cellWidth*(i-0.5)) - 0.5 + leftPadding;
+ var x2 = Math.floor(cellWidth*(i+0.5)) - 0.5 + leftPadding;
+
+ /*sets label on the horizontal scale*/
+ this._drawXAxisLabel(x1,data[i-1]);
+ /*draws a vertical line for the horizontal scale*/
+ this._drawXAxisLine(ctx,x1,height-bottomPadding,topPadding);
+
+ /*a line between items*/
+ if (data.length!=i){
+ var y2 = _graph_point.call(this,data[i]);
+ this._drawLine(ctx,x1,y1,x2,y2,this._settings.line.color(data[i-1]),this._settings.line.width);
+ }
+
+ /*draws prevous item*/
+ this._drawGraphicItem(ctx,x1,y1,data[i-1]);
+
+ /*creates map area*/
+ map.addRect(data[i-1].id,[x1-areaPos,y1-areaPos,x1+areaPos,y1+areaPos]);
+
+ y1=y2;
+ }
+ _graph_point = null;
+ }
+ /*horizontal scale*/
+ this._drawXAxis(ctx,data,width,height);
+ map.render(this._obj);
+ },
+ /**
+ * draws an item and its label
+ * @param: ctx - canvas object
+ * @param: x0 - the x position of a circle
+ * @param: y0 - the y position of a circle
+ * @param: obj - data object
+ */
+ _drawGraphicItem:function(ctx,x0,y0,obj){
+ var R = parseInt(this._settings.item.radius,10);
+ ctx.lineWidth = parseInt(this._settings.item.borderWidth,10);
+ ctx.fillStyle = this._settings.item.color(obj);
+ ctx.strokeStyle = this._settings.item.borderColor(obj);
+ ctx.beginPath();
+ ctx.arc(x0,y0,R,0,Math.PI*2,true);
+ ctx.fill();
+ ctx.stroke();
+ /*item label*/
+ this.renderTextAt(false, true, x0,y0-R-this._settings.labelOffset,this._settings.label(obj));
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'chart_bar.js'*/
+
+
+/*DHX:Depend map.js*/
+
+if (!dhtmlx.chart) dhtmlx.chart = {};
+
+dhtmlx.chart.bar = {
+ /**
+ * renders a bar chart
+ * @param: ctx - canvas object
+ * @param: data - object those need to be displayed
+ * @param: x - the width of the container
+ * @param: y - the height of the container
+ */
+ pvt_render_bar:function(ctx, data, x, y){
+ var maxValue,minValue;
+ /*necessary if maxValue - minValue < 0*/
+ var valueFactor;
+ /*maxValue - minValue*/
+ var relValue;
+
+ var map = new dhtmlx.ui.Map(this._id);
+
+ var yax = !!this._settings.yAxis;
+ var xax = !!this._settings.xAxis;
+
+ if(yax&&(typeof this._settings.yAxis.end!="undefied")&&(typeof this._settings.yAxis.start!="undefied")&&this._settings.yAxis.step){
+ maxValue = parseFloat(this._settings.yAxis.end);
+ minValue = parseFloat(this._settings.yAxis.start);
+ }
+ else{
+ maxValue = this.max(this._settings.value);
+ minValue = this.min(this._settings.value);
+ }
+
+ /*a vertical scale*/
+ this._drawYAxis(ctx,data,x,y,minValue,maxValue);
+
+ /*necessary for automatic scale*/
+ if(yax){
+ maxValue = parseFloat(this._settings.yAxis.end);
+ minValue = parseFloat(this._settings.yAxis.start);
+ }
+
+ var yPadding = parseInt(this._settings.padding.top,10);
+ var xPadding = parseInt(this._settings.padding.left,10);
+ var bottomPadding = parseInt(this._settings.padding.bottom,10);
+ var total_height = y-yPadding-bottomPadding;
+
+ /*unit calculation (bar_height = value*unit)*/
+ var relativeValues = this._getRelativeValue(minValue,maxValue);
+ relValue = relativeValues[0];
+ valueFactor = relativeValues[1];
+
+ var unit = total_height/relValue;
+ if(!yax){
+ /*defines start value for better representation of small values*/
+ var startValue = (unit>10?unit:10);
+ unit = (total_height-startValue)/relValue;
+ }
+
+ /*an available width for one bar*/
+ var cellWidth = Math.floor((x-xPadding-parseInt(this._settings.padding.right,10))/data.length);
+ /*a real bar width */
+ var barWidth = parseInt(this._settings.width,10);
+ if(barWidth>cellWidth) barWidth = cellWidth-2;
+ /*the half of distance between bars*/
+ var barOffset = Math.floor((cellWidth - barWidth)/2);
+ /*the radius of rounding in the top part of each bar*/
+ var radius = Math.round(barWidth/5);
+
+ var inner_gradient = false;
+ var gradient = this._settings.gradient;
+
+ if (gradient === true){
+ inner_gradient = true;
+ gradient = false;
+ } else if (gradient){
+ gradient = ctx.createLinearGradient(0,bottomPadding,0,y-yPadding);
+ this._settings.gradient(gradient);
+ }
+ var scaleY = 0;
+ /*draws a black line if the horizontal scale isn't defined*/
+ if(bottomPadding&&!xax){
+ scaleY = y-bottomPadding;
+ this._drawLine(ctx,0,scaleY+0.5,x,scaleY+0.5,"#000000",1); //hardcoded color!
+ }
+
+ for(var i=0; i < data.length;i ++){
+
+
+ var value = parseFloat(this._settings.value(data[i]));
+ if(value>maxValue) value = maxValue;
+ value -= minValue;
+ value *= valueFactor;
+
+ /*start point (bottom left)*/
+ var x0 = xPadding + barOffset + i*cellWidth;
+ var y0 = y-bottomPadding;
+
+
+ /*sets a label in the horizontal scale*/
+ this._drawXAxisLabel(x0+Math.floor(barWidth/2),data[i]);
+
+ if(value<0||(this._settings.yAxis&&value===0)){
+ this.renderTextAt(true, true, x0+Math.floor(barWidth/2),y0,this._settings.label(data[i]));
+ continue;
+ }
+
+ /*takes start value into consideration*/
+ if(!yax) value += startValue/unit;
+ var color = gradient||this._settings.color.call(this,data[i]);
+
+ /*drawing the gradient border of a bar*/
+ if(this._settings.border){
+ ctx.beginPath();
+ ctx.fillStyle = color;
+ this._setBarPoints(ctx,x0-1,y0,barWidth+2,radius,unit,value,0);
+ ctx.lineTo(x0,0);
+ ctx.fill();
+
+ ctx.fillStyle = "#000000";
+ ctx.globalAlpha = 0.37;
+ ctx.beginPath();
+ this._setBarPoints(ctx,x0-1,y0,barWidth+2,radius,unit,value,0);
+ ctx.fill();
+ }
+
+ /*drawing bar body*/
+ ctx.globalAlpha = this._settings.alpha.call(this,data[i]);
+ ctx.fillStyle = (gradient||this._settings.color.call(this,data[i]));
+ ctx.beginPath();
+ var points = this._setBarPoints(ctx,x0,y0,barWidth,radius,unit,value,(this._settings.border?1:0));
+ if (gradient) ctx.lineTo(x0,0); //fix gradient sphreading
+ ctx.fill();
+ ctx.globalAlpha = 1;
+
+ if (inner_gradient){
+ var i_gradient = ctx.createLinearGradient(0,y0-unit*value+2,0,y0);
+ i_gradient.addColorStop(0,color);
+ i_gradient.addColorStop(0.1,color);
+ i_gradient.addColorStop(1,"#FFFFFF");
+ ctx.fillStyle = i_gradient;
+ ctx.beginPath();
+ var points = this._setBarPoints(ctx,x0+2,y0,barWidth-4,radius,unit,value,1);
+ ctx.fill();
+ }
+
+
+ /*sets a bar label*/
+ this.renderTextAt(true, true, x0+Math.floor(barWidth/2),points[1],this._settings.label(data[i]));
+ /*defines a map area for a bar*/
+ map.addRect(data[i].id,[x0,points[1],points[0],y0]);
+ }
+
+ this._drawXAxis(ctx,data,x,y);
+ map.render(this._obj);
+ },
+ /**
+ * sets points for bar and returns the position of the bottom right point
+ * @param: ctx - canvas object
+ * @param: x0 - the x position of start point
+ * @param: y0 - the y position of start point
+ * @param: barWidth - bar width
+ * @param: radius - the rounding radius of the top
+ * @param: unit - the value defines the correspondence between item value and bar height
+ * @param: value - item value
+ * @param: offset - the offset from expected bar edge (necessary for drawing border)
+ */
+ _setBarPoints:function(ctx,x0,y0,barWidth,radius,unit,value,offset){
+ /*correction for displaing small values (when rounding radius is bigger than bar height)*/
+ var angle_corr = 0;
+ if(radius>unit*value){
+ var cosA = (radius-unit*value)/radius;
+ angle_corr = -Math.acos(cosA)+Math.PI/2;
+ }
+ /*start*/
+ ctx.moveTo(x0,y0);
+ /*start of left rounding*/
+ var y1 = y0 - Math.floor(unit*value) + radius;
+ if(radiusdetails.total_height))
+ y0 += y-details.total_height-ctx.lineWidth/2;
+ /*considers item index*/
+ y0 += i*details.height;
+
+ ctx.moveTo(x0,y0);
+ var x1 = x0+details.marker.width-details.marker.height +1;
+ ctx.lineTo(x1,y0);
+ ctx.stroke();
+
+ /*item text*/
+ this.renderText(x1+details.marker.width/2+5,y0-details.marker.height/2,details.template(obj));
+ },
+ /**
+ * returns calculated pie parameters: center position and radius
+ * @param: x - the width of a container
+ * @param: y - the height of a container
+ */
+ _getPieParameters:function(x,y){
+ var offset = 0;
+ if(this._settings.legend)
+ offset = this._settings.legend.width*(this._settings.legend.align=="right"?-1:1);
+ x = (x + offset)/2;
+ y = y/2;
+ var radius = Math.min(x,y)-this._settings.padding.top;
+ return {"x":x,"y":y,"radius":radius};
+ },
+ /**
+ * creates lower part of sector in 3Dpie
+ * @param: ctx - canvas object
+ * @param: x0 - the horizontal position of the pie center
+ * @param: y0 - the vertical position of the pie center
+ * @param: a0 - the angle that defines the first edge of a sector
+ * @param: a1 - the angle that defines the second edge of a sector
+ * @param: R - pie radius
+ * @param: line (boolean) - if the sector needs a border
+ */
+ _createLowerSector:function(ctx,x0,y0,a1,a2,R,line){
+ ctx.lineWidth = 1;
+ /*checks if the lower sector needs being displayed*/
+ if(!((a1<=0 && a2>=0)||(a1>=0 && a2<=Math.PI)||(a1<=Math.PI && a2>=Math.PI))) return;
+
+ if(a1<=0 && a2>=0){
+ a1 = 0;
+ line = false;
+ this._drawSectorLine(ctx,x0,y0,R,a1,a2);
+ }
+ if(a1<=Math.PI && a2>=Math.PI){
+ a2 = Math.PI;
+ line = false;
+ this._drawSectorLine(ctx,x0,y0,R,a1,a2);
+ }
+ /*the height of 3D pie*/
+ var offset = (this._settings.height||Math.floor(R/4))/this._settings.cant;
+ ctx.beginPath();
+ ctx.arc(x0,y0,R,a1,a2,false);
+ ctx.lineTo(x0+R*Math.cos(a2),y0+R*Math.sin(a2)+offset);
+ ctx.arc(x0,y0+offset,R,a2,a1,true);
+ ctx.lineTo(x0+R*Math.cos(a1),y0+R*Math.sin(a1));
+ ctx.fill();
+ if(line)
+ ctx.stroke();
+ },
+ /**
+ * draws a serctor arc
+ */
+ _drawSectorLine:function(ctx,x0,y0,R,a1,a2){
+ ctx.beginPath();
+ ctx.arc(x0,y0,R,a1,a2,false);
+ ctx.stroke();
+ },
+ /**
+ * adds a shadow to pie
+ * @param: ctx - canvas object
+ * @param: x - the horizontal position of the pie center
+ * @param: y - the vertical position of the pie center
+ * @param: R - pie radius
+ */
+ _addShadow:function(ctx,x,y,R){
+ var shadows = ["#676767","#7b7b7b","#a0a0a0","#bcbcbc","#d1d1d1","#d6d6d6"];
+ for(var i = shadows.length-1;i>-1;i--){
+ ctx.beginPath();
+ ctx.fillStyle = shadows[i];
+ ctx.arc(x+2,y+2,R+i,0,Math.PI*2,true);
+ ctx.fill();
+ }
+ },
+ /**
+ * returns a gray gradient
+ * @param: gradient - gradient object
+ */
+ _getGrayGradient:function(gradient){
+ gradient.addColorStop(0.0,"#ffffff");
+ gradient.addColorStop(0.7,"#7a7a7a");
+ gradient.addColorStop(1.0,"#000000");
+ return gradient;
+ },
+ /**
+ * adds gray radial gradient
+ * @param: ctx - canvas object
+ * @param: x - the horizontal position of the pie center
+ * @param: y - the vertical position of the pie center
+ * @param: radius - pie radius
+ * @param: x0 - the horizontal position of a gradient center
+ * @param: y0 - the vertical position of a gradient center
+ */
+ _showRadialGradient:function(ctx,x,y,radius,x0,y0){
+ ctx.globalAlpha = 0.3;
+ ctx.beginPath();
+ var gradient;
+ if(typeof this._settings.gradient!= "function"){
+ gradient = ctx.createRadialGradient(x0,y0,radius/4,x,y,radius);
+ gradient = this._getGrayGradient(gradient);
+ }
+ else gradient = this._settings.gradient(gradient);
+ ctx.fillStyle = gradient;
+ ctx.arc(x,y,radius,0,Math.PI*2,true);
+ ctx.fill();
+ ctx.globalAlpha = 1;
+ },
+ /**
+ * returns the calculates pie parameters: center position and radius
+ * @param: ctx - canvas object
+ * @param: x0 - the horizontal position of the pie center
+ * @param: y0 - the vertical position of the pie center
+ * @param: R - pie radius
+ * @param: alpha1 - the angle that defines the 1st edge of a sector
+ * @param: alpha2 - the angle that defines the 2nd edge of a sector
+ * @param: ky - the value that defines an angle of inclination
+ * @param: text - label text
+ * @param: in_width (boolean) - if label needs being displayed inside a pie
+ */
+ _drawSectorLabel:function(x0,y0,R,alpha1,alpha2,ky,text,in_width){
+ var t = this.renderText(0,0,text,0,1);
+ if (!t) return;
+
+ //get existing width of text
+ var labelWidth = t.scrollWidth;
+ t.style.width = labelWidth+"px"; //adjust text label to fit all text
+ if (labelWidth>x0) labelWidth = x0; //the text can't be greater than half of view
+
+ //calculate expected correction based on default font metrics
+ var width = 8;
+ if (in_width) width = labelWidth/1.8;
+ var alpha = alpha1+(alpha2-alpha1)/2;
+
+ //calcualteion position and correction
+ R = R-(width-8)/2;
+ var corr_x = - width;
+ var corr_y = -8;
+ var align = "left";
+
+ //for items in right upper sector
+ if(alpha>=Math.PI/2 && alpha=Math.PI){
+ corr_x = -labelWidth-corr_x+1;
+ align = "right";
+ }
+
+ //calculate position of text
+ //basically get point at center of pie sector
+ var y = (y0+Math.floor(R*Math.sin(alpha)))*ky+corr_y;
+ var x = x0+Math.floor((R+width/2)*Math.cos(alpha))+corr_x;
+
+ //if pie sector starts in left of right part pie, related text
+ //must be placed to the left of to the right of pie as well
+ var left_end = (alpha2 < Math.PI/2+0.01)
+ var left_start = (alpha < Math.PI/2);
+ if (left_start && left_end)
+ x = Math.max(x,x0+3); //right part of pie
+ else if (!left_start && !left_end)
+ x = Math.min(x,x0-labelWidth); //left part of pie
+
+
+ /*correction for the lower sector of the 3D pie*/
+ if (!in_width && ky<1 && y > y0*ky){
+ y+= (this._settings.height||Math.floor(R/4));
+ }
+
+ //we need to set position of text manually, based on above calculations
+ t.style.top = y+"px";
+ t.style.left = x+"px";
+ t.style.width = labelWidth+"px";
+ t.style.textAlign = align;
+ t.style.whiteSpace = "nowrap";
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'template.js'*/
+
+
+/*
+ Template - handles html templates
+*/
+
+/*DHX:Depend dhtmlx.js*/
+
+dhtmlx.Template={
+ _cache:{
+ },
+ empty:function(){
+ return "";
+ },
+ setter:function(name, value){
+ return dhtmlx.Template.fromHTML(value);
+ },
+ obj_setter:function(name,value){
+ var f = dhtmlx.Template.setter(name,value);
+ var obj = this;
+ return function(){
+ return f.apply(obj, arguments);
+ };
+ },
+ fromHTML:function(str){
+ if (typeof str == "function") return str;
+ if (this._cache[str])
+ return this._cache[str];
+
+ //supported idioms
+ // {obj} => value
+ // {obj.attr} => named attribute or value of sub-tag in case of xml
+ // {obj.attr?some:other} conditional output
+ // {-obj => sub-template
+ str=(str||"").toString();
+ str=str.replace(/[\r\n]+/g,"\\n");
+ str=str.replace(/\{obj\.([^}?]+)\?([^:]*):([^}]*)\}/g,"\"+(obj.$1?\"$2\":\"$3\")+\"");
+ str=str.replace(/\{common\.([^}\(]*)\}/g,"\"+common.$1+\"");
+ str=str.replace(/\{common\.([^\}\(]*)\(\)\}/g,"\"+(common.$1?common.$1(obj):\"\")+\"");
+ str=str.replace(/\{obj\.([^}]*)\}/g,"\"+obj.$1+\"");
+ str=str.replace(/#([a-z0-9_]+)#/gi,"\"+obj.$1+\"");
+ str=str.replace(/\{obj\}/g,"\"+obj+\"");
+ str=str.replace(/\{-obj/g,"{obj");
+ str=str.replace(/\{-common/g,"{common");
+ str="return \""+str+"\";";
+ return this._cache[str]= Function("obj","common",str);
+ }
+};
+
+dhtmlx.Type={
+ /*
+ adds new template-type
+ obj - object to which template will be added
+ data - properties of template
+ */
+ add:function(obj, data){
+ //auto switch to prototype, if name of class was provided
+ if (!obj.types && obj.prototype.types)
+ obj = obj.prototype;
+ //if (typeof data == "string")
+ // data = { template:data };
+
+ if (dhtmlx.assert_enabled())
+ this.assert_event(data);
+
+ var name = data.name||"default";
+
+ //predefined templates - autoprocessing
+ this._template(data);
+ this._template(data,"edit");
+ this._template(data,"loading");
+
+ obj.types[name]=dhtmlx.extend(dhtmlx.extend({},(obj.types[name]||this._default)),data);
+ return name;
+ },
+ //default template value - basically empty box with 5px margin
+ _default:{
+ css:"default",
+ template:function(){ return ""; },
+ template_edit:function(){ return ""; },
+ template_loading:function(){ return "..."; },
+ width:150,
+ height:80,
+ margin:5,
+ padding:0
+ },
+ //template creation helper
+ _template:function(obj,name){
+ name = "template"+(name?("_"+name):"");
+ var data = obj[name];
+ //if template is a string - check is it plain string or reference to external content
+ if (data && (typeof data == "string")){
+ if (data.indexOf("->")!=-1){
+ data = data.split("->");
+ switch(data[0]){
+ case "html": //load from some container on the page
+ data = dhtmlx.html.getValue(data[1]).replace(/\"/g,"\\\"");
+ break;
+ case "http": //load from external file
+ data = new dhtmlx.ajax().sync().get(data[1],{uid:(new Date()).valueOf()}).responseText;
+ break;
+ default:
+ //do nothing, will use template as is
+ break;
+ }
+ }
+ obj[name] = dhtmlx.Template.fromHTML(data);
+ }
+ }
+};
+
+
+
+/* DHX DEPEND FROM FILE 'single_render.js'*/
+
+
+/*
+ REnders single item.
+ Can be used for elements without datastore, or with complex custom rendering logic
+
+ @export
+ render
+*/
+
+/*DHX:Depend template.js*/
+
+dhtmlx.SingleRender={
+ _init:function(){
+ },
+ //convert item to the HTML text
+ _toHTML:function(obj){
+ /*
+ this one doesn't support per-item-$template
+ it has not sense, because we have only single item per object
+ */
+ return this.type._item_start(obj,this.type)+this.type.template(obj,this.type)+this.type._item_end;
+ },
+ //render self, by templating data object
+ render:function(){
+ if (!this.callEvent || this.callEvent("onBeforeRender",[this.data])){
+ if (this.data)
+ this._dataobj.innerHTML = this._toHTML(this.data);
+ if (this.callEvent) this.callEvent("onAfterRender",[]);
+ }
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'tooltip.js'*/
+
+
+/*
+ UI: Tooltip
+
+ @export
+ show
+ hide
+*/
+
+/*DHX:Depend tooltip.css*/
+/*DHX:Depend template.js*/
+/*DHX:Depend single_render.js*/
+
+dhtmlx.ui.Tooltip=function(container){
+ this.name = "Tooltip";
+ this.version = "3.0";
+
+ if (dhtmlx.assert_enabled()) this._assert();
+
+ if (typeof container == "string"){
+ container = { template:container };
+ }
+
+ dhtmlx.extend(this, dhtmlx.Settings);
+ dhtmlx.extend(this, dhtmlx.SingleRender);
+ this._parseSettings(container,{
+ type:"default",
+ dy:0,
+ dx:20
+ });
+
+ //create container for future tooltip
+ this._dataobj = this._obj = document.createElement("DIV");
+ this._obj.className="dhx_tooltip";
+ dhtmlx.html.insertBefore(this._obj,document.body.firstChild);
+};
+dhtmlx.ui.Tooltip.prototype = {
+ //show tooptip
+ //pos - object, pos.x - left, pox.y - top
+ show:function(data,pos){
+ //render sefl only if new data was provided
+ if (this.data!=data){
+ this.data=data;
+ this.render(data);
+ }
+ //show at specified position
+ this._obj.style.top = pos.y+this._settings.dy+"px";
+ this._obj.style.left = pos.x+this._settings.dx+"px";
+ this._obj.style.display="block";
+ },
+ //hide tooltip
+ hide:function(){
+ this.data=null; //nulify, to be sure that on next show it will be fresh-rendered
+ this._obj.style.display="none";
+ },
+ types:{
+ "default":dhtmlx.Template.fromHTML("{obj.id}")
+ },
+ template_item_start:dhtmlx.Template.empty,
+ template_item_end:dhtmlx.Template.empty
+};
+
+
+
+/* DHX DEPEND FROM FILE 'autotooltip.js'*/
+
+
+/*
+ Behavior: AutoTooltip - links tooltip to data driven item
+*/
+
+/*DHX:Depend tooltip.js*/
+
+dhtmlx.AutoTooltip = {
+ tooltip_setter:function(mode,value){
+ var t = new dhtmlx.ui.Tooltip(value);
+ this.attachEvent("onMouseMove",function(id,e){ //show tooltip on mousemove
+ t.show(this.get(id),dhtmlx.html.pos(e));
+ });
+ this.attachEvent("onMouseOut",function(id,e){ //hide tooltip on mouseout
+ t.hide();
+ });
+ this.attachEvent("onMouseMoving",function(id,e){ //hide tooltip just after moving start
+ t.hide();
+ });
+ return t;
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'datastore.js'*/
+
+
+/*DHX:Depend dhtmlx.js*/
+
+/*
+ DataStore is not a behavior, it standalone object, which represents collection of data.
+ Call provideAPI to map data API
+
+ @export
+ exists
+ idByIndex
+ indexById
+ get
+ set
+ refresh
+ dataCount
+ sort
+ filter
+ next
+ previous
+ clearAll
+ first
+ last
+*/
+dhtmlx.DataStore = function(){
+ this.name = "DataStore";
+
+ dhtmlx.extend(this, dhtmlx.EventSystem);
+
+ this.setDriver("xml"); //default data source is an XML
+ this.pull = {}; //hash of IDs
+ this.order = dhtmlx.toArray(); //order of IDs
+};
+
+dhtmlx.DataStore.prototype={
+ //defines type of used data driver
+ //data driver is an abstraction other different data formats - xml, json, csv, etc.
+ setDriver:function(type){
+ dhtmlx.assert(dhtmlx.DataDriver[type],"incorrect DataDriver");
+ this.driver = dhtmlx.DataDriver[type];
+ },
+ //process incoming raw data
+ _parse:function(data){
+ //get size and position of data
+ var info = this.driver.getInfo(data);
+ //get array of records
+ var recs = this.driver.getRecords(data);
+
+ var from = (info._from||0)*1;
+ var j=0;
+ for (var i=0; ito){ //can be in case of backward shift-selection
+ var a=to; to=from; from=a;
+ }
+ }
+ return this.getIndexRange(from,to);
+ },
+ //converts range of indexes to array of all IDs between them
+ getIndexRange:function(from,to){
+ to=Math.min(to,this.dataCount()-1);
+
+ var ret=dhtmlx.toArray(); //result of method is rich-array
+ for (var i=from; i <= to; i++)
+ ret.push(this.get(this.order[i]));
+ return ret;
+ },
+ //returns total count of elements
+ dataCount:function(){
+ return this.order.length;
+ },
+ //returns truy if item with such ID exists
+ exists:function(id){
+ return !!(this.pull[id]);
+ },
+ //nextmethod is not visible on component level, check DataMove.move
+ //moves item from source index to the target index
+ move:function(sindex,tindex){
+ if (sindex<0 || tindex<0 || tindex >= this.order.length){
+ dhtmlx.error("DataStore::move","Incorrect indexes");
+ return;
+ }
+
+
+ var id = this.idByIndex(sindex);
+ var obj = this.get(id);
+
+ this.order.removeAt(sindex); //remove at old position
+ //if (sindex>tindex) tindex--; //correct shift, caused by element removing
+ this.order.insertAt(id,tindex); //insert at new position
+
+ //repaint signal
+ this.callEvent("onStoreUpdated",[id,obj,"move"]);
+ },
+ //adds item to the store
+ add:function(obj,index){
+ //generate id for the item
+ var id = this.id(obj);
+
+ //by default item is added to the end of the list
+ var data_size = this.dataCount();
+ if (dhtmlx.isNotDefined(index))
+ index = data_size;
+ //check to prevent too big indexes
+ if (index > data_size){
+ dhtmlx.log("Warning","DataStore:add","Index of out of bounds");
+ index = Math.min(this.order.length,index);
+ }
+
+ if (!this.callEvent("onbeforeAdd",[id,index])) return;
+
+ if (this.exists(id)) return dhtmlx.error("Not unique ID");
+
+ this.pull[id]=obj;
+ this.order.insertAt(id,index);
+ if (this._filter_order){ //adding during filtering
+ //we can't know the location of new item in full dataset, making suggestion
+ //put at end by default
+ var original_index = this._filter_order.length;
+ //put at start only if adding to the start and some data exists
+ if (!index && this.order.length)
+ original_index = 0;
+
+ this._filter_order.insertAt(id,original_index);
+ }
+
+ this.callEvent("onafterAdd",[id,index]);
+ //repaint signal
+ this.callEvent("onStoreUpdated",[id,obj,"add"]);
+ return id;
+ },
+
+ //removes element from datastore
+ remove:function(id){
+ //id can be an array of IDs - result of getSelect, for example
+ if (id instanceof Array){
+ for (var i=0; i < id.length; i++)
+ this.remove(id[i]);
+ return;
+ }
+ if (!this.callEvent("onbeforedelete",[id])) return;
+ if (!this.exists(id)) return dhtmlx.error("Not existing ID",id);
+ var obj = this.get(id); //save for later event
+ //clear from collections
+ this.order.remove(id);
+ if (this._filter_order)
+ this._filter_order.remove(id);
+
+ delete this.pull[id];
+ this.callEvent("onafterdelete",[id]);
+ //repaint signal
+ this.callEvent("onStoreUpdated",[id,obj,"delete"]);
+ },
+ //deletes all records in datastore
+ clearAll:function(){
+ //instead of deleting one by one - just reset inner collections
+ this.pull = {};
+ this.order = dhtmlx.toArray();
+ this._filter_order = null;
+ this.callEvent("onClearAll",[]);
+ this.refresh();
+ },
+ //converts id to index
+ idByIndex:function(index){
+ if (index>=this.order.length || index<0)
+ dhtmlx.log("Warning","DataStore::idByIndex Incorrect index");
+
+ return this.order[index];
+ },
+ //converts index to id
+ indexById:function(id){
+ var res = this.order.find(id); //slower than idByIndex
+
+ if (!res && res!==0)
+ dhtmlx.log("Warning","DataStore::indexById Non-existing ID: "+ id);
+
+ return res;
+ },
+ //returns ID of next element
+ next:function(id,step){
+ return this.order[this.indexById(id)+(step||1)];
+ },
+ //returns ID of first element
+ first:function(){
+ return this.order[0];
+ },
+ //returns ID of last element
+ last:function(){
+ return this.order[this.order.length-1];
+ },
+ //returns ID of previous element
+ previous:function(id,step){
+ return this.order[this.indexById(id)-(step||1)];
+ },
+ /*
+ sort data in collection
+ by - settings of sorting
+
+ or
+
+ by - sorting function
+ dir - "asc" or "desc"
+
+ or
+
+ by - property
+ dir - "asc" or "desc"
+ as - type of sortings
+
+ Sorting function will accept 2 parameters and must return 1,0,-1, based on desired order
+ */
+ sort:function(by, dir, as){
+
+ var sort = by;
+ if (typeof by == "function")
+ sort = {as:by, dir:dir};
+ else if (typeof by == "string")
+ sort = {by:by, dir:dir, as:as};
+
+
+ var parameters = [sort.by, sort.dir, sort.as];
+ if (!this.callEvent("onbeforesort",parameters)) return;
+
+ var sorter = dhtmlx.sort.create(sort);
+ //get array of IDs
+ var neworder = this.getRange();
+ neworder.sort(sorter);
+ this.order = neworder.map(function(obj){ return this.id(obj); },this);
+
+ //repaint self
+ this.refresh();
+
+ this.callEvent("onaftersort",parameters);
+ },
+ /*
+ Filter datasource
+
+ text - property, by which filter
+ value - filter mask
+
+ or
+
+ text - filter method
+
+ Filter method will receive data object and must return true or false
+ */
+ filter:function(text,value){
+ //remove previous filtering , if any
+ if (this._filter_order){
+ this.order = this._filter_order;
+ delete this._filter_order;
+ }
+ //if text not define -just unfilter previous state and exit
+ if (text){
+ var filter = text;
+ if (typeof text == "string"){
+ text = dhtmlx.Template.setter(0,text);
+ filter = function(obj,value){ //default filter - string start from, case in-sensitive
+ return text(obj).toLowerCase().indexOf(value)!=-1;
+ };
+ }
+
+ value = (value||"").toString().toLowerCase();
+ var neworder = dhtmlx.toArray();
+ this.order.each(function(id){
+ if (filter(this.get(id),value))
+ neworder.push(id);
+ },this);
+ //set new order of items, store original
+ this._filter_order = this.order;
+ this.order = neworder;
+ }
+ //repaint self
+ this.refresh();
+ },
+ /*
+ Iterate through collection
+ */
+ each:function(method,master){
+ for (var i=0; ib?1:(ab?1:(ab?1:(a max) max = property(obj)*1;
+ });
+ return max;
+ },
+ _split_data_by:function(stats){
+ var any=function(property, data){
+ property = dhtmlx.Template.setter(0,property);
+ return property(data[0]);
+ };
+ var key = dhtmlx.Template.setter(0,stats.by);
+ if (!stats.map[key])
+ stats.map[key] = [key, any];
+
+ var groups = {};
+ var labels = [];
+ this.data.each(function(data){
+ var current = key(data);
+ if (!groups[current]){
+ labels.push({id:current});
+ groups[current] = dhtmlx.toArray();
+ }
+ groups[current].push(data);
+ });
+ for (var prop in stats.map){
+ var functor = (stats.map[prop][1]||any);
+ if (typeof functor != "function")
+ functor = this[functor];
+
+ for (var i=0; i < labels.length; i++) {
+ labels[i][prop]=functor.call(this, stats.map[prop][0], groups[labels[i].id]);
+ }
+ }
+// if (this._settings.sort)
+// labels.sortBy(stats.sort);
+
+ this._not_grouped_data = this.data;
+ this.data = new dhtmlx.DataStore();
+ this.data.provideApi(this,true);
+ this._init_group_data_event(this.data, this);
+ this.parse(labels,"json");
+ },
+ group:function(config,mode){
+ this.ungroup(false);
+ this._split_data_by(config);
+ if (mode!==false)
+ this.render();
+ },
+ ungroup:function(mode){
+ if (this._not_grouped_data){
+ this.data = this._not_grouped_data;
+ this.data.provideApi(this, true);
+ }
+ if (mode!==false)
+ this.render();
+ },
+ group_setter:function(name, config){
+ dhtmlx.assert(typeof config == "object", "Incorrect group value");
+ dhtmlx.assert(config.by,"group.by is mandatory");
+ dhtmlx.assert(config.map,"group.map is mandatory");
+ return config;
+ },
+ //need to be moved to more appropriate object
+ sort_setter:function(name, config){
+ if (typeof config != "object")
+ config = { by:config };
+
+ this._mergeSettings(config,{
+ as:"string",
+ dir:"asc"
+ });
+ return config;
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'key.js'*/
+
+
+/*
+ Behavior:KeyEvents - hears keyboard
+*/
+dhtmlx.KeyEvents = {
+ _init:function(){
+ //attach handler to the main container
+ dhtmlx.event(this._obj,"keypress",this._onKeyPress,this);
+ },
+ //called on each key press , when focus is inside of related component
+ _onKeyPress:function(e){
+ e=e||event;
+ var code = e.keyCode; //FIXME better solution is required
+ this.callEvent((this._edit_id?"onEditKeyPress":"onKeyPress"),[code,e.ctrlKey,e.shiftKey,e]);
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'mouse.js'*/
+
+
+/*
+ Behavior:MouseEvents - provides inner evnets for mouse actions
+*/
+dhtmlx.MouseEvents={
+ _init: function(){
+ //attach dom events if related collection is defined
+ if (this._click){
+ dhtmlx.event(this._obj,"click",this._onClick,this);
+ dhtmlx.event(this._obj,"contextmenu",this._onContext,this);
+ }
+ if (this._dblclick)
+ dhtmlx.event(this._obj,"dblclick",this._onDblClick,this);
+ if (this._mouse_move){
+ dhtmlx.event(this._obj,"mousemove",this._onMouse,this);
+ dhtmlx.event(this._obj,(dhtmlx._isIE?"mouseleave":"mouseout"),this._onMouse,this);
+ }
+
+ },
+ //inner onclick object handler
+ _onClick: function(e) {
+ return this._mouseEvent(e,this._click,"ItemClick");
+ },
+ //inner ondblclick object handler
+ _onDblClick: function(e) {
+ return this._mouseEvent(e,this._dblclick,"ItemDblClick");
+ },
+ //process oncontextmenu events
+ _onContext: function(e) {
+ var id = dhtmlx.html.locate(e, this._id);
+ if (id && !this.callEvent("onBeforeContextMenu", [id,e]))
+ return dhtmlx.html.preventEvent(e);
+ },
+ /*
+ event throttler - ignore events which occurs too fast
+ during mouse moving there are a lot of event firing - we need no so much
+ also, mouseout can fire when moving inside the same html container - we need to ignore such fake calls
+ */
+ _onMouse:function(e){
+ if (dhtmlx._isIE) //make a copy of event, will be used in timed call
+ e = document.createEventObject(event);
+
+ if (this._mouse_move_timer) //clear old event timer
+ window.clearTimeout(this._mouse_move_timer);
+
+ //this event just inform about moving operation, we don't care about details
+ this.callEvent("onMouseMoving",[e]);
+ //set new event timer
+ this._mouse_move_timer = window.setTimeout(dhtmlx.bind(function(){
+ //called only when we have at least 100ms after previous event
+ if (e.type == "mousemove")
+ this._onMouseMove(e);
+ else
+ this._onMouseOut(e);
+ },this),500);
+ },
+ //inner mousemove object handler
+ _onMouseMove: function(e) {
+ if (!this._mouseEvent(e,this._mouse_move,"MouseMove"))
+ this.callEvent("onMouseOut",[e||event]);
+ },
+ //inner mouseout object handler
+ _onMouseOut: function(e) {
+ this.callEvent("onMouseOut",[e||event]);
+ },
+ //common logic for click and dbl-click processing
+ _mouseEvent:function(e,hash,name){
+ e=e||event;
+ var trg=e.target||e.srcElement;
+ var css = "";
+ var id = null;
+ var found = false;
+ //loop through all parents
+ while (trg && trg.parentNode){
+ if (!found){ //if element with ID mark is not detected yet
+ id = trg.getAttribute(this._id); //check id of current one
+ if (id){
+ if (!this.callEvent("on"+name,[id,e,trg])) return; //it will be triggered only for first detected ID, in case of nested elements
+ found = true; //set found flag
+ }
+ }
+ css=trg.className;
+ if (css){ //check if pre-defined reaction for element's css name exists
+ css = css.split(" ");
+ css = css[0]||css[1]; //FIXME:bad solution, workaround css classes which are starting from whitespace
+ if (hash[css])
+ return hash[css].call(this,e,id,trg);
+ }
+ trg=trg.parentNode;
+ }
+ return found; //returns true if item was located and event was triggered
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'config.js'*/
+
+
+/*
+ Behavior:Settings
+
+ @export
+ customize
+ config
+*/
+
+/*DHX:Depend template.js*/
+/*DHX:Depend dhtmlx.js*/
+
+dhtmlx.Settings={
+ _init:function(){
+ /*
+ property can be accessed as this.config.some
+ in same time for inner call it have sense to use _settings
+ because it will be minified in final version
+ */
+ this._settings = this.config=[];
+ this._settings.sort = null; //mask default method
+ },
+ define:function(property, value){
+ if (typeof property == "object")
+ return this._parseSeetingColl(property);
+ return this._define(property, value);
+ },
+ _define:function(property,value){
+ dhtmlx.assert_settings.call(this,property,value);
+
+ //method with name {prop}_setter will be used as property setter
+ //setter is optional
+ var setter = this[property+"_setter"];
+ return this._settings[property]=setter?setter.call(this,property,value):value;
+ },
+ //process configuration object
+ _parseSeetingColl:function(coll){
+ if (coll){
+ for (var a in coll) //for each setting
+ this._define(a,coll[a]); //set value through config
+ }
+ },
+ //helper for object initialization
+ _parseSettings:function(obj,initial){
+ //initial - set of default values
+ var settings = dhtmlx.extend({},initial);
+ //code below will copy all properties over default one
+ if (typeof obj == "object" && !obj.tagName)
+ dhtmlx.extend(settings,obj);
+ //call config for each setting
+ this._parseSeetingColl(settings);
+ },
+ _mergeSettings:function(config, defaults){
+ for (var key in defaults)
+ switch(typeof config[key]){
+ case "object":
+ config[key] = this._mergeSettings((config[key]||{}), defaults[key]);
+ break;
+ case "undefined":
+ config[key] = defaults[key];
+ break;
+ default: //do nothing
+ break;
+ }
+ return config;
+ },
+ //helper for html container init
+ _parseContainer:function(obj,name,fallback){
+ /*
+ parameter can be a config object, in such case real container will be obj.container
+ or it can be html object or ID of html object
+ */
+ if (typeof obj == "object" && !obj.tagName)
+ obj=obj.container;
+ this._obj = dhtmlx.toNode(obj);
+ if (!this._obj && fallback)
+ this._obj = fallback(obj);
+
+ dhtmlx.assert(this._obj, "Incorrect html container");
+
+ this._obj.className+=" "+name;
+ this._obj.onselectstart=function(){return false;}; //block selection by default
+ this._dataobj = this._obj;//separate reference for rendering modules
+ },
+ //apply template-type
+ _set_type:function(name){
+ //parameter can be a hash of settings
+ if (typeof name == "object")
+ return this.type_setter("type",name);
+
+ dhtmlx.assert(this.types, "RenderStack :: Types are not defined");
+ dhtmlx.assert(this.types[name],"RenderStack :: Inccorect type name",name);
+ //or parameter can be a name of existing template-type
+ this.type=dhtmlx.extend({},this.types[name]);
+ this.customize(); //init configs
+ },
+ customize:function(obj){
+ //apply new properties
+ if (obj) dhtmlx.extend(this.type,obj);
+
+ //init tempaltes for item start and item end
+ this.type._item_start = dhtmlx.Template.fromHTML(this.template_item_start(this.type));
+ this.type._item_end = this.template_item_end(this.type);
+
+ //repaint self
+ this.render();
+ },
+ //config.type - creates new template-type, based on configuration object
+ type_setter:function(mode,value){
+ this._set_type(typeof value == "object"?dhtmlx.Type.add(this,value):value);
+ return value;
+ },
+ //config.template - creates new template-type with defined template string
+ template_setter:function(mode,value){
+ return this.type_setter("type",{template:value});
+ },
+ //config.css - css name for top level container
+ css_setter:function(mode,value){
+ this._obj.className += " "+value;
+ return value;
+ }
+};
+
+
+/* DHX DEPEND FROM FILE 'compatibility.js'*/
+
+
+/*
+ Collection of compatibility hacks
+*/
+
+/*DHX:Depend dhtmlx.js*/
+
+dhtmlx.compat=function(name, obj){
+ //check if name hach present, and applies it when necessary
+ if (dhtmlx.compat[name])
+ dhtmlx.compat[name](obj);
+};
+
+
+(function(){
+ if (!window.dhtmlxError){
+ //dhtmlxcommon is not included
+
+ //create fake error tracker for connectors
+ var dummy = function(){};
+ window.dhtmlxError={ catchError:dummy, throwError:dummy };
+ //helpers instead of ones from dhtmlxcommon
+ window.convertStringToBoolean=function(value){
+ return !!value;
+ };
+ window.dhtmlxEventable = function(node){
+ dhtmlx.extend(node,dhtmlx.EventSystem);
+ };
+ //imitate ajax layer of dhtmlxcommon
+ var loader = {
+ getXMLTopNode:function(name){
+
+ },
+ doXPath:function(path){
+ return dhtmlx.DataDriver.xml.xpath(this.xml,path);
+ },
+ xmlDoc:{
+ responseXML:true
+ }
+ };
+ //wrap ajax methods of dataprocessor
+ dhtmlx.compat.dataProcessor=function(obj){
+ //FIXME
+ //this is pretty ugly solution - we replace whole method , so changes in dataprocessor need to be reflected here
+
+ var sendData = "_sendData";
+ var in_progress = "_in_progress";
+ var tMode = "_tMode";
+ var waitMode = "_waitMode";
+
+ obj[sendData]=function(a1,rowId){
+ if (!a1) return; //nothing to send
+ if (rowId)
+ this[in_progress][rowId]=(new Date()).valueOf();
+
+ if (!this.callEvent("onBeforeDataSending",rowId?[rowId,this.getState(rowId)]:[])) return false;
+
+ var a2 = this;
+ var a3=this.serverProcessor;
+ if (this[tMode]!="POST")
+ //use dhtmlx.ajax instead of old ajax layer
+ dhtmlx.ajax().get(a3+((a3.indexOf("?")!=-1)?"&":"?")+a1,"",function(t,x,xml){
+ loader.xml = dhtmlx.DataDriver.xml.checkResponse(t,x);
+ a2.afterUpdate(a2, null, null, null, loader);
+ });
+ else
+ dhtmlx.ajax().post(a3,a1,function(t,x,xml){
+ loader.xml = dhtmlx.DataDriver.xml.checkResponse(t,x);
+ a2.afterUpdate(a2, null, null, null, loader);
+ });
+
+ this[waitMode]++;
+ };
+ };
+ }
+
+})();
+
+
+/* DHX DEPEND FROM FILE 'compatibility_layout.js'*/
+
+
+/*DHX:Depend dhtmlx.js*/
+/*DHX:Depend compatibility.js*/
+
+if (!dhtmlx.attaches)
+ dhtmlx.attaches = {};
+
+dhtmlx.attaches.attachAbstract=function(name, conf){
+ var obj = document.createElement("DIV");
+ obj.id = "CustomObject_"+dhtmlx.uid();
+ obj.style.width = "100%";
+ obj.style.height = "100%";
+ obj.cmp = "grid";
+ document.body.appendChild(obj);
+ this.attachObject(obj.id);
+
+ conf.container = obj.id;
+ this.grid = new window[name](conf);
+
+ this.gridId = obj.id;
+ this.gridObj = obj;
+ return this.grid;
+};
+dhtmlx.attaches.attachDataView = function(conf){
+ return this.attachAbstract("dhtmlXDataView",conf);
+};
+dhtmlx.attaches.attachChart = function(conf){
+ return this.attachAbstract("dhtmlXChart",conf);
+};
+
+dhtmlx.compat.layout = function(){
+if (window.dhtmlXChart)
+ dhtmlXChart.prototype.setSizes = function(){
+ this.resize();
+ };
+if (window.dhtmlXDataView)
+ dhtmlXDataView.prototype.setSizes = function(){
+ this.render();
+ };
+};
+
+
+/* DHX DEPEND FROM FILE 'load.js'*/
+
+
+/*
+ ajax operations
+
+ can be used for direct loading as
+ dhtmlx.ajax(ulr, callback)
+ or
+ dhtmlx.ajax().get(url)
+ dhtmlx.ajax().post(url)
+
+*/
+
+/*DHX:Depend datastore.js*/
+/*DHX:Depend dhtmlx.js*/
+
+dhtmlx.ajax = function(url,call,master){
+ //if parameters was provided - made fast call
+ if (arguments.length!==0){
+ var http_request = new dhtmlx.ajax();
+ if (master) http_request.master=master;
+ http_request.get(url,null,call);
+ }
+ if (!this.getXHR) return new dhtmlx.ajax(); //allow to create new instance without direct new declaration
+
+ return this;
+};
+dhtmlx.ajax.prototype={
+ //creates xmlHTTP object
+ getXHR:function(){
+ if (dhtmlx._isIE)
+ return new ActiveXObject("Microsoft.xmlHTTP");
+ else
+ return new XMLHttpRequest();
+ },
+ /*
+ send data to the server
+ params - hash of properties which will be added to the url
+ call - callback, can be an array of functions
+ */
+ send:function(url,params,call){
+ var x=this.getXHR();
+ if (typeof call == "function")
+ call = [call];
+ //add extra params to the url
+ if (typeof params == "object"){
+ var t=[];
+ for (var a in params)
+ t.push(a+"="+encodeURIComponent(params[a]));// utf-8 escaping
+ params=t.join("&");
+ }
+ if (params && !this.post){
+ url=url+(url.indexOf("?")!=-1 ? "&" : "?")+params;
+ params=null;
+ }
+
+ x.open(this.post?"POST":"GET",url,!this._sync);
+ if (this.post)
+ x.setRequestHeader('Content-type','application/x-www-form-urlencoded');
+
+ //async mode, define loading callback
+ if (!this._sync){
+ var self=this;
+ x.onreadystatechange= function(){
+ if (!x.readyState || x.readyState == 4){
+ dhtmlx.log_full_time("data_loading"); //log rendering time
+ if (call && self)
+ for (var i=0; i < call.length; i++) //there can be multiple callbacks
+ if (call[i])
+ call[i].call((self.master||self),x.responseText,x.responseXML,x);
+ self.master=null;
+ call=x=self=null; //anti-leak
+ }
+ };
+ }
+
+ x.send(params||null);
+ return x; //return XHR, which can be used in case of sync. mode
+ },
+ //GET request
+ get:function(url,params,call){
+ this.post=false;
+ return this.send(url,params,call);
+ },
+ //POST request
+ post:function(url,params,call){
+ this.post=true;
+ return this.send(url,params,call);
+ },
+ sync:function(){
+ this._sync = true;
+ return this;
+ }
+};
+
+
+/*
+ Behavior:DataLoader - load data in the component
+
+ @export
+ load
+ parse
+*/
+dhtmlx.DataLoader={
+ _init:function(){
+ //prepare data store
+ this.data = new dhtmlx.DataStore();
+ },
+ //loads data from external URL
+ load:function(url,call){
+ this.callEvent("onXLS",[]);
+ if (typeof call == "string"){ //second parameter can be a loading type or callback
+ this.data.setDriver(call);
+ call = arguments[2];
+ }
+ //prepare data feed for dyn. loading
+ if (!this.data.feed)
+ this.data.feed = function(from,count){
+ //allow only single request at same time
+ if (this._load_count)
+ return this._load_count=[from,count]; //save last ignored request
+ else
+ this._load_count=true;
+
+ this.load(url+((url.indexOf("?")==-1)?"?":"&")+"posStart="+from+"&count="+count,function(){
+ //after loading check if we have some ignored requests
+ var temp = this._load_count;
+ this._load_count = false;
+ if (typeof temp =="object")
+ this.data.feed.apply(this, temp); //load last ignored request
+ });
+ };
+ //load data by async ajax call
+ dhtmlx.ajax(url,[this._onLoad,call],this);
+ },
+ //loads data from object
+ parse:function(data,type){
+ this.callEvent("onXLS",[]);
+ if (type)
+ this.data.setDriver(type);
+ this._onLoad(data,null);
+ },
+ //default after loading callback
+ _onLoad:function(text,xml,loader){
+ this.data._parse(this.data.driver.toObject(text,xml));
+ this.callEvent("onXLE",[]);
+ }
+};
+
+/*
+ Abstraction layer for different data types
+*/
+
+dhtmlx.DataDriver={};
+dhtmlx.DataDriver.json={
+ //convert json string to json object if necessary
+ toObject:function(data){
+ if (typeof data == "string"){
+ eval ("dhtmlx.temp="+data);
+ return dhtmlx.temp;
+ }
+ return data;
+ },
+ //get array of records
+ getRecords:function(data){
+ if (data && !(data instanceof Array))
+ return [data];
+ return data;
+ },
+ //get hash of properties for single record
+ getDetails:function(data){
+ return data;
+ },
+ //get count of data and position at which new data need to be inserted
+ getInfo:function(data){
+ return {
+ _size:(data.total_count||0),
+ _from:(data.pos||0)
+ };
+ }
+};
+
+dhtmlx.DataDriver.html={
+ /*
+ incoming data can be
+ - collection of nodes
+ - ID of parent container
+ - HTML text
+ */
+ toObject:function(data){
+ if (typeof data == "string"){
+ var t=null;
+ if (data.indexOf("<")==-1) //if no tags inside - probably its an ID
+ t = dhtmlx.toNode(data);
+ if (!t){
+ t=document.createElement("DIV");
+ t.innerHTML = data;
+ }
+
+ return t.getElementsByTagName(this.tag);
+ }
+ return data;
+ },
+ //get array of records
+ getRecords:function(data){
+ if (data.tagName)
+ return data.childNodes;
+ return data;
+ },
+ //get hash of properties for single record
+ getDetails:function(data){
+ return dhtmlx.DataDriver.xml.tagToObject(data);
+ },
+ //dyn loading is not supported by HTML data source
+ getInfo:function(data){
+ return {
+ _size:0,
+ _from:0
+ };
+ },
+ tag: "LI"
+};
+
+dhtmlx.DataDriver.jsarray={
+ //eval jsarray string to jsarray object if necessary
+ toObject:function(data){
+ if (typeof data == "string"){
+ eval ("dhtmlx.temp="+data);
+ return dhtmlx.temp;
+ }
+ return data;
+ },
+ //get array of records
+ getRecords:function(data){
+ return data;
+ },
+ //get hash of properties for single record, in case of array they will have names as "data{index}"
+ getDetails:function(data){
+ var result = {};
+ for (var i=0; i < data.length; i++)
+ result["data"+i]=data[i];
+
+ return result;
+ },
+ //dyn loading is not supported by js-array data source
+ getInfo:function(data){
+ return {
+ _size:0,
+ _from:0
+ };
+ }
+};
+
+dhtmlx.DataDriver.csv={
+ //incoming data always a string
+ toObject:function(data){
+ return data;
+ },
+ //get array of records
+ getRecords:function(data){
+ return data.split(this.row);
+ },
+ //get hash of properties for single record, data named as "data{index}"
+ getDetails:function(data){
+ data = this.stringToArray(data);
+ var result = {};
+ for (var i=0; i < data.length; i++)
+ result["data"+i]=data[i];
+
+ return result;
+ },
+ //dyn loading is not supported by csv data source
+ getInfo:function(data){
+ return {
+ _size:0,
+ _from:0
+ };
+ },
+ //split string in array, takes string surrounding quotes in account
+ stringToArray:function(data){
+ data = data.split(this.cell);
+ for (var i=0; i < data.length; i++)
+ data[i] = data[i].replace(/^[ \t\n\r]*(\"|)/g,"").replace(/(\"|)[ \t\n\r]*$/g,"");
+ return data;
+ },
+ row:"\n", //default row separator
+ cell:"," //default cell separator
+};
+
+dhtmlx.DataDriver.xml={
+ //convert xml string to xml object if necessary
+ toObject:function(text,xml){
+ if (xml && (xml=this.checkResponse(text,xml))) //checkResponse - fix incorrect content type and extra whitespaces errors
+ return xml;
+ if (typeof text == "string"){
+ return this.fromString(text);
+ }
+ return text;
+ },
+ //get array of records
+ getRecords:function(data){
+ return this.xpath(data,this.records);
+ },
+ records:"/*/item",
+ userdata:"/*/userdata",
+ //get hash of properties for single record
+ getDetails:function(data){
+ return this.tagToObject(data,{});
+ },
+ getUserData:function(data,col){
+ col = col || {};
+ var ud = this.xpath(data,this.userdata);
+ for (var i=0; i < ud.length; i++) {
+ var udx = this.tagToObject(ud[i]);
+ col[udx.name] = udx.value;
+ }
+ return col;
+ },
+ //get count of data and position at which new data_loading need to be inserted
+ getInfo:function(data){
+ return {
+ _size:(data.documentElement.getAttribute("total_count")||0),
+ _from:(data.documentElement.getAttribute("pos")||0)
+ };
+ },
+ //xpath helper
+ xpath:function(xml,path){
+ if (window.XPathResult){ //FF, KHTML, Opera
+ var node=xml;
+ if(xml.nodeName.indexOf("document")==-1)
+ xml=xml.ownerDocument;
+ var res = [];
+ var col = xml.evaluate(path, node, null, XPathResult.ANY_TYPE, null);
+ var temp = col.iterateNext();
+ while (temp){
+ res.push(temp);
+ temp = col.iterateNext();
+ }
+ return res;
+ } //IE
+ return xml.selectNodes(path);
+ },
+ //convert xml tag to js object, all subtags and attributes are mapped to the properties of result object
+ tagToObject:function(tag,z){
+ z=z||{};
+ //map attributes
+ var a=tag.attributes;
+ for (var i=0; i5?10:5);
+ step = parseInt(stepVal,10)*calculStep;
+
+ if(step>Math.abs(nmin)) start = (nmin<0?-step:0);
+ else{
+
+ var absNmin = Math.abs(nmin);
+ var powerStart = Math.floor(this.log10(absNmin));
+ var nminVal = absNmin/Math.pow(10,powerStart);
+ start = Math.ceil(nminVal*10)/10*Math.pow(10,powerStart)-step;
+ if(nmin<0) start =-start-2*step;
+ }
+ var end = start;
+ while(end<=nmax){
+ end += step;
+ end = parseFloat((new Number(end)).toFixed(Math.abs(power)));
+ }
+ return {start:start,end:end,step:step,fixNum:Math.abs(power)};
+ },
+ log10:function(n){
+ return Math.floor((Math.log(n)/Math.LN10));
+ },
+ _drawXAxisLabel:function(x,obj){
+ if (!this._settings.xAxis) return;
+ var elem = this.renderTextAt(false, true, x,
+ this._obj.offsetHeight-parseInt(this._settings.padding.bottom,10),
+ this._settings.xAxis.template(obj));
+ },
+ _drawXAxisLine:function(ctx,x,y1,y2){
+ if (!this._settings.xAxis||!this._settings.xAxis.lines) return;
+ this._drawLine(ctx,x,y1,x,y2,this._settings.xAxis.color,0.2);
+ },
+ _drawLine:function(ctx,x1,y1,x2,y2,color,width){
+ ctx.strokeStyle = color;
+ ctx.lineWidth = width;
+ ctx.beginPath();
+ ctx.moveTo(x1,y1);
+ ctx.lineTo(x2,y2);
+ ctx.stroke();
+ },
+ _getRelativeValue:function(minValue,maxValue){
+ var relValue;
+ var valueFactor = 1;
+ if(maxValue != minValue){
+ relValue = maxValue - minValue;
+ if(Math.abs(relValue) < 1){
+ while(Math.abs(relValue)<1){
+ valueFactor *= 10;
+ relValue *= valueFactor;
+ }
+ }
+ }
+ else relValue = minValue;
+ return [relValue,valueFactor];
+ },
+ _rainbow : [
+ function(pos){ return "#FF"+dhtmlx.math.toHex(pos/2,2)+"00";},
+ function(pos){ return "#FF"+dhtmlx.math.toHex(pos/2+128,2)+"00";},
+ function(pos){ return "#"+dhtmlx.math.toHex(255-pos,2)+"FF00";},
+ function(pos){ return "#00FF"+dhtmlx.math.toHex(pos,2);},
+ function(pos){ return "#00"+dhtmlx.math.toHex(255-pos,2)+"FF";},
+ function(pos){ return "#"+dhtmlx.math.toHex(pos,2)+"00FF";}
+ ]
+};
+
+dhtmlx.compat("layout");
\ No newline at end of file
diff --git a/addons/base_graph/static/src/js/graph.js b/addons/base_graph/static/src/js/graph.js
new file mode 100644
index 00000000000..fdf3585449c
--- /dev/null
+++ b/addons/base_graph/static/src/js/graph.js
@@ -0,0 +1,471 @@
+/*---------------------------------------------------------
+ * OpenERP base_graph
+ *---------------------------------------------------------*/
+
+openerp.base_graph = function (openerp) {
+var COLOR_PALETTE = [
+ '#cc99ff', '#ccccff', '#48D1CC', '#CFD784', '#8B7B8B', '#75507b',
+ '#b0008c', '#ff0000', '#ff8e00', '#9000ff', '#0078ff', '#00ff00',
+ '#e6ff00', '#ffff00', '#905000', '#9b0000', '#840067', '#9abe00',
+ '#ffc900', '#510090', '#0000c9', '#009b00', '#75507b', '#3465a4',
+ '#73d216', '#c17d11', '#edd400', '#fcaf3e', '#ef2929', '#ff00c9',
+ '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f', '#f57900',
+ '#cc0000', '#d400a8'];
+
+QWeb.add_template('/base_graph/static/src/xml/base_graph.xml');
+openerp.base.views.add('graph', 'openerp.base_graph.GraphView');
+openerp.base_graph.GraphView = openerp.base.View.extend({
+
+ init: function(view_manager, session, element_id, dataset, view_id) {
+ this._super(session, element_id);
+ this.view_manager = view_manager;
+ this.dataset = dataset;
+ this.dataset_index = 0;
+ this.model = this.dataset.model;
+ this.view_id = view_id;
+ },
+ do_show: function () {
+ // TODO: re-trigger search
+ this.$element.show();
+ },
+ do_hide: function () {
+ this.$element.hide();
+ },
+ start: function() {
+ return this.rpc("/base_graph/graphview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
+ },
+ on_loaded: function(data) {
+ this.all_fields = data.all_fields;
+ this.fields_view = data.fields_view;
+ this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
+ this.view_id = this.fields_view.view_id;
+ this.chart = this.fields_view.arch.attrs.type || 'pie';
+ this.fields = this.fields_view.fields;
+ this.chart_info_fields = [];
+ this.operator_field = '';
+ this.operator_field_one = '';
+ this.operator = [];
+ this.group_field = [];
+ this.orientation = this.fields_view.arch.attrs.orientation || '';
+ this.elem_id = this.$element[0]['id'];
+
+ _.each(this.fields_view.arch.children, function (field) {
+ if (field.attrs.operator) {
+ this.operator.push(field.attrs.name);
+ }
+ else if (field.attrs.group) {
+ this.group_field.push(field.attrs.name);
+ }
+ else {
+ this.chart_info_fields.push(field.attrs.name);
+ }
+ }, this);
+
+ this.operator_field = this.operator[0];
+ if(this.operator.length > 1){
+ this.operator_field_one = this.operator[1];
+ }
+ if(this.operator == ''){
+ this.operator_field = this.chart_info_fields[1];
+ }
+ this.chart_info = this.chart_info_fields[0];
+ this.x_title = this.fields[this.chart_info_fields[0]]['string'];
+ this.y_title = this.fields[this.operator_field]['string'];
+ this.load_chart();
+ },
+
+ load_chart: function(data) {
+ var self = this;
+ if(data){
+ this.x_title = this.all_fields[this.chart_info_fields]['string'];
+ this.y_title = this.all_fields[this.operator_field]['string'];
+ self.schedule_chart(data);
+ }else{
+ this.dataset.read_slice(this.fields, 0, false, function(res) {
+ self.schedule_chart(res);
+ });
+ }
+ },
+
+ schedule_chart: function(results) {
+ this.$element.html(QWeb.render("GraphView", {"fields_view": this.fields_view, "chart": this.chart,'elem_id': this.elem_id}));
+
+ _.each(results, function (result) {
+ _.each(result, function (field_value, field_name) {
+ if (typeof field_value == 'object') {
+ result[field_name] = field_value[field_value.length - 1];
+ }
+ if (typeof field_value == 'string') {
+ var choices = this.all_fields[field_name]['selection'];
+ _.each(choices, function (choice) {
+ if (field_value == choice[0]) {
+ result[field_name] = choice;
+ }
+ });
+ }
+ }, this);
+ }, this);
+
+ var graph_data = {};
+ _.each(results, function (result) {
+ var group_key = [];
+ if(this.group_field.length){
+ _.each(this.group_field, function (res) {
+ result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
+ group_key.push(result[res]);
+ });
+ }else{
+ group_key.push(result[this.group_field]);
+ }
+ var column_key = result[this.chart_info_fields] + "_" + group_key;
+ var column_descriptor = {};
+ if (graph_data[column_key] == undefined) {
+ column_descriptor[this.operator_field] = result[this.operator_field];
+ if (this.operator.length > 1) {
+ column_descriptor[this.operator_field_one] = result[this.operator_field_one];
+ }
+ column_descriptor[this.chart_info_fields] = result[this.chart_info_fields];
+ if(this.group_field.length){
+ _.each(this.group_field, function (res) {
+ column_descriptor[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
+ });
+ }
+ } else {
+ column_descriptor = graph_data[column_key];
+ column_descriptor[this.operator_field] += result[this.operator_field];
+ if (this.operator.length > 1) {
+ column_descriptor[this.operator_field_one] += result[this.operator_field_one];
+ }
+ }
+ graph_data[column_key] = column_descriptor;
+ }, this);
+
+ if (this.chart == 'bar') {
+ return this.schedule_bar(_.values(graph_data));
+ } else if (this.chart == "pie") {
+ return this.schedule_pie(_.values(graph_data));
+ }
+ },
+
+ schedule_bar: function(results) {
+ var self = this;
+ var view_chart = '';
+ var group_list = [];
+ var legend_list = [];
+ var newkey = '', newkey_one;
+ var string_legend = '';
+
+ if((self.group_field.length) && (this.operator.length <= 1)){
+ view_chart = self.orientation == 'horizontal'? 'stackedBarH' : 'stackedBar';
+ }else{
+ view_chart = self.orientation == 'horizontal'? 'barH' : 'bar';
+ }
+
+ _.each(results, function (result) {
+ if ((self.group_field.length) && (this.operator.length <= 1)) {
+ var legend_key = '';
+ _.each(self.group_field, function (res) {
+ result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
+ legend_key += result[res];
+ });
+ newkey = legend_key.replace(/\s+/g,'_').replace(/[^a-zA-Z 0-9]+/g,'_');
+ string_legend = legend_key;
+ } else {
+ newkey = string_legend = "val";
+ }
+
+ if (_.contains(group_list, newkey) && _.contains(legend_list, string_legend)) {
+ return;
+ }
+ group_list.push(newkey);
+ legend_list.push(string_legend);
+
+ if (this.operator.length > 1) {
+ newkey_one = "val1";
+ group_list.push(newkey_one);
+ legend_list.push(newkey_one);
+ }
+ }, this);
+
+ if (group_list.length <=1){
+ group_list = [];
+ legend_list = [];
+ newkey = string_legend = "val";
+ group_list.push(newkey);
+ legend_list.push(string_legend);
+ }
+
+ var abscissa_data = {};
+ _.each(results, function (result) {
+ var label = result[self.chart_info_fields],
+ section = {};
+ if ((self.group_field.length) && (group_list.length > 1) && (self.operator.length <= 1)){
+ var legend_key_two = '';
+ _.each(self.group_field, function (res) {
+ result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
+ legend_key_two += result[res];
+ });
+ newkey = legend_key_two.replace(/\s+/g,'_').replace(/[^a-zA-Z 0-9]+/g,'_');
+ }else{
+ newkey = "val";
+ }
+ if (abscissa_data[label] == undefined){
+ section[self.chart_info_fields] = label;
+ _.each(group_list, function (group) {
+ section[group] = 0;
+ });
+ } else {
+ section = abscissa_data[label];
+ }
+ section[newkey] = result[self.operator_field];
+ if (self.operator.length > 1){
+ section[newkey_one] = result[self.operator_field_one];
+ }
+ abscissa_data[label] = section;
+ });
+
+ //for legend color
+ var grp_color = _.map(legend_list, function (group_legend, index) {
+ var legend = {color: COLOR_PALETTE[index]};
+ if (group_legend == "val"){
+ legend['text'] = self.fields[self.operator_field]['string']
+ }else if(group_legend == "val1"){
+ legend['text'] = self.fields[self.operator_field_one]['string']
+ }else{
+ legend['text'] = group_legend;
+ }
+ return legend;
+ });
+
+ //for axis's value and title
+ var max,min,step;
+ var maximum,minimum;
+ if(_.isEmpty(abscissa_data)){
+ max = 9;
+ min = 0;
+ step=1;
+ }else{
+ var max_min = [];
+ _.each(abscissa_data, function (abscissa_datas) {
+ _.each(group_list, function(res){
+ max_min.push(abscissa_datas[res]);
+ });
+ });
+ maximum = Math.max.apply(Math,max_min);
+ minimum = Math.min.apply(Math,max_min);
+ if (maximum == minimum){
+ if (maximum == 0){
+ max = 9;
+ min = 0;
+ step=1;
+ }else if(maximum > 0){
+ max = maximum + (10 - maximum % 10);
+ min = 0;
+ step = Math.round(max/10);
+ }else{
+ max = 0;
+ min = minimum - (10 + minimum % 10);
+ step = Math.round(Math.abs(min)/10);
+ }
+ }
+ }
+ var abscissa_description = {
+ template: self.chart_info_fields,
+ title: ""+self.x_title+""
+ };
+
+ var ordinate_description = {
+ lines: true,
+ title: ""+self.y_title+"",
+ start: min,
+ step: step,
+ end: max
+ };
+
+ var x_axis, y_axis, tooltip;
+ if (self.orientation == 'horizontal'){
+ x_axis = ordinate_description;
+ y_axis = abscissa_description;
+ }else{
+ x_axis = abscissa_description;
+ y_axis = ordinate_description;
+ }
+ tooltip = self.chart_info_fields;
+
+ var bar_chart = new dhtmlXChart({
+ view: view_chart,
+ container: self.elem_id+"-barchart",
+ value:"#"+group_list[0]+"#",
+ gradient: "3d",
+ border: false,
+ width: 1024,
+ tooltip:{
+ template:"#"+tooltip+"#"+","+grp_color[0]['text']+"="+"#"+group_list[0]+"#"
+ },
+ radius: 0,
+ color:grp_color[0]['color'],
+ origin:0,
+ xAxis:{
+ template:function(obj){
+ if(x_axis['template']){
+ var val = obj[x_axis['template']];
+ val = (typeof val == 'object')?val[1]:(!val?'Undefined':val);
+ if(val.length > 12){
+ val = val.substring(0,12);
+ }
+ return val;
+ }else{
+ return obj;
+ }
+ },
+ title:x_axis['title'],
+ lines:x_axis['lines']
+ },
+ yAxis:{
+ template:function(obj){
+ if(y_axis['template']){
+ var vals = obj[y_axis['template']];
+ vals = (typeof vals == 'object')?vals[1]:(!vals?'Undefined':vals);
+ if(vals.length > 12){
+ vals = vals.substring(0,12);
+ }
+ return vals;
+ }else{
+ return obj;
+ }
+ },
+ title:y_axis['title'],
+ lines: y_axis['lines'],
+ start:y_axis['start'],
+ step:y_axis['step'],
+ end:y_axis['end']
+ },
+ padding: {
+ left: 75
+ },
+ legend: {
+ values: grp_color,
+ align:"left",
+ valign:"top",
+ layout: "x",
+ marker:{
+ type:"round",
+ width:12
+ }
+ }
+ });
+ for (var m = 1; m
+
+
+
+
+
diff --git a/addons/base_hello/__openerp__.py b/addons/base_hello/__openerp__.py
index d52b5c82ec1..7a80645f3b8 100644
--- a/addons/base_hello/__openerp__.py
+++ b/addons/base_hello/__openerp__.py
@@ -4,4 +4,5 @@
"depends": [],
"js": ["static/*/*.js", "static/*/js/*.js"],
"css": [],
+ 'active': False,
}
diff --git a/addons/web_mobile/static/src/js/web_mobile.js b/addons/web_mobile/static/src/js/web_mobile.js
index 08d9e81f4be..059c374d7d4 100644
--- a/addons/web_mobile/static/src/js/web_mobile.js
+++ b/addons/web_mobile/static/src/js/web_mobile.js
@@ -90,11 +90,8 @@ openerp.web_mobile.ListView = openerp.base.Controller.extend({
on_action: function(action) {
var self = this;
var view_id = action.views[0][0];
- var model = action.res_model;
-
- self.rpc('/base/dataset/search_read', {
- model: model
- },function(result){
+ (new openerp.base.DataSetSearch(this.session, action.res_model, null, null))
+ .read_slice(false, false, false, function(result){
this.listview = new openerp.web_mobile.ListView(this.session, "oe_app");
self.$element.html(QWeb.render("ListView", {'records' : result}));
});
diff --git a/doc/source/addons.rst b/doc/source/addons.rst
index 7675b829f92..a99737221ce 100644
--- a/doc/source/addons.rst
+++ b/doc/source/addons.rst
@@ -110,6 +110,79 @@ initializing the addon.
});
}
+Creating new standard roles
+---------------------------
+
+Views
++++++
+
+Views are the standard high-level component in OpenERP. A view type corresponds
+to a way to display a set of data (coming from an OpenERP model).
+
+In OpenERP Web, views are standard objects registered against a dedicated
+object registry, so the :js:class:`~openerp.base.ViewManager` knows where to
+find and how to call them.
+
+Although not mandatory, it is recommended that views inherit from
+:js:class:`openerp.base.View`, which provides a view useful services to its
+children.
+
+Registering a view
+~~~~~~~~~~~~~~~~~~
+
+This is the first task to perform when creating a view, and the simplest by
+far: simply call ``openerp.base.views.add(name, object_path)`` to register
+the object of path ``object_path`` as the view for the view name ``name``.
+
+The view name is the name you gave to your new view in the OpenERP server.
+
+From that point onwards, OpenERP Web will be able to find your object and
+instantiate it.
+
+Standard view behaviors
+~~~~~~~~~~~~~~~~~~~~~~~
+
+In the normal OpenERP Web flow, views have to implement a number of methods so
+view managers can correctly communicate with them:
+
+``start()``
+ This method will always be called after creating the view (via its
+ constructor), but not necessarily immediately.
+
+ It is called with no arguments and should handle the heavy setup work,
+ including remote call (to load the view's setup data from the server via
+ e.g. ``fields_view_get``, for instance).
+
+ ``start`` should return a `promise object`_ which *must* be resolved when
+ the view's setup is completed. This promise is used by view managers to
+ know when they can start interacting with the view.
+
+``do_hide()``
+ Called by the view manager when it wants to replace this view by an other
+ one, but wants to keep this view around to re-activate it later.
+
+ Should put the view in some sort of hibernation mode, and *must* hide its
+ DOM elements.
+
+``do_show()``
+ Called when the view manager wants to re-display the view after having
+ hidden it. The view should refresh its data display upon receiving this
+ notification
+
+``do_search(domains: Array, contexts: Array, groupbys: Array)``
+ If the view is searchable, this method is called to notify it of a search
+ against it.
+
+ It should use the provided query data to perform a search and refresh its
+ internal content (and display).
+
+ All views are searchable by default, but they can be made non-searchable
+ by setting the property ``searchable`` to ``false``.
+
+ This can be done either on the view class itself (at the same level as
+ defining e.g. the ``start`` method) or at the instance level (in the
+ class's ``init``), though you should generally set it on the class.
+
Utility behaviors
-----------------
@@ -423,3 +496,6 @@ Python
.. _nose:
http://somethingaboutorange.com/mrl/projects/nose/1.0.0/
+
+.. _promise object:
+ http://api.jquery.com/deferred.promise/
diff --git a/doc/source/project.rst b/doc/source/project.rst
index faa15da2163..aa465aa3f9f 100644
--- a/doc/source/project.rst
+++ b/doc/source/project.rst
@@ -16,6 +16,158 @@ Source code repository
Merge proposals
+++++++++++++++
+Coding issues and coding conventions
+++++++++++++++++++++++++++++++++++++
+
+Javascript coding
+~~~~~~~~~~~~~~~~~
+
+These are a number of guidelines for javascript code. More than coding
+conventions, these are warnings against potentially harmful or sub-par
+constructs.
+
+Ideally, you should be able to configure your editor or IDE to warn you against
+these kinds of issues.
+
+Use ``var`` for *all* declarations
+**********************************
+
+In javascript (as opposed to Python), assigning to a variable which does not
+already exist and is not explicitly declared (via ``var``) will implicitly
+create a global variable. This is bad for a number of reasons:
+
+* It leaks information outside function scopes
+* It keeps memory of previous run, with potentially buggy behaviors
+* It may conflict with other functions with the same issue
+* It makes code harder to statically check (via e.g. IDE inspectors)
+
+.. note::
+ It is perfectly possible to use ``var`` in ``for`` loops:
+
+ .. code-block:: javascript
+
+ for (var i = 0; i < some_array.length; ++i) {
+ // code here
+ }
+
+ this is not an issue
+
+All local *and global* variables should be declared via ``var``.
+
+.. note:: generally speaking, you should not need globals in OpenERP Web: you
+ can just declare a variable local to your top-level function. This
+ way, if your widget/addon is instantiated several times on the same
+ page (because it's used in embedded mode) each instance will have its
+ own internal but global-to-its-objects data.
+
+Do not leave trailing commas in object literals
+***********************************************
+
+While it is legal to leave trailing commas in Python dictionaries, e.g.
+
+.. code-block:: python
+
+ foo = {
+ 'a': 1,
+ 'b': 2,
+ }
+
+and it's valid in ECMAScript 5 and most browsers support it in Javascript, you
+should *never* use trailing commas in Javascript object literals:
+
+* Internet Explorer does *not* support trailing commas (at least until and
+ including Internet Explorer 8), and trailing comma will cause hard-to-debug
+ errors in it
+
+* JSON does not accept trailing comma (it is a syntax error), and using them
+ in object literals puts you at risks of using them in literal JSON strings
+ as well (though there are few reasons to write JSON by hand)
+
+*Never* use ``for … in`` to iterate on arrays
+*********************************************
+
+:ref:`Iterating over an object with for…in is a bit tricky already
+`, it is far more complex than in Python (where it Just
+Works™) due to the interaction of various Javascript features, but to iterate
+on arrays it becomes downright deadly and errorneous: ``for…in`` really
+iterates over an *object*'s *properties*.
+
+With an array, this has the following consequences:
+
+* It does not necessarily iterate in numerical order, nor does it iterate in
+ any kind of set order. The order is implementation-dependent and may vary
+ from one run to the next depending on a number of reasons and implementation
+ details.
+* If properties are added to an array, to ``Array.prototype`` or to
+ ``Object.prototype`` (the latter two should not happen in well-behaved
+ javascript code, but you never know...) those properties *will* be iterated
+ over by ``for…in``. While ``Object.hasOwnProperty`` will guard against
+ iterating prototype properties, they will not guard against properties set
+ on the array instance itself (as memoizers for instance).
+
+ Note that this includes setting negative keys on arrays.
+
+For this reason, ``for…in`` should **never** be used on array objects. Instead,
+you should use either a normal ``for`` or (even better, unless you have
+profiled the code and found a hotspot) one of Underscore's array iteration
+methods (`_.each`_, `_.map`_, `_.filter`_, etc...).
+
+Underscore is guaranteed to be bundled and available in OpenERP Web scopes.
+
+.. _for-in-iteration:
+
+Use ``hasOwnProperty`` when iterating on an object with ``for … in``
+********************************************************************
+
+``for…in`` is Javascript's built-in facility for iterating over and object's
+properties.
+
+`It is also fairly tricky to use`_: it iterates over *all* non-builtin
+properties of your objects [#]_, which includes methods of an object's class.
+
+As a result, when iterating over an object with ``for…in`` the first line of
+the body *should* generally be a call to `Object.hasOwnProperty`_. This call
+will check whether the property was set directly on the object or comes from
+the object's class:
+
+.. code-block:: javascript
+
+ for(var key in ob) {
+ if (!ob.hasOwnProperty(key)) {
+ // comes from ob's class
+ continue;
+ }
+ // do stuff with key
+ }
+
+Since properties can be added directly to e.g. ``Object.prototype`` (even
+though it's usually considered bad style), you should not assume you ever know
+which properties ``for…in`` is going to iterate over.
+
+An alternative is to use Underscore's iteration methods, which generally work
+over objects as well as arrays:
+
+Instead of
+
+.. code-block:: javascript
+
+ for (var key in ob) {
+ if (!ob.hasOwnProperty(key)) { continue; }
+ var value = ob[key];
+ // Do stuff with key and value
+ }
+
+you could write:
+
+.. code-block:: javascript
+
+ _.each(ob, function (value, key) {
+ // do stuff with key and value
+ });
+
+and not worry about the details of the iteration: underscore should do the
+right thing for you on its own [#]_.
+
Writing documentation
+++++++++++++++++++++
@@ -44,14 +196,18 @@ RST, using Sphinx's `Python domain`_ [#]_:
docstring, using Sphinx's `info fields`_
For parameters types, built-in and stdlib types should be using the
- combined syntax::
+ combined syntax:
+
+ .. code-block:: restructuredtext
:param dict foo: what the purpose of foo is
unless a more extensive explanation needs to be given (e.g. the
specification that the input should be a list of 3-tuple needs to
use ``:type:`` even though all types involved are built-ins). Any
- other type should be specified in full using the ``:type:`` field::
+ other type should be specified in full using the ``:type:`` field
+
+ .. code-block:: restructuredtext
:param foo: what the purpose of foo is
:type foo: some.addon.Class
@@ -87,12 +243,16 @@ when writing javascript API documentation:
are not documented, or JsDoc will not understand what they are and
will not generate documentation for their content.
- As a result, the bare minimum for a namespace is::
+ As a result, the bare minimum for a namespace is:
+
+ .. code-block:: javascript
/** @namespace */
foo.bar.baz = {};
- while for a class it is::
+ while for a class it is:
+
+ .. code-block:: javascript
/** @class */
foo.bar.baz.Qux = [...]
@@ -189,7 +349,6 @@ with the descriptions and irrelevant atttributes stripped):
.. code-block:: javascript
-
openerp.base.search.Widget = openerp.base.Controller.extend(
/** @lends openerp.base.search.Widget# */{
/**
@@ -220,6 +379,30 @@ Roadmap
Release notes
+++++++++++++
+.. [#] More precisely, it iterates over all *enumerable* properties. It just
+ happens that built-in properties (such as ``String.indexOf`` or
+ ``Object.toString``) are set to non-enumerable.
+
+ The enumerability of a property can be checked using
+ `Object.propertyIsEnumeable`_.
+
+ Before ECMAScript 5, it was not possible for user-defined properties
+ to be non-enumerable in a portable manner. ECMAScript 5 introduced
+ `Object.defineProperty`_ which lets user code create non-enumerable
+ properties (and more, read-only properties for instance, or implicit
+ getters and setters). However, support for these is not fully complete
+ at this point, and they are not being used in OpenERP Web code anyway.
+
+.. [#] While using underscore is generally the preferred method (simpler,
+ more reliable and easier to write than a *correct* ``for…in``
+ iteration), it is also probably slower (due to the overhead of
+ calling a bunch of functions).
+
+ As a result, if you profile some code and find out that an underscore
+ method adds unacceptable overhead in a tight loop, you may want to
+ replace it with a ``for…in`` (or a regular ``for`` statement for
+ arrays).
+
.. [#] Because Python is the default domain, the ``py:`` markup prefix
is optional and should be left out.
@@ -252,3 +435,17 @@ Release notes
http://code.google.com/p/jsdoc-toolkit/
.. _John Resig's Class implementation:
http://ejohn.org/blog/simple-javascript-inheritance/
+.. _\_.each:
+ http://documentcloud.github.com/underscore/#each
+.. _\_.map:
+ http://documentcloud.github.com/underscore/#map
+.. _\_.filter:
+ http://documentcloud.github.com/underscore/#select
+.. _It is also fairly tricky to use:
+ https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in#Description
+.. _Object.propertyIsEnumeable:
+ https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable
+.. _Object.defineProperty:
+ https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty
+.. _Object.hasOwnProperty:
+ https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
diff --git a/openerpweb/nonliterals.py b/openerpweb/nonliterals.py
index f999a30e20d..5b61f39fc83 100644
--- a/openerpweb/nonliterals.py
+++ b/openerpweb/nonliterals.py
@@ -7,8 +7,10 @@ can't be sent there themselves).
import binascii
import hashlib
import simplejson.encoder
+import time
+import datetime
-__all__ = ['Domain', 'Context', 'NonLiteralEncoder, non_literal_decoder']
+__all__ = ['Domain', 'Context', 'NonLiteralEncoder, non_literal_decoder', 'CompoundDomain', 'CompoundContext']
#: 48 bits should be sufficient to have almost no chance of collision
#: with a million hashes, according to hg@67081329d49a
@@ -31,12 +33,14 @@ class NonLiteralEncoder(simplejson.encoder.JSONEncoder):
elif isinstance(object, CompoundDomain):
return {
'__ref': 'compound_domain',
- '__domains': object.domains
+ '__domains': object.domains,
+ '__eval_context': object.get_eval_context()
}
elif isinstance(object, CompoundContext):
return {
'__ref': 'compound_context',
- '__contexts': object.contexts
+ '__contexts': object.contexts,
+ '__eval_context': object.get_eval_context()
}
raise TypeError('Could not encode unknown non-literal %s' % object)
@@ -61,12 +65,14 @@ def non_literal_decoder(dct):
elif dct["__ref"] == "compound_domain":
cdomain = CompoundDomain()
for el in dct["__domains"]:
- cdomain.domains.append(non_literal_decoder(el))
+ cdomain.domains.append(el)
+ cdomain.set_eval_context(dct.get("__eval_context"))
return cdomain
elif dct["__ref"] == "compound_context":
ccontext = CompoundContext()
for el in dct["__contexts"]:
- ccontext.contexts.append(non_literal_decoder(el))
+ ccontext.contexts.append(el)
+ ccontext.set_eval_context(dct.get("__eval_context"))
return ccontext
return dct
@@ -121,7 +127,7 @@ class Domain(BaseDomain):
ctx = self.session.evaluation_context(context)
if self.own:
ctx.update(self.own)
- return eval(self.get_domain_string(), ctx)
+ return eval(self.get_domain_string(), SuperDict(ctx))
class Context(BaseContext):
def __init__(self, session, context_string=None, key=None):
@@ -166,13 +172,25 @@ class Context(BaseContext):
ctx = self.session.evaluation_context(context)
if self.own:
ctx.update(self.own)
- return eval(self.get_context_string(),
- ctx)
+ return eval(self.get_context_string(), SuperDict(ctx))
+
+class SuperDict(dict):
+ def __getattr__(self, name):
+ try:
+ return self[name]
+ except KeyError:
+ raise AttributeError(name)
+ def __getitem__(self, key):
+ tmp = super(SuperDict, self).__getitem__(key)
+ if isinstance(tmp, dict):
+ return SuperDict(tmp)
+ return tmp
class CompoundDomain(BaseDomain):
def __init__(self, *domains):
self.domains = []
self.session = None
+ self.eval_context = None
for domain in domains:
self.add(domain)
@@ -188,8 +206,8 @@ class CompoundDomain(BaseDomain):
continue
ctx = dict(context or {})
- ctx['context'] = ctx
-
+ ctx.update(self.get_eval_context() or {})
+
domain.session = self.session
final_domain.extend(domain.evaluate(ctx))
return final_domain
@@ -197,16 +215,25 @@ class CompoundDomain(BaseDomain):
def add(self, domain):
self.domains.append(domain)
return self
+
+ def set_eval_context(self, eval_context):
+ self.eval_context = eval_context
+ return self
+
+ def get_eval_context(self):
+ return self.eval_context
class CompoundContext(BaseContext):
def __init__(self, *contexts):
self.contexts = []
+ self.eval_context = None
self.session = None
for context in contexts:
self.add(context)
def evaluate(self, context=None):
ctx = dict(context or {})
+ ctx.update(self.get_eval_context() or {})
final_context = {}
for context_to_eval in self.contexts:
if not isinstance(context_to_eval, (dict, BaseContext)):
@@ -218,8 +245,7 @@ class CompoundContext(BaseContext):
continue
ctx.update(final_context)
- ctx["context"] = ctx
-
+
context_to_eval.session = self.session
final_context.update(context_to_eval.evaluate(ctx))
return final_context
@@ -227,4 +253,10 @@ class CompoundContext(BaseContext):
def add(self, context):
self.contexts.append(context)
return self
+
+ def set_eval_context(self, eval_context):
+ self.eval_context = eval_context
+ return self
+ def get_eval_context(self):
+ return self.eval_context
diff --git a/openerpweb/openerpweb.py b/openerpweb/openerpweb.py
index 9da14d5ace0..eee0a4eec20 100644
--- a/openerpweb/openerpweb.py
+++ b/openerpweb/openerpweb.py
@@ -74,8 +74,7 @@ class OpenERPSession(object):
Used to store references to non-literal domains which need to be
round-tripped to the client browser.
"""
- def __init__(self, server='127.0.0.1', port=8069,
- model_factory=OpenERPModel):
+ def __init__(self, server='127.0.0.1', port=8069, model_factory=OpenERPModel):
self._server = server
self._port = port
self._db = False
@@ -138,15 +137,13 @@ class OpenERPSession(object):
"""
assert self._uid, "The user needs to be logged-in to initialize his context"
self.context = self.model('res.users').context_get(self.context)
- # set bin_size to True all the time
self.context = self.context or {}
- self.context["bin_size"] = True
-
+
self.client_timezone = self.context.get("tz", False)
# invalid code, anyway we decided the server will be in UTC
#if self.client_timezone:
# self.remote_timezone = self.execute('common', 'timezone_get')
-
+
self._locale = self.context.get('lang','en_US')
lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
if lang_ids:
@@ -177,10 +174,10 @@ class OpenERPSession(object):
:returns: the augmented context
:rtype: dict
"""
- d = {}
- d.update(self.base_eval_context)
+ d = dict(self.base_eval_context)
if context:
d.update(context)
+ d['context'] = d
return d
def eval_context(self, context_to_eval, context=None):
@@ -200,10 +197,9 @@ class OpenERPSession(object):
ctx = dict(
self.base_eval_context,
**(context or {}))
- ctx['context'] = ctx
# adding the context of the session to send to the openerp server
- ccontext = nonliterals.CompoundContext(self.context, context_to_eval)
+ ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {})
ccontext.session = self
return ccontext.evaluate(ctx)
@@ -225,22 +221,15 @@ class OpenERPSession(object):
if isinstance(domain, list):
return domain
- ctx = dict(context or {})
- ctx['context'] = ctx
-
cdomain = nonliterals.CompoundDomain(domain)
cdomain.session = self
- return cdomain.evaluate(ctx)
+ return cdomain.evaluate(context or {})
#----------------------------------------------------------
# OpenERP Web RequestHandler
#----------------------------------------------------------
class JsonRequest(object):
- """ JSON-RPC2 over HTTP POST using non standard POST encoding.
- Difference with the standard:
-
- * the json string is passed as a form parameter named "request"
- * method is currently ignored
+ """ JSON-RPC2 over HTTP.
Sucessful request::
@@ -277,15 +266,20 @@ class JsonRequest(object):
self.request = request
self.params = request.get("params", {})
self.applicationsession = applicationsession
+ self.httprequest = cherrypy.request
+ self.httpresponse = cherrypy.response
+ self.httpsession = cherrypy.session
self.httpsession_id = "cookieid"
self.httpsession = cherrypy.session
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
- self.session = self.httpsession.setdefault(self.session_id, OpenERPSession())
+ host = cherrypy.config['openerp.server.host']
+ port = cherrypy.config['openerp.server.port']
+ self.session = self.httpsession.setdefault(self.session_id, OpenERPSession(host, port))
self.context = self.params.pop('context', None)
return self.params
def dispatch(self, controller, method, requestf=None, request=None):
- ''' Calls the method asked for by the JSON-RPC2 request
+ """ Calls the method asked for by the JSON-RPC2 request
:param controller: the instance of the controller which received the request
:type controller: type
@@ -298,17 +292,19 @@ class JsonRequest(object):
:returns: a string-encoded JSON-RPC2 reply
:rtype: bytes
- '''
+ """
# Read POST content or POST Form Data named "request"
if requestf:
request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
else:
request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
+
+ response = {"jsonrpc": "2.0", "id": request.get('id')}
try:
print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
error = None
self.parse(request)
- result = method(controller, self, **self.params)
+ response["result"] = method(controller, self, **self.params)
except OpenERPUnboundException:
error = {
'code': 100,
@@ -340,11 +336,8 @@ class JsonRequest(object):
'debug': "Client %s" % traceback.format_exc()
}
}
- response = {"jsonrpc": "2.0", "id": request.get('id')}
if error:
response["error"] = error
- else:
- response["result"] = result
print "<--", response
print
@@ -371,7 +364,9 @@ class HttpRequest(object):
self.httpsession_id = "cookieid"
self.httpsession = cherrypy.session
self.context = kw.get('context', {})
- self.session = self.httpsession.setdefault(kw.get('session_id', None), OpenERPSession())
+ host = cherrypy.config['openerp.server.host']
+ port = cherrypy.config['openerp.server.port']
+ self.session = self.httpsession.setdefault(kw.get('session_id'), OpenERPSession(host, port))
self.result = ""
if request.method == 'GET':
print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
@@ -456,43 +451,40 @@ class Root(object):
def main(argv):
# change the timezone of the program to the OpenERP server's assumed timezone
os.environ["TZ"] = "UTC"
-
+
DEFAULT_CONFIG = {
- 'server.socket_port': 8002,
'server.socket_host': '0.0.0.0',
'tools.sessions.on': True,
'tools.sessions.storage_type': 'file',
- 'tools.sessions.storage_path': os.path.join(tempfile.gettempdir(), "cpsessions"),
'tools.sessions.timeout': 60
}
-
+
# Parse config
op = optparse.OptionParser()
- op.add_option("-p", "--port", dest="server.socket_port", help="listening port",
- type="int", metavar="NUMBER")
- op.add_option("-s", "--session-path", dest="tools.sessions.storage_path",
- help="directory used for session storage", metavar="DIR")
+ op.add_option("-p", "--port", dest="server.socket_port", default=8002, help="listening port", type="int", metavar="NUMBER")
+ op.add_option("-s", "--session-path", dest="tools.sessions.storage_path", default=os.path.join(tempfile.gettempdir(), "cpsessions"), help="directory used for session storage", metavar="DIR")
+ op.add_option("--server-host", dest="openerp.server.host", default='127.0.0.1', help="OpenERP server hostname", metavar="HOST")
+ op.add_option("--server-port", dest="openerp.server.port", default=8069, help="OpenERP server port", type="int", metavar="NUMBER")
+ op.add_option("--db-filter", dest="openerp.dbfilter", default='.*', help="Filter listed database", metavar="REGEXP")
(o, args) = op.parse_args(argv[1:])
o = vars(o)
for k in o.keys():
- if o[k] == None:
+ if o[k] is None:
del(o[k])
# Setup and run cherrypy
cherrypy.tree.mount(Root())
-
+
cherrypy.config.update(config=DEFAULT_CONFIG)
- if os.path.exists(os.path.join(os.path.dirname(
- os.path.dirname(__file__)),'openerp-web.cfg')):
- cherrypy.config.update(os.path.join(os.path.dirname(
- os.path.dirname(__file__)),'openerp-web.cfg'))
+ if os.path.exists(os.path.join(path_root,'openerp-web.cfg')):
+ cherrypy.config.update(os.path.join(path_root,'openerp-web.cfg'))
if os.path.exists(os.path.expanduser('~/.openerp_webrc')):
cherrypy.config.update(os.path.expanduser('~/.openerp_webrc'))
cherrypy.config.update(o)
-
+
if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
- os.mkdir(cherrypy.config['tools.sessions.storage_path'], 0700)
-
+ os.makedirs(cherrypy.config['tools.sessions.storage_path'], 0700)
+
cherrypy.server.subscribe()
cherrypy.engine.start()
cherrypy.engine.block()