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 = """ + + + + + + + +

Chart data can be loaded from different sources: xml,json,JS array,csv
This samples demonstrates loading from xml file.

+
+
+ + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/02_load_json.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/02_load_json.html new file mode 100644 index 00000000000..e2ce20ae764 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/02_load_json.html @@ -0,0 +1,63 @@ + + + + + Data Loading: JSON + + + + + + + +
+
+ + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/03_load_csv.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/03_load_csv.html new file mode 100644 index 00000000000..64c39cfaef1 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/03_load_csv.html @@ -0,0 +1,65 @@ + + + + + Data Loading: CSV + + + + + + + +

When data are loaded from CSV string, you should use "data" and field index to define field in the template, for example, "#data0#","#data1#",etc.
Charts in this sample represents values in the first column. Therefore, we have set value:"#data0#" in a chart contructor.

+
+
+ + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/04_load_jsarray.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/04_load_jsarray.html new file mode 100644 index 00000000000..023cb0a503a --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/04_load_jsarray.html @@ -0,0 +1,68 @@ + + + + + Data Loading: JS array + + + + + + + + +

When data are loaded from JS array, you should use "data" and field index to define field in the template, for example, "#data0#","#data1#",etc.
Charts in this sample represents values in the first column. Therefore, we have set value:"#data0#" in a chart contructor.

+
+
+ + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/05_series.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/05_series.html new file mode 100644 index 00000000000..0600c05741d --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/01_initialization/05_series.html @@ -0,0 +1,119 @@ + + + + + PieChart: Initiazation + + + + +

BarChart: Scales

+ + + + + + + + + + +
Automatic vertical scaleCustom vertical scale
+ + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/01_default.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/01_default.html new file mode 100644 index 00000000000..39b2f5e3865 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/01_default.html @@ -0,0 +1,66 @@ + + + + + Colors: default + + + + + + + +

Chart colors are defined by color property. If it is not set, chart is colored using "rainbow" algorithm.

+
+
+ + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/02_custom.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/02_custom.html new file mode 100644 index 00000000000..8157a8f82b2 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/02_custom.html @@ -0,0 +1,67 @@ + + + + + Colors: custom colors + + + + + + + +

Colors can be defined with template. In this sample colors and defined in data by "color" field.

+
+
+ + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/03_custom_logic.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/03_custom_logic.html new file mode 100644 index 00000000000..e88c37b3283 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/03_custom_logic.html @@ -0,0 +1,77 @@ + + + + + Colors: custom logic + + + + + + + +

Colors can be defined using function. In this sample color of bars depends on sales value, and colors of pie sectors are alternative.

+
+
+ + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/04_gradient.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/04_gradient.html new file mode 100644 index 00000000000..973d9cdc008 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/02_color/04_gradient.html @@ -0,0 +1,64 @@ + + + + + Colors: color gradient + + + + + + + + + +

The color of Bar Chart can be defined by gradient property.
+ In this case you should set a function that takes a gradient object as an argument. And using addColorStop you may define colors for chart.

+
+ + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/03_group/01_basic.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/03_group/01_basic.html new file mode 100644 index 00000000000..401dbdae24f --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/03_group/01_basic.html @@ -0,0 +1,96 @@ + + + + + Grouping and Sorting + + + + + + + + +

It's possible to group chart data by a certain property. You may use group method or group property in a chart constructor.

+
+ + +
+ + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/03_group/02_scales.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/03_group/02_scales.html new file mode 100644 index 00000000000..75746062b29 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/03_group/02_scales.html @@ -0,0 +1,126 @@ + + + + + Scales and grouping + + + + + + + + +

To update configuration properties after grouping you need to cal define method with new settings.

+
+
+ + +

+ + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/01_init.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/01_init.html new file mode 100644 index 00000000000..63c82ecf42c --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/01_init.html @@ -0,0 +1,67 @@ + + + + + Initialization + + + + + + + + + + + + + + +
Pie chart with Automatic radius and center positionPie chart with Custom radius and center position
+ + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/02_text.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/02_text.html new file mode 100644 index 00000000000..7ba118f19e2 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/02_text.html @@ -0,0 +1,52 @@ + + + + + Labels + + + + + +

There are two properties to define sectors labels: +

  • label - defines text outside sectors
  • +
  • pieInnerText - set text inside sectors
  • +

    + +
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/03_3d_chart.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/03_3d_chart.html new file mode 100644 index 00000000000..957f7ed7954 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/03_3d_chart.html @@ -0,0 +1,48 @@ + + + + + 3D view + + + + + +
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/04_legend.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/04_legend.html new file mode 100644 index 00000000000..2d5c45c0151 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/04_pie_chart/04_legend.html @@ -0,0 +1,81 @@ + + + + + Details Block + + + + + +

    Pie Chart provides property "details" that displayes informationa about all sectors in a separate block.

    + + + + + + + + + + +
    PieChart with default details blockPieChart with custom details block
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/01_init.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/01_init.html new file mode 100644 index 00000000000..626a1e216fe --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/01_init.html @@ -0,0 +1,55 @@ + + + + + Initialization + + + + + +
    + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/02_style.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/02_style.html new file mode 100644 index 00000000000..39dfe7eb41f --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/02_style.html @@ -0,0 +1,153 @@ + + + + + Style definition + + + + + + + + + + + + + + +
    + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/03_scale.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/03_scale.html new file mode 100644 index 00000000000..161cbf7ee53 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/03_scale.html @@ -0,0 +1,101 @@ + + + + + Scales + + + + + + + + + + + + + + +
    Automatic vertical scaleCustom vertical scale
    + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/04_spline.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/04_spline.html new file mode 100644 index 00000000000..4f266ee8983 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/04_spline.html @@ -0,0 +1,57 @@ + + + + + Initialization + + + + + +
    + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/05_series.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/05_series.html new file mode 100644 index 00000000000..532cd385519 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/05_line_chart/05_series.html @@ -0,0 +1,94 @@ + + + + + Scales + + + + + + + + + +
    + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/01_init.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/01_init.html new file mode 100644 index 00000000000..e31ae20dd39 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/01_init.html @@ -0,0 +1,74 @@ + + + + + PieChart: Initiazation + + + + + +

    BarChart: Initiazation

    + + + + + + + + + + +
    + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/02_text.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/02_text.html new file mode 100644 index 00000000000..ee378b194f1 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/02_text.html @@ -0,0 +1,80 @@ + + + + + PieChart: Initiazation + + + + +

    BarChart: Initiazation

    + + + + + + + + + + +
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/03_scales.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/03_scales.html new file mode 100644 index 00000000000..edc1f6ca45d --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/03_scales.html @@ -0,0 +1,99 @@ + + + + + PieChart: Initiazation + + + + +

    BarChart: Scales

    + + + + + + + + + + +
    Custom vertical scale and originAutomatic vertical scale
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/04_styles.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/04_styles.html new file mode 100644 index 00000000000..0877a95de16 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/04_styles.html @@ -0,0 +1,207 @@ + + + + + Chart + + + + +

    Chart

    + + + + + + + + + + + + + + + +
    + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/05_stacked_chart.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/05_stacked_chart.html new file mode 100644 index 00000000000..d165d0bd488 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/05_stacked_chart.html @@ -0,0 +1,87 @@ + + + + + StackedBar Chart + + + + +

    StackedBar Chart

    +
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/06_series.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/06_series.html new file mode 100644 index 00000000000..3c5fb695692 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/06_series.html @@ -0,0 +1,81 @@ + + + + + Series + + + + +

    Series

    + +
    + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/07_horizonal_bars.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/07_horizonal_bars.html new file mode 100644 index 00000000000..6c8046c5855 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/07_horizonal_bars.html @@ -0,0 +1,120 @@ + + + + + Horizontal bar Chart + + + + +

    Horizontal bar Chart

    + + + + + + + +
    + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/08_horizonal_stacked_bars.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/08_horizonal_stacked_bars.html new file mode 100644 index 00000000000..fe4d8509376 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/06_bar_chart/08_horizonal_stacked_bars.html @@ -0,0 +1,75 @@ + + + + + Horizontal Stacked Bars + + + + + +
    + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/01_init.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/01_init.html new file mode 100644 index 00000000000..5195e7476e1 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/01_init.html @@ -0,0 +1,55 @@ + + + + + Initialization + + + + + +
    + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/02_scale.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/02_scale.html new file mode 100644 index 00000000000..e3d65bd6d55 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/02_scale.html @@ -0,0 +1,101 @@ + + + + + Scales + + + + + + + + + + + + + + + +
    Custom vertical scaleAutomatic vertical scale
    + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/03_series.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/03_series.html new file mode 100644 index 00000000000..c071386c348 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/03_series.html @@ -0,0 +1,90 @@ + + + + + Scales + + + + + + +
    + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/03_stacked_area.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/03_stacked_area.html new file mode 100644 index 00000000000..55fccb93e84 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/07_area_chart/03_stacked_area.html @@ -0,0 +1,88 @@ + + + + + Scales + + + + + + +
    + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/01_add.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/01_add.html new file mode 100644 index 00000000000..7a3b45f8cee --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/01_add.html @@ -0,0 +1,83 @@ + + + + + Adding + + + + + + + + + +
    + + + + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/02_events.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/02_events.html new file mode 100644 index 00000000000..495d42bf795 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/02_events.html @@ -0,0 +1,96 @@ + + + + + Events + + + + + + + + + +
    +
    + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/03_sorting.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/03_sorting.html new file mode 100644 index 00000000000..cb8b254fe10 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/03_sorting.html @@ -0,0 +1,73 @@ + + + + + Sorting + + + + + + + + + +
    + + + + + + + + + diff --git a/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/04_filtering.html b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/04_filtering.html new file mode 100644 index 00000000000..230aa820800 --- /dev/null +++ b/addons/base_graph/static/lib/dhtmlxGraph/samples/08_dynamic/04_filtering.html @@ -0,0 +1,81 @@ + + + + + Filtering + + + + + + + + + +
    + + + + + + + + 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=""+this._map.join("\n")+""; + + 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()