842 lines
31 KiB
JavaScript
842 lines
31 KiB
JavaScript
import {
|
|
deserializeDate,
|
|
deserializeDateTime,
|
|
parseDate,
|
|
serializeDate,
|
|
serializeDateTime,
|
|
} from "@web/core/l10n/dates";
|
|
import { ORM } from "@web/core/orm_service";
|
|
import { registry } from "@web/core/registry";
|
|
import { cartesian, sortBy as arraySortBy } from "@web/core/utils/arrays";
|
|
import { parseServerValue } from "./relational_model/utils";
|
|
|
|
class UnimplementedRouteError extends Error {}
|
|
|
|
let searchReadNumber = 0;
|
|
|
|
/**
|
|
* Helper function returning the value from a list of sample strings
|
|
* corresponding to the given ID.
|
|
* @param {number} id
|
|
* @param {string[]} sampleTexts
|
|
* @returns {string}
|
|
*/
|
|
function getSampleFromId(id, sampleTexts) {
|
|
return sampleTexts[(id - 1) % sampleTexts.length];
|
|
}
|
|
|
|
function serializeGroupDateValue(range, field) {
|
|
if (!range) {
|
|
return false;
|
|
}
|
|
let dateValue = parseServerValue(field, range.to);
|
|
dateValue = dateValue.minus({
|
|
[field.type === "date" ? "day" : "second"]: 1,
|
|
});
|
|
return field.type === "date" ? serializeDate(dateValue) : serializeDateTime(dateValue);
|
|
}
|
|
|
|
/**
|
|
* Helper function returning a regular expression specifically matching
|
|
* a given 'term' in a fieldName. For example `fieldNameRegex('abc')`:
|
|
* will match:
|
|
* - "abc"
|
|
* - "field_abc__def"
|
|
* will not match:
|
|
* - "aabc"
|
|
* - "abcd_ef"
|
|
* @param {...string} term
|
|
* @returns {RegExp}
|
|
*/
|
|
function fieldNameRegex(...terms) {
|
|
return new RegExp(`\\b((\\w+)?_)?(${terms.join("|")})(_(\\w+)?)?\\b`);
|
|
}
|
|
|
|
const MEASURE_SPEC_REGEX = /(?<measure>\w+):(?<aggregateFunction>\w+)(\((?<fieldName>\w+)\))?/;
|
|
const DESCRIPTION_REGEX = fieldNameRegex("description", "label", "title", "subject", "message");
|
|
const EMAIL_REGEX = fieldNameRegex("email");
|
|
const PHONE_REGEX = fieldNameRegex("phone");
|
|
const URL_REGEX = fieldNameRegex("url");
|
|
|
|
/**
|
|
* Sample server class
|
|
*
|
|
* Represents a static instance of the server used when a RPC call sends
|
|
* empty values/groups while the attribute 'sample' is set to true on the
|
|
* view.
|
|
*
|
|
* This server will generate fake data and send them in the adequate format
|
|
* according to the route/method used in the RPC.
|
|
*/
|
|
export class SampleServer {
|
|
/**
|
|
* @param {string} modelName
|
|
* @param {Object} fields
|
|
*/
|
|
constructor(modelName, fields) {
|
|
this.mainModel = modelName;
|
|
this.data = {};
|
|
this.data[modelName] = {
|
|
fields,
|
|
records: [],
|
|
};
|
|
// Generate relational fields' co models
|
|
for (const fieldName in fields) {
|
|
const field = fields[fieldName];
|
|
if (["many2one", "one2many", "many2many"].includes(field.type)) {
|
|
this.data[field.relation] = this.data[field.relation] || {
|
|
fields: {
|
|
display_name: { type: "char" },
|
|
id: { type: "integer" },
|
|
color: { type: "integer" },
|
|
},
|
|
records: [],
|
|
};
|
|
}
|
|
}
|
|
// On some models, empty grouped Kanban or List view still contain
|
|
// real (empty) groups. In this case, we re-use the result of the
|
|
// web_read_group rpc to tweak sample data s.t. those real groups
|
|
// contain sample records.
|
|
this.existingGroups = null;
|
|
// Sample records generation is only done if necessary, so we delay
|
|
// it to the first "mockRPC" call. These flags allow us to know if
|
|
// the records have been generated or not.
|
|
this.populated = false;
|
|
this.existingGroupsPopulated = false;
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// Public
|
|
//---------------------------------------------------------------------
|
|
|
|
/**
|
|
* This is the main entry point of the SampleServer. Mocks a request to
|
|
* the server with sample data.
|
|
* @param {Object} params
|
|
* @returns {any} the result obtained with the sample data
|
|
* @throws {Error} If called on a route/method we do not handle
|
|
*/
|
|
mockRpc(params) {
|
|
if (!(params.model in this.data)) {
|
|
throw new Error(`SampleServer: unknown model ${params.model}`);
|
|
}
|
|
this._populateModels();
|
|
switch (params.method || params.route) {
|
|
case "web_search_read":
|
|
return this._mockWebSearchReadUnity(params);
|
|
case "web_read_group":
|
|
return this._mockWebReadGroup(params);
|
|
case "read_group":
|
|
return this._mockReadGroup(params);
|
|
case "read_progress_bar":
|
|
return this._mockReadProgressBar(params);
|
|
case "read":
|
|
return this._mockRead(params);
|
|
}
|
|
// this rpc can't be mocked by the SampleServer itself, so check if there is an handler
|
|
// in the registry: either specific for this model (with key 'model/method'), or
|
|
// global (with key 'method')
|
|
const method = params.method || params.route;
|
|
// This allows to register mock version of methods or routes,
|
|
// for all models:
|
|
// registry.category("sample_server").add('some_route', () => "abcd");
|
|
// for a specific model (e.g. 'res.partner'):
|
|
// registry.category("sample_server").add('res.partner/some_method', () => 23);
|
|
const mockFunction =
|
|
registry.category("sample_server").get(`${params.model}/${method}`, null) ||
|
|
registry.category("sample_server").get(method, null);
|
|
if (mockFunction) {
|
|
return mockFunction.call(this, params);
|
|
}
|
|
console.log(`SampleServer: unimplemented route "${params.method || params.route}"`);
|
|
throw new SampleServer.UnimplementedRouteError();
|
|
}
|
|
|
|
setExistingGroups(groups) {
|
|
this.existingGroups = groups;
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// Private
|
|
//---------------------------------------------------------------------
|
|
|
|
/**
|
|
* @param {Object[]} measures, each measure has the form { fieldName, type }
|
|
* @param {Object[]} records
|
|
* @returns {Object}
|
|
*/
|
|
_aggregateFields(measures, records) {
|
|
const values = {};
|
|
for (const { fieldName, type, aggregateFunction } of measures) {
|
|
if (["float", "integer", "monetary"].includes(type)) {
|
|
if (aggregateFunction === "array_agg") {
|
|
values[fieldName] = (records || []).map((r) => r[fieldName]);
|
|
} else if (records.length) {
|
|
let value = 0;
|
|
for (const record of records) {
|
|
value += record[fieldName];
|
|
}
|
|
values[fieldName] = this._sanitizeNumber(value);
|
|
} else {
|
|
values[fieldName] = null;
|
|
}
|
|
}
|
|
if (type === "many2one") {
|
|
const ids = new Set(records.map((r) => r[fieldName]));
|
|
values.fieldName = ids.size || null;
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
/**
|
|
* @param {any} value
|
|
* @param {Object} options
|
|
* @param {string} [options.interval]
|
|
* @param {string} [options.relation]
|
|
* @param {string} [options.type]
|
|
* @returns {any}
|
|
*/
|
|
_formatValue(value, options) {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
const { type, interval, relation } = options;
|
|
if (["date", "datetime"].includes(type)) {
|
|
const fmt = SampleServer.FORMATS[interval];
|
|
return parseDate(value).toFormat(fmt);
|
|
} else if (["many2one", "many2many"].includes(type)) {
|
|
const rec = this.data[relation].records.find(({ id }) => id === value);
|
|
return [value, rec.display_name];
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates field values based on heuristics according to field types
|
|
* and names.
|
|
*
|
|
* @private
|
|
* @param {string} modelName
|
|
* @param {string} fieldName
|
|
* @param {number} id the record id
|
|
* @returns {any} the field value
|
|
*/
|
|
_generateFieldValue(modelName, fieldName, id) {
|
|
const field = this.data[modelName].fields[fieldName];
|
|
switch (field.type) {
|
|
case "boolean":
|
|
return fieldName === "active" ? true : this._getRandomBool();
|
|
case "char":
|
|
case "text":
|
|
if (["display_name", "name"].includes(fieldName)) {
|
|
if (SampleServer.PEOPLE_MODELS.includes(modelName)) {
|
|
return getSampleFromId(id, SampleServer.SAMPLE_PEOPLE);
|
|
} else if (modelName === "res.country") {
|
|
return getSampleFromId(id, SampleServer.SAMPLE_COUNTRIES);
|
|
}
|
|
}
|
|
if (fieldName === "display_name") {
|
|
return getSampleFromId(id, SampleServer.SAMPLE_TEXTS);
|
|
} else if (["name", "reference"].includes(fieldName)) {
|
|
return `REF${String(id).padStart(4, "0")}`;
|
|
} else if (DESCRIPTION_REGEX.test(fieldName)) {
|
|
return getSampleFromId(id, SampleServer.SAMPLE_TEXTS);
|
|
} else if (EMAIL_REGEX.test(fieldName)) {
|
|
const emailName = getSampleFromId(id, SampleServer.SAMPLE_PEOPLE)
|
|
.replace(/ /, ".")
|
|
.toLowerCase();
|
|
return `${emailName}@sample.demo`;
|
|
} else if (PHONE_REGEX.test(fieldName)) {
|
|
return `+1 555 754 ${String(id).padStart(4, "0")}`;
|
|
} else if (URL_REGEX.test(fieldName)) {
|
|
return `http://sample${id}.com`;
|
|
}
|
|
return false;
|
|
case "date":
|
|
case "datetime": {
|
|
const datetime = this._getRandomDate();
|
|
return field.type === "date"
|
|
? serializeDate(datetime)
|
|
: serializeDateTime(datetime);
|
|
}
|
|
case "float":
|
|
return this._getRandomFloat(SampleServer.MAX_FLOAT);
|
|
case "integer": {
|
|
let max = SampleServer.MAX_INTEGER;
|
|
if (fieldName.includes("color")) {
|
|
max = this._getRandomBool() ? SampleServer.MAX_COLOR_INT : 0;
|
|
}
|
|
return this._getRandomInt(max);
|
|
}
|
|
case "monetary":
|
|
return this._getRandomInt(SampleServer.MAX_MONETARY);
|
|
case "many2one":
|
|
if (field.relation === "res.currency") {
|
|
/** @todo return session.company_currency_id */
|
|
return 1;
|
|
}
|
|
if (field.relation === "ir.attachment") {
|
|
return false;
|
|
}
|
|
return this._getRandomSubRecordId();
|
|
case "one2many":
|
|
case "many2many": {
|
|
const ids = [this._getRandomSubRecordId(), this._getRandomSubRecordId()];
|
|
return [...new Set(ids)];
|
|
}
|
|
case "selection": {
|
|
return this._getRandomSelectionValue(modelName, field);
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {any[]} array
|
|
* @returns {any}
|
|
*/
|
|
_getRandomArrayEl(array) {
|
|
return array[Math.floor(Math.random() * array.length)];
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {boolean}
|
|
*/
|
|
_getRandomBool() {
|
|
return Math.random() < 0.5;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {DateTime}
|
|
*/
|
|
_getRandomDate() {
|
|
const delta = Math.floor((Math.random() - Math.random()) * SampleServer.DATE_DELTA);
|
|
return luxon.DateTime.local().plus({ hours: delta });
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {number} max
|
|
* @returns {number} float in [O, max[
|
|
*/
|
|
_getRandomFloat(max) {
|
|
return this._sanitizeNumber(Math.random() * max);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {number} max
|
|
* @returns {number} int in [0, max[
|
|
*/
|
|
_getRandomInt(max) {
|
|
return Math.floor(Math.random() * max);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {string}
|
|
*/
|
|
_getRandomSelectionValue(modelName, field) {
|
|
if (field.selection.length > 0) {
|
|
return this._getRandomArrayEl(field.selection)[0];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {number} id in [1, SUB_RECORDSET_SIZE]
|
|
*/
|
|
_getRandomSubRecordId() {
|
|
return Math.floor(Math.random() * SampleServer.SUB_RECORDSET_SIZE) + 1;
|
|
}
|
|
|
|
/**
|
|
* Mocks calls to the read method.
|
|
* @private
|
|
* @param {Object} params
|
|
* @param {string} params.model
|
|
* @param {Array[]} params.args (args[0] is the list of ids, args[1] is
|
|
* the list of fields)
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockRead(params) {
|
|
const model = this.data[params.model];
|
|
const ids = params.args[0];
|
|
const fieldNames = params.args[1];
|
|
const records = [];
|
|
for (const r of model.records) {
|
|
if (!ids.includes(r.id)) {
|
|
continue;
|
|
}
|
|
const record = { id: r.id };
|
|
for (const fieldName of fieldNames) {
|
|
const field = model.fields[fieldName];
|
|
if (!field) {
|
|
record[fieldName] = false; // unknown field
|
|
} else if (field.type === "many2one") {
|
|
const relModel = this.data[field.relation];
|
|
const relRecord = relModel.records.find((relR) => r[fieldName] === relR.id);
|
|
record[fieldName] = relRecord ? [relRecord.id, relRecord.display_name] : false;
|
|
} else {
|
|
record[fieldName] = r[fieldName];
|
|
}
|
|
}
|
|
records.push(record);
|
|
}
|
|
return records;
|
|
}
|
|
|
|
/**
|
|
* Mocks calls to the read_group method.
|
|
*
|
|
* @param {Object} params
|
|
* @param {string} params.model
|
|
* @param {string[]} [params.fields] defaults to the list of all fields
|
|
* @param {string[]} params.groupBy
|
|
* @param {boolean} [params.lazy=true]
|
|
* @returns {Object[]} Object with keys groups and length
|
|
*/
|
|
_mockReadGroup(params) {
|
|
const lazy = "lazy" in params ? params.lazy : true;
|
|
const model = params.model;
|
|
const fields = this.data[model].fields;
|
|
const records = this.data[model].records;
|
|
|
|
const normalizedGroupBys = [];
|
|
let groupBy = [];
|
|
if (params.groupBy.length) {
|
|
groupBy = lazy ? [params.groupBy[0]] : params.groupBy;
|
|
}
|
|
for (const groupBySpec of groupBy) {
|
|
let [fieldName, interval] = groupBySpec.split(":");
|
|
interval = interval || "month";
|
|
const { type, relation } = fields[fieldName];
|
|
if (type) {
|
|
const gb = { fieldName, type, interval, relation, alias: groupBySpec };
|
|
normalizedGroupBys.push(gb);
|
|
}
|
|
}
|
|
|
|
const groupsFromRecord = (record) => {
|
|
const values = [];
|
|
for (const gb of normalizedGroupBys) {
|
|
const { fieldName, type } = gb;
|
|
let fieldVals;
|
|
if (["date", "datetime"].includes(type)) {
|
|
fieldVals = [this._formatValue(record[fieldName], gb)];
|
|
} else if (type === "many2many") {
|
|
fieldVals = record[fieldName].length ? record[fieldName] : [false];
|
|
} else {
|
|
fieldVals = [record[fieldName]];
|
|
}
|
|
values.push(fieldVals.map((val) => ({ [fieldName]: val })));
|
|
}
|
|
const cart = cartesian(...values);
|
|
return cart.map((tuple) => {
|
|
if (!Array.isArray(tuple)) {
|
|
tuple = [tuple];
|
|
}
|
|
return Object.assign({}, ...tuple);
|
|
});
|
|
};
|
|
|
|
const groups = {};
|
|
for (const record of records) {
|
|
const recordGroups = groupsFromRecord(record);
|
|
for (const group of recordGroups) {
|
|
const groupId = JSON.stringify(group);
|
|
if (!(groupId in groups)) {
|
|
groups[groupId] = [];
|
|
}
|
|
groups[groupId].push(record);
|
|
}
|
|
}
|
|
|
|
const measures = [];
|
|
for (const measureSpec of params.fields || Object.keys(fields)) {
|
|
const matches = measureSpec.match(MEASURE_SPEC_REGEX);
|
|
let { fieldName, aggregateFunction, measure } = (matches && matches.groups) || {};
|
|
if (!aggregateFunction && fieldName in fields && fields[fieldName].aggregator) {
|
|
aggregateFunction = fields[fieldName].aggregator;
|
|
measure = fieldName;
|
|
}
|
|
if (!fieldName && !measure) {
|
|
continue; // this is for _count measure
|
|
}
|
|
const fName = fieldName || measure;
|
|
const { type } = fields[fName];
|
|
if (
|
|
!params.groupBy.includes(fName) &&
|
|
type &&
|
|
(type !== "many2one" || aggregateFunction !== "count_distinct")
|
|
) {
|
|
measures.push({ fieldName: fName, type, aggregateFunction });
|
|
}
|
|
}
|
|
|
|
let result = [];
|
|
for (const id in groups) {
|
|
const records = groups[id];
|
|
const group = { __domain: [] };
|
|
let countKey = `__count`;
|
|
if (normalizedGroupBys.length && lazy) {
|
|
countKey = `${normalizedGroupBys[0].fieldName}_count`;
|
|
}
|
|
group[countKey] = records.length;
|
|
const firstElem = records[0];
|
|
const parsedId = JSON.parse(id);
|
|
for (const gb of normalizedGroupBys) {
|
|
const { alias, fieldName, type } = gb;
|
|
if (type === "many2many") {
|
|
group[alias] = this._formatValue(parsedId[fieldName], gb);
|
|
} else {
|
|
group[alias] = this._formatValue(firstElem[fieldName], gb);
|
|
if (["date", "datetime"].includes(type)) {
|
|
group.__range = {};
|
|
const val = firstElem[fieldName];
|
|
if (val) {
|
|
const deserialize =
|
|
type === "date" ? deserializeDate : deserializeDateTime;
|
|
const serialize = type === "date" ? serializeDate : serializeDateTime;
|
|
const from = deserialize(val).startOf(gb.interval);
|
|
const to = SampleServer.INTERVALS[gb.interval](from);
|
|
group.__range[alias] = { from: serialize(from), to: serialize(to) };
|
|
} else {
|
|
group.__range[alias] = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Object.assign(group, this._aggregateFields(measures, records));
|
|
result.push(group);
|
|
}
|
|
if (normalizedGroupBys.length > 0) {
|
|
const { alias, interval, type } = normalizedGroupBys[0];
|
|
result = arraySortBy(result, (group) => {
|
|
const val = group[alias];
|
|
if (["date", "datetime"].includes(type)) {
|
|
return parseDate(val, { format: SampleServer.FORMATS[interval] });
|
|
}
|
|
return val;
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Mocks calls to the read_progress_bar method.
|
|
* @private
|
|
* @param {Object} params
|
|
* @param {string} params.model
|
|
* @param {string} params.group_by
|
|
* @param {Object} params.progress_bar
|
|
* @return {Object}
|
|
*/
|
|
_mockReadProgressBar(params) {
|
|
const groupBy = params.group_by.split(":")[0];
|
|
const progress_bar = params.progress_bar;
|
|
const groupByField = this.data[params.model].fields[groupBy];
|
|
const data = {};
|
|
for (const record of this.data[params.model].records) {
|
|
let groupByValue = record[groupBy];
|
|
if (groupByField.type === "many2one") {
|
|
const relatedRecords = this.data[groupByField.relation].records;
|
|
const relatedRecord = relatedRecords.find((r) => r.id === groupByValue);
|
|
groupByValue = relatedRecord.display_name;
|
|
}
|
|
// special case for bool values: rpc call response with capitalized strings
|
|
if (!(groupByValue in data)) {
|
|
if (groupByValue === true) {
|
|
groupByValue = "True";
|
|
} else if (groupByValue === false) {
|
|
groupByValue = "False";
|
|
}
|
|
}
|
|
if (!(groupByValue in data)) {
|
|
data[groupByValue] = {};
|
|
for (const key in progress_bar.colors) {
|
|
data[groupByValue][key] = 0;
|
|
}
|
|
}
|
|
const fieldValue = record[progress_bar.field];
|
|
if (fieldValue in data[groupByValue]) {
|
|
data[groupByValue][fieldValue]++;
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
_mockWebSearchReadUnity(params) {
|
|
const fields = Object.keys(params.specification);
|
|
let result;
|
|
if (this.existingGroups) {
|
|
const groups = this.existingGroups;
|
|
const group = groups[searchReadNumber++ % groups.length];
|
|
result = {
|
|
records: this._mockRead({
|
|
model: params.model,
|
|
args: [group.__recordIds, fields],
|
|
}),
|
|
length: group.__recordIds.length,
|
|
};
|
|
} else {
|
|
const model = this.data[params.model];
|
|
const rawRecords = model.records.slice(0, SampleServer.SEARCH_READ_LIMIT);
|
|
const records = this._mockRead({
|
|
model: params.model,
|
|
args: [rawRecords.map((r) => r.id), fields],
|
|
});
|
|
result = { records, length: records.length };
|
|
}
|
|
// populate many2one and x2many values
|
|
for (const fieldName in params.specification) {
|
|
const field = this.data[params.model].fields[fieldName];
|
|
if (field.type === "many2one") {
|
|
for (const record of result.records) {
|
|
record[fieldName] = record[fieldName]
|
|
? {
|
|
id: record[fieldName][0],
|
|
display_name: record[fieldName][1],
|
|
}
|
|
: false;
|
|
}
|
|
}
|
|
if (field.type === "one2many" || field.type === "many2many") {
|
|
const relFields = Object.keys(params.specification[fieldName].fields || {});
|
|
if (relFields.length) {
|
|
const relIds = result.records.map((r) => r[fieldName]).flat();
|
|
const relRecords = {};
|
|
const _relRecords = this._mockRead({
|
|
model: field.relation,
|
|
args: [relIds, relFields],
|
|
});
|
|
for (const relRecord of _relRecords) {
|
|
relRecords[relRecord.id] = relRecord;
|
|
}
|
|
for (const record of result.records) {
|
|
record[fieldName] = record[fieldName].map((resId) => relRecords[resId]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Mocks calls to the web_read_group method to return groups populated
|
|
* with sample records. Only handles the case where the real call to
|
|
* web_read_group returned groups, but none of these groups contain
|
|
* records. In this case, we keep the real groups, and populate them
|
|
* with sample records.
|
|
* @private
|
|
* @param {Object} params
|
|
* @param {Object} [result] the result of a real call to web_read_group
|
|
* @returns {{ groups: Object[], length: number }}
|
|
*/
|
|
_mockWebReadGroup(params) {
|
|
let groups;
|
|
if (this.existingGroups) {
|
|
this._tweakExistingGroups(params);
|
|
groups = this.existingGroups;
|
|
} else {
|
|
groups = this._mockReadGroup(params);
|
|
}
|
|
return {
|
|
groups,
|
|
length: groups.length,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Updates the sample data such that the existing groups (in database)
|
|
* also exists in the sample, and such that there are sample records in
|
|
* those groups.
|
|
* @private
|
|
* @param {Object[]} groups empty groups returned by the server
|
|
* @param {Object} params
|
|
* @param {string} params.model
|
|
* @param {string[]} params.groupBy
|
|
*/
|
|
_populateExistingGroups(params) {
|
|
const groups = this.existingGroups;
|
|
const groupBy = params.groupBy[0].split(":")[0];
|
|
const groupByField = this.data[params.model].fields[groupBy];
|
|
const groupedByM2O = groupByField.type === "many2one";
|
|
if (groupedByM2O) {
|
|
// re-populate co model with relevant records
|
|
this.data[groupByField.relation].records = groups.map((g) => {
|
|
return { id: g[groupBy][0], display_name: g[groupBy][1] };
|
|
});
|
|
}
|
|
for (const r of this.data[params.model].records) {
|
|
const group = getSampleFromId(r.id, groups);
|
|
if (["date", "datetime"].includes(groupByField.type)) {
|
|
r[groupBy] = serializeGroupDateValue(
|
|
group.__range[params.groupBy[0]],
|
|
groupByField
|
|
);
|
|
} else if (groupByField.type === "many2one") {
|
|
r[groupBy] = group[params.groupBy[0]] ? group[params.groupBy[0]][0] : false;
|
|
} else {
|
|
r[groupBy] = group[params.groupBy[0]];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates sample records for the models in this.data. Records will be
|
|
* generated once, and subsequent calls to this function will be skipped.
|
|
* @private
|
|
*/
|
|
_populateModels() {
|
|
if (!this.populated) {
|
|
for (const modelName in this.data) {
|
|
const model = this.data[modelName];
|
|
const fieldNames = Object.keys(model.fields).filter((f) => f !== "id");
|
|
const size =
|
|
modelName === this.mainModel
|
|
? SampleServer.MAIN_RECORDSET_SIZE
|
|
: SampleServer.SUB_RECORDSET_SIZE;
|
|
for (let id = 1; id <= size; id++) {
|
|
const record = { id };
|
|
for (const fieldName of fieldNames) {
|
|
record[fieldName] = this._generateFieldValue(modelName, fieldName, id);
|
|
}
|
|
model.records.push(record);
|
|
}
|
|
}
|
|
this.populated = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rounds the given number value according to the configured precision.
|
|
* @private
|
|
* @param {number} value
|
|
* @returns {number}
|
|
*/
|
|
_sanitizeNumber(value) {
|
|
return parseFloat(value.toFixed(SampleServer.FLOAT_PRECISION));
|
|
}
|
|
|
|
/**
|
|
* A real (web_)read_group call has been done, and it has returned groups,
|
|
* but they are all empty. This function updates the sample data such
|
|
* that those group values exist and those groups contain sample records.
|
|
* @private
|
|
* @param {Object[]} groups empty groups returned by the server
|
|
* @param {Object} params
|
|
* @param {string} params.model
|
|
* @param {string[]} params.fields
|
|
* @param {string[]} params.groupBy
|
|
* @returns {Object[]} groups with count and aggregate values updated
|
|
*
|
|
* TODO: rename
|
|
*/
|
|
_tweakExistingGroups(params) {
|
|
const groups = this.existingGroups;
|
|
this._populateExistingGroups(params);
|
|
|
|
// update count and aggregates for each group
|
|
const fullGroupBy = params.groupBy[0];
|
|
const groupBy = fullGroupBy.split(":")[0];
|
|
const groupByField = this.data[params.model].fields[groupBy];
|
|
const records = this.data[params.model].records;
|
|
const fields = params.fields.map((aggregate_spec) => aggregate_spec.split(":")[0])
|
|
for (const g of groups) {
|
|
const recordsInGroup = records.filter((r) => {
|
|
if (["date", "datetime"].includes(groupByField.type)) {
|
|
return (
|
|
r[groupBy] === serializeGroupDateValue(g.__range[fullGroupBy], groupByField)
|
|
);
|
|
} else if (groupByField.type === "many2one") {
|
|
return (!r[groupBy] && !g[fullGroupBy]) || r[groupBy] === g[fullGroupBy][0];
|
|
}
|
|
return r[groupBy] === g[fullGroupBy];
|
|
});
|
|
for (const field of fields) {
|
|
const fieldType = this.data[params.model].fields[field].type;
|
|
if (["integer, float", "monetary"].includes(fieldType)) {
|
|
g[field] = recordsInGroup.reduce((acc, r) => acc + r[field], 0);
|
|
}
|
|
}
|
|
g[`${groupBy}_count`] = recordsInGroup.length;
|
|
g.__recordIds = recordsInGroup.map((r) => r.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
SampleServer.FORMATS = {
|
|
day: "yyyy-MM-dd",
|
|
week: "'W'WW kkkk",
|
|
month: "MMMM yyyy",
|
|
quarter: "'Q'q yyyy",
|
|
year: "y",
|
|
};
|
|
SampleServer.INTERVALS = {
|
|
day: (dt) => dt.plus({ days: 1 }),
|
|
week: (dt) => dt.plus({ weeks: 1 }),
|
|
month: (dt) => dt.plus({ months: 1 }),
|
|
quarter: (dt) => dt.plus({ months: 3 }),
|
|
year: (dt) => dt.plus({ years: 1 }),
|
|
};
|
|
SampleServer.DISPLAY_FORMATS = Object.assign({}, SampleServer.FORMATS, { day: "dd MMM yyyy" });
|
|
|
|
SampleServer.MAIN_RECORDSET_SIZE = 16;
|
|
SampleServer.SUB_RECORDSET_SIZE = 5;
|
|
SampleServer.SEARCH_READ_LIMIT = 10;
|
|
|
|
SampleServer.MAX_FLOAT = 100;
|
|
SampleServer.MAX_INTEGER = 50;
|
|
SampleServer.MAX_COLOR_INT = 7;
|
|
SampleServer.MAX_MONETARY = 100000;
|
|
SampleServer.DATE_DELTA = 24 * 60; // in hours -> 60 days
|
|
SampleServer.FLOAT_PRECISION = 2;
|
|
|
|
SampleServer.SAMPLE_COUNTRIES = ["Belgium", "France", "Portugal", "Singapore", "Australia"];
|
|
SampleServer.SAMPLE_PEOPLE = [
|
|
"John Miller",
|
|
"Henry Campbell",
|
|
"Carrie Helle",
|
|
"Wendi Baltz",
|
|
"Thomas Passot",
|
|
];
|
|
SampleServer.SAMPLE_TEXTS = [
|
|
"Laoreet id",
|
|
"Volutpat blandit",
|
|
"Integer vitae",
|
|
"Viverra nam",
|
|
"In massa",
|
|
];
|
|
SampleServer.PEOPLE_MODELS = [
|
|
"res.users",
|
|
"res.partner",
|
|
"hr.employee",
|
|
"mail.followers",
|
|
"mailing.contact",
|
|
];
|
|
|
|
SampleServer.UnimplementedRouteError = UnimplementedRouteError;
|
|
|
|
export function buildSampleORM(resModel, fields, user) {
|
|
const sampleServer = new SampleServer(resModel, fields);
|
|
const fakeRPC = async (_, params) => {
|
|
const { args, kwargs, method, model } = params;
|
|
const { groupby: groupBy } = kwargs;
|
|
return sampleServer.mockRpc({ method, model, args, ...kwargs, groupBy });
|
|
};
|
|
const sampleORM = new ORM(user);
|
|
sampleORM.rpc = fakeRPC;
|
|
sampleORM.isSample = true;
|
|
sampleORM.setGroups = (groups) => sampleServer.setExistingGroups(groups);
|
|
return sampleORM;
|
|
}
|