merge upstream
bzr revid: chs@openerp.com-20120911123437-3gzoblvfpseujf0t
This commit is contained in:
@@ -49,7 +49,6 @@ This module provides the core of the OpenERP Web Client.
|
||||
"static/src/js/views.js",
|
||||
"static/src/js/data.js",
|
||||
"static/src/js/data_export.js",
|
||||
"static/src/js/data_import.js",
|
||||
"static/src/js/search.js",
|
||||
"static/src/js/view_form.js",
|
||||
"static/src/js/view_list.js",
|
||||
@@ -64,7 +63,6 @@ This module provides the core of the OpenERP Web Client.
|
||||
"static/lib/jquery.textext/jquery.textext.css",
|
||||
"static/src/css/base.css",
|
||||
"static/src/css/data_export.css",
|
||||
"static/src/css/data_import.css",
|
||||
"static/lib/cleditor/jquery.cleditor.css",
|
||||
],
|
||||
'qweb' : [
|
||||
|
||||
@@ -7,6 +7,7 @@ import cgi
|
||||
import contextlib
|
||||
import functools
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
@@ -420,6 +421,10 @@ def session_context(request, storage_path, session_cookie='httpsessionid'):
|
||||
#----------------------------------------------------------
|
||||
# OpenERP Web WSGI Application
|
||||
#----------------------------------------------------------
|
||||
# Add potentially missing (older ubuntu) font mime types
|
||||
mimetypes.add_type('application/font-woff', '.woff')
|
||||
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
|
||||
mimetypes.add_type('application/x-font-ttf', '.ttf')
|
||||
class DisableCacheMiddleware(object):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
@@ -484,15 +484,13 @@ def fix_view_modes(action):
|
||||
if not action.get('views'):
|
||||
generate_views(action)
|
||||
|
||||
id_form = None
|
||||
for index, (id, mode) in enumerate(action['views']):
|
||||
if mode == 'form':
|
||||
id_form = id
|
||||
break
|
||||
|
||||
if action.pop('view_type', 'form') != 'form':
|
||||
return action
|
||||
|
||||
if 'view_mode' in 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']
|
||||
@@ -1351,27 +1349,6 @@ class View(openerpweb.Controller):
|
||||
def load(self, req, model, view_id, view_type, toolbar=False):
|
||||
return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
|
||||
|
||||
class ListView(View):
|
||||
_cp_path = "/web/listview"
|
||||
|
||||
def process_colors(self, view, row, context):
|
||||
colors = view['arch']['attrs'].get('colors')
|
||||
|
||||
if not colors:
|
||||
return None
|
||||
|
||||
color = [
|
||||
pair.split(':')[0]
|
||||
for pair in colors.split(';')
|
||||
if eval(pair.split(':')[1], dict(context, **row))
|
||||
]
|
||||
|
||||
if not color:
|
||||
return None
|
||||
elif len(color) == 1:
|
||||
return color[0]
|
||||
return 'maroon'
|
||||
|
||||
class TreeView(View):
|
||||
_cp_path = "/web/treeview"
|
||||
|
||||
|
||||
@@ -1503,10 +1503,10 @@
|
||||
.openerp .oe_view_manager_current > .oe_view_manager_header .oe_header_row:first-child td {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.openerp .oe_view_manager_inline {
|
||||
.openerp .oe_view_manager_inline, .openerp .oe_view_manager_inlineview {
|
||||
height: 100%;
|
||||
}
|
||||
.openerp .oe_view_manager_inline > .oe_view_manager_header {
|
||||
.openerp .oe_view_manager_inline > .oe_view_manager_header, .openerp .oe_view_manager_inlineview > .oe_view_manager_header {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_popup_form > .oe_formview > .oe_form_pager {
|
||||
@@ -2574,10 +2574,13 @@
|
||||
.openerp .oe_form .oe_form_field_many2many > .oe_list .oe_list_pager_single_page {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_list_buttons .oe_alternative {
|
||||
visibility: hidden;
|
||||
}
|
||||
.openerp .oe_list_buttons .oe_list_save, .openerp .oe_list_buttons .oe_list_discard {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_add, .openerp .oe_list_buttons.oe_editing .oe_list_button_import {
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_add {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_save {
|
||||
@@ -2586,6 +2589,9 @@
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_discard {
|
||||
display: inline;
|
||||
}
|
||||
.openerp .oe_list_buttons.oe_editing .oe_alternative {
|
||||
visibility: visible;
|
||||
}
|
||||
.openerp .oe_list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1155,7 +1155,7 @@ $sheet-max-width: 860px
|
||||
padding-top: 8px
|
||||
// }}}
|
||||
// ViewManager inline {{{
|
||||
.oe_view_manager_inline
|
||||
.oe_view_manager_inline, .oe_view_manager_inlineview
|
||||
height: 100%
|
||||
> .oe_view_manager_header
|
||||
display: none
|
||||
@@ -1971,15 +1971,19 @@ $sheet-max-width: 860px
|
||||
// }}}
|
||||
// ListView {{{
|
||||
.oe_list_buttons
|
||||
.oe_alternative
|
||||
visibility: hidden
|
||||
.oe_list_save, .oe_list_discard
|
||||
display: none
|
||||
&.oe_editing
|
||||
.oe_list_add, .oe_list_button_import
|
||||
.oe_list_add
|
||||
display: none
|
||||
.oe_list_save
|
||||
display: inline-block
|
||||
.oe_list_discard
|
||||
display: inline
|
||||
.oe_alternative
|
||||
visibility: visible
|
||||
|
||||
.oe_list
|
||||
position: relative
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
.openerp .oe_import_grid {
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.openerp .oe_import_grid-header .oe_import_grid-cell {
|
||||
background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC;
|
||||
border-bottom: 1px solid #E3E3E3;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
.openerp .oe_import_grid-row .oe_import_grid-cell {
|
||||
border-bottom: 1px solid #E3E3E3;
|
||||
}
|
||||
|
||||
.openerp .oe_import_no_result .oe_import_result {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_import fieldset {
|
||||
cursor: pointer;
|
||||
}
|
||||
.openerp .oe_import fieldset legend:before {
|
||||
content: '\25BC ';
|
||||
}
|
||||
.openerp .oe_import fieldset.oe_closed legend:before {
|
||||
content: '\25B6 ';
|
||||
}
|
||||
.openerp .oe_import fieldset.oe_closed table {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_import .separator.horizontal {
|
||||
font-weight: bold;
|
||||
border-bottom-width: 1px;
|
||||
margin: 6px 4px 6px 1px;
|
||||
height: 20px;
|
||||
}
|
||||
.openerp .duplicate_fld{
|
||||
background-color:#FF6666;
|
||||
}
|
||||
.openerp .select_fld{
|
||||
background: none repeat scroll 0 0 white;
|
||||
}
|
||||
.openerp .ui-autocomplete {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-right: 20px;
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
openerp.web.data_import = function(instance) {
|
||||
var QWeb = instance.web.qweb,
|
||||
_t = instance.web._t;
|
||||
/**
|
||||
* Safari does not deal well at all with raw JSON data being returned. As a
|
||||
* result, we're going to cheat by using a pseudo-jsonp: instead of getting
|
||||
* JSON data in the iframe, we're getting a ``script`` tag which consists of a
|
||||
* function call and the returned data (the json dump).
|
||||
*
|
||||
* The function is an auto-generated name bound to ``window``, which calls
|
||||
* back into the callback provided here.
|
||||
*
|
||||
* @param {Object} form the form element (DOM or jQuery) to use in the call
|
||||
* @param {Object} attributes jquery.form attributes object
|
||||
* @param {Function} callback function to call with the returned data
|
||||
*/
|
||||
function jsonp(form, attributes, callback) {
|
||||
attributes = attributes || {};
|
||||
var options = {jsonp: _.uniqueId('import_callback_')};
|
||||
window[options.jsonp] = function () {
|
||||
delete window[options.jsonp];
|
||||
callback.apply(null, arguments);
|
||||
};
|
||||
if ('data' in attributes) {
|
||||
_.extend(attributes.data, options);
|
||||
} else {
|
||||
_.extend(attributes, {data: options});
|
||||
}
|
||||
$(form).ajaxSubmit(attributes);
|
||||
}
|
||||
|
||||
instance.web.DataImport = instance.web.Dialog.extend({
|
||||
template: 'ImportDataView',
|
||||
dialog_title: {toString: function () { return _t("Import Data"); }},
|
||||
init: function(parent, dataset){
|
||||
var self = this;
|
||||
this._super(parent, {});
|
||||
this.model = parent.model;
|
||||
this.fields = [];
|
||||
this.all_fields = [];
|
||||
this.fields_with_defaults = [];
|
||||
this.required_fields = null;
|
||||
|
||||
var convert_fields = function (root, prefix) {
|
||||
prefix = prefix || '';
|
||||
_(root.fields).each(function (f) {
|
||||
self.all_fields.push(prefix + f.name);
|
||||
if (f.fields) {
|
||||
convert_fields(f, prefix + f.id + '/');
|
||||
}
|
||||
});
|
||||
};
|
||||
this.ready = $.Deferred.queue().then(function () {
|
||||
self.required_fields = _(self.fields).chain()
|
||||
.filter(function (field) {
|
||||
return field.required &&
|
||||
!_.include(self.fields_with_defaults, field.id); })
|
||||
.pluck('id')
|
||||
.uniq()
|
||||
.value();
|
||||
convert_fields(self);
|
||||
self.all_fields.sort();
|
||||
});
|
||||
},
|
||||
start: function() {
|
||||
var self = this;
|
||||
this._super();
|
||||
this.open({
|
||||
buttons: [
|
||||
{text: _t("Close"), click: function() { self.destroy(); }},
|
||||
{text: _t("Import File"), click: function() { self.do_import(); }, 'class': 'oe_import_dialog_button'}
|
||||
],
|
||||
close: function(event, ui) {
|
||||
self.destroy();
|
||||
}
|
||||
});
|
||||
this.toggle_import_button(false);
|
||||
this.$el.find('#csvfile').change(this.on_autodetect_data);
|
||||
this.$el.find('fieldset').change(this.on_autodetect_data);
|
||||
this.$el.delegate('fieldset legend', 'click', function() {
|
||||
$(this).parent().toggleClass('oe_closed');
|
||||
});
|
||||
this.ready.push(new instance.web.DataSet(this, this.model).call(
|
||||
'fields_get', [], function (fields) {
|
||||
self.graft_fields(fields);
|
||||
self.ready.push(new instance.web.DataSet(self, self.model)
|
||||
.default_get(_.pluck(self.fields, 'id')).then(function (fields) {
|
||||
_.each(fields, function(val, key) {
|
||||
if (val) {
|
||||
self.fields_with_defaults.push(key);
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
}));
|
||||
},
|
||||
graft_fields: function (fields, parent, level) {
|
||||
parent = parent || this;
|
||||
level = level || 0;
|
||||
|
||||
var self = this;
|
||||
if (level === 0) {
|
||||
parent.fields.push({
|
||||
id: 'id',
|
||||
name: 'id',
|
||||
string: _t('External ID'),
|
||||
required: false
|
||||
});
|
||||
}
|
||||
_(fields).each(function (field, field_name) {
|
||||
// Ignore spec for id field
|
||||
// Don't import function fields (function and related)
|
||||
if (field_name === 'id') {
|
||||
return;
|
||||
}
|
||||
// Skip if there's no state which could disable @readonly,
|
||||
// if a field is ever always readonly we can't import it
|
||||
if (field.readonly) {
|
||||
// no states at all
|
||||
if (_.isEmpty(field.states)) { return; }
|
||||
// no state altering @readonly
|
||||
if (!_.any(field.states, function (modifiers) {
|
||||
return _(modifiers).chain().pluck(0).contains('readonly').value();
|
||||
})) { return; }
|
||||
}
|
||||
|
||||
var f = {
|
||||
id: field_name,
|
||||
name: field_name,
|
||||
string: field.string,
|
||||
required: field.required
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
case 'many2many':
|
||||
case 'many2one':
|
||||
// push a copy for the bare many2one field, to allow importing
|
||||
// using name_search too - even if we default to exporting the XML ID
|
||||
var many2one_field = _.extend({}, f)
|
||||
parent.fields.push(many2one_field);
|
||||
f.name += '/id';
|
||||
break;
|
||||
case 'one2many':
|
||||
f.name += '/id';
|
||||
f.fields = [];
|
||||
// only fetch sub-fields to a depth of 2 levels
|
||||
if (level < 2) {
|
||||
self.ready.push(new instance.web.DataSet(self, field.relation).call(
|
||||
'fields_get', [], function (fields) {
|
||||
self.graft_fields(fields, f, level+1);
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
parent.fields.push(f);
|
||||
});
|
||||
},
|
||||
toggle_import_button: function (newstate) {
|
||||
instance.web.dialog(this.$el, 'widget')
|
||||
.find('.oe_import_dialog_button')
|
||||
.button('option', 'disabled', !newstate);
|
||||
},
|
||||
do_import: function() {
|
||||
if(!this.$el.find('#csvfile').val()) { return; }
|
||||
var lines_to_skip = parseInt(this.$el.find('#csv_skip').val(), 10);
|
||||
var with_headers = this.$el.find('#file_has_headers').prop('checked');
|
||||
if (!lines_to_skip && with_headers) {
|
||||
lines_to_skip = 1;
|
||||
}
|
||||
var indices = [], fields = [];
|
||||
this.$el.find(".sel_fields").each(function(index, element) {
|
||||
var val = element.value;
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
indices.push(index);
|
||||
fields.push(val);
|
||||
});
|
||||
|
||||
jsonp(this.$el.find('#import_data'), {
|
||||
url: '/web/import/import_data',
|
||||
data: {
|
||||
model: this.model,
|
||||
meta: JSON.stringify({
|
||||
skip: lines_to_skip,
|
||||
indices: indices,
|
||||
fields: fields
|
||||
})
|
||||
}
|
||||
}, this.on_import_results);
|
||||
},
|
||||
on_autodetect_data: function() {
|
||||
if(!this.$el.find('#csvfile').val()) { return; }
|
||||
jsonp(this.$el.find('#import_data'), {
|
||||
url: '/web/import/detect_data'
|
||||
}, this.on_import_results);
|
||||
},
|
||||
on_import_results: function(results) {
|
||||
this.$el.find('#result').empty();
|
||||
var headers, result_node = this.$el.find("#result");
|
||||
|
||||
if (results['error']) {
|
||||
result_node.append(QWeb.render('ImportView.error', {
|
||||
'error': results['error']}));
|
||||
this.$el.find('fieldset').removeClass('oe_closed');
|
||||
return;
|
||||
}
|
||||
if (results['success']) {
|
||||
if (this.getParent().getParent().active_view == "list") {
|
||||
this.getParent().reload_content();
|
||||
}
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (results['records']) {
|
||||
var lines_to_skip = parseInt(this.$el.find('#csv_skip').val(), 10),
|
||||
with_headers = this.$el.find('#file_has_headers').prop('checked');
|
||||
headers = with_headers ? results.records[0] : null;
|
||||
|
||||
result_node.append(QWeb.render('ImportView.result', {
|
||||
'headers': headers,
|
||||
'records': lines_to_skip ? results.records.slice(lines_to_skip)
|
||||
: with_headers ? results.records.slice(1)
|
||||
: results.records
|
||||
}));
|
||||
this.$el.find('fieldset').addClass('oe_closed');
|
||||
}
|
||||
this.$el.find('form').removeClass('oe_import_no_result');
|
||||
|
||||
this.$el.delegate('.oe_m2o_drop_down_button', 'click', function () {
|
||||
$(this).prev('input').focus();
|
||||
});
|
||||
|
||||
var self = this;
|
||||
this.ready.then(function () {
|
||||
var $fields = self.$el.find('.sel_fields').bind('blur', function () {
|
||||
if (this.value && !_(self.all_fields).contains(this.value)) {
|
||||
this.value = '';
|
||||
}
|
||||
}).autocomplete({
|
||||
minLength: 0,
|
||||
source: self.all_fields,
|
||||
change: self.on_check_field_values
|
||||
}).focus(function () {
|
||||
$(this).autocomplete('search');
|
||||
});
|
||||
// Column auto-detection
|
||||
_(headers).each(function (header, index) {
|
||||
var field_name = self.match_column_to_field(header);
|
||||
if (field_name) {
|
||||
$fields.eq(index).val(field_name);
|
||||
}
|
||||
});
|
||||
self.on_check_field_values();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns the name of the field (nested) matching the provided column name
|
||||
*
|
||||
* @param {String} name column name to look for
|
||||
* @param {Array} [fields] fields to look into for the provided name
|
||||
* @returns {String|undefined}
|
||||
*/
|
||||
match_column_to_field: function (name, fields) {
|
||||
fields = fields || this.fields;
|
||||
var f;
|
||||
f = _(fields).detect(function (field) {
|
||||
return field.name === name
|
||||
});
|
||||
if (!f) {
|
||||
f = _(fields).detect(function (field) {
|
||||
// TODO: levenshtein between header and field.string
|
||||
return field.string.toLowerCase() === name.toLowerCase();
|
||||
});
|
||||
}
|
||||
if (f) { return f.name; }
|
||||
|
||||
// if ``name`` is a path (o2m), we need to recurse through its .fields
|
||||
var index = name.indexOf('/');
|
||||
if (index === -1) { return undefined; }
|
||||
// Get the first path section, try to find the matching field
|
||||
var column_name = name.substring(0, index);
|
||||
f = _(fields).detect(function (field) {
|
||||
// field.name for o2m is $foo/id, so we want to match on id
|
||||
return field.id === column_name;
|
||||
});
|
||||
if (!f) {
|
||||
f = _(fields).detect(function (field) {
|
||||
return field.string.toLowerCase() === column_name.toLowerCase();
|
||||
});
|
||||
}
|
||||
if (!f) { return undefined; }
|
||||
|
||||
// if we found a matching field for the first path section, recurse in
|
||||
// its own .fields to try and get the rest of the path matched
|
||||
var rest = this.match_column_to_field(
|
||||
name.substring(index+1), f.fields);
|
||||
if (!rest) { return undefined; }
|
||||
return f.id + '/' + rest;
|
||||
},
|
||||
/**
|
||||
* Looks through all the field selections, and tries to find if two
|
||||
* (or more) columns were matched to the same model field.
|
||||
*
|
||||
* Returns a map of the multiply-mapped fields to an array of offending
|
||||
* columns (not actually columns, but the inputs containing the same field
|
||||
* names).
|
||||
*
|
||||
* Also has the side-effect of marking the discovered inputs with the class
|
||||
* ``duplicate_fld``.
|
||||
*
|
||||
* @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
|
||||
*/
|
||||
find_duplicate_fields: function() {
|
||||
// Maps values to DOM nodes, in order to discover duplicates
|
||||
var values = {}, duplicates = {};
|
||||
this.$el.find(".sel_fields").each(function(index, element) {
|
||||
var value = element.value;
|
||||
var $el = $(element).removeClass('duplicate_fld');
|
||||
if (!value) { return; }
|
||||
|
||||
if (!(value in values)) {
|
||||
values[value] = element;
|
||||
} else {
|
||||
var same_valued_field = values[value];
|
||||
if (value in duplicates) {
|
||||
duplicates[value].push(element);
|
||||
} else {
|
||||
duplicates[value] = [same_valued_field, element];
|
||||
}
|
||||
$el.add(same_valued_field).addClass('duplicate_fld');
|
||||
}
|
||||
});
|
||||
return duplicates;
|
||||
},
|
||||
on_check_field_values: function () {
|
||||
this.$el.find("#message, #msg").remove();
|
||||
|
||||
var required_valid = this.check_required();
|
||||
|
||||
var duplicates = this.find_duplicate_fields();
|
||||
if (_.isEmpty(duplicates)) {
|
||||
this.toggle_import_button(required_valid);
|
||||
} else {
|
||||
var $err = $('<div id="msg" style="color: red;">'+_t("Destination fields should only be selected once, some fields are selected more than once:")+'</div>').insertBefore(this.$el.find('#result'));
|
||||
var $dupes = $('<dl>').appendTo($err);
|
||||
_(duplicates).each(function(elements, value) {
|
||||
$('<dt>').text(value).appendTo($dupes);
|
||||
_(elements).each(function(element) {
|
||||
var cell = $(element).closest('td');
|
||||
$('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes);
|
||||
});
|
||||
});
|
||||
this.toggle_import_button(false);
|
||||
}
|
||||
|
||||
},
|
||||
check_required: function() {
|
||||
var self = this;
|
||||
if (!self.required_fields.length) { return true; }
|
||||
|
||||
// Resolve field id based on column name, as there may be
|
||||
// several ways to provide the value for a given field and
|
||||
// thus satisfy the requirement.
|
||||
// (e.g. m2o_id or m2o_id/id columns may be provided)
|
||||
var resolve_field_id = function(column_name) {
|
||||
var f = _.detect(self.fields, function(field) {
|
||||
return field.name === column_name;
|
||||
});
|
||||
if (!f) { return column_name; };
|
||||
return f.id;
|
||||
};
|
||||
|
||||
var selected_fields = _(this.$el.find('.sel_fields').get()).chain()
|
||||
.pluck('value')
|
||||
.compact()
|
||||
.map(resolve_field_id)
|
||||
.value();
|
||||
|
||||
var missing_fields = _.difference(this.required_fields, selected_fields);
|
||||
if (missing_fields.length) {
|
||||
this.$el.find("#result").before('<div id="message" style="color:red">' + _t("*Required Fields are not selected :") + missing_fields + '.</div>');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
destroy: function() {
|
||||
this.$el.remove();
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -2398,6 +2398,13 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
|
||||
*/
|
||||
instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
|
||||
template: 'FieldTextHtml',
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.field.type !== 'html' && ! this.options.safe) {
|
||||
throw new Error(_.str.sprintf(
|
||||
_t("Error with field %s, it is not allowed to use the widget 'html' with any other field type than 'html'"), this.string));
|
||||
}
|
||||
},
|
||||
initialize_content: function() {
|
||||
var self = this;
|
||||
if (! this.get("effective_readonly")) {
|
||||
|
||||
@@ -21,8 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||
// whether the view rows can be reordered (via vertical drag & drop)
|
||||
'reorderable': true,
|
||||
'action_buttons': true,
|
||||
// if true, the 'Import', 'Export', etc... buttons will be shown
|
||||
'import_enabled': true,
|
||||
},
|
||||
/**
|
||||
* Core class for list-type displays.
|
||||
@@ -281,7 +279,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||
self.reload_content();
|
||||
});
|
||||
|
||||
// Add button and Import link
|
||||
// Add button
|
||||
if (!this.$buttons) {
|
||||
this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self}));
|
||||
if (this.options.$buttons) {
|
||||
@@ -292,10 +290,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||
this.$buttons.find('.oe_list_add')
|
||||
.click(this.proxy('do_add_record'))
|
||||
.prop('disabled', grouped);
|
||||
this.$buttons.on('click', '.oe_list_button_import', function() {
|
||||
self.on_sidebar_import();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Pager
|
||||
@@ -358,7 +352,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||
this.sidebar = new instance.web.Sidebar(this);
|
||||
this.sidebar.appendTo(this.options.$sidebar);
|
||||
this.sidebar.add_items('other', _.compact([
|
||||
self.is_action_enabled('create') && { label: _t("Import"), callback: this.on_sidebar_import },
|
||||
{ label: _t("Export"), callback: this.on_sidebar_export },
|
||||
self.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected }
|
||||
]));
|
||||
@@ -480,7 +473,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||
if (this.embedded_view) {
|
||||
return $.Deferred().then(callback).resolve(this.embedded_view);
|
||||
} else {
|
||||
return this.rpc('/web/listview/load', {
|
||||
return this.rpc('/web/view/load', {
|
||||
model: this.model,
|
||||
view_id: this.view_id,
|
||||
view_type: "tree",
|
||||
|
||||
@@ -1249,9 +1249,6 @@ instance.web.View = instance.web.Widget.extend({
|
||||
},
|
||||
do_search: function(view) {
|
||||
},
|
||||
on_sidebar_import: function() {
|
||||
new instance.web.DataImport(this, this.dataset).open();
|
||||
},
|
||||
on_sidebar_export: function() {
|
||||
new instance.web.DataExport(this, this.dataset).open();
|
||||
},
|
||||
|
||||
@@ -652,9 +652,6 @@
|
||||
<button type="button" class="oe_button oe_list_add oe_highlight">
|
||||
<t t-esc="widget.options.addable"/>
|
||||
</button>
|
||||
<span class="oe_alternative" t-if="widget.options.import_enabled">
|
||||
<span class="oe_fade">or</span> <a href="#" class="oe_bold oe_list_button_import">Import</a>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<t t-name="ListView.pager">
|
||||
@@ -698,9 +695,10 @@
|
||||
<t t-jquery="button.oe_list_add" t-operation="after">
|
||||
<button class="oe_button oe_list_save oe_highlight"
|
||||
type="button">Save</button>
|
||||
</t>
|
||||
<t t-jquery="a.oe_list_button_import" t-operation="after">
|
||||
<a href="#" class="oe_bold oe_list_discard">Discard</a>
|
||||
<span class="oe_alternative">
|
||||
<span class="oe_fade">or</span>
|
||||
<a href="#" class="oe_bold oe_list_discard">Discard</a>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-extend="ListView.row">
|
||||
|
||||
@@ -192,7 +192,7 @@ $(document).ready(function () {
|
||||
|
||||
var records = {};
|
||||
_.extend(instance.session.responses, {
|
||||
'/web/listview/load': function () {
|
||||
'/web/view/load': function () {
|
||||
return {result: {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
@@ -268,7 +268,7 @@ $(document).ready(function () {
|
||||
setup: function () {
|
||||
baseSetup();
|
||||
_.extend(instance.session.responses, {
|
||||
'/web/listview/load': function () {
|
||||
'/web/view/load': function () {
|
||||
return {result: {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,81 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import mock
|
||||
import unittest2
|
||||
import web.controllers.main
|
||||
|
||||
class TestDataSetController(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.dataset = web.controllers.main.DataSet()
|
||||
self.request = mock.Mock()
|
||||
self.read = self.request.session.model().read
|
||||
self.search = self.request.session.model().search
|
||||
|
||||
@unittest2.skip
|
||||
def test_empty_find(self):
|
||||
self.search.return_value = []
|
||||
self.read.return_value = []
|
||||
|
||||
self.assertFalse(self.dataset.do_search_read(self.request, 'fake.model'))
|
||||
self.read.assert_called_once_with([], False, self.request.context)
|
||||
|
||||
@unittest2.skip
|
||||
def test_regular_find(self):
|
||||
self.search.return_value = [1, 2, 3]
|
||||
|
||||
self.dataset.do_search_read(self.request, 'fake.model')
|
||||
self.read.assert_called_once_with([1, 2, 3], False,
|
||||
self.request.context)
|
||||
|
||||
@unittest2.skip
|
||||
def test_ids_shortcut(self):
|
||||
self.search.return_value = [1, 2, 3]
|
||||
self.read.return_value = [
|
||||
{'id': 1, 'name': 'foo'},
|
||||
{'id': 2, 'name': 'bar'},
|
||||
{'id': 3, 'name': 'qux'}
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
self.dataset.do_search_read(self.request, 'fake.model', ['id']),
|
||||
[{'id': 1}, {'id': 2}, {'id': 3}])
|
||||
self.assertFalse(self.read.called)
|
||||
|
||||
@unittest2.skip
|
||||
def test_get(self):
|
||||
self.read.return_value = [
|
||||
{'id': 1, 'name': 'baz'},
|
||||
{'id': 3, 'name': 'foo'},
|
||||
{'id': 2, 'name': 'bar'}
|
||||
]
|
||||
|
||||
result = self.dataset.do_get(
|
||||
self.request, 'fake.model', [3, 2, 1])
|
||||
self.read.assert_called_once_with(
|
||||
[3, 2, 1], False)
|
||||
self.assertFalse(self.search.called)
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
[
|
||||
{'id': 3, 'name': 'foo'},
|
||||
{'id': 2, 'name': 'bar'},
|
||||
{'id': 1, 'name': 'baz'}
|
||||
]
|
||||
)
|
||||
|
||||
def test_get_missing_result(self):
|
||||
self.read.return_value = [
|
||||
{'id': 1, 'name': 'baz'},
|
||||
{'id': 2, 'name': 'bar'}
|
||||
]
|
||||
|
||||
result = self.dataset.do_get(
|
||||
self.request, 'fake.model', [3, 2, 1])
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
[
|
||||
{'id': 2, 'name': 'bar'},
|
||||
{'id': 1, 'name': 'baz'}
|
||||
]
|
||||
)
|
||||
@@ -1,248 +0,0 @@
|
||||
import copy
|
||||
import xml.etree.ElementTree
|
||||
import mock
|
||||
|
||||
import unittest2
|
||||
import simplejson
|
||||
|
||||
import web.controllers.main
|
||||
from ..common import nonliterals, session as s
|
||||
|
||||
def field_attrs(fields_view_get, fieldname):
|
||||
(field,) = filter(lambda f: f['attrs'].get('name') == fieldname,
|
||||
fields_view_get['arch']['children'])
|
||||
return field['attrs']
|
||||
|
||||
#noinspection PyCompatibility
|
||||
class DomainsAndContextsTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.view = web.controllers.main.View()
|
||||
|
||||
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=s.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'), nonliterals.Domain)
|
||||
self.assertEqual(
|
||||
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=s.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'), nonliterals.Context)
|
||||
self.assertEqual(
|
||||
nonliterals.Context(
|
||||
session, key=e.get('context').key).get_context_string(),
|
||||
context_string)
|
||||
|
||||
class AttrsNormalizationTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.view = web.controllers.main.View()
|
||||
|
||||
def test_identity(self):
|
||||
web_view = """
|
||||
<form string="Title">
|
||||
<group>
|
||||
<field name="some_field"/>
|
||||
<field name="some_other_field"/>
|
||||
</group>
|
||||
<field name="stuff"/>
|
||||
</form>
|
||||
"""
|
||||
|
||||
pristine = xml.etree.ElementTree.fromstring(web_view)
|
||||
transformed = self.view.transform_view(web_view, None)
|
||||
|
||||
self.assertEqual(
|
||||
xml.etree.ElementTree.tostring(transformed),
|
||||
xml.etree.ElementTree.tostring(pristine)
|
||||
)
|
||||
|
||||
@unittest2.skip
|
||||
def test_transform_states(self):
|
||||
element = xml.etree.ElementTree.Element(
|
||||
'field', states="open,closed")
|
||||
self.view.normalize_attrs(element, {})
|
||||
|
||||
self.assertIsNone(element.get('states'))
|
||||
self.assertEqual(
|
||||
simplejson.loads(element.get('attrs')),
|
||||
{'invisible': [['state', 'not in', ['open', 'closed']]]})
|
||||
|
||||
@unittest2.skip
|
||||
def test_transform_invisible(self):
|
||||
element = xml.etree.ElementTree.Element(
|
||||
'field', invisible="context.get('invisible_country', False)")
|
||||
|
||||
empty_context = copy.deepcopy(element)
|
||||
self.view.normalize_attrs(empty_context, {})
|
||||
self.assertEqual(empty_context.get('invisible'), None)
|
||||
|
||||
full_context = copy.deepcopy(element)
|
||||
self.view.normalize_attrs(full_context, {'invisible_country': True})
|
||||
self.assertEqual(full_context.get('invisible'), '1')
|
||||
|
||||
@unittest2.skip
|
||||
def test_transform_invisible_list_column(self):
|
||||
req = mock.Mock()
|
||||
req.context = {'set_editable':True, 'set_visible':True,
|
||||
'gtd_visible':True, 'user_invisible':True}
|
||||
req.session.evaluation_context = \
|
||||
s.OpenERPSession().evaluation_context
|
||||
req.session.model('project.task').fields_view_get.return_value = {
|
||||
'arch': '''
|
||||
<tree colors="grey:state in ('cancelled','done');blue:state == 'pending';red:date_deadline and (date_deadline<current_date) and (state in ('draft','pending','open'))" string="Tasks">
|
||||
<field name="sequence" invisible="not context.get('seq_visible', False)"/>
|
||||
<field name="user_id" invisible="context.get('user_invisible', False)"/>
|
||||
<field name="delegated_user_id" invisible="context.get('show_delegated', True)"/>
|
||||
<field name="total_hours" invisible="1"/>
|
||||
<field name="date_deadline" invisible="context.get('deadline_visible',True)"/>
|
||||
<field name="type_id" invisible="context.get('set_visible',False)"/>
|
||||
</tree>
|
||||
'''}
|
||||
parsed_view = web.controllers.main.View().fields_view_get(
|
||||
req, 'project.task', 42, 'tree')
|
||||
|
||||
self.assertTrue(field_attrs(parsed_view, 'sequence')['invisible'])
|
||||
self.assertTrue(field_attrs(parsed_view, 'user_id')['invisible'])
|
||||
self.assertTrue(
|
||||
field_attrs(parsed_view, 'delegated_user_id')['invisible'])
|
||||
self.assertTrue(field_attrs(parsed_view, 'total_hours')['invisible'])
|
||||
self.assertTrue(
|
||||
field_attrs(parsed_view, 'date_deadline')['invisible'])
|
||||
self.assertTrue(field_attrs(parsed_view, 'type_id')['invisible'])
|
||||
|
||||
class ListViewTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.view = web.controllers.main.ListView()
|
||||
self.request = mock.Mock()
|
||||
self.request.context = {'set_editable': True}
|
||||
|
||||
@unittest2.skip
|
||||
def test_no_editable_editable_context(self):
|
||||
self.request.session.model('fake').fields_view_get.return_value = \
|
||||
{'arch': '<tree><field name="foo"/></tree>'}
|
||||
view = self.view.fields_view_get(self.request, 'fake', False, False)
|
||||
|
||||
self.assertEqual(view['arch']['attrs']['editable'],
|
||||
'bottom')
|
||||
|
||||
@unittest2.skip
|
||||
def test_editable_top_editable_context(self):
|
||||
self.request.session.model('fake').fields_view_get.return_value = \
|
||||
{'arch': '<tree editable="top"><field name="foo"/></tree>'}
|
||||
view = self.view.fields_view_get(self.request, 'fake', False)
|
||||
|
||||
self.assertEqual(view['arch']['attrs']['editable'],
|
||||
'top')
|
||||
|
||||
@unittest2.skip
|
||||
def test_editable_bottom_editable_context(self):
|
||||
self.request.session.model('fake').fields_view_get.return_value = \
|
||||
{'arch': '<tree editable="bottom"><field name="foo"/></tree>'}
|
||||
view = self.view.fields_view_get(self.request, 'fake', False)
|
||||
|
||||
self.assertEqual(view['arch']['attrs']['editable'],
|
||||
'bottom')
|
||||
|
||||
def test_color_nocolor(self):
|
||||
self.assertEqual(
|
||||
self.view.process_colors(
|
||||
{'arch': {'attrs': {}, 'children': []}}, {}, {}),
|
||||
None)
|
||||
def test_color_literal(self):
|
||||
self.assertEqual(
|
||||
self.view.process_colors(
|
||||
{'arch': {'attrs': {'colors': 'black:1'}}, 'children': []},
|
||||
{}, {}),
|
||||
'black')
|
||||
def test_color_miss(self):
|
||||
self.assertEqual(
|
||||
self.view.process_colors(
|
||||
{'arch': {'attrs': {'colors': "grey:state in ('cancelled','done');blue:state in ('pending')"}},
|
||||
'children': []
|
||||
}, {'state': 'open'}, {}),
|
||||
None)
|
||||
def test_color_compute(self):
|
||||
self.assertEqual(
|
||||
self.view.process_colors(
|
||||
{'arch': {'attrs': {'colors': "grey:state in ('cancelled','done');blue:state in ('pending')"}},
|
||||
'children': []
|
||||
}, {'state': 'done'}, {}),
|
||||
'grey')
|
||||
def test_color_multiple(self):
|
||||
self.assertEqual(
|
||||
self.view.process_colors(
|
||||
{'arch': {'attrs': {'colors': "grey:state in ('cancelled','done');blue:state in ('done')"}},
|
||||
'children': []
|
||||
}, {'state': 'done'}, {}),
|
||||
'maroon')
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_dataset, test_menu, test_serving_base, test_view
|
||||
|
||||
fast_suite = []
|
||||
checks = [
|
||||
test_dataset,
|
||||
test_menu,
|
||||
test_serving_base,
|
||||
test_view,
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import mock
|
||||
import unittest2
|
||||
import web.controllers.main
|
||||
|
||||
class TestDataSetController(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.dataset = web.controllers.main.DataSet()
|
||||
self.request = mock.Mock()
|
||||
self.read = self.request.session.model().read
|
||||
self.search = self.request.session.model().search
|
||||
|
||||
def test_empty_find(self):
|
||||
self.search.return_value = []
|
||||
self.read.return_value = []
|
||||
|
||||
self.assertEqual(
|
||||
self.dataset.do_search_read(self.request, 'fake.model'),
|
||||
{'records': [], 'length': 0})
|
||||
self.read.assert_called_once_with(
|
||||
[], False, self.request.session.eval_context())
|
||||
|
||||
def test_regular_find(self):
|
||||
self.search.return_value = [1, 2, 3]
|
||||
|
||||
self.dataset.do_search_read(self.request, 'fake.model')
|
||||
self.read.assert_called_once_with(
|
||||
[1, 2, 3], False,self.request.session.eval_context())
|
||||
|
||||
def test_ids_shortcut(self):
|
||||
self.search.return_value = [1, 2, 3]
|
||||
self.read.return_value = [
|
||||
{'id': 1, 'name': 'foo'},
|
||||
{'id': 2, 'name': 'bar'},
|
||||
{'id': 3, 'name': 'qux'}
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
self.dataset.do_search_read(self.request, 'fake.model', ['id']),
|
||||
{'records': [{'id': 1}, {'id': 2}, {'id': 3}], 'length': 3})
|
||||
self.assertFalse(self.read.called)
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import collections
|
||||
import mock
|
||||
import unittest2
|
||||
|
||||
@@ -13,42 +14,56 @@ class Placeholder(object):
|
||||
class LoadTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.menu = main.Menu()
|
||||
self.menus_mock = mock.Mock()
|
||||
self.request = Placeholder(session=OpenERPSession())
|
||||
self.request = mock.Mock()
|
||||
|
||||
# Have self.request.session.model() return a different mock object for
|
||||
# each model (but always the same mock for a given model name)
|
||||
models = collections.defaultdict(mock.Mock)
|
||||
model = self.request.session.model.side_effect = \
|
||||
lambda model_name: models[model_name]
|
||||
|
||||
self.MockMenus = model('ir.ui.menu')
|
||||
# Mock the absence of custom menu
|
||||
model('res.users').read.return_value = [{
|
||||
'menu_id': False
|
||||
}]
|
||||
|
||||
def tearDown(self):
|
||||
del self.request
|
||||
del self.menus_mock
|
||||
del self.MockMenus
|
||||
del self.menu
|
||||
|
||||
@unittest2.skip
|
||||
def test_empty(self):
|
||||
self.menus_mock.search = mock.Mock(return_value=[])
|
||||
self.menus_mock.read = mock.Mock(return_value=[])
|
||||
self.MockMenus.search.return_value = []
|
||||
self.MockMenus.read.return_value = []
|
||||
|
||||
root = self.menu.do_load(self.request)
|
||||
|
||||
self.menus_mock.search.assert_called_with([])
|
||||
self.menus_mock.read.assert_called_with(
|
||||
[], ['name', 'sequence', 'parent_id'])
|
||||
self.MockMenus.search.assert_called_with(
|
||||
[], 0, False, False, self.request.session.eval_context())
|
||||
self.MockMenus.read.assert_called_with(
|
||||
[], ['name', 'sequence', 'parent_id',
|
||||
'action', 'needaction_enabled', 'needaction_counter'],
|
||||
self.request.session.eval_context())
|
||||
|
||||
self.assertListEqual(
|
||||
root['children'],
|
||||
[])
|
||||
|
||||
@unittest2.skip
|
||||
def test_applications_sort(self):
|
||||
self.menus_mock.search = mock.Mock(return_value=[1, 2, 3])
|
||||
self.menus_mock.read = mock.Mock(return_value=[
|
||||
{'id': 2, 'sequence': 3, 'parent_id': False},
|
||||
{'id': 3, 'sequence': 2, 'parent_id': False},
|
||||
self.MockMenus.search.return_value = [1, 2, 3]
|
||||
self.MockMenus.read.side_effect = lambda *args: [
|
||||
{'id': 1, 'sequence': 1, 'parent_id': False},
|
||||
])
|
||||
{'id': 3, 'sequence': 2, 'parent_id': False},
|
||||
{'id': 2, 'sequence': 3, 'parent_id': False},
|
||||
]
|
||||
|
||||
root = self.menu.do_load(self.request)
|
||||
|
||||
self.menus_mock.read.assert_called_with(
|
||||
[1, 2, 3], ['name', 'sequence', 'parent_id'])
|
||||
self.MockMenus.read.assert_called_with(
|
||||
[1, 2, 3], ['name', 'sequence', 'parent_id',
|
||||
'action', 'needaction_enabled', 'needaction_counter'],
|
||||
self.request.session.eval_context())
|
||||
|
||||
self.assertEqual(
|
||||
root['children'],
|
||||
@@ -63,15 +78,18 @@ class LoadTest(unittest2.TestCase):
|
||||
'parent_id': False, 'children': []
|
||||
}])
|
||||
|
||||
@unittest2.skip
|
||||
def test_deep(self):
|
||||
self.menus_mock.search = mock.Mock(return_value=[1, 2, 3, 4])
|
||||
self.menus_mock.read = mock.Mock(return_value=[
|
||||
{'id': 1, 'sequence': 1, 'parent_id': False},
|
||||
{'id': 2, 'sequence': 2, 'parent_id': [1, '']},
|
||||
{'id': 3, 'sequence': 1, 'parent_id': [2, '']},
|
||||
{'id': 4, 'sequence': 2, 'parent_id': [2, '']},
|
||||
])
|
||||
self.MockMenus.search.side_effect = lambda domain, *args: (
|
||||
[1] if domain == [('parent_id', '=', False)] else [1, 2, 3, 4])
|
||||
|
||||
root = {'id': 1, 'sequence': 1, 'parent_id': False}
|
||||
self.MockMenus.read.side_effect = lambda ids, *args: (
|
||||
[root] if ids == [1] else [
|
||||
{'id': 1, 'sequence': 1, 'parent_id': False},
|
||||
{'id': 2, 'sequence': 2, 'parent_id': [1, '']},
|
||||
{'id': 3, 'sequence': 1, 'parent_id': [2, '']},
|
||||
{'id': 4, 'sequence': 2, 'parent_id': [2, '']},
|
||||
])
|
||||
|
||||
root = self.menu.do_load(self.request)
|
||||
|
||||
@@ -117,7 +135,6 @@ class ActionMungerTest(unittest2.TestCase):
|
||||
|
||||
self.assertEqual(changed, action)
|
||||
|
||||
@unittest2.skip
|
||||
def test_list_view(self):
|
||||
action = {
|
||||
"views": [[False, "tree"], [False, "form"],
|
||||
@@ -135,7 +152,6 @@ class ActionMungerTest(unittest2.TestCase):
|
||||
"view_mode": "list,form,calendar"
|
||||
})
|
||||
|
||||
@unittest2.skip
|
||||
def test_redundant_views(self):
|
||||
|
||||
action = {
|
||||
@@ -3,7 +3,7 @@
|
||||
import random
|
||||
import unittest2
|
||||
|
||||
from ..controllers.main import topological_sort
|
||||
from ..controllers.main import module_topological_sort as sort
|
||||
|
||||
def sample(population):
|
||||
return random.sample(
|
||||
@@ -22,7 +22,7 @@ class TestModulesLoading(unittest2.TestCase):
|
||||
ms = dict(modules)
|
||||
|
||||
seen = set()
|
||||
sorted_modules = topological_sort(ms)
|
||||
sorted_modules = sort(ms)
|
||||
for module in sorted_modules:
|
||||
deps = ms[module]
|
||||
self.assertGreaterEqual(
|
||||
@@ -0,0 +1,128 @@
|
||||
import copy
|
||||
import xml.etree.ElementTree
|
||||
import mock
|
||||
|
||||
import unittest2
|
||||
import simplejson
|
||||
|
||||
import web.controllers.main
|
||||
from ..common import nonliterals, session as s
|
||||
|
||||
def field_attrs(fields_view_get, fieldname):
|
||||
(field,) = filter(lambda f: f['attrs'].get('name') == fieldname,
|
||||
fields_view_get['arch']['children'])
|
||||
return field['attrs']
|
||||
|
||||
#noinspection PyCompatibility
|
||||
class DomainsAndContextsTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.view = web.controllers.main.View()
|
||||
|
||||
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=s.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'), nonliterals.Domain)
|
||||
self.assertEqual(
|
||||
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=s.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'), nonliterals.Context)
|
||||
self.assertEqual(
|
||||
nonliterals.Context(
|
||||
session, key=e.get('context').key).get_context_string(),
|
||||
context_string)
|
||||
|
||||
class AttrsNormalizationTest(unittest2.TestCase):
|
||||
def setUp(self):
|
||||
self.view = web.controllers.main.View()
|
||||
|
||||
def test_identity(self):
|
||||
web_view = """
|
||||
<form string="Title">
|
||||
<group>
|
||||
<field name="some_field"/>
|
||||
<field name="some_other_field"/>
|
||||
</group>
|
||||
<field name="stuff"/>
|
||||
</form>
|
||||
"""
|
||||
|
||||
pristine = xml.etree.ElementTree.fromstring(web_view)
|
||||
transformed = self.view.transform_view(web_view, None)
|
||||
|
||||
self.assertEqual(
|
||||
xml.etree.ElementTree.tostring(transformed),
|
||||
xml.etree.ElementTree.tostring(pristine)
|
||||
)
|
||||
@@ -72,14 +72,7 @@
|
||||
left: 4px;
|
||||
top: -2px;
|
||||
}
|
||||
li {
|
||||
.graph-menu li {
|
||||
padding: 2px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
a .graph-menu-options {
|
||||
color: white;
|
||||
}
|
||||
div .graph-menu-options {
|
||||
float: right;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@charset "utf-8";
|
||||
.openerp .oe_kanban_view {
|
||||
background: white;
|
||||
height: inherit;
|
||||
@@ -140,9 +141,6 @@
|
||||
.openerp .oe_kanban_view .oe_kanban_add {
|
||||
float: right;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_quick_create {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_quick_create_buttons {
|
||||
margin: 4px 0;
|
||||
}
|
||||
@@ -151,10 +149,14 @@
|
||||
padding: 10px;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_quick_create input {
|
||||
outline: none;
|
||||
border: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
-box-shadow: none;
|
||||
display: block;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_vignette {
|
||||
@@ -334,7 +336,8 @@
|
||||
display: inline-block;
|
||||
line-height: 1em;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card {
|
||||
.openerp .oe_kanban_view .oe_kanban_card, .openerp .oe_kanban_view .oe_kanban_quick_create {
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
display: block;
|
||||
background: white;
|
||||
@@ -346,27 +349,27 @@
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card:hover {
|
||||
.openerp .oe_kanban_view .oe_kanban_card:hover, .openerp .oe_kanban_view .oe_kanban_quick_create:hover {
|
||||
border: 1px solid #7c7bad;
|
||||
-moz-box-shadow: 0 0 4px #7c7bad;
|
||||
-webkit-box-shadow: 0 0 4px #7c7bad;
|
||||
-box-shadow: 0 0 4px #7c7bad;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card:hover .oe_dropdown_kanban > span {
|
||||
.openerp .oe_kanban_view .oe_kanban_card:hover .oe_dropdown_kanban > span, .openerp .oe_kanban_view .oe_kanban_quick_create:hover .oe_dropdown_kanban > span {
|
||||
visibility: visible;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card h3 {
|
||||
.openerp .oe_kanban_view .oe_kanban_card h3, .openerp .oe_kanban_view .oe_kanban_quick_create h3 {
|
||||
margin: 0 16px 0 0;
|
||||
color: #4c4c4c;
|
||||
text-decoration: none;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card h3:hover {
|
||||
.openerp .oe_kanban_view .oe_kanban_card h3:hover, .openerp .oe_kanban_view .oe_kanban_quick_create h3:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card .oe_dropdown_kanban {
|
||||
.openerp .oe_kanban_view .oe_kanban_card .oe_dropdown_kanban, .openerp .oe_kanban_view .oe_kanban_quick_create .oe_dropdown_kanban {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_card .oe_dropdown_kanban .oe_kanban_project_times li {
|
||||
.openerp .oe_kanban_view .oe_kanban_card .oe_dropdown_kanban .oe_kanban_project_times li, .openerp .oe_kanban_view .oe_kanban_quick_create .oe_dropdown_kanban .oe_kanban_project_times li {
|
||||
float: left;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_star {
|
||||
@@ -470,6 +473,10 @@
|
||||
left: 0;
|
||||
top: 28px;
|
||||
min-width: 160px;
|
||||
padding: 2px;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_dropdown_kanban .oe_dropdown_menu > li {
|
||||
padding: 3px;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_dropdown_kanban.oe_opened > span {
|
||||
visibility: visible;
|
||||
|
||||
@@ -153,18 +153,18 @@
|
||||
.oe_kanban_add
|
||||
float: right
|
||||
|
||||
.oe_kanban_quick_create
|
||||
margin-bottom: 4px
|
||||
.oe_kanban_quick_create_buttons
|
||||
margin: 4px 0
|
||||
.oe_kanban_no_group .oe_kanban_quick_create
|
||||
width: 200px
|
||||
padding: 10px
|
||||
.oe_kanban_quick_create input
|
||||
outline: none
|
||||
border: none
|
||||
@include box-shadow(none)
|
||||
display: block
|
||||
margin: 0 0 8px 0
|
||||
font-size: 13px
|
||||
box-sizing: border-box
|
||||
width: 100%
|
||||
// }}}
|
||||
// KanbanRecords {{{
|
||||
@@ -302,7 +302,8 @@
|
||||
white-space: nowrap
|
||||
display: inline-block
|
||||
line-height: 1em
|
||||
.oe_kanban_card
|
||||
.oe_kanban_card, .oe_kanban_quick_create
|
||||
margin-bottom: 4px
|
||||
position: relative
|
||||
display: block
|
||||
background: white
|
||||
@@ -396,6 +397,9 @@
|
||||
left: 0
|
||||
top: 28px
|
||||
min-width: 160px
|
||||
padding: 2px
|
||||
> li
|
||||
padding: 3px
|
||||
.oe_dropdown_kanban
|
||||
&.oe_opened > span
|
||||
visibility: visible
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</li>
|
||||
</t>
|
||||
<div t-name="KanbanView.quick_create" class="oe_kanban_quick_create">
|
||||
<input t-att-placeholder="_t('Create a new record')"/>
|
||||
<input/>
|
||||
<t t-if="widget._buttons">
|
||||
<div class="oe_kanban_quick_create_buttons">
|
||||
<button class="oe_kanban_quick_create_add">Add</button>
|
||||
|
||||
Reference in New Issue
Block a user