[MERGE] xmo search views
bzr revid: al@openerp.com-20110329125042-5vaketmxekhv4r39
This commit is contained in:
+128
-22
@@ -6,7 +6,12 @@ from cStringIO import StringIO
|
||||
import simplejson
|
||||
|
||||
import openerpweb
|
||||
import openerpweb.ast
|
||||
import openerpweb.nonliterals
|
||||
|
||||
__all__ = ['Session', 'Menu', 'DataSet', 'DataRecord',
|
||||
'View', 'FormView', 'ListView', 'SearchView',
|
||||
'Action']
|
||||
|
||||
class Xml2Json:
|
||||
# xml2json-direct
|
||||
@@ -16,7 +21,8 @@ class Xml2Json:
|
||||
# URL: http://code.google.com/p/xml2json-direct/
|
||||
@staticmethod
|
||||
def convert_to_json(s):
|
||||
return simplejson.dumps(Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
|
||||
return simplejson.dumps(
|
||||
Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
|
||||
|
||||
@staticmethod
|
||||
def convert_to_structure(s):
|
||||
@@ -113,6 +119,57 @@ class Session(openerpweb.Controller):
|
||||
return concat
|
||||
js.exposed = True
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def eval_domain_and_context(self, req, contexts, domains,
|
||||
group_by_seq=None):
|
||||
""" Evaluates sequences of domains and contexts, composing them into
|
||||
a single context, domain or group_by sequence.
|
||||
|
||||
:param list contexts: list of contexts to merge together. Contexts are
|
||||
evaluated in sequence, all previous contexts
|
||||
are part of their own evaluation context
|
||||
(starting at the session context).
|
||||
:param list domains: list of domains to merge together. Domains are
|
||||
evaluated in sequence and appended to one another
|
||||
(implicit AND), their evaluation domain is the
|
||||
result of merging all contexts.
|
||||
:param list group_by_seq: list of domains (which may be in a different
|
||||
order than the ``contexts`` parameter),
|
||||
evaluated in sequence, their ``'group_by'``
|
||||
key is extracted if they have one.
|
||||
:returns:
|
||||
a 3-dict of:
|
||||
|
||||
context (``dict``)
|
||||
the global context created by merging all of
|
||||
``contexts``
|
||||
|
||||
domain (``list``)
|
||||
the concatenation of all domains
|
||||
|
||||
group_by (``list``)
|
||||
a list of fields to group by, potentially empty (in which case
|
||||
no group by should be performed)
|
||||
"""
|
||||
context = req.session.eval_contexts(contexts)
|
||||
domain = req.session.eval_domains(domains, context)
|
||||
|
||||
group_by_sequence = []
|
||||
for candidate in (group_by_seq or []):
|
||||
ctx = req.session.eval_context(candidate, context)
|
||||
group_by = ctx.get('group_by')
|
||||
if not group_by:
|
||||
continue
|
||||
elif isinstance(group_by, basestring):
|
||||
group_by_sequence.append(group_by)
|
||||
else:
|
||||
group_by_sequence.extend(group_by)
|
||||
|
||||
return {
|
||||
'context': context,
|
||||
'domain': domain,
|
||||
'group_by': group_by_sequence
|
||||
}
|
||||
|
||||
class Menu(openerpweb.Controller):
|
||||
_cp_path = "/base/menu"
|
||||
@@ -157,12 +214,23 @@ class Menu(openerpweb.Controller):
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def action(self, req, menu_id):
|
||||
print "QUERY"
|
||||
m = req.session.model('ir.values')
|
||||
r = m.get('action', 'tree_but_open', [('ir.ui.menu', menu_id)], False, {})
|
||||
print r
|
||||
res = {"action": r}
|
||||
return res
|
||||
Values = req.session.model('ir.values')
|
||||
actions = Values.get('action', 'tree_but_open', [('ir.ui.menu', menu_id)], False, {})
|
||||
|
||||
for _, _, action in actions:
|
||||
# values come from the server, we can just eval them
|
||||
if isinstance(action['context'], basestring):
|
||||
action['context'] = eval(
|
||||
action['context'],
|
||||
req.session.evaluation_context()) or {}
|
||||
|
||||
if isinstance(action['domain'], basestring):
|
||||
action['domain'] = eval(
|
||||
action['domain'],
|
||||
req.session.evaluation_context(
|
||||
action['context'])) or []
|
||||
|
||||
return {"action": actions}
|
||||
|
||||
|
||||
class DataSet(openerpweb.Controller):
|
||||
@@ -184,20 +252,14 @@ class DataSet(openerpweb.Controller):
|
||||
|
||||
:param request: a JSON-RPC request object
|
||||
:type request: openerpweb.JsonRequest
|
||||
:param model: the name of the model to search on
|
||||
:type model: str
|
||||
:param str model: the name of the model to search on
|
||||
:param fields: a list of the fields to return in the result records
|
||||
:type fields: [str]
|
||||
:param offset: from which index should the results start being returned
|
||||
:type offset: int
|
||||
:param limit: the maximum number of records to return
|
||||
:type limit: int
|
||||
:param domain: the search domain for the query
|
||||
:type domain: list
|
||||
:param context: the context in which the search should be executed
|
||||
:type context: dict
|
||||
:param sort: sorting directives
|
||||
:type sort: list
|
||||
:param int offset: from which index should the results start being returned
|
||||
:param int limit: the maximum number of records to return
|
||||
:param list domain: the search domain for the query
|
||||
:param dict context: the context in which the search should be executed
|
||||
:param list sort: sorting directives
|
||||
:returns: a list of result records
|
||||
:rtype: list
|
||||
"""
|
||||
@@ -248,14 +310,13 @@ class DataRecord(openerpweb.Controller):
|
||||
value = r[0]
|
||||
return {'value': value}
|
||||
|
||||
|
||||
class View(openerpweb.Controller):
|
||||
def fields_view_get(self, session, model, view_id, view_type, transform=True):
|
||||
Model = session.model(model)
|
||||
r = Model.fields_view_get(view_id, view_type)
|
||||
if transform:
|
||||
context = {} # TODO: dict(ctx_sesssion, **ctx_action)
|
||||
xml = self.transform_view(r['arch'], context)
|
||||
xml = self.transform_view(r['arch'], session, context)
|
||||
else:
|
||||
xml = ElementTree.fromstring(r['arch'])
|
||||
r['arch'] = Xml2Json.convert_element(xml)
|
||||
@@ -294,7 +355,7 @@ class View(openerpweb.Controller):
|
||||
if a == 'invisible' and 'attrs' in elem.attrib:
|
||||
del elem.attrib['attrs']
|
||||
|
||||
def transform_view(self, view_string, context=None):
|
||||
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
|
||||
parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
|
||||
@@ -304,8 +365,53 @@ class View(openerpweb.Controller):
|
||||
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
|
||||
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 session: Current OpenERP session
|
||||
:type session: openerpweb.openerpweb.OpenERPSession
|
||||
"""
|
||||
domain = elem.get(attr_name)
|
||||
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))
|
||||
|
||||
def parse_domains_and_contexts(self, elem, session):
|
||||
""" Converts domains and contexts from the view into Python objects,
|
||||
either literals if they can be parsed by literal_eval or a special
|
||||
placeholder object if the domain or context refers to free variables.
|
||||
|
||||
:param elem: the current node being parsed
|
||||
:type param: xml.etree.ElementTree.Element
|
||||
:param session: OpenERP session object, used to store and retrieve
|
||||
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')
|
||||
if context_string:
|
||||
try:
|
||||
elem.set('context',
|
||||
openerpweb.ast.literal_eval(context_string))
|
||||
except ValueError:
|
||||
elem.set('context',
|
||||
openerpweb.nonliterals.Context(
|
||||
session, context_string))
|
||||
|
||||
class FormView(View):
|
||||
_cp_path = "/base/formview"
|
||||
|
||||
@@ -348,6 +348,7 @@ body.openerp {
|
||||
}
|
||||
.openerp .filter_label_group {
|
||||
padding-right: 0.4em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.openerp .filter_label_group button:first-child {
|
||||
border-left: 1px solid #666;
|
||||
|
||||
@@ -272,86 +272,84 @@
|
||||
</button>
|
||||
</t>
|
||||
<t t-name="SearchView">
|
||||
<h3 class="title"><t t-esc="fields_view.arch.attrs['string']"/></h3>
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<t t-call="SearchView.node">
|
||||
<t t-set="node" t-value="fields_view.arch"/>
|
||||
<t t-set="previous_node_type"></t>
|
||||
</t>
|
||||
</tr>
|
||||
<h3 class="title"><t t-esc="view.attrs['string']"/></h3>
|
||||
|
||||
<form>
|
||||
<t t-call="SearchView.render_lines"/>
|
||||
|
||||
<div style="text-align:right;">
|
||||
<input type="submit" value="Search"/>
|
||||
<input type="reset" value="Clear"/>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
<t t-name="SearchView.render_lines">
|
||||
<table border="0" cellspacing="0" cellpadding="0"
|
||||
t-foreach="lines" t-as="line">
|
||||
<tr>
|
||||
<td t-foreach="line" t-as="widget">
|
||||
<t t-raw="widget.render(defaults)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="text-align:right;">
|
||||
<input id="search" type="button" value="Search"/>
|
||||
<input type="button" value="Clear"/>
|
||||
</t>
|
||||
<button t-name="SearchView.filter" type="button"
|
||||
t-att-id="element_id"
|
||||
t-att-title="attrs.help"
|
||||
t-att-class="classes.join(' ')">
|
||||
<img t-if="attrs.icon" t-att-src="'/base/static/openerp/img/icons/' + attrs.icon + '.png'" width="16" height="16"/>
|
||||
<br t-if="attrs.icon and attrs.string"/>
|
||||
<t t-esc="attrs.string"/>
|
||||
</button>
|
||||
<span t-name="SearchView.filters" class="filter_label_group"
|
||||
><t t-foreach="filters" t-as="filter"
|
||||
><t t-raw="filter.render(defaults)"/></t
|
||||
></span>
|
||||
<t t-name="SearchView.field">
|
||||
<label style="display: block" t-att-title="attrs.help"
|
||||
t-att-for="element_id">
|
||||
<t t-esc="attrs.string || attrs.name"/>
|
||||
<span t-if="attrs.help">(?)</span>
|
||||
</label>
|
||||
<div style="white-space: nowrap;">
|
||||
<input type="text" t-att-name="attrs.name"
|
||||
t-att-id="element_id"
|
||||
t-att-value="defaults[attrs.name] || ''"/>
|
||||
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="SearchView.node" t-trim="inner">
|
||||
<t t-if="node.tag == 'filter'">
|
||||
<t t-if="previous_node_type != 'filter'">
|
||||
<td nowrap="true" valign="bottom" class="filter_label_group">
|
||||
</t>
|
||||
<button type="button" t-att-title="node.attrs.help" t-att-class="node.attrs.string ? 'filter_label' : 'filter_icon'">
|
||||
<img t-if="node.attrs.icon" t-att-src="'/base/static/openerp/img/icons/' + node.attrs.icon + '.png'" width="16" height="16"/>
|
||||
<br t-if="node.attrs.icon and node.attrs.string"/>
|
||||
<t t-esc="node.attrs.string"/>
|
||||
</button>
|
||||
<t t-if="next_node_type != 'filter'">
|
||||
</td>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="node.tag == 'separator'">
|
||||
<!-- nothing in search view ? -->
|
||||
</t>
|
||||
<t t-if="node.tag == 'newline'">
|
||||
<!-- TODO: check qweb support for cdata -->
|
||||
</tr>
|
||||
</table>
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
</t>
|
||||
<t t-if="node.tag == 'field'">
|
||||
<t t-set="orm" t-value="fields_view.fields[node.attrs.name] || {}"/>
|
||||
<td>
|
||||
<label style="display: block" t-att-title="orm.help || node.attrs.help">
|
||||
<t t-esc="orm.string || node.attrs.string || node.attrs.name"/>
|
||||
<span t-if="orm.help || node.attrs.help">(?)</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text" t-att-name="node.attrs.name"/>
|
||||
</div>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="node.tag == 'group'">
|
||||
<!-- if group is called without a <newline/> before, the table should be closed.
|
||||
I think we'll need preprocessing, all those escaped tables are too ugly -->
|
||||
<td>
|
||||
<div t-att-class="'searchview_group ' + (node.attrs.expand == '0' ? 'folded' : 'expanded')">
|
||||
<a class="searchview_group_string" href="#" onclick="$(this).parent().toggleClass('folded expanded')"><t t-esc="node.attrs.string"/></a>
|
||||
<div class="searchview_group_content">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<t t-call="SearchView.recurse"/>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
</t>
|
||||
<t t-if="node.children and node.tag != 'newline' and node.tag != 'group'">
|
||||
<t t-call="SearchView.recurse"/>
|
||||
</t>
|
||||
<t t-name="SearchView.field.selection">
|
||||
<label style="display: block" t-att-title="attrs.help"
|
||||
t-att-for="element_id">
|
||||
<t t-esc="attrs.string || attrs.name"/>
|
||||
<span t-if="attrs.help">(?)</span>
|
||||
</label>
|
||||
<div style="white-space: nowrap;">
|
||||
<select t-att-name="attrs.name" t-att-id="element_id">
|
||||
<option/>
|
||||
<t t-foreach="attrs.selection" t-as="option">
|
||||
<t t-set="selected" t-value="defaults[attrs.name] === option[0]"/>
|
||||
<option t-if="selected"
|
||||
t-att-value="option[0]" selected="selected">
|
||||
<t t-esc="option[1]"/>
|
||||
</option>
|
||||
<option t-if="!selected" t-att-value="option[0]">
|
||||
<t t-esc="option[1]"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="SearchView.recurse" t-trim="inner">
|
||||
<t t-foreach="node.children" t-as="node">
|
||||
<t t-call="SearchView.node">
|
||||
<t t-set="previous_node_type" t-value="node_all[node_index - 1] ? node_all[node_index - 1].tag : ''"/>
|
||||
<t t-set="next_node_type" t-value="node_all[node_index + 1] ? node_all[node_index + 1].tag : ''"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-name="SearchView.group">
|
||||
<div t-att-class="'searchview_group ' + (attrs.expand == '0' ? 'folded' : 'expanded')"
|
||||
t-att-id="element_id">
|
||||
<a t-if="attrs.string" class="searchview_group_string" href="#">
|
||||
<t t-esc="attrs.string"/>
|
||||
</a>
|
||||
<div class="searchview_group_content">
|
||||
<t t-call="SearchView.render_lines"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -527,7 +527,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({
|
||||
|
||||
this.session.on_session_invalid.add(this.login.do_ask_login);
|
||||
this.session.on_session_valid.add_last(this.header.do_update);
|
||||
this.session.on_session_valid.add_last(this.on_loggued);
|
||||
this.session.on_session_valid.add_last(this.on_logged);
|
||||
|
||||
this.menu = new openerp.base.Menu(this.session, "oe_menu", "oe_secondary_menu");
|
||||
this.menu.on_action.add(this.on_menu_action);
|
||||
@@ -538,7 +538,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({
|
||||
this.login.start();
|
||||
this.menu.start();
|
||||
},
|
||||
on_loggued: function() {
|
||||
on_logged: function() {
|
||||
this.action = new openerp.base.ActionManager(this.session, "oe_application");
|
||||
this.action.start();
|
||||
},
|
||||
|
||||
@@ -41,14 +41,27 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
|
||||
start: function() {
|
||||
},
|
||||
on_mode_switch: function(view_type) {
|
||||
for (var i in this.views) {
|
||||
if(i == view_type) {
|
||||
this.views[i].controller.$element.show();
|
||||
} else {
|
||||
this.views[i].controller.$element.hide();
|
||||
}
|
||||
for (var i in this.views) {
|
||||
this.views[i].controller.$element.toggle(i === view_type);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Extract search view defaults from the current action's context.
|
||||
*
|
||||
* These defaults are of the form {search_default_*: value}
|
||||
*
|
||||
* @returns {Object} a clean defaults mapping of {field_name: value}
|
||||
*/
|
||||
search_defaults: function () {
|
||||
var defaults = {};
|
||||
_.each(this.action.context, function (value, key) {
|
||||
var match = /^search_default_(.*)$/.exec(key);
|
||||
if (match) {
|
||||
defaults[match[1]] = value;
|
||||
}
|
||||
});
|
||||
return defaults;
|
||||
},
|
||||
do_action_window: function(action) {
|
||||
var self = this;
|
||||
var prefix_id = "#" + this.element_id;
|
||||
@@ -61,8 +74,17 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
|
||||
this.searchview_id = false;
|
||||
if(this.search_visible && action.search_view_id) {
|
||||
this.searchview_id = action.search_view_id[0];
|
||||
this.searchview = new openerp.base.SearchView(this.session, this.element_id + "_search", this.dataset, this.searchview_id);
|
||||
this.searchview.start();
|
||||
var searchview = this.searchview = new openerp.base.SearchView(
|
||||
this.session, this.element_id + "_search",
|
||||
this.dataset, this.searchview_id,
|
||||
this.search_defaults());
|
||||
searchview.on_search.add(this.do_search);
|
||||
searchview.start();
|
||||
|
||||
if (action['auto_search']) {
|
||||
searchview.on_loaded.add_last(
|
||||
searchview.do_search);
|
||||
}
|
||||
}
|
||||
for(var i = 0; i < action.views.length; i++) {
|
||||
var view_id, controller;
|
||||
@@ -92,6 +114,20 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
|
||||
on_remove: function() {
|
||||
},
|
||||
on_edit: function() {
|
||||
},
|
||||
do_search: function (domains, contexts, groupbys) {
|
||||
var self = this;
|
||||
this.rpc('/base/session/eval_domain_and_context', {
|
||||
domains: domains,
|
||||
contexts: contexts,
|
||||
group_by_seq: groupbys
|
||||
}, function (results) {
|
||||
// TODO: handle non-empty results.group_by with read_group
|
||||
self.dataset.set({
|
||||
context: results.context,
|
||||
domain: results.domain
|
||||
}).fetch(0, self.action.limit);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -358,75 +394,562 @@ openerp.base.DataRecord = openerp.base.Controller.extend({
|
||||
});
|
||||
|
||||
openerp.base.SearchView = openerp.base.Controller.extend({
|
||||
init: function(session, element_id, dataset, view_id) {
|
||||
init: function(session, element_id, dataset, view_id, defaults) {
|
||||
this._super(session, element_id);
|
||||
this.dataset = dataset;
|
||||
this.model = dataset.model;
|
||||
this.view_id = view_id;
|
||||
this.input_index = 0;
|
||||
this.input_ids = {};
|
||||
this.domain = [];
|
||||
|
||||
this.defaults = defaults || {};
|
||||
|
||||
this.inputs = [];
|
||||
this.enabled_filters = [];
|
||||
},
|
||||
start: function() {
|
||||
//this.log('Starting SearchView '+this.model+this.view_id)
|
||||
this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
|
||||
},
|
||||
/**
|
||||
* Builds a list of widget rows (each row is an array of widgets)
|
||||
*
|
||||
* @param {Array} items a list of nodes to convert to widgets
|
||||
* @param {Object} fields a mapping of field names to (ORM) field attributes
|
||||
* @returns Array
|
||||
*/
|
||||
make_widgets: function (items, fields) {
|
||||
var rows = [],
|
||||
row = [];
|
||||
rows.push(row);
|
||||
var filters = [];
|
||||
_.each(items, function (item) {
|
||||
if (filters.length && item.tag !== 'filter') {
|
||||
row.push(
|
||||
new openerp.base.search.FilterGroup(
|
||||
filters, this));
|
||||
filters = [];
|
||||
}
|
||||
|
||||
if (item.tag === 'newline') {
|
||||
row = [];
|
||||
rows.push(row);
|
||||
} else if (item.tag === 'filter') {
|
||||
filters.push(
|
||||
new openerp.base.search.Filter(
|
||||
item, this));
|
||||
} else if (item.tag === 'separator') {
|
||||
// a separator is a no-op
|
||||
} else {
|
||||
if (item.tag === 'group') {
|
||||
// TODO: group and field should be fetched from registries, maybe even filters
|
||||
row.push(
|
||||
new openerp.base.search.Group(
|
||||
item, this, fields));
|
||||
} else if (item.tag === 'field') {
|
||||
row.push(
|
||||
this.make_field(
|
||||
item, fields[item['attrs'].name]));
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
if (filters.length) {
|
||||
row.push(new openerp.base.search.FilterGroup(filters, this));
|
||||
}
|
||||
|
||||
return rows;
|
||||
},
|
||||
/**
|
||||
* Creates a field for the provided field descriptor item (which comes
|
||||
* from fields_view_get)
|
||||
*
|
||||
* @param {Object} item fields_view_get node for the field
|
||||
* @param {Object} field fields_get result for the field
|
||||
* @returns openerp.base.search.Field
|
||||
*/
|
||||
make_field: function (item, field) {
|
||||
// TODO: should fetch from an actual registry
|
||||
// TODO: register fields in self?
|
||||
switch (field.type) {
|
||||
case 'char':
|
||||
case 'text':
|
||||
return new openerp.base.search.CharField(
|
||||
item, field, this);
|
||||
case 'boolean':
|
||||
return new openerp.base.search.BooleanField(
|
||||
item, field, this);
|
||||
case 'integer':
|
||||
return new openerp.base.search.IntegerField(
|
||||
item, field, this);
|
||||
case 'float':
|
||||
return new openerp.base.search.FloatField(
|
||||
item, field, this);
|
||||
case 'selection':
|
||||
return new openerp.base.search.SelectionField(
|
||||
item, field, this);
|
||||
case 'datetime':
|
||||
return new openerp.base.search.DateTimeField(
|
||||
item, field, this);
|
||||
case 'date':
|
||||
return new openerp.base.search.DateField(
|
||||
item, field, this);
|
||||
case 'one2many':
|
||||
return new openerp.base.search.OneToManyField(
|
||||
item, field, this);
|
||||
case 'many2one':
|
||||
return new openerp.base.search.ManyToOneField(
|
||||
item, field, this);
|
||||
case 'many2many':
|
||||
return new openerp.base.search.ManyToManyField(
|
||||
item, field, this);
|
||||
default:
|
||||
console.group('Unknown field type ' + field.type);
|
||||
console.error('View node', item);
|
||||
console.info('View field', field);
|
||||
console.info('In view', this);
|
||||
console.groupEnd();
|
||||
return null;
|
||||
}
|
||||
},
|
||||
on_loaded: function(data) {
|
||||
this.fields_view = data.fields_view;
|
||||
this.log(this.fields_view);
|
||||
this.input_ids = {};
|
||||
this.$element.html(QWeb.render("SearchView", {"fields_view": this.fields_view}));
|
||||
this.$element.find("#search").bind('click',this.on_search);
|
||||
// TODO bind click event on all button
|
||||
// TODO we don't do many2one yet, but in the future bind a many2one controller on them
|
||||
this.log(this.$element.find("#search"));
|
||||
},
|
||||
register_input: function(node) {
|
||||
// self should be passed in the qweb dict to do:
|
||||
// <input t-add-id="self.register_input(node)"/>
|
||||
var lines = this.make_widgets(
|
||||
data.fields_view['arch'].children,
|
||||
data.fields_view.fields);
|
||||
|
||||
// generate id
|
||||
var id = this.element_id + "_" + this.input_index++;
|
||||
// TODO construct a nice object
|
||||
// save it in our registry
|
||||
this.input_ids[id] = {
|
||||
node: node,
|
||||
type: "filter",
|
||||
domain: "",
|
||||
context: "",
|
||||
disabled: false
|
||||
};
|
||||
var render = QWeb.render("SearchView", {
|
||||
'view': data.fields_view['arch'],
|
||||
'lines': lines,
|
||||
'defaults': this.defaults
|
||||
});
|
||||
this.$element.html(render);
|
||||
|
||||
return id;
|
||||
this.$element.find('form')
|
||||
.submit(this.do_search)
|
||||
.bind('reset', this.do_clear);
|
||||
// start() all the widgets
|
||||
_(lines).chain().flatten().each(function (widget) {
|
||||
widget.start();
|
||||
});
|
||||
},
|
||||
on_click: function() {
|
||||
// event catched on a button
|
||||
// flip the disabled flag
|
||||
// adjust the css class
|
||||
/**
|
||||
* Performs the search view collection of widget data.
|
||||
*
|
||||
* If the collection went well (all fields are valid), then triggers
|
||||
* :js:func:`openerp.base.SearchView.on_search`.
|
||||
*
|
||||
* If at least one field failed its validation, triggers
|
||||
* :js:func:`openerp.base.SearchView.on_invalid` instead.
|
||||
*
|
||||
* @param e jQuery event object coming from the "Search" button
|
||||
*/
|
||||
do_search: function (e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
var domains = [], contexts = [];
|
||||
|
||||
var errors = [];
|
||||
_.each(this.inputs, function (input) {
|
||||
try {
|
||||
var domain = input.get_domain();
|
||||
if (domain) {
|
||||
domains.push(domain);
|
||||
}
|
||||
|
||||
var context = input.get_context();
|
||||
if (context) {
|
||||
contexts.push(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof openerp.base.search.Invalid) {
|
||||
errors.push(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
this.on_invalid(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: do we need to handle *fields* with group_by in their context?
|
||||
var groupbys = _(this.enabled_filters)
|
||||
.chain()
|
||||
.map(function (filter) { return filter.get_context();})
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
this.on_search(domains, contexts, groupbys);
|
||||
},
|
||||
on_search: function() {
|
||||
this.log("on_search");
|
||||
// collect all non disabled domains definitions, AND them
|
||||
// evaluate as python expression
|
||||
// save the result in this.domain
|
||||
this.dataset.fetch();
|
||||
/**
|
||||
* Triggered after the SearchView has collected all relevant domains and
|
||||
* contexts.
|
||||
*
|
||||
* It is provided with an Array of domains and an Array of contexts, which
|
||||
* may or may not be evaluated (each item can be either a valid domain or
|
||||
* context, or a string to evaluate in order in the sequence)
|
||||
*
|
||||
* It is also passed an array of contexts used for group_by (they are in
|
||||
* the correct order for group_by evaluation, which contexts may not be)
|
||||
*
|
||||
* @event
|
||||
* @param {Array} domains an array of literal domains or domain references
|
||||
* @param {Array} contexts an array of literal contexts or context refs
|
||||
* @param {Array} groupbys ordered contexts which may or may not have group_by keys
|
||||
*/
|
||||
on_search: function (domains, contexts, groupbys) { },
|
||||
/**
|
||||
* Triggered after a validation error in the SearchView fields.
|
||||
*
|
||||
* Error objects have three keys:
|
||||
* * ``field`` is the name of the invalid field
|
||||
* * ``value`` is the invalid value
|
||||
* * ``message`` is the (in)validation message provided by the field
|
||||
*
|
||||
* @event
|
||||
* @param {Array} errors a never-empty array of error objects
|
||||
*/
|
||||
on_invalid: function (errors) { },
|
||||
do_clear: function (e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.on_clear();
|
||||
},
|
||||
on_clear: function() {
|
||||
/**
|
||||
* Triggered when the search view gets cleared
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
on_clear: function () { },
|
||||
/**
|
||||
* Called by a filter propagating its state changes
|
||||
*
|
||||
* @param {openerp.base.search.Filter} filter a filter which got toggled
|
||||
* @param {Boolean} default_enabled filter got enabled through the default values, at render time.
|
||||
*/
|
||||
do_toggle_filter: function (filter, default_enabled) {
|
||||
if (default_enabled || filter.is_enabled()) {
|
||||
this.enabled_filters.push(filter);
|
||||
} else {
|
||||
this.enabled_filters = _.without(
|
||||
this.enabled_filters, filter);
|
||||
}
|
||||
|
||||
if (!default_enabled) {
|
||||
// selecting a filter after initial loading automatically
|
||||
// triggers refresh
|
||||
this.$element.find('form').submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
openerp.base.SearchViewInput = openerp.base.Controller.extend({
|
||||
// TODO not sure should we create a controller for every input ?
|
||||
openerp.base.search = {};
|
||||
openerp.base.search.Invalid = Class.extend({
|
||||
init: function (field, value, message) {
|
||||
this.field = field;
|
||||
this.value = value;
|
||||
this.message = message;
|
||||
},
|
||||
toString: function () {
|
||||
return ('Incorrect value for field ' + this.field +
|
||||
': [' + this.value + '] is ' + this.message);
|
||||
}
|
||||
});
|
||||
openerp.base.search.Widget = openerp.base.Controller.extend({
|
||||
template: null,
|
||||
init: function (view) {
|
||||
this.view = view;
|
||||
},
|
||||
/**
|
||||
* Sets and returns a globally unique identifier for the widget.
|
||||
*
|
||||
* If a prefix is specified, the identifier will be appended to it.
|
||||
*
|
||||
* @params prefix prefix sections, empty/falsy sections will be removed
|
||||
*/
|
||||
make_id: function () {
|
||||
this.element_id = _.uniqueId(
|
||||
['search'].concat(
|
||||
_.compact(_.toArray(arguments)),
|
||||
['']).join('_'));
|
||||
return this.element_id;
|
||||
},
|
||||
/**
|
||||
* "Starts" the widgets. Called at the end of the rendering, this allows
|
||||
* widgets to hook themselves to their view sections.
|
||||
*
|
||||
* On widgets, if they kept a reference to a view and have an element_id,
|
||||
* will fetch and set their root element on $element.
|
||||
*/
|
||||
start: function () {
|
||||
this._super();
|
||||
if (this.view && this.element_id) {
|
||||
// id is unique, and no getElementById on elements
|
||||
this.$element = $(document.getElementById(
|
||||
this.element_id));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* "Stops" the widgets. Called when the view destroys itself, this
|
||||
* lets the widgets clean up after themselves.
|
||||
*/
|
||||
stop: function () {
|
||||
delete this.view;
|
||||
this._super();
|
||||
},
|
||||
render: function (defaults) {
|
||||
return QWeb.render(
|
||||
this.template, _.extend(this, {
|
||||
defaults: defaults
|
||||
}));
|
||||
}
|
||||
});
|
||||
openerp.base.search.FilterGroup = openerp.base.search.Widget.extend({
|
||||
template: 'SearchView.filters',
|
||||
init: function (filters, view) {
|
||||
this._super(view);
|
||||
this.filters = filters;
|
||||
},
|
||||
start: function () {
|
||||
this._super();
|
||||
_.each(this.filters, function (filter) {
|
||||
filter.start();
|
||||
});
|
||||
}
|
||||
});
|
||||
openerp.base.search.Group = openerp.base.search.Widget.extend({
|
||||
template: 'SearchView.group',
|
||||
// TODO: contain stuff
|
||||
// TODO: @expand
|
||||
init: function (view_section, view, fields) {
|
||||
this._super(view);
|
||||
this.attrs = view_section.attrs;
|
||||
this.lines = view.make_widgets(
|
||||
view_section.children, fields);
|
||||
this.make_id('group');
|
||||
},
|
||||
start: function () {
|
||||
this._super();
|
||||
_(this.lines)
|
||||
.chain()
|
||||
.flatten()
|
||||
.each(function (widget) { widget.start(); });
|
||||
var $root = this.$element;
|
||||
$root.find('a.searchview_group_string').click(function (e) {
|
||||
$root.toggleClass('folded expanded');
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
});
|
||||
openerp.base.search.Input = openerp.base.search.Widget.extend({
|
||||
init: function (view) {
|
||||
this._super(view);
|
||||
this.view.inputs.push(this);
|
||||
},
|
||||
get_context: function () {
|
||||
throw new Error(
|
||||
"get_context not implemented for widget " + this.attrs.type);
|
||||
},
|
||||
get_domain: function () {
|
||||
throw new Error(
|
||||
"get_domain not implemented for widget " + this.attrs.type);
|
||||
}
|
||||
});
|
||||
openerp.base.search.Filter = openerp.base.search.Input.extend({
|
||||
template: 'SearchView.filter',
|
||||
// TODO: force rendering
|
||||
init: function (node, view) {
|
||||
this._super(view);
|
||||
this.attrs = node.attrs;
|
||||
this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
|
||||
this.make_id('filter', this.attrs.name);
|
||||
},
|
||||
start: function () {
|
||||
this._super();
|
||||
var self = this;
|
||||
this.$element.click(function () {
|
||||
$(this).toggleClass('enabled');
|
||||
self.view.do_toggle_filter(self);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns whether the filter is currently enabled (in use) or not.
|
||||
*
|
||||
* @returns a boolean
|
||||
*/
|
||||
is_enabled:function () {
|
||||
return this.$element.hasClass('enabled');
|
||||
},
|
||||
/**
|
||||
* If the filter is present in the defaults (and has a truthy value),
|
||||
* enable the filter.
|
||||
*
|
||||
* @param {Object} defaults the search view's default values
|
||||
*/
|
||||
render: function (defaults) {
|
||||
if (this.attrs.name && defaults[this.attrs.name]) {
|
||||
this.classes.push('enabled');
|
||||
this.view.do_toggle_filter(this, true);
|
||||
}
|
||||
return this._super(defaults);
|
||||
},
|
||||
get_context: function () {
|
||||
if (!this.is_enabled()) {
|
||||
return;
|
||||
}
|
||||
return this.attrs.context;
|
||||
},
|
||||
get_domain: function () {
|
||||
if (!this.is_enabled()) {
|
||||
return;
|
||||
}
|
||||
return this.attrs.domain;
|
||||
}
|
||||
});
|
||||
openerp.base.search.Field = openerp.base.search.Input.extend({
|
||||
template: 'SearchView.field',
|
||||
default_operator: '=',
|
||||
// TODO: set default values
|
||||
// TODO: get context, domain
|
||||
// TODO: holds Filters
|
||||
init: function (view_section, field, view) {
|
||||
this._super(view);
|
||||
this.attrs = _.extend({}, field, view_section.attrs);
|
||||
this.filters = new openerp.base.search.FilterGroup(_.map(
|
||||
view_section.children, function (filter_node) {
|
||||
return new openerp.base.search.Filter(
|
||||
filter_node, view);
|
||||
}), view);
|
||||
this.make_id('input', field.type, this.attrs.name);
|
||||
},
|
||||
start: function () {
|
||||
this._super();
|
||||
this.filters.start();
|
||||
},
|
||||
get_context: function () {
|
||||
var val = this.get_value();
|
||||
// A field needs a value to be "active", and a context to send when
|
||||
// active
|
||||
var has_value = (val !== null && val !== '');
|
||||
var context = this.attrs.context;
|
||||
if (!(has_value && context)) {
|
||||
return;
|
||||
}
|
||||
return _.extend(
|
||||
{}, context,
|
||||
{own_values: {self: val}});
|
||||
},
|
||||
get_domain: function () {
|
||||
var val = this.get_value();
|
||||
|
||||
// of we just keep a simple dict for each input in
|
||||
// openerp.base.SearchView#input_ids
|
||||
// and use if when we get an event depending on the type
|
||||
// i think it's less bloated to avoid useless controllers
|
||||
|
||||
// but i think for many2one a controller would be nice
|
||||
// so simple dict for simple inputs
|
||||
// an controller for many2one ?
|
||||
var has_value = (val !== null && val !== '');
|
||||
if(!has_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
var domain = this.attrs['filter_domain'];
|
||||
if (!domain) {
|
||||
return [[
|
||||
this.attrs.name,
|
||||
this.attrs.operator || this.default_operator,
|
||||
this.get_value()
|
||||
]];
|
||||
}
|
||||
return _.extend(
|
||||
{}, domain,
|
||||
{own_values: {self: val}});
|
||||
}
|
||||
});
|
||||
openerp.base.search.CharField = openerp.base.search.Field.extend({
|
||||
default_operator: 'ilike',
|
||||
get_value: function () {
|
||||
return this.$element.val();
|
||||
}
|
||||
});
|
||||
openerp.base.search.BooleanField = openerp.base.search.Field.extend({
|
||||
template: 'SearchView.field.selection',
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.attrs.selection = [
|
||||
['true', 'Yes'],
|
||||
['false', 'No']
|
||||
];
|
||||
},
|
||||
/**
|
||||
* Search defaults likely to be boolean values (for a boolean field).
|
||||
*
|
||||
* In the HTML, we only get strings, and our strings here are
|
||||
* <code>'true'</code> and <code>'false'</code>, so ensure we get only
|
||||
* those by truth-testing the default value.
|
||||
*
|
||||
* @param {Object} defaults default values for this search view
|
||||
*/
|
||||
render: function (defaults) {
|
||||
var name = this.attrs.name;
|
||||
if (name in defaults) {
|
||||
defaults[name] = defaults[name] ? "true" : "false";
|
||||
}
|
||||
return this._super(defaults);
|
||||
},
|
||||
get_value: function () {
|
||||
switch (this.$element.val()) {
|
||||
case 'false': return false;
|
||||
case 'true': return true;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
openerp.base.search.IntegerField = openerp.base.search.Field.extend({
|
||||
get_value: function () {
|
||||
if (!this.$element.val()) {
|
||||
return null;
|
||||
}
|
||||
var val = parseInt(this.$element.val());
|
||||
var check = Number(this.$element.val());
|
||||
if (isNaN(check) || val !== check) {
|
||||
this.$element.addClass('error');
|
||||
throw new openerp.base.search.Invalid(
|
||||
this.attrs.name, this.$element.val(), "not a valid integer");
|
||||
}
|
||||
this.$element.removeClass('error');
|
||||
return val;
|
||||
}
|
||||
});
|
||||
openerp.base.search.FloatField = openerp.base.search.Field.extend({
|
||||
get_value: function () {
|
||||
var val = Number(this.$element.val());
|
||||
if (isNaN(val)) {
|
||||
this.$element.addClass('error');
|
||||
throw new openerp.base.search.Invalid(
|
||||
this.attrs.name, this.$element.val(), "not a valid number");
|
||||
}
|
||||
this.$element.removeClass('error');
|
||||
return val;
|
||||
}
|
||||
});
|
||||
openerp.base.search.SelectionField = openerp.base.search.Field.extend({
|
||||
template: 'SearchView.field.selection',
|
||||
get_value: function () {
|
||||
return this.$element.val();
|
||||
}
|
||||
});
|
||||
openerp.base.search.DateTimeField = openerp.base.search.Field.extend({
|
||||
get_value: function () {
|
||||
return this.$element.val();
|
||||
}
|
||||
});
|
||||
openerp.base.search.DateField = openerp.base.search.Field.extend({
|
||||
get_value: function () {
|
||||
return this.$element.val();
|
||||
}
|
||||
});
|
||||
openerp.base.search.OneToManyField = openerp.base.search.IntegerField.extend({
|
||||
// TODO: .relation, .context, .domain
|
||||
});
|
||||
openerp.base.search.ManyToOneField = openerp.base.search.IntegerField.extend({
|
||||
// TODO: @widget
|
||||
// TODO: .relation, .selection, .context, .domain
|
||||
});
|
||||
openerp.base.search.ManyToManyField = openerp.base.search.IntegerField.extend({
|
||||
// TODO: .related_columns (Array), .context, .domain
|
||||
});
|
||||
|
||||
openerp.base.FormView = openerp.base.Controller.extend({
|
||||
@@ -457,10 +980,10 @@ openerp.base.FormView = openerp.base.Controller.extend({
|
||||
}
|
||||
// bind to all wdigets that have onchange ??
|
||||
|
||||
this.dataset.on_fetch.add(this.on_record_loaded);
|
||||
this.dataset.on_active_id.add(this.on_record_loaded);
|
||||
},
|
||||
on_record_loaded: function(records) {
|
||||
this.datarecord = records[0];
|
||||
on_record_loaded: function(record) {
|
||||
this.datarecord = record;
|
||||
for (var f in this.fields) {
|
||||
this.fields[f].set_value(this.datarecord.values[f]);
|
||||
}
|
||||
@@ -514,25 +1037,9 @@ openerp.base.ListView = openerp.base.Controller.extend({
|
||||
}
|
||||
this.dataset.fields = this.cols;
|
||||
this.dataset.on_fetch.add(this.do_fill_table);
|
||||
},
|
||||
do_fill_table: function(records) {
|
||||
this.log("do_fill_table");
|
||||
|
||||
var self = this;
|
||||
//this.log(this.dataset.data);
|
||||
var rows = [];
|
||||
for(var i = 0; i < records.length; i++) {
|
||||
// TODO very strange is sometimes non existing ? even as admin ? example ir.ui.menu
|
||||
var row = records[i].values;
|
||||
if(row)
|
||||
rows.push(row);
|
||||
// else
|
||||
// debugger;
|
||||
}
|
||||
//this.log(rows);
|
||||
var width = this.$element.width();
|
||||
this.$table.jqGrid({
|
||||
data: rows,
|
||||
datatype: "local",
|
||||
height: "100%",
|
||||
rowNum: 100,
|
||||
@@ -543,11 +1050,23 @@ openerp.base.ListView = openerp.base.Controller.extend({
|
||||
viewrecords: true,
|
||||
caption: this.name
|
||||
}).setGridWidth(width);
|
||||
|
||||
var self = this;
|
||||
$(window).bind('resize', function() {
|
||||
self.$element.children().hide();
|
||||
self.$table.setGridWidth(self.$element.width());
|
||||
self.$element.children().show();
|
||||
}).trigger('resize');
|
||||
},
|
||||
do_fill_table: function(records) {
|
||||
this.log("do_fill_table");
|
||||
|
||||
this.$table
|
||||
.clearGridData()
|
||||
.addRowData('id', _.map(records, function (record) {
|
||||
return record.values;
|
||||
}));
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ var QWeb = {
|
||||
if (e = this.templates[name]) {
|
||||
return this.render_node(e, v);
|
||||
}
|
||||
return "template " + name + " not found";
|
||||
throw new Error("template " + name + " not found");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import xml.etree.ElementTree
|
||||
import mock
|
||||
|
||||
import unittest2
|
||||
|
||||
import base.controllers.main
|
||||
import openerpweb.nonliterals
|
||||
import openerpweb.openerpweb
|
||||
|
||||
#noinspection PyCompatibility
|
||||
class ViewTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.view = base.controllers.main.View()
|
||||
def test_identity(self):
|
||||
view = base.controllers.main.View()
|
||||
base_view = """
|
||||
<form string="Title">
|
||||
<group>
|
||||
@@ -18,10 +23,95 @@ class ViewTest(unittest2.TestCase):
|
||||
"""
|
||||
|
||||
pristine = xml.etree.ElementTree.fromstring(base_view)
|
||||
transformed = view.transform_view(base_view)
|
||||
transformed = self.view.transform_view(base_view, None)
|
||||
|
||||
self.assertEqual(
|
||||
xml.etree.ElementTree.tostring(transformed),
|
||||
xml.etree.ElementTree.tostring(pristine)
|
||||
)
|
||||
|
||||
def test_convert_literal_domain(self):
|
||||
e = xml.etree.ElementTree.Element(
|
||||
'field', domain="[('somefield', '=', 3)]")
|
||||
self.view.parse_domains_and_contexts(e, None)
|
||||
|
||||
self.assertEqual(
|
||||
e.get('domain'),
|
||||
[('somefield', '=', 3)])
|
||||
|
||||
def test_convert_complex_domain(self):
|
||||
e = xml.etree.ElementTree.Element(
|
||||
'field',
|
||||
domain="[('account_id.type','in',['receivable','payable']),"
|
||||
"('reconcile_id','=',False),"
|
||||
"('reconcile_partial_id','=',False),"
|
||||
"('state', '=', 'valid')]"
|
||||
)
|
||||
self.view.parse_domains_and_contexts(e, None)
|
||||
|
||||
self.assertEqual(
|
||||
e.get('domain'),
|
||||
[('account_id.type', 'in', ['receivable', 'payable']),
|
||||
('reconcile_id', '=', False),
|
||||
('reconcile_partial_id', '=', False),
|
||||
('state', '=', 'valid')]
|
||||
)
|
||||
|
||||
def test_retrieve_nonliteral_domain(self):
|
||||
session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession)
|
||||
session.domains_store = {}
|
||||
domain_string = ("[('month','=',(datetime.date.today() - "
|
||||
"datetime.timedelta(365/12)).strftime('%%m'))]")
|
||||
e = xml.etree.ElementTree.Element(
|
||||
'field', domain=domain_string)
|
||||
|
||||
self.view.parse_domains_and_contexts(e, session)
|
||||
|
||||
self.assertIsInstance(e.get('domain'), openerpweb.nonliterals.Domain)
|
||||
self.assertEqual(
|
||||
openerpweb.nonliterals.Domain(
|
||||
session, key=e.get('domain').key).get_domain_string(),
|
||||
domain_string)
|
||||
|
||||
def test_convert_literal_context(self):
|
||||
e = xml.etree.ElementTree.Element(
|
||||
'field', context="{'some_prop': 3}")
|
||||
self.view.parse_domains_and_contexts(e, None)
|
||||
|
||||
self.assertEqual(
|
||||
e.get('context'),
|
||||
{'some_prop': 3})
|
||||
|
||||
def test_convert_complex_context(self):
|
||||
e = xml.etree.ElementTree.Element(
|
||||
'field',
|
||||
context="{'account_id.type': ['receivable','payable'],"
|
||||
"'reconcile_id': False,"
|
||||
"'reconcile_partial_id': False,"
|
||||
"'state': 'valid'}"
|
||||
)
|
||||
self.view.parse_domains_and_contexts(e, None)
|
||||
|
||||
self.assertEqual(
|
||||
e.get('context'),
|
||||
{'account_id.type': ['receivable', 'payable'],
|
||||
'reconcile_id': False,
|
||||
'reconcile_partial_id': False,
|
||||
'state': 'valid'}
|
||||
)
|
||||
|
||||
def test_retrieve_nonliteral_context(self):
|
||||
session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession)
|
||||
session.contexts_store = {}
|
||||
context_string = ("{'month': (datetime.date.today() - "
|
||||
"datetime.timedelta(365/12)).strftime('%%m')}")
|
||||
e = xml.etree.ElementTree.Element(
|
||||
'field', context=context_string)
|
||||
|
||||
self.view.parse_domains_and_contexts(e, session)
|
||||
|
||||
self.assertIsInstance(e.get('context'), openerpweb.nonliterals.Context)
|
||||
self.assertEqual(
|
||||
openerpweb.nonliterals.Context(
|
||||
session, key=e.get('context').key).get_context_string(),
|
||||
context_string)
|
||||
|
||||
@@ -275,6 +275,15 @@ convenient ways to refer to them and their properties.
|
||||
|
||||
.. js:attribute:: sort
|
||||
|
||||
Python
|
||||
++++++
|
||||
|
||||
.. autoclass:: openerpweb.openerpweb.OpenERPSession
|
||||
:members:
|
||||
|
||||
.. autoclass:: openerpweb.openerpweb.OpenERPModel
|
||||
:members:
|
||||
|
||||
* Addons lifecycle (loading, execution, events, ...)
|
||||
|
||||
* Python-side
|
||||
|
||||
+285
-3
@@ -1,5 +1,5 @@
|
||||
Contributing to OpenERP Web
|
||||
===========================
|
||||
OpenERP Web Core and standard addons
|
||||
====================================
|
||||
|
||||
* General organization and core ideas (design philosophies)
|
||||
* Internal documentation, autodoc, Python and JS domains
|
||||
@@ -8,14 +8,296 @@ Contributing to OpenERP Web
|
||||
* Style guide and coding conventions (PEP8? More)
|
||||
* Test frameworks in JS?
|
||||
|
||||
Standard Views
|
||||
--------------
|
||||
|
||||
Search View
|
||||
+++++++++++
|
||||
|
||||
The OpenERP search view really is a sub-view, used in support of views
|
||||
acting on collections of records (list view or graph view, for
|
||||
instance).
|
||||
|
||||
Its main goal is to collect information from its widgets (themselves
|
||||
collecting information from the users) and make those available to the
|
||||
rest of the client.
|
||||
|
||||
The search view's root is :js:class:`~openerp.base.SearchView`. This
|
||||
object should never need to be created or managed directly, its
|
||||
lifecycle should be driven by the
|
||||
:js:class:`~openerp.base.ViewManager`.
|
||||
|
||||
.. TODO: insert SearchView constructor here
|
||||
|
||||
The search view defines a number of internal and external protocols to
|
||||
communicate with the objects around and within it. Most of these
|
||||
protocols are informal, and types available for inheritance are more
|
||||
mixins than mandatory.
|
||||
|
||||
Events
|
||||
""""""
|
||||
|
||||
``on_loaded``
|
||||
|
||||
.. TODO: method openerp.base.SearchView.on_loaded
|
||||
|
||||
Fires when the search view receives its view data (the result of
|
||||
``fields_view_get``). Hooking up before the event allows for
|
||||
altering view data before it can be used.
|
||||
|
||||
By the time ``on_loaded`` is done, the search view is guaranteed to
|
||||
be fully set up and ready to use.
|
||||
|
||||
``on_search``
|
||||
|
||||
.. TODO: method openerp.base.SearchView.on_search
|
||||
|
||||
Event triggered after a user asked for a search. The search view
|
||||
fires this event after collecting all input data (contexts, domains
|
||||
and group_by contexts). Note that the search view does *not* merge
|
||||
those (or therwise evaluate them), they are returned as provided by
|
||||
the various inputs within the view.
|
||||
|
||||
``on_clear``
|
||||
|
||||
.. TODO: method openerp.base.SearchView.on_clear
|
||||
|
||||
Triggered after a user asked for a form clearing.
|
||||
|
||||
Input management
|
||||
""""""""""""""""
|
||||
|
||||
An important concept in the search view is that of input. It is both
|
||||
an informal protocol and an abstract type that can be inherited from.
|
||||
|
||||
Inputs are widgets which can contain user data (a char widget for
|
||||
instance, or a selection box). They are able of action and of
|
||||
reaction:
|
||||
|
||||
.. _views-search-registration:
|
||||
|
||||
``registration``
|
||||
|
||||
This is an input action. Inputs have to register themselves to the
|
||||
main view (which they receive as a constructor argument). This is
|
||||
performed by pushing themselves on the
|
||||
:js:attr:`openerp.base.SearchView.inputs` array.
|
||||
|
||||
``get_context``
|
||||
|
||||
An input reaction. When it needs to collect contexts, the view calls
|
||||
``get_context()`` on all its inputs.
|
||||
|
||||
Inputs can react in the following manners:
|
||||
|
||||
* Return a context (an object), this is the "normal" response if the
|
||||
input holds a value.
|
||||
|
||||
* Return a falsy value (generally ``null``). This value indicates
|
||||
the input does not contain any value and will not take part in the
|
||||
research.
|
||||
|
||||
* Raise :js:class:`openerp.base.search.Invalid` to indicate that it
|
||||
holds a value but this value can not be used in the search
|
||||
(because it is incorrectly formatted or nonsensical). Raising
|
||||
:js:class:`~openerp.base.search.Invalid` is guaranteed to cancel
|
||||
the search process.
|
||||
|
||||
:js:class:`~openerp.base.search.Invalid` takes three mandatory
|
||||
arguments: an indentifier (a name for instance), the invalid value
|
||||
and a validation message indicating the issue.
|
||||
|
||||
``get_domain``
|
||||
|
||||
The second input reaction, the possible behaviors of inputs are the
|
||||
same as for ``get_context``.
|
||||
|
||||
The :js:class:`openerp.base.search.Input` type implements registration
|
||||
on its own, but its implementations of ``get_context`` and
|
||||
``get_domain`` simply raise errors and *have* to be overridden.
|
||||
|
||||
One last action is for filters, as an activation order has to be kept
|
||||
on them for some controls (establish the correct grouping sequence for
|
||||
instance).
|
||||
|
||||
To that end, filters can call
|
||||
:js:func:`openerp.base.Search.do_toggle_filter`, providing themselves
|
||||
as first argument.
|
||||
|
||||
Filters calling :js:func:`~openerp.base.Search.do_toggle_filter` also
|
||||
need to implement a method called
|
||||
:js:func:`~openerp.base.search.Filter.is_enabled`, which the search
|
||||
view will use to know the current status of the filter.
|
||||
|
||||
The search view automatically triggers a search after calls to
|
||||
:js:func:`~openerp.base.Search.do_toggle_filter`.
|
||||
|
||||
Life cycle
|
||||
""""""""""
|
||||
|
||||
The search view has a pretty simple and linear life cycle, in three main steps:
|
||||
|
||||
:js:class:`init <openerp.base.SearchView>`
|
||||
|
||||
Nothing interesting happens here
|
||||
|
||||
:js:func:`~openerp.base.SearchView.start`
|
||||
|
||||
Called by the main view's creator, this is the main initialization
|
||||
step for the list view.
|
||||
|
||||
It begins with a remote call to fetch the view's descriptors
|
||||
(``fields_view_get``).
|
||||
|
||||
Once the remote call is complete, the ``on_loaded`` even happens,
|
||||
holding three main operations:
|
||||
|
||||
:js:func:`~openerp.base.SearchView.make_widgets`
|
||||
|
||||
Builds and returns the top-level widgets of the search
|
||||
view. Because it returns an array of widget lines (a 2-dimensional
|
||||
matrix of widgets) it should be called recursively by container
|
||||
widgets (:js:class:`openerp.base.search.Group` for instance).
|
||||
|
||||
:js:func:`~openerp.base.search.Widget.render`
|
||||
|
||||
Called by the search view on all top-level widgets. Container
|
||||
widgets should recursively call this method on their own children
|
||||
widgets.
|
||||
|
||||
Widgets are provided with a mapping of ``{name: value}`` holding
|
||||
default values for the search view. They can freely pick their
|
||||
initial values from there, but must pass the mapping to their
|
||||
children widgets if they have any.
|
||||
|
||||
:js:func:`~openerp.base.search.Widget.start`
|
||||
|
||||
The last operation of the search view startup is to initialize all
|
||||
its widgets in order. This is again done recursively (the search
|
||||
view starts its children, which have to start their own children).
|
||||
|
||||
:js:func:`~openerp.base.SearchView.stop`
|
||||
|
||||
Used before discarding a search view, allows the search view to
|
||||
disable its events and pass the message to its own widgets,
|
||||
gracefully shutting down the whole view.
|
||||
|
||||
Widgets
|
||||
"""""""
|
||||
|
||||
In a search view, the widget is simply a unit of display.
|
||||
|
||||
All widgets must be able to react to three events, which will be
|
||||
called in this order:
|
||||
|
||||
:js:func:`~openerp.base.search.Widget.render`
|
||||
|
||||
Called with a map of default values. The widget must return a
|
||||
``String``, which is its HTML representation. That string can be
|
||||
empty (if the widget should not be represented).
|
||||
|
||||
Widgets are responsible for asking their children for rendering, and
|
||||
for passing along the default values.
|
||||
|
||||
:js:func:`~openerp.base.search.Widget.start`
|
||||
|
||||
Called without arguments. At this point, the widget has been fully
|
||||
rendered and can set its events up, if any.
|
||||
|
||||
The widget is responsible for starting its children, if it has any.
|
||||
|
||||
:js:func:`~openerp.base.search.Widget.stop`
|
||||
|
||||
Gives the widget the opportunity to unbind its events, remove itself
|
||||
from the DOM and perform any other cleanup task it may have.
|
||||
|
||||
Event if the widget does not do anything itself, it is responsible
|
||||
for shutting down its children.
|
||||
|
||||
An abstract type is available and can be inherited from, to simplify
|
||||
the implementation of those tasks:
|
||||
|
||||
.. TODO: insert Widget here
|
||||
|
||||
.. remember to document all methods
|
||||
|
||||
Inputs
|
||||
""""""
|
||||
|
||||
The search namespace (``openerp.base.search``) provides two more
|
||||
abstract types, used to implement input widgets:
|
||||
|
||||
* :js:class:`openerp.base.search.Input` is the most basic input type,
|
||||
it only implements :ref:`input registration
|
||||
<views-search-registration>`.
|
||||
|
||||
If inherited from, descendant classes should not call its
|
||||
implementations of ``get_context`` and ``get_domain``.
|
||||
|
||||
* :js:class:`openerp.base.search.Field` is used to implement more
|
||||
"field" widgets (which allow the user to input potentially complex
|
||||
values).
|
||||
|
||||
It provides various services for its subclasses:
|
||||
|
||||
* Sets up the field attributes, using attributes from the field and
|
||||
the view node.
|
||||
|
||||
* It fills the widget with :js:class:`~openerp.base.search.Filter`
|
||||
if the field has any child filter.
|
||||
|
||||
* It automatically generates an identifier based on the field type
|
||||
and the field name, using
|
||||
:js:func:`~openerp.base.search.Widget.make_id`.
|
||||
|
||||
* It sets up a basic (overridable)
|
||||
:js:attr:`~opererp.base.search.Field.template` attribute, combined
|
||||
with the previous tasks, this makes subclasses of
|
||||
:js:class:`~openerp.base.search.Field` render themselves "for
|
||||
free".
|
||||
|
||||
* It provides basic implementations of ``get_context`` and
|
||||
``get_domain``, both hinging on the subclasses implementing
|
||||
``get_value()`` (which should return a correct, converted
|
||||
Javascript value):
|
||||
|
||||
:js:func:`~openerp.base.search.Field.get_context`
|
||||
|
||||
Checks if the field has a non-``null`` and non-empty
|
||||
(``String``) value, and that the field has a ``context`` attr.
|
||||
|
||||
If both conditions are fullfilled, returns the context.
|
||||
|
||||
:js:func:`~openerp.base.search.Field.get_domain`
|
||||
|
||||
Only requires that the field has a non-``null`` and non-empty
|
||||
value.
|
||||
|
||||
If the field has a ``filter_domain``, returns it
|
||||
immediately. Otherwise, builds a context using the field's
|
||||
name, the field :js:attr:`~openerp.base.search.Field.operator`
|
||||
and the field value, and returns it.
|
||||
|
||||
.. TODO: insert Input, Field, Filter, and just about every Field subclass
|
||||
|
||||
Internal API Doc
|
||||
----------------
|
||||
|
||||
Python
|
||||
++++++
|
||||
|
||||
.. autoclass:: base.controllers.main.DataSet
|
||||
These classes should be moved to other sections of the doc as needed,
|
||||
probably.
|
||||
|
||||
.. automodule:: openerpweb.openerpweb
|
||||
:members: JsonRequest
|
||||
|
||||
See also: :class:`~openerpweb.openerpweb.OpenERPSession`,
|
||||
:class:`~openerpweb.openerpweb.OpenERPModel`
|
||||
|
||||
.. automodule:: base.controllers.main
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Backport of Python 2.6's ast.py for Python 2.5
|
||||
"""
|
||||
__all__ = ['literal_eval']
|
||||
try:
|
||||
from ast import literal_eval
|
||||
except ImportError:
|
||||
from _ast import *
|
||||
from _ast import __version__
|
||||
|
||||
|
||||
def parse(expr, filename='<unknown>', mode='exec'):
|
||||
"""
|
||||
Parse an expression into an AST node.
|
||||
Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
|
||||
"""
|
||||
return compile(expr, filename, mode, PyCF_ONLY_AST)
|
||||
|
||||
|
||||
def literal_eval(node_or_string):
|
||||
"""
|
||||
Safely evaluate an expression node or a string containing a Python
|
||||
expression. The string or node provided may only consist of the
|
||||
following Python literal structures: strings, numbers, tuples, lists,
|
||||
dicts, booleans, and None.
|
||||
"""
|
||||
_safe_names = {'None': None, 'True': True, 'False': False}
|
||||
if isinstance(node_or_string, basestring):
|
||||
node_or_string = parse(node_or_string, mode='eval')
|
||||
if isinstance(node_or_string, Expression):
|
||||
node_or_string = node_or_string.body
|
||||
def _convert(node):
|
||||
if isinstance(node, Str):
|
||||
return node.s
|
||||
elif isinstance(node, Num):
|
||||
return node.n
|
||||
elif isinstance(node, Tuple):
|
||||
return tuple(map(_convert, node.elts))
|
||||
elif isinstance(node, List):
|
||||
return list(map(_convert, node.elts))
|
||||
elif isinstance(node, Dict):
|
||||
return dict((_convert(k), _convert(v)) for k, v
|
||||
in zip(node.keys, node.values))
|
||||
elif isinstance(node, Name):
|
||||
if node.id in _safe_names:
|
||||
return _safe_names[node.id]
|
||||
raise ValueError('malformed string')
|
||||
return _convert(node_or_string)
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Manages the storage and lifecycle of non-literal domains and contexts
|
||||
(and potentially other structures) which have to be evaluated with client data,
|
||||
but still need to be safely round-tripped to and from the browser (and thus
|
||||
can't be sent there themselves).
|
||||
"""
|
||||
import binascii
|
||||
import hashlib
|
||||
import simplejson.decoder
|
||||
import simplejson.encoder
|
||||
|
||||
__all__ = ['Domain', 'Context', 'NonLiteralEncoder, non_literal_decoder']
|
||||
|
||||
#: 48 bits should be sufficient to have almost no chance of collision
|
||||
#: with a million hashes, according to hg@67081329d49a
|
||||
SHORT_HASH_BYTES_SIZE = 6
|
||||
|
||||
class NonLiteralEncoder(simplejson.encoder.JSONEncoder):
|
||||
def default(self, object):
|
||||
if isinstance(object, Domain):
|
||||
return {
|
||||
'__ref': 'domain',
|
||||
'__id': object.key
|
||||
}
|
||||
elif isinstance(object, Context):
|
||||
return {
|
||||
'__ref': 'context',
|
||||
'__id': object.key
|
||||
}
|
||||
return super(NonLiteralEncoder, self).default(object)
|
||||
|
||||
def non_literal_decoder(dct):
|
||||
""" Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
|
||||
magic attribute tags.
|
||||
|
||||
Also handles private context section for the domain or section via the
|
||||
``own_values`` dict key.
|
||||
"""
|
||||
if '__ref' in dct:
|
||||
if dct['__ref'] == 'domain':
|
||||
domain = Domain(None, key=dct['__id'])
|
||||
if 'own_values' in dct:
|
||||
domain.own = dct['own_values']
|
||||
return domain
|
||||
elif dct['__ref'] == 'context':
|
||||
context = Context(None, key=dct['__id'])
|
||||
if 'own_values' in dct:
|
||||
context.own = dct['own_values']
|
||||
return context
|
||||
return dct
|
||||
|
||||
class Domain(object):
|
||||
def __init__(self, session, domain_string=None, key=None):
|
||||
""" Uses session information to store the domain string and map it to a
|
||||
domain key, which can be safely round-tripped to the client.
|
||||
|
||||
If initialized with a domain string, will generate a key for that
|
||||
string and store the domain string out of the way. When initialized
|
||||
with a key, considers this key is a reference to an existing domain
|
||||
string.
|
||||
|
||||
:param session: the OpenERP Session to use when evaluating the domain
|
||||
:type session: openerpweb.openerpweb.OpenERPSession
|
||||
:param str domain_string: a non-literal domain in string form
|
||||
:param str key: key used to retrieve the domain string
|
||||
"""
|
||||
if domain_string and key:
|
||||
raise ValueError("A nonliteral domain can not take both a key "
|
||||
"and a domain string")
|
||||
|
||||
self.session = session
|
||||
self.own = {}
|
||||
if domain_string:
|
||||
self.key = binascii.hexlify(
|
||||
hashlib.sha256(domain_string).digest()[:SHORT_HASH_BYTES_SIZE])
|
||||
self.session.domains_store[self.key] = domain_string
|
||||
elif key:
|
||||
self.key = key
|
||||
|
||||
def get_domain_string(self):
|
||||
""" Retrieves the domain string linked to this non-literal domain in
|
||||
the provided session.
|
||||
|
||||
:param session: the OpenERP Session used to store the domain string in
|
||||
the first place.
|
||||
:type session: openerpweb.openerpweb.OpenERPSession
|
||||
"""
|
||||
return self.session.domains_store[self.key]
|
||||
|
||||
def evaluate(self, context=None):
|
||||
""" Forces the evaluation of the linked domain, using the provided
|
||||
context (as well as the session's base context), and returns the
|
||||
evaluated result.
|
||||
"""
|
||||
ctx = self.session.evaluation_context(context)
|
||||
if self.own:
|
||||
ctx.update(self.own)
|
||||
return eval(self.get_domain_string(), ctx)
|
||||
|
||||
class Context(object):
|
||||
def __init__(self, session, context_string=None, key=None):
|
||||
""" Uses session information to store the context string and map it to
|
||||
a key (stored in a secret location under a secret mountain), which can
|
||||
be safely round-tripped to the client.
|
||||
|
||||
If initialized with a context string, will generate a key for that
|
||||
string and store the context string out of the way. When initialized
|
||||
with a key, considers this key is a reference to an existing context
|
||||
string.
|
||||
|
||||
:param session: the OpenERP Session to use when evaluating the context
|
||||
:type session: openerpweb.openerpweb.OpenERPSession
|
||||
:param str context_string: a non-literal context in string form
|
||||
:param str key: key used to retrieve the context string
|
||||
"""
|
||||
if context_string and key:
|
||||
raise ValueError("A nonliteral domain can not take both a key "
|
||||
"and a domain string")
|
||||
|
||||
self.session = session
|
||||
self.own = {}
|
||||
if context_string:
|
||||
self.key = binascii.hexlify(
|
||||
hashlib.sha256(context_string).digest()[:SHORT_HASH_BYTES_SIZE])
|
||||
self.session.contexts_store[self.key] = context_string
|
||||
elif key:
|
||||
self.key = key
|
||||
|
||||
def get_context_string(self):
|
||||
""" Retrieves the context string linked to this non-literal context in
|
||||
the provided session.
|
||||
|
||||
:param session: the OpenERP Session used to store the context string in
|
||||
the first place.
|
||||
:type session: openerpweb.openerpweb.OpenERPSession
|
||||
"""
|
||||
return self.session.contexts_store[self.key]
|
||||
|
||||
def evaluate(self, context=None):
|
||||
""" Forces the evaluation of the linked context, using the provided
|
||||
context (as well as the session's base context), and returns the
|
||||
evaluated result.
|
||||
"""
|
||||
ctx = self.session.evaluation_context(context)
|
||||
if self.own:
|
||||
ctx.update(self.own)
|
||||
return eval(self.get_context_string(),
|
||||
ctx)
|
||||
+187
-5
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/python
|
||||
import datetime
|
||||
import functools
|
||||
import optparse
|
||||
import time
|
||||
import dateutil.relativedelta
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -12,6 +15,7 @@ import cherrypy
|
||||
import cherrypy.lib.static
|
||||
import simplejson
|
||||
|
||||
import nonliterals
|
||||
import xmlrpctimeout
|
||||
|
||||
#----------------------------------------------------------
|
||||
@@ -39,6 +43,22 @@ class OpenERPModel(object):
|
||||
|
||||
|
||||
class OpenERPSession(object):
|
||||
"""
|
||||
An OpenERP RPC session, a given user can own multiple such sessions
|
||||
in a web session.
|
||||
|
||||
.. attribute:: context
|
||||
|
||||
The session context, a ``dict``. Can be reloaded by calling
|
||||
:meth:`openerpweb.openerpweb.OpenERPSession.get_context`
|
||||
|
||||
.. attribute:: domains_store
|
||||
|
||||
A ``dict`` matching domain keys to evaluable (but non-literal) domains.
|
||||
|
||||
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):
|
||||
self._server = server
|
||||
@@ -49,6 +69,10 @@ class OpenERPSession(object):
|
||||
self._password = False
|
||||
self.model_factory = model_factory
|
||||
|
||||
self.context = {}
|
||||
self.contexts_store = {}
|
||||
self.domains_store = {}
|
||||
|
||||
def proxy(self, service):
|
||||
s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
|
||||
return s
|
||||
@@ -62,6 +86,9 @@ class OpenERPSession(object):
|
||||
uid = self.proxy('common').login(db, login, password)
|
||||
self.bind(db, uid, password)
|
||||
self._login = login
|
||||
|
||||
if uid: self.get_context()
|
||||
|
||||
return uid
|
||||
|
||||
def execute(self, model, func, *l, **d):
|
||||
@@ -71,8 +98,159 @@ class OpenERPSession(object):
|
||||
return r
|
||||
|
||||
def model(self, model):
|
||||
""" Get an RPC proxy for the object ``model``, bound to this session.
|
||||
|
||||
:param model: an OpenERP model name
|
||||
:type model: str
|
||||
:rtype: :class:`openerpweb.openerpweb.OpenERPModel`
|
||||
"""
|
||||
return self.model_factory(self, model)
|
||||
|
||||
def get_context(self):
|
||||
""" Re-initializes the current user's session context (based on
|
||||
his preferences) by calling res.users.get_context() with the old
|
||||
context
|
||||
|
||||
:returns: the new context
|
||||
"""
|
||||
assert self._uid, "The user needs to be logged-in to initialize his context"
|
||||
self.context = self.model('res.users').context_get(self.context)
|
||||
return self.context
|
||||
|
||||
@property
|
||||
def base_eval_context(self):
|
||||
""" Default evaluation context for the session.
|
||||
|
||||
Used to evaluate contexts and domains.
|
||||
"""
|
||||
base = dict(
|
||||
uid=self._uid,
|
||||
current_date=datetime.date.today().strftime('%Y-%m-%d'),
|
||||
time=time,
|
||||
datetime=datetime,
|
||||
relativedelta=dateutil.relativedelta.relativedelta
|
||||
)
|
||||
base.update(self.context)
|
||||
return base
|
||||
|
||||
def evaluation_context(self, context=None):
|
||||
""" Returns the session's evaluation context, augmented with the
|
||||
provided context if any.
|
||||
|
||||
:param dict context: to add merge in the session's base eval context
|
||||
:returns: the augmented context
|
||||
:rtype: dict
|
||||
"""
|
||||
d = {}
|
||||
d.update(self.base_eval_context)
|
||||
if context:
|
||||
d.update(context)
|
||||
return d
|
||||
|
||||
def eval_context(self, context_to_eval, context=None):
|
||||
""" Evaluates the provided context_to_eval in the context (haha) of
|
||||
the context.
|
||||
|
||||
:param context_to_eval: a context to evaluate. Must be a dict or a
|
||||
non-literal context. If it's a dict, will be
|
||||
returned as-is
|
||||
:type context_to_eval: openerpweb.nonliterals.Context
|
||||
:returns: the evaluated context
|
||||
:rtype: dict
|
||||
|
||||
:raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
|
||||
a Context
|
||||
"""
|
||||
if not isinstance(context_to_eval, (dict, nonliterals.Domain)):
|
||||
raise TypeError("Context %r is not a dict or a nonliteral Context",
|
||||
context_to_eval)
|
||||
|
||||
if isinstance(context_to_eval, dict):
|
||||
return context_to_eval
|
||||
|
||||
ctx = context or {}
|
||||
ctx['context'] = ctx
|
||||
|
||||
# if the domain was unpacked from JSON, it needs the current
|
||||
# OpenERPSession for its data retrieval
|
||||
context_to_eval.session = self
|
||||
return context_to_eval.evaluate(ctx)
|
||||
|
||||
def eval_contexts(self, contexts, context=None):
|
||||
""" Evaluates a sequence of contexts to build a single final result
|
||||
|
||||
:param list contexts: a list of Context or dict contexts
|
||||
:param dict context: a base context, if needed
|
||||
:returns: the final combination of all provided contexts
|
||||
:rtype: dict
|
||||
"""
|
||||
# This is the context we use to evaluate stuff
|
||||
current_context = dict(
|
||||
self.base_eval_context,
|
||||
**(context or {}))
|
||||
# this is our result, it should not contain the values
|
||||
# of the base context above
|
||||
final_context = {}
|
||||
for ctx in contexts:
|
||||
# evaluate the current context in the sequence, merge it into
|
||||
# the result
|
||||
final_context.update(
|
||||
self.eval_context(
|
||||
ctx, current_context))
|
||||
# update the current evaluation context so that future
|
||||
# evaluations can use the results we just gathered
|
||||
current_context.update(final_context)
|
||||
return final_context
|
||||
|
||||
def eval_domain(self, domain, context=None):
|
||||
""" Evaluates the provided domain using the provided context
|
||||
(merged with the session's evaluation context)
|
||||
|
||||
:param domain: an OpenERP domain as a list or as a
|
||||
:class:`openerpweb.nonliterals.Domain` instance
|
||||
|
||||
In the second case, it will be evaluated and returned.
|
||||
:type domain: openerpweb.nonliterals.Domain
|
||||
:param dict context: the context to use in the evaluation, if any.
|
||||
:returns: the evaluated domain
|
||||
:rtype: list
|
||||
|
||||
:raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
|
||||
"""
|
||||
if not isinstance(domain, (list, nonliterals.Domain)):
|
||||
raise TypeError("Domain %r is not a list or a nonliteral Domain",
|
||||
domain)
|
||||
|
||||
if isinstance(domain, list):
|
||||
return domain
|
||||
|
||||
ctx = context or {}
|
||||
ctx['context'] = ctx
|
||||
|
||||
# if the domain was unpacked from JSON, it needs the current
|
||||
# OpenERPSession for its data retrieval
|
||||
domain.session = self
|
||||
return domain.evaluate(ctx)
|
||||
|
||||
def eval_domains(self, domains, context=None):
|
||||
""" Evaluates and concatenates the provided domains using the
|
||||
provided context for all of them.
|
||||
|
||||
Returns the final, concatenated result.
|
||||
|
||||
:param list domains: a list of Domain or list domains
|
||||
:param dict context: the context in which the domains
|
||||
should be evaluated (if evaluations need
|
||||
to happen)
|
||||
:returns: the final combination of all domains in the sequence
|
||||
:rtype: list
|
||||
"""
|
||||
final_domain = []
|
||||
for domain in domains:
|
||||
final_domain.extend(
|
||||
self.eval_domain(domain, context))
|
||||
return final_domain
|
||||
|
||||
#----------------------------------------------------------
|
||||
# OpenERP Web RequestHandler
|
||||
#----------------------------------------------------------
|
||||
@@ -80,8 +258,9 @@ class OpenERPSession(object):
|
||||
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
|
||||
|
||||
* the json string is passed as a form parameter named "request"
|
||||
* method is currently ignored
|
||||
|
||||
Sucessful request:
|
||||
--> {"jsonrpc": "2.0", "method": "call", "params": {"session_id": "SID", "context": {}, "arg1": "val1" }, "id": null}
|
||||
@@ -116,9 +295,11 @@ class JsonRequest(object):
|
||||
:rtype: bytes
|
||||
'''
|
||||
if requestf:
|
||||
request = simplejson.load(requestf)
|
||||
request = simplejson.load(
|
||||
requestf, object_hook=nonliterals.non_literal_decoder)
|
||||
else:
|
||||
request = simplejson.loads(request)
|
||||
request = simplejson.loads(
|
||||
request, object_hook=nonliterals.non_literal_decoder)
|
||||
try:
|
||||
print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
|
||||
error = None
|
||||
@@ -161,7 +342,8 @@ class JsonRequest(object):
|
||||
print "<--", response
|
||||
print
|
||||
|
||||
content = simplejson.dumps(response)
|
||||
content = simplejson.dumps(
|
||||
response, cls=nonliterals.NonLiteralEncoder)
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json'
|
||||
cherrypy.response.headers['Content-Length'] = len(content)
|
||||
return content
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import mock
|
||||
import simplejson
|
||||
import unittest2
|
||||
|
||||
from openerpweb.nonliterals import Domain, Context
|
||||
import openerpweb.nonliterals
|
||||
import openerpweb.openerpweb
|
||||
|
||||
class NonLiteralDomainTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession)
|
||||
self.session.domains_store = {}
|
||||
def test_store_domain(self):
|
||||
d = Domain(self.session, "some arbitrary string")
|
||||
|
||||
self.assertEqual(
|
||||
self.session.domains_store[d.key],
|
||||
"some arbitrary string")
|
||||
|
||||
def test_get_domain_back(self):
|
||||
d = Domain(self.session, "some arbitrary string")
|
||||
|
||||
self.assertEqual(
|
||||
d.get_domain_string(),
|
||||
"some arbitrary string")
|
||||
def test_retrieve_second_domain(self):
|
||||
""" A different domain should be able to retrieve the nonliteral set
|
||||
previously
|
||||
"""
|
||||
key = Domain(self.session, "some arbitrary string").key
|
||||
|
||||
self.assertEqual(
|
||||
Domain(self.session, key=key).get_domain_string(),
|
||||
"some arbitrary string")
|
||||
|
||||
def test_key_and_string(self):
|
||||
self.assertRaises(
|
||||
ValueError, Domain, None, domain_string="a", key="b")
|
||||
|
||||
def test_eval(self):
|
||||
self.session.evaluation_context.return_value = {'foo': 3}
|
||||
result = Domain(self.session, "[('a', '=', foo)]").evaluate({'foo': 3})
|
||||
self.assertEqual(
|
||||
result, [('a', '=', 3)])
|
||||
|
||||
def test_own_values(self):
|
||||
self.session.evaluation_context.return_value = {}
|
||||
domain = Domain(self.session, "[('a', '=', self)]")
|
||||
domain.own = {'self': 3}
|
||||
result = domain.evaluate()
|
||||
self.assertEqual(
|
||||
result, [('a', '=', 3)])
|
||||
|
||||
class NonLiteralContextTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession)
|
||||
self.session.contexts_store = {}
|
||||
def test_store_domain(self):
|
||||
c = Context(self.session, "some arbitrary string")
|
||||
|
||||
self.assertEqual(
|
||||
self.session.contexts_store[c.key],
|
||||
"some arbitrary string")
|
||||
|
||||
def test_get_domain_back(self):
|
||||
c = Context(self.session, "some arbitrary string")
|
||||
|
||||
self.assertEqual(
|
||||
c.get_context_string(),
|
||||
"some arbitrary string")
|
||||
def test_retrieve_second_domain(self):
|
||||
""" A different domain should be able to retrieve the nonliteral set
|
||||
previously
|
||||
"""
|
||||
key = Context(self.session, "some arbitrary string").key
|
||||
|
||||
self.assertEqual(
|
||||
Context(self.session, key=key).get_context_string(),
|
||||
"some arbitrary string")
|
||||
|
||||
def test_key_and_string(self):
|
||||
self.assertRaises(
|
||||
ValueError, Context, None, context_string="a", key="b")
|
||||
|
||||
def test_eval(self):
|
||||
self.session.evaluation_context.return_value = {'foo': 3}
|
||||
result = Context(self.session, "[('a', '=', foo)]")\
|
||||
.evaluate({'foo': 3})
|
||||
self.assertEqual(
|
||||
result, [('a', '=', 3)])
|
||||
|
||||
def test_own_values(self):
|
||||
self.session.evaluation_context.return_value = {}
|
||||
context = Context(self.session, "{'a': self}")
|
||||
context.own = {'self': 3}
|
||||
result = context.evaluate()
|
||||
self.assertEqual(
|
||||
result, {'a': 3})
|
||||
|
||||
class NonLiteralJSON(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession)
|
||||
self.session.domains_store = {}
|
||||
self.session.contexts_store = {}
|
||||
|
||||
def test_encode_domain(self):
|
||||
d = Domain(self.session, "some arbitrary string")
|
||||
self.assertEqual(
|
||||
simplejson.dumps(d, cls=openerpweb.nonliterals.NonLiteralEncoder),
|
||||
simplejson.dumps({'__ref': 'domain', '__id': d.key}))
|
||||
|
||||
def test_decode_domain(self):
|
||||
encoded = simplejson.dumps(
|
||||
Domain(self.session, "some arbitrary string"),
|
||||
cls=openerpweb.nonliterals.NonLiteralEncoder)
|
||||
|
||||
domain = simplejson.loads(
|
||||
encoded, object_hook=openerpweb.nonliterals.non_literal_decoder)
|
||||
domain.session = self.session
|
||||
|
||||
self.assertEqual(
|
||||
domain.get_domain_string(),
|
||||
"some arbitrary string"
|
||||
)
|
||||
|
||||
def test_encode_context(self):
|
||||
c = Context(self.session, "some arbitrary string")
|
||||
self.assertEqual(
|
||||
simplejson.dumps(c, cls=openerpweb.nonliterals.NonLiteralEncoder),
|
||||
simplejson.dumps({'__ref': 'context', '__id': c.key}))
|
||||
|
||||
def test_decode_context(self):
|
||||
encoded = simplejson.dumps(
|
||||
Context(self.session, "some arbitrary string"),
|
||||
cls=openerpweb.nonliterals.NonLiteralEncoder)
|
||||
|
||||
context = simplejson.loads(
|
||||
encoded, object_hook=openerpweb.nonliterals.non_literal_decoder)
|
||||
context.session = self.session
|
||||
|
||||
self.assertEqual(
|
||||
context.get_context_string(),
|
||||
"some arbitrary string"
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest2
|
||||
from openerpweb.nonliterals import Domain
|
||||
import openerpweb.openerpweb
|
||||
|
||||
class TestOpenERPSession(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.module = object()
|
||||
self.session = openerpweb.openerpweb.OpenERPSession()
|
||||
self.session._uid = -1
|
||||
self.session.context = {
|
||||
'current_date': '1945-08-05',
|
||||
'date': self.module,
|
||||
'time': self.module,
|
||||
'datetime': self.module,
|
||||
'relativedelta': self.module
|
||||
}
|
||||
def test_base_eval_context(self):
|
||||
self.assertEqual(type(self.session.base_eval_context), dict)
|
||||
self.assertEqual(
|
||||
self.session.base_eval_context,
|
||||
{'uid': -1, 'current_date': '1945-08-05',
|
||||
'date': self.module, 'datetime': self.module, 'time': self.module,
|
||||
'relativedelta': self.module}
|
||||
)
|
||||
|
||||
def test_evaluation_context_nocontext(self):
|
||||
self.assertEqual(
|
||||
type(self.session.evaluation_context()),
|
||||
dict
|
||||
)
|
||||
self.assertEqual(
|
||||
self.session.evaluation_context(),
|
||||
self.session.base_eval_context
|
||||
)
|
||||
|
||||
def test_evaluation_context(self):
|
||||
ctx = self.session.evaluation_context({'foo': 3})
|
||||
self.assertEqual(
|
||||
type(ctx),
|
||||
dict
|
||||
)
|
||||
self.assertIn('foo', ctx)
|
||||
|
||||
def test_eval_with_context(self):
|
||||
self.assertEqual(
|
||||
eval('current_date', self.session.evaluation_context()),
|
||||
'1945-08-05')
|
||||
|
||||
self.assertEqual(
|
||||
eval('foo + 3', self.session.evaluation_context({'foo': 4})),
|
||||
7)
|
||||
|
||||
def test_eval_domain_typeerror(self):
|
||||
self.assertRaises(
|
||||
TypeError, self.session.eval_domain, "foo")
|
||||
|
||||
def test_eval_domain_list(self):
|
||||
self.assertEqual(
|
||||
self.session.eval_domain([]),
|
||||
[])
|
||||
|
||||
def test_eval_nonliteral_domain(self):
|
||||
d = Domain(self.session, "[('foo', 'is', 3)]")
|
||||
self.assertEqual(
|
||||
self.session.eval_domain(d),
|
||||
[('foo', 'is', 3)])
|
||||
|
||||
def test_eval_nonliteral_domain_bykey(self):
|
||||
key = Domain(
|
||||
self.session, "[('foo', 'is', 3)]").key
|
||||
|
||||
d = Domain(None, key=key)
|
||||
self.assertEqual(
|
||||
self.session.eval_domain(d),
|
||||
[('foo', 'is', 3)])
|
||||
|
||||
def test_eval_empty_domains(self):
|
||||
self.assertEqual(
|
||||
self.session.eval_domains([]),
|
||||
[])
|
||||
|
||||
def test_eval_literal_domains(self):
|
||||
domains = [
|
||||
[('a', 'is', 3)],
|
||||
[('b', 'ilike', 'foo')],
|
||||
['|',
|
||||
('c', '=', False),
|
||||
('c', 'in', ['a', 'b', 'c'])]
|
||||
]
|
||||
self.assertEqual(
|
||||
self.session.eval_domains(domains),
|
||||
[
|
||||
('a', 'is', 3),
|
||||
('b', 'ilike', 'foo'),
|
||||
'|',
|
||||
('c', '=', False),
|
||||
('c', 'in', ['a', 'b', 'c'])
|
||||
])
|
||||
def test_eval_nonliteral_domains(self):
|
||||
domains = [
|
||||
Domain(self.session, "[('uid', '=', uid)]"),
|
||||
Domain(self.session,
|
||||
"['|', ('date', '<', current_date),"
|
||||
" ('date', '>', current_date)]")]
|
||||
self.assertEqual(
|
||||
self.session.eval_domains(domains),
|
||||
[('uid', '=', -1),
|
||||
'|', ('date', '<', '1945-08-05'), ('date', '>', '1945-08-05')]
|
||||
)
|
||||
Reference in New Issue
Block a user