Commit fcc0bc74 authored by Rafael Monnerat's avatar Rafael Monnerat

Update from upstream/master

parents 049fd1cb 29e138eb
......@@ -25,76 +25,17 @@
#
##############################################################################
import zipfile, cStringIO, re
import xmlrpclib, base64
from Products.CMFCore.utils import getToolByName
def extractContent(data):
"""
extract text content from ODF data
directly by unzipping (no need for oood here)
"""
# XXX probably not used - to really get text content it should
# strip xml too
cs = cStringIO.StringIO()
cs.write(data)
try:
z = zipfile.ZipFile(cs)
except zipfile.BadZipfile:
cs.close()
return ''
s = z.read('content.xml')
cs.close()
z.close()
return s
###### XXX these methods repeat what is in OOoDocument class
# maybe redundant, but we need to access them from Script (Python)
def convertToOdf(self, name, data):
"""
convert data into ODF format
to be used in ingestion when we don't yet have an ERP5 object
to work with (and we for example have to figure out portal_type)
"""
sp = mkProxy(self)
kw = sp.run_convert(name,base64.encodestring(data))
odf = base64.decodestring(kw['data'])
return odf
def mkProxy(self):
pref = getToolByName(self,'portal_preferences')
adr = pref.getPreferredDmsOoodocServerAddress()
nr = pref.getPreferredDmsOoodocServerPortNumber()
if adr is None or nr is None:
raise Exception('you should set conversion server coordinates in preferences')
sp = xmlrpclib.ServerProxy('http://%s:%d' % (adr,nr), allow_none=True)
return sp
def generateFile(self, name, data, format): # pylint: disable=redefined-builtin
sp = mkProxy(self)
kw = sp.run_generate(name, data, None, format)
res = base64.decodestring(kw['data'])
return res
def getAttrFromFilename(self, fname):
"""
parse file name using regexp specified in preferences
"""
rx_parse = re.compile(self.portal_preferences.getPreferredDmsFilenameRegexp())
m = rx_parse.match(fname)
if m is None:
return {}
return m.groupdict()
import re
def getLastWorkflowDate(self, state_name='simulation_state', state=('released','public')):
'''we can make something more generic out of it
or JP says "there is an API for it" and we trash this one'''
if not hasattr(self, 'workflow_history'):
return None
for wflow in self.workflow_history.values():
if wflow is None or len(wflow) == 0: continue # empty history
if wflow[0].get(state_name) is None: continue # not the right one
if wflow is None or len(wflow) == 0:
continue # empty history
if wflow[0].get(state_name) is None:
continue # not the right one
for i in range(len(wflow)):
ch = wflow[-1-i]
act = ch.get('action', '')
......@@ -105,16 +46,6 @@ def getLastWorkflowDate(self, state_name='simulation_state', state=('released','
#############################################################################
# Mail management
def findAddress(txt):
"""
find email address in a string
"""
validchars = r'0-9A-Za-z.\-_'
r=re.compile('[%s]+@[%s]+' % (validchars,validchars))
m=r.search(txt)
return m and m.group()
def extractParams(txt):
"""
extract parameters given in mail body
......
......@@ -56,7 +56,7 @@ var EnemyDroneAPI = /** @class */ (function () {
/*
** Function called on every drone update, right before onUpdate AI script
*/
EnemyDroneAPI.prototype.internal_update = function (context, delta_time) {
EnemyDroneAPI.prototype.internal_position_update = function (context, delta_time) {
context._speed += context._acceleration * delta_time / 1000;
if (context._speed > context._maxSpeed) {
context._speed = context._maxSpeed;
......@@ -81,7 +81,7 @@ var EnemyDroneAPI = /** @class */ (function () {
/*
** Function called on every drone update, right after onUpdate AI script
*/
EnemyDroneAPI.prototype.internal_post_update = function (drone) {
EnemyDroneAPI.prototype.internal_info_update = function (drone) {
var _this = this, drone_position = drone.getCurrentPosition(), drone_info;
if (drone_position) {
drone_info = {
......@@ -307,6 +307,9 @@ var EnemyDroneAPI = /** @class */ (function () {
EnemyDroneAPI.prototype.getMaxHeight = function () {
return 800;
};
EnemyDroneAPI.prototype.getOnUpdateInterval = function () {
return 0;
};
EnemyDroneAPI.prototype.getFlightParameters = function () {
return this._flight_parameters;
};
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60631.26636.59528</string> </value>
<value> <string>1015.63148.58654.57634</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709288499.16</float>
<float>1713426942.44</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -6,6 +6,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
"use strict";
var DEFAULT_SPEED = 16,
PARACHUTE_SPEED = 8,
EARTH_GRAVITY = 9.81,
LOITER_LIMIT = 30,
MAX_ACCELERATION = 6,
......@@ -48,7 +49,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
throw new Error('max acceleration must be superior to 0');
}
drone._minSpeed = this.getMinSpeed();
if (drone._minSpeed <= 0) {
if (drone._minSpeed < 0) {
throw new Error('min speed must be superior to 0');
}
drone._maxSpeed = this.getMaxSpeed();
......@@ -97,9 +98,13 @@ var FixedWingDroneAPI = /** @class */ (function () {
/*
** Function called on every drone update, right before onUpdate AI script
*/
FixedWingDroneAPI.prototype.internal_update = function (context, delta_time) {
this._updateSpeed(context, delta_time);
this._updatePosition(context, delta_time);
FixedWingDroneAPI.prototype.internal_position_update = function (context, delta_time) {
if (context.position.z > 0) {
this._updateSpeed(context, delta_time);
this._updatePosition(context, delta_time);
} else {
context.setDirection(0, 0, 0);
}
context._controlMesh.computeWorldMatrix(true);
context._mesh.computeWorldMatrix(true);
......@@ -107,7 +112,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
/*
** Function called on every drone update, right after onUpdate AI script
*/
FixedWingDroneAPI.prototype.internal_post_update = function (drone) {
FixedWingDroneAPI.prototype.internal_info_update = function (drone) {
var _this = this, drone_position = drone.getCurrentPosition(), drone_info;
/*if (_this._start_altitude > 0) { //TODO move start_altitude here
_this.reachAltitude(drone);
......@@ -486,8 +491,8 @@ var FixedWingDroneAPI = /** @class */ (function () {
function (altitude_diff, max_climb_rate, speed, max_pitch) {
var maxVerticalSpeed =
Math.min(altitude_diff, Math.min(max_climb_rate, speed));
return (this._toDeg(Math.asin(maxVerticalSpeed / speed)) > max_pitch) ?
speed * Math.sin(this._toRad(max_pitch))
return (this._toDeg(Math.asin(maxVerticalSpeed / speed)) > max_pitch)
? speed * Math.sin(this._toRad(max_pitch))
: maxVerticalSpeed;
};
FixedWingDroneAPI.prototype._toRad = function (angle) {
......@@ -511,11 +516,16 @@ var FixedWingDroneAPI = /** @class */ (function () {
};
FixedWingDroneAPI.prototype.land = function (drone) {
var drone_pos = drone.getCurrentPosition();
drone.setTargetCoordinates(
this._flight_parameters.drone.minSpeed = 0;
drone._speed = 0;
drone._acceleration = EARTH_GRAVITY;
this._flight_parameters.drone.maxSinkRate = PARACHUTE_SPEED;
this._flight_parameters.drone.minPitchAngle = -90;
drone._internal_setTargetCoordinates(
drone_pos.latitude,
drone_pos.longitude,
0,
drone.get3DSpeed()
-PARACHUTE_SPEED,
PARACHUTE_SPEED
);
this._is_ready_to_fly = false;
this._is_landing = true;
......@@ -538,6 +548,9 @@ var FixedWingDroneAPI = /** @class */ (function () {
FixedWingDroneAPI.prototype.getMaxHeight = function () {
return 800;
};
FixedWingDroneAPI.prototype.getOnUpdateInterval = function () {
return this._flight_parameters.drone.onUpdateInterval;
};
FixedWingDroneAPI.prototype.getFlightParameters = function () {
return this._flight_parameters;
};
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60733.7318.44953</string> </value>
<value> <string>1015.64140.4755.42274</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709564488.61</float>
<float>1713430403.75</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -6,6 +6,56 @@ var GAMEPARAMETERS = {}, TEAM_USER = "user", TEAM_ENEMY = "enemy";
//for DEBUG/TEST mode
var baseLogFunction = console.log, console_log = "";
function spawnDrone(spawnDrone_x, spawnDrone_y, spawnDrone_z, spawnDrone_index,
spawnDrone_drone_info, spawnDrone_api, spawnDrone_team,
spawnDrone_scene, spawnDrone_droneList_user,
spawnDrone_droneList_enemy) {
"use strict";
var default_drone_AI = spawnDrone_api.getDroneAI(), spawnDrone_code,
code_eval;
if (default_drone_AI) {
spawnDrone_code = default_drone_AI;
} else {
spawnDrone_code = spawnDrone_drone_info.script_content;
}
if (!spawnDrone_code.includes("me.onStart")) {
spawnDrone_code = "me.onStart = function () { me.exit(); };";
}
code_eval = "let spawnDrone_drone = new DroneManager(spawnDrone_scene, " +
spawnDrone_index + ', spawnDrone_api, spawnDrone_team);' +
"let droneMe = function(NativeDate, me, Math, window, DroneManager," +
" GameManager, FixedWingDroneAPI, EnemyDroneAPI, BABYLON, " +
"GAMEPARAMETERS) {" +
"Date.now = function () {" +
"return me._API._gameManager.getCurrentTime();}; " +
"function Date() {if (!(this instanceof Date)) " +
"{throw new Error('Missing new operator');} " +
"if (arguments.length === 0) {return new NativeDate(Date.now());} " +
"else {return new NativeDate(...arguments);}}";
// Simple desactivation of direct access of all globals
// It is still accessible in reality, but it will me more visible
// if people really access them
if (spawnDrone_x !== null && spawnDrone_y !== null && spawnDrone_z !== null) {
code_eval += "me.setStartingPosition(" + spawnDrone_x + ", "
+ spawnDrone_y + ", " + spawnDrone_z + ");";
}
//base = code_eval;
code_eval +=
spawnDrone_code + "}; droneMe(Date, spawnDrone_drone, Math, {});";
//base +=
// "};spawnDrone_droneList_" + spawnDrone_team + ".push(spawnDrone_drone)";
code_eval +=
"spawnDrone_droneList_" + spawnDrone_team + ".push(spawnDrone_drone)";
/*jslint evil: true*/
eval(code_eval);
/*jslint evil: false*/
/*try {
eval(code_eval);
} catch (error) {
eval(base);
}*/
}
/******************************* DRONE MANAGER ********************************/
var DroneManager = /** @class */ (function () {
"use strict";
......@@ -27,6 +77,7 @@ var DroneManager = /** @class */ (function () {
this._maxClimbRate = 0;
this._maxCommandFrequency = 0;
this._last_command_timestamp = 0;
this._last_onUpdate_timestamp = 0;
this._speed = 0;
this._acceleration = 0;
this._direction = new BABYLON.Vector3(0, 0, 1); // North
......@@ -135,29 +186,39 @@ var DroneManager = /** @class */ (function () {
this._canPlay = true;
this._canCommunicate = true;
try {
return this.onStart(this._API._gameManager._game_duration);
return this.onStart(this._API._gameManager._start_time);
} catch (error) {
console.warn('Drone crashed on start due to error:', error);
this._internal_crash(error);
}
};
DroneManager.prototype._callSetTargetCommand =
function (latitude, longitude, altitude, speed, radius) {
var current_time = this._API._gameManager.getCurrentTime();
if (!this.isReadyToFly()) {
return;
}
if (current_time - this._last_command_timestamp
< 1000 / this._API.getMaxCommandFrequency()) {
this._internal_crash(new Error('Minimum interval between commands is ' +
1000 / this._API.getMaxCommandFrequency() + ' milliseconds'));
}
this._internal_setTargetCoordinates(latitude, longitude, altitude, speed,
radius);
this._last_command_timestamp = current_time;
};
/**
* Set a target point to move
*/
DroneManager.prototype.setTargetCoordinates =
function (latitude, longitude, altitude, speed) {
this._internal_setTargetCoordinates(latitude, longitude, altitude, speed);
this._callSetTargetCommand(latitude, longitude, altitude, speed);
};
DroneManager.prototype._internal_setTargetCoordinates =
function (latitude, longitude, altitude, speed, radius) {
if (!this._canPlay || !this.isReadyToFly()) {
if (!this._canPlay) {
return;
}
if (this._API._gameManager._game_duration - this._last_command_timestamp
< 1000 / this._API.getMaxCommandFrequency()) {
this._internal_crash(new Error('Minimum interval between commands is ' +
1000 / this._API.getMaxCommandFrequency() + ' milliseconds'));
}
//each drone API process coordinates on its needs
//e.g. fixedwing drone converts real geo-coordinates to virtual x-y
this._targetCoordinates =
......@@ -168,7 +229,6 @@ var DroneManager = /** @class */ (function () {
speed,
radius
);
this._last_command_timestamp = this._API._gameManager._game_duration;
};
DroneManager.prototype.getDroneDict = function () {
return this._API._drone_dict_list;
......@@ -184,15 +244,29 @@ var DroneManager = /** @class */ (function () {
return;
};
DroneManager.prototype.internal_update = function (delta_time) {
var context = this, milliseconds;
var context = this,
current_time = this._API._gameManager.getCurrentTime(),
onUpdate_interval = this._API.getOnUpdateInterval(),
onUpdate_start;
if (this._controlMesh) {
context._API.internal_update(context, delta_time);
if (context._canUpdate) {
context._API.internal_position_update(context, delta_time);
if (context._canUpdate &&
current_time - this._last_onUpdate_timestamp >= onUpdate_interval) {
context._canUpdate = false;
return new RSVP.Queue()
.push(function () {
milliseconds = Math.floor(context._API._gameManager._game_duration);
return context.onUpdate(milliseconds);
onUpdate_start = Date.now();
context._last_onUpdate_timestamp = current_time;
context.onUpdate(current_time);
if (onUpdate_interval > 0 &&
Date.now() - onUpdate_start > onUpdate_interval) {
throw new Error('onUpdate execution took ' +
(Date.now() - onUpdate_start) +
' milliseconds but loop interval is only ' +
onUpdate_interval +
' milliseconds');
}
return;
})
.push(function () {
context._canUpdate = true;
......@@ -201,7 +275,7 @@ var DroneManager = /** @class */ (function () {
context._internal_crash(error);
})
.push(function () {
context._API.internal_post_update(context);
context._API.internal_info_update(context);
})
.push(undefined, function (error) {
console.warn('Drone crashed on update due to error:', error);
......@@ -305,7 +379,7 @@ var DroneManager = /** @class */ (function () {
this._controlMesh.position.z,
this._controlMesh.position.y
);
position.timestamp = this._API._gameManager._game_duration;
position.timestamp = this._API._gameManager.getCurrentTime();
//Backward compatibility sanitation
position.x = position.latitude;
position.y = position.longitude;
......@@ -319,13 +393,7 @@ var DroneManager = /** @class */ (function () {
*/
DroneManager.prototype.loiter =
function (latitude, longitude, altitude, radius, speed) {
this._internal_setTargetCoordinates(
latitude,
longitude,
altitude,
speed,
radius
);
this._callSetTargetCommand(latitude, longitude, altitude, speed, radius);
};
DroneManager.prototype.getFlightParameters = function () {
if (this._API.getFlightParameters) {
......@@ -333,6 +401,9 @@ var DroneManager = /** @class */ (function () {
}
return null;
};
DroneManager.prototype.getMaxCommandFrequency = function () {
return this._API.getMaxCommandFrequency();
};
DroneManager.prototype.getYaw = function () {
if (this._API.getYaw !== undefined) {
return this._API.getYaw(this);
......@@ -358,7 +429,9 @@ var DroneManager = /** @class */ (function () {
return this._API.takeOff();
};
DroneManager.prototype.land = function () {
return this._API.land(this);
if (!this.isLanding()) {
return this._API.land(this);
}
};
DroneManager.prototype.exit = function () {
return this._internal_crash();
......@@ -916,7 +989,6 @@ var GameManager = /** @class */ (function () {
this._droneList.forEach(function (drone) {
queue.push(function () {
drone._tick += 1;
if (drone.can_play) {
if (drone.getCurrentPosition().altitude <= 0) {
if (!drone.isLanding()) {
......@@ -976,7 +1048,8 @@ var GameManager = /** @class */ (function () {
this._game_duration += delta_time;
var color, drone_position, game_manager = this, geo_coordinates,
log_count, map_info, map_manager, material, position_obj,
seconds = Math.floor(this._game_duration / 1000), trace_objects;
current_time = this.getCurrentTime(),
seconds = Math.floor(current_time / 1000), trace_objects;
if (GAMEPARAMETERS.log_drone_flight || GAMEPARAMETERS.draw_flight_path) {
this._droneList_user.forEach(function (drone, index) {
......@@ -995,7 +1068,7 @@ var GameManager = /** @class */ (function () {
drone_position.z
);
game_manager._flight_log[index].push([
game_manager._game_duration, geo_coordinates.latitude,
current_time, geo_coordinates.latitude,
geo_coordinates.longitude,
map_info.start_AMSL + drone_position.z,
drone_position.z, drone.getYaw(), drone.getSpeed(),
......@@ -1043,8 +1116,7 @@ var GameManager = /** @class */ (function () {
};
GameManager.prototype._timeOut = function () {
var seconds = Math.floor(this._game_duration / 1000);
return this._totalTime - seconds <= 0;
return this._totalTime - this._game_duration <= 0;
};
GameManager.prototype._allDronesFinished = function () {
......@@ -1226,20 +1298,19 @@ var GameManager = /** @class */ (function () {
_this.ongoing_update_promise = null;
_this.finish_deferred = RSVP.defer();
console.log("Simulation started.");
this._game_duration = Date.now();
this._totalTime = GAMEPARAMETERS.gameTime + this._game_duration;
this._start_time = Date.now();
this._game_duration = 0;
this._totalTime = GAMEPARAMETERS.gameTime * 1000;
return new RSVP.Queue()
.push(function () {
promise_list = [];
_this._droneList_user.forEach(function (drone) {
drone._tick = 0;
promise_list.push(drone.internal_start());
});
start_msg = GAMEPARAMETERS.operator_init_msg;
promise_list.push(_this._droneList_user[0].sendMsg(start_msg));
_this._droneList_enemy.forEach(function (drone) {
drone._tick = 0;
promise_list.push(drone.internal_start());
});
return RSVP.all(promise_list);
......@@ -1310,47 +1381,6 @@ var GameManager = /** @class */ (function () {
}
return false;
}
function spawnDrone(x, y, z, index, drone_info, api, team) {
var default_drone_AI = api.getDroneAI(), code, code_eval;
if (default_drone_AI) {
code = default_drone_AI;
} else {
code = drone_info.script_content;
}
if (!code.includes("me.onStart")) {
code = "me.onStart = function () { me.exit(); };";
}
code_eval = "let drone = new DroneManager(ctx._scene, " +
index + ', api, team);' +
"let droneMe = function(NativeDate, me, Math, window, DroneManager," +
" GameManager, FixedWingDroneAPI, EnemyDroneAPI, BABYLON, " +
"GAMEPARAMETERS) {" +
"var start_time = (new Date(2070, 0, 0, 0, 0, 0, 0)).getTime();" +
"Date.now = function () {" +
"return start_time + drone._tick * 1000/60;}; " +
"function Date() {if (!(this instanceof Date)) " +
"{throw new Error('Missing new operator');} " +
"if (arguments.length === 0) {return new NativeDate(Date.now());} " +
"else {return new NativeDate(...arguments);}}";
// Simple desactivation of direct access of all globals
// It is still accessible in reality, but it will me more visible
// if people really access them
if (x !== null && y !== null && z !== null) {
code_eval += "me.setStartingPosition(" + x + ", " + y + ", " + z + ");";
}
//base = code_eval;
code_eval += code + "}; droneMe(Date, drone, Math, {});";
//base += "};ctx._droneList_" + team + ".push(drone)";
code_eval += "ctx._droneList_" + team + ".push(drone)";
/*jslint evil: true*/
eval(code_eval);
/*jslint evil: false*/
/*try {
eval(code_eval);
} catch (error) {
eval(base);
}*/
}
function randomSpherePoint(x0, y0, z0, rx0, ry0, rz0) {
var u = Math.random(), v = Math.random(),
rx = Math.random() * rx0, ry = Math.random() * ry0,
......@@ -1384,11 +1414,16 @@ var GameManager = /** @class */ (function () {
i + id_offset
);
spawnDrone(position.x, position.y, position.z, i + id_offset,
drone_list[i], api, team);
drone_list[i], api, team, ctx._scene, ctx._droneList_user,
ctx._droneList_enemy);
}
}
};
GameManager.prototype.getCurrentTime = function () {
return this._start_time + this._game_duration;
};
return GameManager;
}());
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.65026.25145.27272</string> </value>
<value> <string>1016.21978.22579.46609</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709560433.33</float>
<float>1714742387.62</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -381,6 +381,19 @@
<div class="line"></div>
<!-- getMaxCommandFrequency -->
<h4 class="item-name" id="getMaxCommandFrequency"><span>getMaxCommandFrequency</span><span>: void</span></h4>
<p class="item-descr">Get maximum frequency in hertz at which direction commands (setTargetCoordinates and loiter) can be called.</p>
<div>
<h5 class="item-param-1">Example</h5>
</div>
<p class="item-param-1">me.getMaxCommandFrequency();<br>
</p>
<div class="line"></div>
<!-- takeOff -->
<h4 class="item-name" id="takeOff"><span>takeOff</span><span>: void</span></h4>
<p class="item-descr">Trigger drone's takeoff (has only effect on multicopters as fixed wings drones need to take off manually).</p>
......
......@@ -244,7 +244,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.65292.62763.3464</string> </value>
<value> <string>1015.13928.44848.25668</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -264,7 +264,7 @@
</tuple>
<state>
<tuple>
<float>1709564153.67</float>
<float>1710867839.5</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -185,6 +185,7 @@ var OperatorAPI = /** @class */ (function () {
DRAW = true,
LOG = true,
LOG_TIME = 1662.7915426540285,
ONUPDATE_INTERVAL = 100,
LOGIC_FILE_LIST = [
'gadget_erp5_page_drone_capture_flag_logic.js',
'gadget_erp5_page_drone_capture_map_utils.js',
......@@ -566,6 +567,17 @@ var OperatorAPI = /** @class */ (function () {
"hidden": 0,
"type": "IntegerField"
},
"my_onupdate_interval": {
"description": "Minimum interval (in milliseconds) between 2 executions of onUpdate function as well as periodicity to send telemetry to the swarm",
"title": "OnUpdate interval",
"default": gadget.state.onupdate_interval,
"css_class": "",
"required": 1,
"editable": 1,
"key": "onupdate_interval",
"hidden": 0,
"type": "IntegerField"
},
"my_drone_min_speed": {
"description": "",
"title": "Drone min speed",
......@@ -708,7 +720,7 @@ var OperatorAPI = /** @class */ (function () {
form_definition: {
group_list: [[
"left",
[["my_simulation_speed"], ["my_simulation_time"],
[["my_simulation_speed"], ["my_simulation_time"], ["my_onupdate_interval"],
["my_number_of_drones"], ["my_map_seed"]]
], [
"right",
......@@ -845,6 +857,7 @@ var OperatorAPI = /** @class */ (function () {
"maxSinkRate": parseFloat(gadget.state.drone_max_sink_rate),
"maxClimbRate": parseFloat(gadget.state.drone_max_climb_rate),
"maxCommandFrequency": parseFloat(gadget.state.drone_max_command_frequency),
"onUpdateInterval": parseInt(gadget.state.onupdate_interval, 10),
"list": drone_list
},
"gameTime": parseInt(gadget.state.simulation_time, 10),
......@@ -970,6 +983,7 @@ var OperatorAPI = /** @class */ (function () {
drone_max_speed: MAX_SPEED,
drone_speed: DEFAULT_SPEED,
drone_min_speed: MIN_SPEED,
onupdate_interval: ONUPDATE_INTERVAL,
simulation_time: SIMULATION_TIME,
simulation_speed: SIMULATION_SPEED,
operator_init_msg: {},
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.48194.32365.65399</string> </value>
<value> <string>1016.21921.52144.47906</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1708709921.07</float>
<float>1714738877.83</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -3,8 +3,10 @@
(function (window, rJS, domsugar, document) {
"use strict";
var SIMULATION_SPEED = 10,
SIMULATION_TIME = 270,
var SIMULATION_SPEED = 1,
LOOP_INTERVAL = 1000 / 60,
ON_UPDATE_INTERVAL = LOOP_INTERVAL,
SIMULATION_TIME = LOOP_INTERVAL / 1000,
MIN_LAT = 45.6364,
MAX_LAT = 45.65,
MIN_LON = 14.2521,
......@@ -74,9 +76,10 @@
' me.getCurrentPosition().longitude\n' +
' ).toFixed(8),\n' +
' time_interval = timestamp - me.start_time,\n' +
' expected_interval = 1000 / 60,\n' +
' expected_interval = ' + LOOP_INTERVAL + ',\n' +
' expectedDistance = (me.getSpeed() * expected_interval / 1000).toFixed(8);\n' +
' assert(time_interval, Math.floor(expected_interval), "Timestamp");\n' +
' assert(time_interval.toFixed(4), expected_interval.toFixed(4), "Timestamp");\n' +
' assert(Date.now(), timestamp, "Date");\n' +
' assert(realDistance, expectedDistance, "Distance");\n' +
' current_position.latitude = current_position.latitude.toFixed(7);\n' +
' compare(current_position, {\n' +
......@@ -84,7 +87,6 @@
' longitude: me.initialPosition.longitude,\n' +
' altitude: me.initialPosition.altitude\n' +
' });\n' +
' me.exit(me.land());\n' +
'};',
DRAW = true,
LOG = true,
......@@ -120,19 +122,19 @@
"script_content": DEFAULT_SCRIPT_CONTENT};
}
map_json = {
"height": parseInt(map_height, 10),
"start_AMSL": parseFloat(start_AMSL),
"min_lat": parseFloat(MIN_LAT),
"max_lat": parseFloat(MAX_LAT),
"min_lon": parseFloat(MIN_LON),
"max_lon": parseFloat(MAX_LON),
"height": map_height,
"start_AMSL": start_AMSL,
"min_lat": MIN_LAT,
"max_lat": MAX_LAT,
"min_lon": MIN_LON,
"max_lon": MAX_LON,
"flag_list": [],
"obstacle_list" : [],
"enemy_list" : [],
"initial_position": {
"longitude": parseFloat(INIT_LON),
"latitude": parseFloat(INIT_LAT),
"altitude": parseFloat(INIT_ALT)
"longitude": INIT_LON,
"latitude": INIT_LAT,
"altitude": INIT_ALT
}
};
operator_init_msg = {
......@@ -142,20 +144,21 @@
game_parameters_json = {
"debug_test_mode": true,
"drone": {
"maxAcceleration": parseInt(MAX_ACCELERATION, 10),
"maxDeceleration": parseInt(MAX_DECELERATION, 10),
"minSpeed": parseInt(MIN_SPEED, 10),
"speed": parseFloat(DEFAULT_SPEED),
"maxSpeed": parseInt(MAX_SPEED, 10),
"maxRoll": parseFloat(MAX_ROLL),
"minPitchAngle": parseFloat(MIN_PITCH),
"maxPitchAngle": parseFloat(MAX_PITCH),
"maxSinkRate": parseFloat(MAX_SINK_RATE),
"maxClimbRate": parseFloat(MAX_CLIMB_RATE),
"maxAcceleration": MAX_ACCELERATION,
"maxDeceleration": MAX_DECELERATION,
"minSpeed": MIN_SPEED,
"speed": DEFAULT_SPEED,
"maxSpeed": MAX_SPEED,
"maxRoll": MAX_ROLL,
"minPitchAngle": MIN_PITCH,
"maxPitchAngle": MAX_PITCH,
"maxSinkRate": MAX_SINK_RATE,
"maxClimbRate": MAX_CLIMB_RATE,
"onUpdateInterval": ON_UPDATE_INTERVAL,
"list": DRONE_LIST
},
"gameTime": parseInt(SIMULATION_TIME, 10),
"simulation_speed": parseInt(SIMULATION_SPEED, 10),
"gameTime": SIMULATION_TIME,
"simulation_speed": SIMULATION_SPEED,
"latency": {
"information": 0,
"communication": 0
......@@ -214,19 +217,24 @@
})
.push(function (result) {
var div = domsugar('div', { text: "CONSOLE LOG ENTRIES:" }), lines,
l, node;
l, test_log_node = document.querySelector('.test_log');
document.querySelector('.container').parentNode.appendChild(div);
function createLogNode(message) {
function appendToTestLog(test_log_node, message) {
var log_node = document.createElement("div"),
textNode = document.createTextNode(message);
log_node.appendChild(textNode);
return log_node;
test_log_node.appendChild(log_node);
}
lines = result.console_log.split('\n');
for (l = 0; l < lines.length; l += 1) {
node = createLogNode(lines[l]);
document.querySelector('.test_log').appendChild(node);
if (lines[l] !== 'TIMEOUT!') {
appendToTestLog(test_log_node, lines[l]);
} else {
appendToTestLog(test_log_node, 'Timeout: OK');
return;
}
}
appendToTestLog(test_log_node, 'Timeout: FAILED');
}, function (error) {
return gadget.notifySubmitted({message: "Error: " + error.message,
status: 'error'});
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60680.20078.34286</string> </value>
<value> <string>1015.64187.34381.50346</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709286290.59</float>
<float>1713428877.4</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -80,7 +80,7 @@ var DroneLogAPI = /** @class */ (function () {
/*
** Function called on every drone update, right before onUpdate AI script
*/
DroneLogAPI.prototype.internal_update = function (context, delta_time) {
DroneLogAPI.prototype.internal_position_update = function (context, delta_time) {
var updateSpeed;
context._speed += context._acceleration * delta_time / 1000;
if (context._speed > context._maxSpeed) {
......@@ -104,7 +104,7 @@ var DroneLogAPI = /** @class */ (function () {
/*
** Function called on every drone update, right after onUpdate AI script
*/
DroneLogAPI.prototype.internal_post_update = function (drone) {
DroneLogAPI.prototype.internal_info_update = function (drone) {
return;
};
DroneLogAPI.prototype.internal_setTargetCoordinates =
......@@ -213,6 +213,9 @@ var DroneLogAPI = /** @class */ (function () {
DroneLogAPI.prototype.getMaxCommandFrequency = function () {
return Infinity;
};
DroneLogAPI.prototype.getOnUpdateInterval = function () {
return 0;
};
return DroneLogAPI;
}());
\ No newline at end of file
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60631.26636.59528</string> </value>
<value> <string>1015.64101.28159.26163</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709288518.39</float>
<float>1713425784.77</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -7,6 +7,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
// var TAKEOFF_RADIUS = 60,
var DEFAULT_SPEED = 16,
PARACHUTE_SPEED = 8,
EARTH_GRAVITY = 9.81,
LOITER_LIMIT = 30,
MAX_ACCELERATION = 6,
......@@ -52,7 +53,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
throw new Error('max acceleration must be superior to 0');
}
drone._minSpeed = this.getMinSpeed();
if (drone._minSpeed <= 0) {
if (drone._minSpeed < 0) {
throw new Error('min speed must be superior to 0');
}
drone._maxSpeed = this.getMaxSpeed();
......@@ -101,7 +102,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
/*
** Function called on every drone update, right before onUpdate AI script
*/
FixedWingDroneAPI.prototype.internal_update = function (context, delta_time) {
FixedWingDroneAPI.prototype.internal_position_update = function (context, delta_time) {
this._updateSpeed(context, delta_time);
this._updatePosition(context, delta_time);
......@@ -111,7 +112,7 @@ var FixedWingDroneAPI = /** @class */ (function () {
/*
** Function called on every drone update, right after onUpdate AI script
*/
FixedWingDroneAPI.prototype.internal_post_update = function (drone) {
FixedWingDroneAPI.prototype.internal_info_update = function (drone) {
var _this = this, drone_position = drone.getCurrentPosition(), drone_info;
/*if (_this._start_altitude > 0) { //TODO move start_altitude here
_this.reachAltitude(drone);
......@@ -373,8 +374,6 @@ var FixedWingDroneAPI = /** @class */ (function () {
var processed_coordinates =
this._mapManager.convertToLocalCoordinates(lat, lon, z);
processed_coordinates.z -= this._map_dict.start_AMSL;
//this._last_altitude_point_reached = -1;
//this.takeoff_path = [];
return processed_coordinates;
};
FixedWingDroneAPI.prototype.getCurrentPosition = function (x, y, z) {
......@@ -470,11 +469,16 @@ var FixedWingDroneAPI = /** @class */ (function () {
};
FixedWingDroneAPI.prototype.land = function (drone) {
var drone_pos = drone.getCurrentPosition();
drone.setTargetCoordinates(
this._flight_parameters.drone.minSpeed = 0;
drone._speed = 0;
drone._acceleration = EARTH_GRAVITY;
this._flight_parameters.drone.maxSinkRate = PARACHUTE_SPEED;
this._flight_parameters.drone.minPitchAngle = -90;
drone._internal_setTargetCoordinates(
drone_pos.latitude,
drone_pos.longitude,
0,
drone.get3DSpeed()
PARACHUTE_SPEED
);
this._is_ready_to_fly = false;
this._is_landing = true;
......@@ -497,6 +501,9 @@ var FixedWingDroneAPI = /** @class */ (function () {
FixedWingDroneAPI.prototype.getMaxHeight = function () {
return 800;
};
FixedWingDroneAPI.prototype.getOnUpdateInterval = function () {
return this._flight_parameters.drone.onUpdateInterval;
};
FixedWingDroneAPI.prototype.getFlightParameters = function () {
return this._flight_parameters;
};
......
......@@ -240,7 +240,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60733.7318.44953</string> </value>
<value> <string>1015.64203.48820.61559</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +260,7 @@
</tuple>
<state>
<tuple>
<float>1709562449.93</float>
<float>1713429850.09</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -6,6 +6,50 @@ var GAMEPARAMETERS = {};
//for DEBUG/TEST mode
var baseLogFunction = console.log, console_log = "";
function spawnDrone(spawnDrone_x, spawnDrone_y, spawnDrone_z, spawnDrone_index,
spawnDrone_drone_info, spawnDrone_api, spawnDrone_scene,
spawnDrone_droneList) {
"use strict";
var default_drone_AI = spawnDrone_api.getDroneAI(), spawnDrone_code,
spawnDrone_base, code_eval;
if (default_drone_AI) {
spawnDrone_code = default_drone_AI;
} else {
spawnDrone_code = spawnDrone_drone_info.script_content;
}
code_eval = "let spawnDrone_drone = new DroneManager(spawnDrone_scene, " +
spawnDrone_index + ', spawnDrone_api);' +
"let droneMe = function(NativeDate, me, Math, window, DroneManager," +
" GameManager, DroneLogAPI, FixedWingDroneAPI, BABYLON, " +
"GAMEPARAMETERS) {" +
"Date.now = function () {" +
"return me._API._gameManager.getCurrentTime();}; " +
"function Date() {if (!(this instanceof Date)) " +
"{throw new Error('Missing new operator');} " +
"if (arguments.length === 0) {return new NativeDate(Date.now());} " +
"else {return new NativeDate(...arguments);}}";
// Simple desactivation of direct access of all globals
// It is still accessible in reality, but it will me more visible
// if people really access them
if (spawnDrone_x !== null && spawnDrone_y !== null && spawnDrone_z !== null) {
code_eval += "me.setStartingPosition(" + spawnDrone_x + ", "
+ spawnDrone_y + ", " + spawnDrone_z + ");";
}
spawnDrone_base = code_eval;
code_eval +=
spawnDrone_code + "}; droneMe(Date, spawnDrone_drone, Math, {});";
spawnDrone_base += "};spawnDrone_droneList.push(spawnDrone_drone)";
code_eval += "spawnDrone_droneList.push(spawnDrone_drone)";
/*jslint evil: true*/
try {
eval(code_eval);
} catch (error) {
console.error(error);
eval(spawnDrone_base);
}
/*jslint evil: false*/
}
/******************************* DRONE MANAGER ********************************/
var DroneManager = /** @class */ (function () {
"use strict";
......@@ -27,6 +71,7 @@ var DroneManager = /** @class */ (function () {
this._maxClimbRate = 0;
this._maxCommandFrequency = 0;
this._last_command_timestamp = 0;
this._last_onUpdate_timestamp = 0;
this._speed = 0;
this._acceleration = 0;
this._direction = new BABYLON.Vector3(0, 0, 1); // North
......@@ -124,29 +169,39 @@ var DroneManager = /** @class */ (function () {
this._canCommunicate = true;
this._targetCoordinates = initial_position;
try {
return this.onStart(this._API._gameManager._game_duration);
return this.onStart(this._API._gameManager._start_time);
} catch (error) {
console.warn('Drone crashed on start due to error:', error);
this._internal_crash(error);
}
};
DroneManager.prototype._callSetTargetCommand =
function (latitude, longitude, altitude, speed, radius) {
var current_time = this._API._gameManager.getCurrentTime();
if (!this.isReadyToFly()) {
return;
}
if (current_time - this._last_command_timestamp
< 1000 / this._API.getMaxCommandFrequency()) {
this._internal_crash(new Error('Minimum interval between commands is ' +
1000 / this._API.getMaxCommandFrequency() + ' milliseconds'));
}
this._internal_setTargetCoordinates(latitude, longitude, altitude, speed,
radius);
this._last_command_timestamp = current_time;
};
/**
* Set a target point to move
*/
DroneManager.prototype.setTargetCoordinates =
function (latitude, longitude, altitude, speed) {
this._internal_setTargetCoordinates(latitude, longitude, altitude, speed);
this._callSetTargetCommand(latitude, longitude, altitude, speed);
};
DroneManager.prototype._internal_setTargetCoordinates =
function (latitude, longitude, altitude, speed, radius) {
if (!this._canPlay || !this.isReadyToFly()) {
if (!this._canPlay) {
return;
}
if (this._API._gameManager._game_duration - this._last_command_timestamp
< 1000 / this._API.getMaxCommandFrequency()) {
this._internal_crash(new Error('Minimum interval between commands is ' +
1000 / this._API.getMaxCommandFrequency() + ' milliseconds'));
}
//convert real geo-coordinates to virtual x-y coordinates
this._targetCoordinates =
this._API.processCoordinates(latitude, longitude, altitude);
......@@ -156,17 +211,30 @@ var DroneManager = /** @class */ (function () {
speed,
radius
);
this._last_command_timestamp = this._API._gameManager._game_duration;
};
DroneManager.prototype.internal_update = function (delta_time) {
var context = this;
var context = this,
current_time = this._API._gameManager.getCurrentTime(),
onUpdate_interval = this._API.getOnUpdateInterval(),
onUpdate_start;
if (this._controlMesh) {
context._API.internal_update(context, delta_time);
if (context._canUpdate) {
context._API.internal_position_update(context, delta_time);
if (context._canUpdate &&
current_time - this._last_onUpdate_timestamp >= onUpdate_interval) {
context._canUpdate = false;
return new RSVP.Queue()
.push(function () {
return context.onUpdate(context._API._gameManager._game_duration);
onUpdate_start = Date.now();
context._last_onUpdate_timestamp = current_time;
context.onUpdate(current_time);
if (onUpdate_interval > 0 &&
Date.now() - onUpdate_start > onUpdate_interval) {
throw new Error('onUpdate execution took ' +
(Date.now() - onUpdate_start) +
' milliseconds but loop interval is only ' +
onUpdate_interval +
' milliseconds');
}
})
.push(function () {
context._canUpdate = true;
......@@ -175,7 +243,7 @@ var DroneManager = /** @class */ (function () {
context._internal_crash(error);
})
.push(function () {
context._API.internal_post_update(context);
context._API.internal_info_update(context);
})
.push(undefined, function (error) {
console.warn('Drone crashed on update due to error:', error);
......@@ -274,7 +342,7 @@ var DroneManager = /** @class */ (function () {
this._controlMesh.position.z,
this._controlMesh.position.y
);
position.timestamp = this._API._gameManager._game_duration;
position.timestamp = this._API._gameManager.getCurrentTime();
return position;
}
return null;
......@@ -284,13 +352,7 @@ var DroneManager = /** @class */ (function () {
*/
DroneManager.prototype.loiter =
function (latitude, longitude, altitude, radius, speed) {
this._internal_setTargetCoordinates(
latitude,
longitude,
altitude,
speed,
radius
);
this._callSetTargetCommand(latitude, longitude, altitude, speed, radius);
};
DroneManager.prototype.getFlightParameters = function () {
if (this._API.getFlightParameters) {
......@@ -298,6 +360,9 @@ var DroneManager = /** @class */ (function () {
}
return null;
};
DroneManager.prototype.getMaxCommandFrequency = function () {
return this._API.getMaxCommandFrequency();
};
DroneManager.prototype.getYaw = function () {
return this._API.getYaw(this);
};
......@@ -314,7 +379,9 @@ var DroneManager = /** @class */ (function () {
return this._API.takeOff();
};
DroneManager.prototype.land = function () {
return this._API.land(this);
if (!this.isLanding()) {
return this._API.land(this);
}
};
DroneManager.prototype.exit = function () {
return this._internal_crash();
......@@ -659,7 +726,6 @@ var GameManager = /** @class */ (function () {
this._droneList.forEach(function (drone) {
queue.push(function () {
drone._tick += 1;
if (drone._API.isCollidable && drone.can_play) {
if (drone.getCurrentPosition().altitude <= 0) {
if (!drone.isLanding()) {
......@@ -698,7 +764,8 @@ var GameManager = /** @class */ (function () {
this._game_duration += delta_time;
var color, drone_position, game_manager = this, geo_coordinates,
log_count, map_info, map_manager, material, position_obj,
seconds = Math.floor(this._game_duration / 1000), trace_objects;
current_time = this.getCurrentTime(),
seconds = Math.floor(current_time / 1000), trace_objects;
if (GAMEPARAMETERS.log_drone_flight || GAMEPARAMETERS.draw_flight_path) {
this._droneList.forEach(function (drone, index) {
......@@ -717,7 +784,7 @@ var GameManager = /** @class */ (function () {
drone_position.z
);
game_manager._flight_log[index].push([
game_manager._game_duration, geo_coordinates.latitude,
current_time, geo_coordinates.latitude,
geo_coordinates.longitude,
map_info.start_AMSL + drone_position.z,
drone_position.z, drone.getYaw(), drone.getSpeed(),
......@@ -763,8 +830,7 @@ var GameManager = /** @class */ (function () {
};
GameManager.prototype._timeOut = function () {
var seconds = Math.floor(this._game_duration / 1000);
return this._totalTime - seconds <= 0;
return this._totalTime - this._game_duration <= 0;
};
GameManager.prototype._allDronesFinished = function () {
......@@ -903,14 +969,14 @@ var GameManager = /** @class */ (function () {
_this.ongoing_update_promise = null;
_this.finish_deferred = RSVP.defer();
console.log("Simulation started.");
this._game_duration = Date.now();
this._totalTime = GAMEPARAMETERS.gameTime + this._game_duration;
this._start_time = Date.now();
this._game_duration = 0;
this._totalTime = GAMEPARAMETERS.gameTime * 1000;
return new RSVP.Queue()
.push(function () {
promise_list = [];
_this._droneList.forEach(function (drone) {
drone._tick = 0;
promise_list.push(drone.internal_start(
_this._mapManager.getMapInfo().initial_position
));
......@@ -980,44 +1046,6 @@ var GameManager = /** @class */ (function () {
}
return false;
}
function spawnDrone(x, y, z, index, drone_info, api) {
var default_drone_AI = api.getDroneAI(), code, base, code_eval;
if (default_drone_AI) {
code = default_drone_AI;
} else {
code = drone_info.script_content;
}
code_eval = "let drone = new DroneManager(ctx._scene, " +
index + ', api);' +
"let droneMe = function(NativeDate, me, Math, window, DroneManager," +
" GameManager, DroneLogAPI, FixedWingDroneAPI, BABYLON, " +
"GAMEPARAMETERS) {" +
"var start_time = (new Date(2070, 0, 0, 0, 0, 0, 0)).getTime();" +
"Date.now = function () {" +
"return start_time + drone._tick * 1000/60;}; " +
"function Date() {if (!(this instanceof Date)) " +
"{throw new Error('Missing new operator');} " +
"if (arguments.length === 0) {return new NativeDate(Date.now());} " +
"else {return new NativeDate(...arguments);}}";
// Simple desactivation of direct access of all globals
// It is still accessible in reality, but it will me more visible
// if people really access them
if (x !== null && y !== null && z !== null) {
code_eval += "me.setStartingPosition(" + x + ", " + y + ", " + z + ");";
}
base = code_eval;
code_eval += code + "}; droneMe(Date, drone, Math, {});";
base += "};ctx._droneList.push(drone)";
code_eval += "ctx._droneList.push(drone)";
/*jslint evil: true*/
try {
eval(code_eval);
} catch (error) {
console.error(error);
eval(base);
}
/*jslint evil: false*/
}
function randomSpherePoint(x0, y0, z0, rx0, ry0, rz0) {
var u = Math.random(), v = Math.random(),
rx = Math.random() * rx0, ry = Math.random() * ry0,
......@@ -1045,11 +1073,15 @@ var GameManager = /** @class */ (function () {
i
);
spawnDrone(position.x, position.y, position.z, i,
drone_list[i], api);
drone_list[i], api, ctx._scene, ctx._droneList);
}
}
};
GameManager.prototype.getCurrentTime = function () {
return this._start_time + this._game_duration;
};
return GameManager;
}());
......
......@@ -240,7 +240,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.65027.38618.31573</string> </value>
<value> <string>1016.21987.28184.16844</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +260,7 @@
</tuple>
<state>
<tuple>
<float>1709560536.57</float>
<float>1714742619.34</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -122,11 +122,13 @@
' }\n' +
' return;\n' +
' }\n' +
' me.exit(0);\n' +
' if (!me.isLanding()) { me.land() };\n' +
' if (me.getCurrentPosition().altitude <= 0) { me.exit(0) };\n' +
'};',
DRAW = true,
LOG = true,
LOG_TIME = 1662.7915426540285,
ONUPDATE_INTERVAL = 100,
DRONE_LIST = [],
WIDTH = 680,
HEIGHT = 340,
......@@ -191,6 +193,17 @@
"hidden": 0,
"type": "IntegerField"
},
"my_onupdate_interval": {
"description": "Minimum interval (in milliseconds) between 2 executions of onUpdate function as well as periodicity to send telemetry to the swarm",
"title": "OnUpdate interval",
"default": ONUPDATE_INTERVAL,
"css_class": "",
"required": 1,
"editable": 1,
"key": "onupdate_interval",
"hidden": 0,
"type": "IntegerField"
},
"my_drone_min_speed": {
"description": "",
"title": "Drone min speed",
......@@ -444,8 +457,8 @@
form_definition: {
group_list: [[
"left",
[["my_simulation_speed"], ["my_simulation_time"], ["my_number_of_drones"],
["my_minimum_latitud"], ["my_maximum_latitud"],
[["my_simulation_speed"], ["my_simulation_time"], ["my_onupdate_interval"],
["my_number_of_drones"], ["my_minimum_latitud"], ["my_maximum_latitud"],
["my_minimum_longitud"], ["my_maximum_longitud"],
["my_init_pos_lat"], ["my_init_pos_lon"], ["my_init_pos_alt"],
["my_map_height"]]
......@@ -494,7 +507,8 @@
"maxPitchAngle": parseFloat(options.drone_max_pitch),
"maxSinkRate": parseFloat(options.drone_max_sink_rate),
"maxClimbRate": parseFloat(options.drone_max_climb_rate),
"maxCommandFrequency": parseFloat(options.drone_max_command_frequency)
"maxCommandFrequency": parseFloat(options.drone_max_command_frequency),
"onUpdateInterval": parseInt(options.onupdate_interval, 10)
},
"gameTime": parseInt(options.simulation_time, 10),
"simulation_speed": parseInt(options.simulation_speed, 10),
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60725.60577.24917</string> </value>
<value> <string>1015.64120.46679.6946</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709289017.41</float>
<float>1713425712.2</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -3,11 +3,10 @@
(function (window, rJS, domsugar, document, URLSearchParams, Blob) {
"use strict";
var SIMULATION_SPEED = 10,
SIMULATION_TIME = 270,
MAP_SIZE = 600,
map_height = 700,
start_AMSL = 595,
var SIMULATION_SPEED = 1,
LOOP_INTERVAL = 1000 / 60,
ON_UPDATE_INTERVAL = LOOP_INTERVAL,
SIMULATION_TIME = LOOP_INTERVAL / 1000,
DEFAULT_SPEED = 16,
MAX_ACCELERATION = 6,
MAX_DECELERATION = 1,
......@@ -19,7 +18,6 @@
MAX_CLIMB_RATE = 8,
MAX_SINK_RATE = 3,
NUMBER_OF_DRONES = 1,
FLAG_WEIGHT = 5,
MIN_LAT = 45.6364,
MAX_LAT = 45.65,
MIN_LON = 14.2521,
......@@ -78,9 +76,10 @@
' me.getCurrentPosition().longitude\n' +
' ).toFixed(8),\n' +
' time_interval = timestamp - me.start_time,\n' +
' expected_interval = 1000 / 60,\n' +
' expected_interval = ' + LOOP_INTERVAL + ',\n' +
' expectedDistance = (me.getSpeed() * expected_interval / 1000).toFixed(8);\n' +
' assert(time_interval.toFixed(4), expected_interval.toFixed(4), "Timestamp");\n' +
' assert(Date.now(), timestamp, "Date");\n' +
' assert(realDistance, expectedDistance, "Distance");\n' +
' current_position.latitude = current_position.latitude.toFixed(7);\n' +
' compare(current_position, {\n' +
......@@ -88,7 +87,6 @@
' longitude: me.initialPosition.longitude,\n' +
' altitude: me.initialPosition.altitude\n' +
' });\n' +
' me.exit(me.land());\n' +
'};',
DRAW = true,
LOG = true,
......@@ -175,35 +173,36 @@
game_parameters_json = {
"debug_test_mode": true,
"drone": {
"maxAcceleration": parseInt(MAX_ACCELERATION, 10),
"maxDeceleration": parseInt(MAX_DECELERATION, 10),
"minSpeed": parseInt(MIN_SPEED, 10),
"speed": parseFloat(DEFAULT_SPEED),
"maxSpeed": parseInt(MAX_SPEED, 10),
"maxRoll": parseFloat(MAX_ROLL),
"minPitchAngle": parseFloat(MIN_PITCH),
"maxPitchAngle": parseFloat(MAX_PITCH),
"maxSinkRate": parseFloat(MAX_SINK_RATE),
"maxClimbRate": parseFloat(MAX_CLIMB_RATE)
"maxAcceleration": MAX_ACCELERATION,
"maxDeceleration": MAX_DECELERATION,
"minSpeed": MIN_SPEED,
"speed": DEFAULT_SPEED,
"maxSpeed": MAX_SPEED,
"maxRoll": MAX_ROLL,
"minPitchAngle": MIN_PITCH,
"maxPitchAngle": MAX_PITCH,
"maxSinkRate": MAX_SINK_RATE,
"maxClimbRate": MAX_CLIMB_RATE,
"onUpdateInterval": ON_UPDATE_INTERVAL
},
"gameTime": parseInt(SIMULATION_TIME, 10),
"simulation_speed": parseInt(SIMULATION_SPEED, 10),
"gameTime": SIMULATION_TIME,
"simulation_speed": SIMULATION_SPEED,
"latency": {
"information": 0,
"communication": 0
},
"map": {
"min_lat": parseFloat(MIN_LAT),
"max_lat": parseFloat(MAX_LAT),
"min_lon": parseFloat(MIN_LON),
"max_lon": parseFloat(MAX_LON),
"height": parseInt(HEIGHT),
"start_AMSL": parseFloat(start_AMSL)
"min_lat": MIN_LAT,
"max_lat": MAX_LAT,
"min_lon": MIN_LON,
"max_lon": MAX_LON,
"height": HEIGHT,
"start_AMSL": start_AMSL
},
"initialPosition": {
"longitude": parseFloat(INIT_LON),
"latitude": parseFloat(INIT_LAT),
"altitude": parseFloat(INIT_ALT)
"longitude": INIT_LON,
"latitude": INIT_LAT,
"altitude": INIT_ALT
},
"draw_flight_path": DRAW,
"temp_flight_path": true,
......@@ -257,19 +256,27 @@
return form_gadget.getContent();
})
.push(function (result) {
var div = domsugar('div', { text: "CONSOLE LOG ENTRIES:" });
var div = domsugar('div', { text: "CONSOLE LOG ENTRIES:" }),
lines = result.console_log.split('\n'),
line_nb,
node,
test_log_node = document.querySelector('.test_log');;
document.querySelector('.container').parentNode.appendChild(div);
function createLogNode(message) {
var node = document.createElement("div");
var textNode = document.createTextNode(message);
node.appendChild(textNode);
return node;
function appendToTestLog(test_log_node, message) {
var log_node = document.createElement("div"),
textNode = document.createTextNode(message);
log_node.appendChild(textNode);
test_log_node.appendChild(log_node);
}
var lines = result.console_log.split('\n');
for (var i = 0;i < lines.length;i++) {
var node = createLogNode(lines[i]);
document.querySelector('.test_log').appendChild(node);
for (line_nb = 0; line_nb < lines.length; line_nb += 1) {
if (lines[line_nb] !== 'TIMEOUT!') {
appendToTestLog(test_log_node, lines[line_nb]);
} else {
appendToTestLog(test_log_node, 'Timeout: OK');
return;
}
}
appendToTestLog(test_log_node, 'Timeout: FAILED');
}, function (error) {
return gadget.notifySubmitted({message: "Error: " + error.message,
status: 'error'});
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1014.60681.20667.37171</string> </value>
<value> <string>1015.64176.45813.63488</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -266,7 +266,7 @@
</tuple>
<state>
<tuple>
<float>1709286436.53</float>
<float>1713428376.53</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -25,9 +25,13 @@
#
##############################################################################
import base64
import binascii
import json
import typing
import six
from six.moves.urllib.parse import unquote
if typing.TYPE_CHECKING:
from typing import Any, Callable, Optional
from erp5.component.document.OpenAPITypeInformation import OpenAPIOperation, OpenAPIParameter
......@@ -258,10 +262,8 @@ class OpenAPIService(XMLObject):
parameter,
parameter.getJSONSchema(),
)
requestBody = self.validateParameter(
'request body',
requestBody = self.validateRequestBody(
operation.getRequestBodyValue(request),
{},
operation.getRequestBodyJSONSchema(request),
)
if requestBody:
......@@ -296,6 +298,37 @@ class OpenAPIService(XMLObject):
parameter_name=parameter_name, e=e.message), str(e))
return parameter_value
security.declareProtected(
Permissions.AccessContentsInformation, 'validateRequestBody')
def validateRequestBody(self, parameter_value, schema):
# type: (str, dict) -> Any
"""Validate the request body raising a ParameterValidationError
when the parameter is not valid according to the corresponding schema.
"""
if schema is not None:
if schema.get('type') == 'string':
if schema.get('format') == 'base64':
try:
return base64.b64decode(parameter_value)
except (binascii.Error, TypeError) as e:
if isinstance(e, TypeError):
# BBB on python2 this raises a generic type error
# but we don't want to ignore potential TypeErrors
# on python3 here
if six.PY3:
raise
raise ParameterValidationError(
'Error validating request body: {e}'.format(e=str(e)))
elif schema.get('format') == 'binary':
return parameter_value or b''
return self.validateParameter(
'request body',
parameter_value,
{},
schema,
)
def executeMethod(self, request):
# type: (HTTPRequest) -> Any
operation = self.getMatchingOperation(request)
......
......@@ -90,7 +90,7 @@ ModuleSecurityInfo(__name__).declarePublic(
)
# On python2, make sure we use UTF-8 strings for the json schemas, so that we don't
# have ugly u' prefixs in the reprs. This also transforms the collections.OrderedDict
# have ugly u' prefixes in the reprs. This also transforms the collections.OrderedDict
# to simple dicts, because the former also have an ugly representation.
# http://stackoverflow.com/a/13105359
if six.PY2:
......@@ -105,7 +105,7 @@ if six.PY2:
return [byteify(element) for element in string]
elif isinstance(string, tuple):
return tuple(byteify(element) for element in string)
elif isinstance(string, unicode):
elif isinstance(string, six.text_type):
return string.encode('utf-8')
else:
return string
......@@ -175,13 +175,15 @@ class OpenAPIOperation(dict):
# type: (HTTPRequest) -> Optional[dict]
"""Returns the schema for the request body, or None if no `requestBody` defined
"""
request_content_type = request.getHeader('content-type')
# TODO there might be $ref ?
request_body_definition = self.get(
'requestBody', {'content': {}})['content'].get(request_content_type)
if request_body_definition:
return SchemaWithComponents(
self._schema, request_body_definition.get('schema', {}))
exact_request_content_type = request.getHeader('content-type')
wildcard_request_content_type = '%s/*' % ((exact_request_content_type or '').split('/')[0])
for request_content_type in exact_request_content_type, wildcard_request_content_type, '*/*':
# TODO there might be $ref ?
request_body_definition = self.get(
'requestBody', {'content': {}})['content'].get(request_content_type)
if request_body_definition:
return SchemaWithComponents(
self._schema, request_body_definition.get('schema', {}))
class OpenAPIParameter(dict):
......@@ -340,7 +342,10 @@ class OpenAPITypeInformation(ERP5TypeInformation):
security.declareObjectProtected(Permissions.AccessContentsInformation)
def getSchema(self):
stream = io.BytesIO(self.getTextContent() or b'{}')
text_content = self.getTextContent() or '{}'
if six.PY3:
text_content = text_content.encode()
stream = io.BytesIO(text_content)
if self.getContentType() == 'application/x-yaml':
try:
import yaml # pylint:disable=import-error
......
......@@ -25,6 +25,13 @@
#
##############################################################################
import six
# pylint:disable=no-name-in-module
if six.PY2:
from base64 import encodestring as base64_encodebytes
else:
from base64 import encodebytes as base64_encodebytes
# pylint:enable=no-name-in-module
import io
import json
import unittest
......@@ -39,7 +46,7 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
class OpenAPITestCase(ERP5TypeTestCase):
_type_id = NotImplemented # type: str
_open_api_schema = NotImplemented # type: bytes
_open_api_schema = NotImplemented # type: str
_open_api_schema_content_type = 'application/json'
_public_api = True
......@@ -375,7 +382,7 @@ class TestOpenAPIServicePetController(OpenAPIPetStoreTestCase):
class TestOpenAPIServiceYaml(OpenAPITestCase):
_type_id = 'Test Open API YAML'
_open_api_schema_content_type = 'application/x-yaml'
_open_api_schema = b'''
_open_api_schema = '''
openapi: 3.0.3
info:
title: TestOpenAPIServiceYaml
......@@ -456,7 +463,7 @@ class TestPathParameterSerialization(OpenAPITestCase):
}
}
}
}).encode()
})
def test_primitive_parameter_serialization(self):
self.addPythonScript(
......@@ -532,7 +539,7 @@ class TestQueryParameterSerialization(OpenAPITestCase):
}
}
}
}).encode()
})
def test_array_parameter_serialization(self):
self.addPythonScript(
......@@ -707,7 +714,7 @@ class TestOpenAPINonAsciiParameters(OpenAPIPetStoreTestCase):
class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
_type_id = 'Test Open API Common Parameters'
_open_api_schema = (
b'''
'''
{
"openapi": "3.0.3",
"info": {
......@@ -718,7 +725,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
'''
# https://swagger.io/docs/specification/describing-parameters/#common-for-path
b'''
'''
"/common-for-path": {
"parameters": [
{
......@@ -749,7 +756,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
},'''
# https://swagger.io/docs/specification/describing-parameters/#common-for-various-paths
b'''
'''
"/common-for-various-paths": {
"get": {
"operationId": "testGET2",
......@@ -761,7 +768,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
'''
# here we also excercice $refs in parameter schemas
b'''
'''
"$ref": "#/components/schemas/custom-number"
}
},
......@@ -781,7 +788,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
# https://spec.openapis.org/oas/v3.1.0#fixed-fields-6
# $refs: Allows for a referenced definition of this path item.
# The referenced structure MUST be in the form of a Path Item Object.
b'''
'''
"/alias": {
"$ref": "#/paths/~1common-for-path"
}
......@@ -895,7 +902,7 @@ class TestOpenAPIMissingParameters(OpenAPIPetStoreTestCase):
}
}
}
}).encode()
})
def test_required_query(self):
self.addPythonScript(
......@@ -980,7 +987,7 @@ class TestOpenAPIErrorHandling(OpenAPIPetStoreTestCase):
self.addPythonScript(
'TestPetStoreOpenAPI_findPetsByStatus',
'status',
'1/0',
'1//0',
)
response = self.publish(
self.connector.getPath() + '/pet/findByStatus?status=available')
......@@ -1097,7 +1104,7 @@ class TestPathParameterAndAcquisition(OpenAPIPetStoreTestCase):
"""
def afterSetUp(self):
super(TestPathParameterAndAcquisition, self).afterSetUp()
if not '789' in self.portal.portal_web_services.objectIds():
if '789' not in self.portal.portal_web_services.objectIds():
self.portal.portal_web_services.newContent(
id='789',
portal_type=self.portal.portal_web_services.allowedContentTypes()
......@@ -1242,3 +1249,72 @@ class TestURLPathWithWebSiteAndVirtualHost(OpenAPIPetStoreTestCase):
self.connector.getRelativeUrl()
))
self.assertEqual(response.getBody(), b'"ok"')
class TestOpenAPIRequestBody(OpenAPITestCase):
_type_id = 'Test Open API Request Body'
_open_api_schema = json.dumps(
{
'openapi': '3.0.3',
'info': {
'title': 'TestOpenAPIRequestBody',
'version': '0.0.0'
},
'paths': {
'/post': {
'post': {
'operationId': 'testPostByContentType',
'requestBody': {
'content': {
'image/*': {
'schema': {
'type': 'string',
'format': 'binary',
}
},
'application/x-base64': {
'schema': {
'type': 'string',
'format': 'base64',
}
}
}
}
}
}
}
})
def test_request_body_content_encoding(self):
self.addPythonScript(
'TestOpenAPIRequestBody_testPostByContentType',
'body=None',
'container.REQUEST.RESPONSE.setHeader("Content-Type", "application/octet-stream")\n'
'return body',
)
response = self.publish(
self.connector.getPath() + '/post',
request_method='POST',
stdin=io.BytesIO(b'png file content'),
env={"CONTENT_TYPE": 'image/png'})
self.assertEqual(response.getBody(), b'png file content')
self.assertEqual(response.getStatus(), 200)
response = self.publish(
self.connector.getPath() + '/post',
request_method='POST',
stdin=io.BytesIO(base64_encodebytes(b'base64 file content')),
env={"CONTENT_TYPE": 'application/x-base64'})
self.assertEqual(response.getBody(), b'base64 file content')
self.assertEqual(response.getStatus(), 200)
response = self.publish(
self.connector.getPath() + '/post',
request_method='POST',
stdin=io.BytesIO(b'not base64'),
env={"CONTENT_TYPE": 'application/x-base64'})
self.assertEqual(response.getStatus(), 400)
body = json.loads(response.getBody())
self.assertEqual(body['type'], 'parameter-validation-error')
self.assertIn('Error validating request body:', body['title'])
......@@ -12,8 +12,19 @@ for open_order_line in context.objectValues():
if getattr(item.aq_explicit, 'updateSimulation', None) is not None and \
item not in subscription_item_set:
subscription_item_set.add(item)
# If start_date is in futur, do not look for unreachable period
stop_date = item.getNextPeriodicalDate(max(now, ob.getStartDate()))
# Logic duplicated from SubscriptionItem.py
start_date = ob.getStartDate()
# if there is no stop_date, block the generation
# to today
stop_date = ob.getStopDate()
current_date = start_date
if (start_date == stop_date) or (stop_date is None):
# stop_date seems acquired from start_date
stop_date = now
while current_date < stop_date:
current_date = item.getNextPeriodicalDate(current_date)
# Do not expand subscription item if there is
# no new simulation movement to create
# (expand always reindex the full simulation tree,
......@@ -21,8 +32,8 @@ for open_order_line in context.objectValues():
simulation_movement_list = portal.portal_simulation.getMovementHistoryList(
portal_type='Simulation Movement',
aggregate__uid=item.getUid(),
from_date=stop_date,
at_date=stop_date,
from_date=current_date,
at_date=current_date,
only_accountable=False,
)
if len(simulation_movement_list) == 0:
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_list</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_list</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/ReceiptRecognitionModule_viewReceiptRecognitionList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>receipt_convert</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>Modify portal content</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Convert Receipt</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/ReceiptRecognition_convertImage</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/ReceiptRecognition_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# -*- coding: utf-8 -*-
"""
Extension to find receipts inside images.
Most of the functions is code from ocropy binaries modified
to work inside erp5 and adapt to receipt binaries and with more
explanation
https://github.com/tmbdev/ocropy
"""
# pylint: disable=unpacking-non-sequence
# Pylint is confused by ocropy.
import numpy as np
import scipy.ndimage as ndi
import StringIO
from matplotlib import pylab
import matplotlib.image as mpimg
import scipy.stats as stats
import re
import cPickle
import ocrolib
def getReceiptValue(self, image_data, model_name = "en-default.pyrnn"):
"""
Function called from an erp5 script through externalMethod
that take an image and its name and save its binarized
version in the image portal.
----------------------------
@args:
- self: object
Represent the erp5 object from which externalmethods or module
objects can be called
- image_data:
Representation of the image to analyse
@return:
- anon: float
Represent total value paid on the receipt
----------------------------
This function look for euros only and return a price with a two digit
precison like "135.79" or "43,89".
"""
image_as_string = StringIO.StringIO(image_data)
image_as_array = mpimg.imread(image_as_string, format = 'JPG')
line_list, cleared = getLinesFromPicture(image_as_array)
# Start the neural network
network, lnorm = initRnnModel(model_name)
return findReceiptValue(line_list, cleared, network, lnorm)
def findReceiptValue(line_list, cleared, network, lnorm):
"""
Function that run the neural network through the receipt and extract
meaningfull value
-----------------------------
@args:
- lines: array list
Represent lines of text that will be extracted
from the image
- cleared:2D array
Represent binarized image cropped and cleaned,
from which we will extract text lines
- network: lstm object
Represent the trained neural net
- lnorm: method from lstm object
Represent the size of the lstm object. Is used to scale the objects
to recognize from original size to the average network object.
@return:
- anon: float
Represent total value paid on the receipt
-----------------------------
This function can bemodified to add more field to detect. It might be
possible to run a classification neural net on the result.
"""
value_list = []
tofind = r"(EUR)|€|(TOT)"
for _, line in enumerate(line_list):
binline = ocrolib.extract_masked(1 - cleared, line, 3, 3)
# Corner case: he dewarping function from the normalizer fail
# sometimes on empty lines. Can be corrected with better segmentation
try:
evaluate = getStringFromImage(binline, lnorm, network)
if re.search(tofind, evaluate.upper()):
number = re.findall(r"\d+[\.|,]\d\d", evaluate)
value_list += [float(char.replace(',', '.')) for char in number]
except ValueError:
pass
return round(max(value_list), 2)
def getRnnModelFromDataStream(self, model_name="en-default.pyrnn"):
"""
This function load a neural network from a dataStream
----------------------------
@args:
- model_name: string, default: en-default.pyrnn
Id of the object in data_stream_module that contain the rnn model
@return:
- network: lstm object
Represent the trained neural net
- lnorm: method from lstm object
Represent the size of the lstm object. Is used to scale the objects
to recognize from original size to the average network object.
----------------------------
WARNING: This function present a security issue and should NOT be called with
an user-defined model name (see cpickle security issue)
"""
network = cPickle.loads(self.data_stream_module[model_name].getData())
lnorm = getattr(network, "lnorm", None)
return network, lnorm
def initRnnModel(model_name = "en-default.pyrnn"):
"""
This function load the neural network model from slapos backend
and initialise it.
----------------------------
@args:
- model_name: string, default: en-default.pyrnn
Id of the object in the filesystem that contain the rnn model
@return:
- network: lstm object
Represent the trained neural net
- lnorm: method from lstm object
Represent the size of the lstm object. Is used to scale the objects
to recognize from original size to the average network object.
----------------------------
LSTM meaning: https://en.wikipedia.org/wiki/Long_short-term_memory
lnorm is extracted for clarity. This function initialize the neural net after
loading.
"""
network = ocrolib.load_object(model_name)
for node in network.walk():
if isinstance(node, ocrolib.lstm.LSTM):
node.allocate(5000)
lnorm = getattr(network, "lnorm", None)
return network, lnorm
def getLinesFromPicture(image_as_array):
"""
Function that take an colorized image in argument and return a
cleared binarized image and a list of lines
----------------------------
@args:
- image_as_array: 3d array
Represent a colored image
@return:
- lines: array list
Represent lines of text that will be extracted
from the image
- cleared:2D array
Represent binarized image cropped and cleaned,
from which we will extract text lines
----------------------------
This function find the text position of text line in an image
The neural network recognition only works on independant lines
of a binarized image.
Could be improved by making this function return each line as
independant picture
"""
grey_image = convertGreyscale(image_as_array)
cropped_image = cropImage(grey_image)
binarized_image = imageBinarization(cropped_image)
binary = 1 - binarized_image
cleaned, scale = removeObjects(binary)
angle = getEstimatedSkewAngle(cleaned, np.linspace(-4, 4, 24))
cleaned = ndi.interpolation.rotate(
cleaned, angle, mode='constant', reshape = 0
)
segmentation = getSegmentizedImage(cleaned, scale)
# Sort the labels from top to bottom
line_list = ocrolib.compute_lines(segmentation, scale)
order = ocrolib.reading_order([line.bounds for line in line_list])
sorted_lines = ocrolib.topsort(order)
# Renumber the labels with growing values
total_labels = np.amax(segmentation) + 1
renumber = np.zeros(total_labels, 'i')
for i, line in enumerate(sorted_lines):
renumber[line_list[line].label] = 0x010000 + (i + 1)
segmentation = renumber[segmentation]
line_list = [line_list[i] for i in sorted_lines]
return line_list, cleaned
def getStringFromImage(image, lnorm, network):
"""
Function that use a neural network to get a string of character from
an image that contain a line of text
------------------------------------
@args:
- image: 2D array
Represent a binarized image that contain a line of text
- lnorm: norm attribute of the neural network
Used to scale the letters from the image to the text of the
neural network
- network: neural network
Used to predict text
@return:
- pred: string
Text extracted from image
------------------------------------
Getting the text of each line allow the neural network not to be confused
with noise.
"""
temp = np.amax(image) - image
temp = temp * 1.0 / np.amax(temp)
lnorm.measure(temp)
image = lnorm.normalize(image, cval = np.amax(image))
image = ocrolib.lstm.prepare_line(image, 16)
return network.predictString(image)
def getLineSeed(binary, scale, bottom, top):
"""
Function that find "line seeds" inside a binarized image. The line
seeds are multiple boxes that surround text in a picture.
--------------------------------------
@args:
- binary: 2D array
Represent the image from which line seed will be extracted
- scale: double
Represent the size of the average object
- bottom: 2D array
Represent the bottom boundary of line seeds
- top: 2D array
Represent the top boundary of line seeds
--------------------------------------
The line seeds act as a "mask" that isolate lines from each other
to allow segmentation
"""
# Use scale as a int
vrange = int(scale)
threshold = 0.1
# Find the bottom boundary
bmarked = ndi.maximum_filter(
bottom == ndi.maximum_filter(bottom, (vrange, 0)), (2, 2))
bmarked = bmarked*(bottom > threshold * np.amax(bottom) * threshold)
# Find the top boundary
tmarked = ndi.maximum_filter(
top == ndi.maximum_filter(top,(vrange, 0)), (2, 2))
tmarked = tmarked * (top > threshold * np.amax(top) * threshold / 2)
tmarked = ndi.maximum_filter(tmarked, (1, 20))
# Create seeds
seeds = np.zeros(binary.shape, 'i')
line_spacing = vrange / 2
for x in range(bmarked.shape[1]):
transitions = sorted([(y, 1) for y in pylab.find(bmarked[ : , x])] \
+ [(y, 0) for y in pylab.find(tmarked[ : , x])])[:: -1]
transitions += [(0, 0)]
for i in range(len(transitions) - 1):
y0, s0 = transitions[i]
if s0 == 0:
continue
seeds[y0 - line_spacing : y0, x] = 1
y1, s1 = transitions[i + 1]
if s1 == 0 and (y0 - y1) < 5 * scale:
seeds[y1 : y0, x] = 1
return ndi.maximum_filter(seeds, (1, vrange))
def getSegmentizedImage(binary, scale):
"""
Function that give every pixel of every text character on a same line
the same value so it can be easily separated latter
This function find the upper and lower boundaries of text lines with
a filter using the first derivative of gaussian kernel to get
horizontal change of intensity and stretching it
----------------------------
@args:
binary: 2D array
Represent a clean, straight binarized image from witch we will
find the seeds
scale: double
Represent the size of the mean object
@return:
segmentation: 2D array
Represent a picture with different pixel value for each line
of text.
----------------------------
The segmentization give us independant text lines. Rotating the
picture to make lines more straight could improve result.
Some args could be added to play on the stretching of the blur or
the threeshold for finding the mask boundaries
"""
boxmap = ocrolib.compute_boxmap(binary, scale, threshold = (.25, 4))
cleaned_image = boxmap * binary
# Finding horizontals gradient
gradient = ndi.gaussian_filter(1.0 * cleaned_image, (0.5 * scale,
scale * 6), order = (1, 0))
gradient = ndi.uniform_filter(gradient,(1, 20))
# Find the bottom (whiter) and top (darker) zones from grad
bottom = ocrolib.norm_max((gradient < 0) * (-gradient))
top = ocrolib.norm_max((gradient > 0) * gradient)
seeds = getLineSeed(cleaned_image, scale, bottom, top)
# Label the seeds then group them by line inside boxmaps
seeds,_ = ocrolib.morph.label(seeds)
labels = ocrolib.morph.propagate_labels(boxmap, seeds, conflict = 1)
spread = ocrolib.morph.spread_labels(seeds, maxdist = (scale / 4))
labels = np.where(labels > 0, labels, spread * binary)
return labels * binary
def removeObjects(binarized):
"""
This function identify objects as contigunious spot of white on a
black image and remove the bigger and smaller ones.
---------------------------
@args:
binarized: 2D array
Represent the inversed color binarized picture from which
objects bigger than the mean will be taken from.
@return:
binarized: 2D array
"cleaned" array
scale: double
Represent the size of the mean object
---------------------------
This function improve line detection and "clean" the image.
A coefficient could be added to the function to change the value of
height and width of the deleted object, or to choose only horizontal
or vertical objects.
"""
labels, n = ocrolib.morph.label(binarized)
objects = ocrolib.morph.find_objects(labels)
# Calculate the median of all objects
bysize = sorted(objects, key = ocrolib.sl.area)
scalemap = np.zeros(binarized.shape)
for sub_object in bysize:
if np.amax(scalemap[sub_object]) > 0:
continue
scalemap[sub_object] = ocrolib.sl.area(sub_object) ** 0.5
scale = ndi.median(scalemap[(scalemap > 3) & (scalemap < 100)])
# Take the mesurement of small objects
sums = ndi.measurements.sum(binarized, labels, range(n + 1))
sums = sums[labels]
# Find all objects and remove big ones
for i, b in enumerate(objects):
if (ocrolib.sl.width(b) > scale * 8) \
or (ocrolib.sl.height(b) > scale * 8):
labels[b][labels[b] == i + 1] = 0
# Recreate an array without big objects
binarized = np.array(labels != 0, 'B')
# Remove small objects from this image
binarized = np.minimum(binarized, 1 - (sums > 0) * (sums < scale))
return binarized, scale
def getEstimatedSkewAngle(image, angle_list):
"""
Function that estimate at which angle the image is the most
straight
-------------------------------
@args:
- image: 2d array
Represent the greyscale original image
- angle_list: float list
Represent the list of angles that will be tested in the
function
@return:
a: float
Represent the value of the angle (in degrees) for which the
image is the less skew
-------------------------------
This function is needed to get a better segmentation (since the
segmentation is done horizontally). It calculate the mean of each
line in the picture then calculate the variance, and return the
angle value for which the variance is the highest.
"""
estimates = []
for angle in angle_list:
mean = np.mean(ndi.interpolation.rotate(
image, angle, order = 0, mode = 'constant'), axis = 1)
variance = np.var(mean)
estimates.append((variance, angle))
_, angle = max(estimates)
return angle
def removeBackground(image, percentile=50):
"""
Function that help flatten the image by estimating locals
whitelevels. This remove local extremes and give an image with
homogenous background and no details
------------------------------
@args:
- image: 2D array
Represent a greyscale image
- percentile: integer between -100 and 100
A percentile filter with a value of 50 is basically a
median filter, value of 0 is a minimum filter and with
a value of 100 a maximum filter
@return:
- 2D array
Represent a greyscale image with no local extreme
------------------------------
The filter result will be substracted from the original image
to make that only local extremes stand out.
A Kuwahara filter might give better results.
"""
# Reduce extreme differences in the greyscale image
image = image - pylab.amin(image)
image /= pylab.amax(image)
white_image = ndi.filters.percentile_filter(image, percentile, size=(80, 2))
white_image = ndi.filters.percentile_filter(white_image, percentile, size=(2, 80))
# Get the difference between the whiteleveled image and the
# original one and put them betewwn 0 an 1
return np.clip(image - white_image + 1, 0, 1)
def cropImage(image):
"""
Function that perform cropping and flattening -- Removing
homogenous background and small extremes-- on an image.
-------------------------------
@args:
grey: 2D array
Represent the original greyscale image
@return:
flat: 2D array
Represent the original image cropped and flattened
-------------------------------
This function reduce the size of the image and clear noise from
homogenous background
"""
# Reduce extreme differences in the greyscale image
white_image = removeBackground(image)
# Calculate coordinate to crop the image, can be done in another
# function to improve readability
mask = ndi.gaussian_filter(
white_image, 7.0) < 0.9 * np.amax(white_image)
coords = np.argwhere(mask)
# Bounding box of kept pixels.
x_min, y_min = coords.min(axis = 0)
x_max, y_max = coords.max(axis = 0)
return white_image[x_min - 10 : x_max + 10, y_min - 10 : y_max + 10]
def imageBinarization(flattened_image):
"""
Fontion that take a flattened image and binarize it to make OCR
easier
-------------------------------
@args:
- flattened_image: 2D array
Represent a greyscale image
@return:
- binarized_image: 2D array
Represent a binarized image
-------------------------------
Binarized image is needed to get text boundaries and make text
easier to distinguish from background.
"""
gaussian_image = ndi.gaussian_filter(flattened_image, 1)
# Applying a Gaussian filter with a small sigma and substracting
# it from the "original" image allow to do an approximation of
# Laplacian.
blurred_gaussian = ndi.gaussian_filter(gaussian_image, 3)
# Add the scaled Laplacian to the smoothed image to sharpen its edges
sharpened = 1.5 * gaussian_image - blurred_gaussian
# Estimate low and high threshold
low = stats.scoreatpercentile(sharpened.ravel(), 5)
high = stats.scoreatpercentile(sharpened.ravel(), 70)
# rescale the image to enhance differences
sharpened = (sharpened - low) / (high - low)
sharpened = np.clip(sharpened, 0, 1)
# Binarization
return sharpened > 0.7
def convertGreyscale(image):
"""
Function that take a three dimension numpy array
representing a RGB picture and return a two dimension
numpy array of this picture in greyscale with values between
0 and 1
@args:
- image: 3 dimension numpy array from a RGB picture with
values between 0 and 255
@return:
grey: 2 dimension numpy array with values between 0 and 1
The greyscale conversion is needed to detect edge in
the preprocessing and later to extract easier to process data
"""
return image.astype(float).sum(axis = -1) / 3. / 255
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Extension Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>ReceiptRecognition</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>extension.erp5.ReceiptRecognition</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Extension Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<module>
<id>receipt_recognition_module</id>
<permission_list>
<permission type='tuple'>
<name>Access Transient Objects</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Access contents information</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Access session data</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Add portal content</name>
<role>Assignor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Add portal folders</name>
<role>Assignor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Change local roles</name>
<role>Assignor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Copy or Move</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Delete objects</name>
<role>Assignor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>List folder contents</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Modify portal content</name>
<role>Assignor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>View</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>View History</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
</permission_list>
<portal_type>Receipt Recognition Module</portal_type>
<title>Receipt Recognition</title>
</module>
\ No newline at end of file
<allowed_content_type_list>
<portal_type id="Receipt Recognition Module">
<item>Receipt Recognition</item>
</portal_type>
</allowed_content_type_list>
\ No newline at end of file
<base_category_list>
<portal_type id="Receipt Recognition Module">
<item>business_application</item>
</portal_type>
</base_category_list>
\ No newline at end of file
<property_sheet_list>
<portal_type id="Receipt Recognition">
<item>Document</item>
</portal_type>
</property_sheet_list>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_property_domain_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>acquire_local_roles</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>content_icon</string> </key>
<value> <string>folder_icon.gif</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>factory</string> </key>
<value> <string>addFolder</string> </value>
</item>
<item>
<key> <string>filter_content_types</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<tuple>
<string>module</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Receipt Recognition Module</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value>
</item>
<item>
<key> <string>searchable_text_property_id</string> </key>
<value>
<tuple>
<string>title</string>
<string>description</string>
<string>reference</string>
<string>short_title</string>
</tuple>
</value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>Folder</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>short_title</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="TranslationInformation" module="Products.ERP5Type.TranslationProviderBase"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>domain_name</string> </key>
<value> <string>erp5_ui</string> </value>
</item>
<item>
<key> <string>property_name</string> </key>
<value> <string>short_title</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="TranslationInformation" module="Products.ERP5Type.TranslationProviderBase"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>domain_name</string> </key>
<value> <string>erp5_ui</string> </value>
</item>
<item>
<key> <string>property_name</string> </key>
<value> <string>title</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>acquire_local_roles</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>content_icon</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>factory</string> </key>
<value> <string>addXMLObject</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Receipt Recognition</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value>
</item>
<item>
<key> <string>searchable_text_property_id</string> </key>
<value>
<tuple>
<string>title</string>
<string>description</string>
<string>reference</string>
<string>short_title</string>
</tuple>
</value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>XMLObject</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Folder" module="OFS.Folder"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_doSelect</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list>
<string>listbox</string>
</list>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ReceiptRecognitionModule_viewReceiptRecognitionList</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>ReceiptRecognitionModule_viewReceiptRecognitionList</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_list</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt Recognition</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>columns</string>
<string>portal_types</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>listbox</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>columns</string> </key>
<value>
<list>
<tuple>
<string>title</string>
<string>Title</string>
</tuple>
<tuple>
<string>total</string>
<string>Total</string>
</tuple>
<tuple>
<string>follow_up_title</string>
<string>Image</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_list_mode_listbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value>
<list>
<tuple>
<string>Receipt Recognition</string>
<string>Receipt Recognition</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>portal_types</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt Recognition</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
image = context.getFollowUpValue()
if image is not None:
try:
total = container.ReceiptRecognition_getReceiptValue(image.getData())
message = "Total found"
context.edit(
total = total,
)
except ValueError as e:
message = "Could not find value, please submit it manually"
else:
message = "Cannot find the image"
if batch_mode:
return
context.Base_redirect(
'view', keep_items = dict(portal_status_message=message, my_source="test"))
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>batch_mode=False, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ReceiptRecognition_convertImage</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>getReceiptValue</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>ReceiptRecognition</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ReceiptRecognition_getReceiptValue</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_edit</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_title</string>
<string>my_total</string>
<string>my_follow_up_title</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ReceiptRecognition_view</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>ReceiptRecognition_view</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_view</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt Recognition</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>portal_type</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_follow_up_title</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_relation_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value>
<list>
<tuple>
<string>Image</string>
<string>Image</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Follow Up</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_title</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Title</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_total</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Total</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Receipt Recognition Module | view
Receipt Recognition | receipt_convert
Receipt Recognition | view
\ No newline at end of file
extension.erp5.ReceiptRecognition
\ No newline at end of file
receipt_recognition_module
\ No newline at end of file
Receipt Recognition Module | Receipt Recognition
\ No newline at end of file
Receipt Recognition Module | business_application
\ No newline at end of file
Receipt Recognition
Receipt Recognition Module
\ No newline at end of file
Receipt Recognition | Document
\ No newline at end of file
erp5_receipt_recognition
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_receipt_recognition
\ No newline at end of file
0.1.0
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>1cbedf94ee11f2ecf3c3784438c7ce4e</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0107.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_001</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt00</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>72d018e3e436cf176233eb75e48f6914</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0105.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_002</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt01</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>ca8b2ab2191e7ac2dbc80817b97728bc</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0106.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_003</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt02</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>709b63a3648d5ccd920743f997367e6e</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0104.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_004</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt03</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>7f9696e9dc5ce485adf05663dd635480</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0101.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_005</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt04</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>909a682d7bfd0131ef39405edacc52cc</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0095.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_006</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt05</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>14e4ef10a849cd6897e8f8f981a37ccb</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0102.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_007</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt06</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>cfe6c8f28055281e7327b95a85e52b2f</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>pièce jointe5.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_008</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt07</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Image" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>d38c726e8354b2775a305ecdf49e1656</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>image/jpeg</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>IMG_0093.jpg</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>2048</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_receipt_recognition_test_image_sample_009</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Image</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Receipt08</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>1536</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
##############################################################################
#
# Copyright (c) 2002-2017 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
class Test(ERP5TypeTestCase):
"""
A Sample Test Class
"""
def getTitle(self):
return "testReceiptRecognition"
def getBusinessTemplateList(self):
"""
Tuple of Business Templates we need to install
"""
return ('erp5_base', 'erp5_dms', 'erp5_wendelin', 'erp5_receipt_recognition', 'test_receipt_recognition')
def _run_recognition(self, module_id):
data = self.portal.image_module[module_id]
result = self.portal.ReceiptRecognition_getReceiptValue(data.getData())
return result
def test_bank_receipt(self):
"""
This test is successfull. The receipt have a grey text on the whole
diagonal (name of the bank) but it still works
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_001"), 28.0)
def test_distorted_receipt(self):
"""
This receipt is sghtly curved: the middle is higher than the top and
bottom
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_002"), 311.35)
def test_noisy_image(self):
"""
The following image should contain the value "28.00" but fail due to
its base quality.
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_003"), 24.09)
def test_cropped_receipt(self):
"""
This test is successfull. The left and top sid of the receip
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_004"), 7.8)
def test_defolded_receipt1(self):
"""
This test is successfull.
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_005"), 5.85)
def test_defolded_receipt2(self):
"""
This image should contain 5.85, but the module fail to recognize
the keyword "EUR" in the line.
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_006"), 5.55)
def test_monoprix_receipt(self):
"""
On this receipt, the OCR cannot read the total value because of the
weird typeset (to do this, we need our own models), but find which
value was paid with credit card.
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_007"), 21.27)
def test_coiled_receipt(self):
"""
This test is successfull. The receipt is vertically coiled. It leads
to darker areas in the middle of the receipt that used to break the
cleaning and segementation.
"""
self.assertEqual(self._run_recognition("erp5_receipt_recognition_test_image_sample_008"), 6.8)
def test_failure(self):
"""
This should fail
This receipt was folded multiple time and is filled with un-clearable
noise.
"""
with self.assertRaises(ValueError):
self._run_recognition("erp5_receipt_recognition_test_image_sample_009")
def test_create_object(self):
"""
Test creation of receipt object and call of the action function
"""
follow_up = self.portal.image_module["erp5_receipt_recognition_test_image_sample_001"]
receipt = self.portal.receipt_recognition_module.newContent(
id="0",
title="test",
follow_up_value=follow_up
)
receipt.ReceiptRecognition_convertImage(batch_mode=True)
self.assertEqual(receipt.total, 28.0)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testReceiptRecognition</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testReceiptRecognition</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
erp5_dms
erp5_receipt_recognition
\ No newline at end of file
image_module/erp5_receipt_recognition_test_image_sample_001
image_module/erp5_receipt_recognition_test_image_sample_002
image_module/erp5_receipt_recognition_test_image_sample_003
image_module/erp5_receipt_recognition_test_image_sample_004
image_module/erp5_receipt_recognition_test_image_sample_005
image_module/erp5_receipt_recognition_test_image_sample_006
image_module/erp5_receipt_recognition_test_image_sample_007
image_module/erp5_receipt_recognition_test_image_sample_008
image_module/erp5_receipt_recognition_test_image_sample_009
\ No newline at end of file
test.erp5.testReceiptRecognition
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_receipt_recognition_test
\ No newline at end of file
......@@ -279,7 +279,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: modules[\'datetime\'].timedelta(seconds=context.getProperty(\'duration\'))</string> </value>
<value> <string>python: str(modules[\'datetime\'].timedelta(seconds=int(context.getProperty(\'duration\'))))</string> </value>
</item>
</dictionary>
</pickle>
......
import datetime
from Products.PythonScripts.standard import Object
return [Object(duration=datetime.timedelta(seconds=context.getProperty('duration', 0)),
return [Object(duration=str(datetime.timedelta(seconds=int(context.getProperty('duration', 0)))),
all_tests=context.getProperty('all_tests'),
errors=context.getProperty('errors'),
failures=context.getProperty('failures'),
......
......@@ -279,7 +279,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: modules[\'datetime\'].timedelta(seconds=cell.getProperty(\'duration\'))</string> </value>
<value> <string>python: str(modules[\'datetime\'].timedelta(seconds=int(cell.getProperty(\'duration\'))))</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -64,7 +64,6 @@ import struct
from base64 import b64encode, b64decode
from Products.ERP5Type.Message import translateString
from zLOG import LOG, INFO, WARNING
from base64 import decodestring
import subprocess
import time
from Products.ERP5Type.Utils import bytes2str
......@@ -404,16 +403,6 @@ class TemplateTool (BaseTool):
bt.setPublicationUrl(url)
return bt
security.declareProtected('Import/Export objects', 'importBase64EncodedText')
def importBase64EncodedText(self, file_data=None, id=None, REQUEST=None,
batch_mode=False, **kw):
"""
Import Business Template from passed base64 encoded text.
"""
import_file = StringIO(decodestring(file_data))
return self.importFile(import_file = import_file, id = id, REQUEST = REQUEST,
batch_mode = batch_mode, **kw)
security.declareProtected('Import/Export objects', 'importFile')
def importFile(self, import_file=None, id=None, REQUEST=None,
batch_mode=False, **kw):
......
......@@ -18,6 +18,10 @@ from waitress.server import create_server
import ZConfig
import Zope2
from Zope2.Startup.run import make_wsgi_app
from App.config import getConfiguration
from zope.component import getGlobalSiteManager
import zope.interface
import ZPublisher.interfaces
try:
from ZPublisher.WSGIPublisher import _MODULES
......@@ -86,6 +90,12 @@ class TransLogger(object):
self.logger.info(message)
@zope.interface.implementer(ZPublisher.interfaces.IXmlrpcChecker)
class XMLRPCDisabler:
def __call__(self, request):
return False
def app_wrapper(large_file_threshold, webdav_ports):
try:
from Products.DeadlockDebugger.dumper import dump_threads, dump_url
......@@ -186,6 +196,7 @@ def runwsgi():
'--with-max-rlimit-nofile',
help='Set soft limit of file descriptors erp5 can open to hard limit',
action="store_true")
parser.add_argument('--enable-xml-rpc', help='Enable XML-RPC interface', action='store_true')
args = parser.parse_args()
if not sys.warnoptions:
......@@ -239,6 +250,12 @@ def runwsgi():
schema = ZConfig.loadSchema(os.path.join(startup, 'zopeschema.xml'))
conf, _ = ZConfig.loadConfig(schema, args.zope_conf)
if not args.enable_xml_rpc:
getGlobalSiteManager().registerUtility(
XMLRPCDisabler(),
ZPublisher.interfaces.IXmlrpcChecker,
)
if conf.debug_mode:
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(logging.Formatter(
......
......@@ -334,19 +334,6 @@ class Amount(Base, VariatedMixin):
(property_id, property_value, self.getRelativeUrl()))
raise
security.declareProtected(Permissions.AccessContentsInformation,
'getQuantityUnitRangeItemList')
def getQuantityUnitRangeItemList(self, base_category_list=()):
resource = self.getDefaultResourceValue()
if resource is not None:
result = resource.getQuantityUnitList()
else:
result = ()
if result is ():
return self.portal_categories.quantity_unit.getFormItemList()
else:
return result
security.declareProtected(Permissions.AccessContentsInformation, 'getResourceDefaultQuantityUnit')
def getResourceDefaultQuantityUnit(self):
"""
......
......@@ -311,24 +311,6 @@ class IntrospectionTool(LogMixin, BaseTool):
#
# Instance variable definition access
#
security.declareProtected(Permissions.ManagePortal, '_loadExternalConfig')
def _loadExternalConfig(self):
"""
Load configuration from one external file, this configuration
should be set for security reasons to prevent people access
forbidden areas in the system.
"""
def cached_loadExternalConfig():
from six.moves import configparser
config = configparser.ConfigParser()
config.readfp(open('/etc/erp5.cfg'))
return config
cached_loadExternalConfig = CachingMethod(cached_loadExternalConfig,
id='IntrospectionTool__loadExternalConfig',
cache_factory='erp5_content_long')
return cached_loadExternalConfig()
security.declareProtected(Permissions.ManagePortal, '_getSoftwareHome')
def _getSoftwareHome(self):
"""
......
......@@ -743,7 +743,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase, functional.F
"""
Shortcut function to log a message
"""
ZopeTestCase._print('\n%s ' % message)
ZopeTestCase._print('\n%s' % message)
LOG('Testing ... ', DEBUG, message)
def publish(self, path, basic=None, env=None, extra=None,
......@@ -1320,7 +1320,7 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
# Log out
if not quiet:
ZopeTestCase._print('Logout ... \n')
ZopeTestCase._print('Logout ...\n')
noSecurityManager()
if not quiet:
ZopeTestCase._print('done (%.3fs)\n' % (time.time()-_start,))
......
"""Manage coverage reporting for ERP5 test runner.
This file is used in two contexts:
- at the end of every test, this module is imported and `upload` is called to upload
coverage data to WebDAV server.
- this is ran as a unittest at the end, to download all coverage data from the WebDAV
server and run coverage report.
"""
from __future__ import print_function
import json
import logging
import os
import sys
import time
import unittest
import coverage
import requests
import six
import uritemplate
from six.moves.urllib.parse import urlparse
from Products.ERP5Type.tests.runUnitTest import log_directory
def _get_auth_list_from_url(parsed_url):
if parsed_url.username:
# try Digest and Basic authentication
return (
requests.auth.HTTPDigestAuth(parsed_url.username, parsed_url.password),
requests.auth.HTTPBasicAuth(parsed_url.username, parsed_url.password),
)
return (None,)
def _expand_uri_template(url, **kw):
# Environment variables are set in product/ERP5Type/tests/runTestSuite.py
kw.setdefault(
'test_result_id',
os.environ.get('ERP5_TEST_RESULT_ID', 'unknown_test_result_id'),
)
kw.setdefault(
'test_result_revision',
os.environ.get('ERP5_TEST_RESULT_REVISION', 'unknown_test_result_revision'),
)
return uritemplate.URITemplate(url).expand(**kw)
def upload(filename, upload_url_template, test_name):
upload_url = _expand_uri_template(upload_url_template, test_name=test_name)
parsed_url = urlparse(upload_url)
hostname = parsed_url.hostname
with requests.Session() as session:
for retry in range(5):
for auth in _get_auth_list_from_url(parsed_url):
with open(filename, 'rb') as f:
try:
resp = session.put(upload_url, data=f, auth=auth, timeout=30)
except requests.exceptions.RequestException as e:
error = e
else:
if resp.ok:
print('Uploaded coverage data to {hostname}'.format(hostname=hostname))
return
error = resp.status_code
if (
retry
): # don't print error on first time, because `auth` might be wrong class
print(
'Error {error} uploading coverage data to {hostname} with {auth.__class__.__name__}'.format(
error=error, hostname=hostname, auth=auth
)
)
time.sleep(retry)
class CoverageReport(unittest.TestCase):
def setUp(self):
self._logger = logging.getLogger(__name__)
self._coverage_process = coverage.Coverage.current()
self._coverage_process.stop()
with open(os.environ['ERP5_TEST_RUNNER_CONFIGURATION']) as f:
self._test_runner_configuration = json.load(f)
downloaded_coverage_path_set = self._download_coverage_data()
self._coverage_process.combine(
data_paths=downloaded_coverage_path_set,
)
self._coverage_process.save()
def _download_coverage_data(self):
downloaded_coverage_path_set = set()
download_url_template = self._test_runner_configuration['coverage']['upload-url']
assert download_url_template
coverage_data_directory = os.path.join(log_directory, 'coverage_data')
if not os.path.exists(coverage_data_directory):
os.makedirs(coverage_data_directory)
to_download = set(
json.loads(
# ERP5_TEST_TEST_LIST is set in product/ERP5Type/tests/runTestSuite.py
# it contains the list of tests as returned by ERP5TypeTestSuite.getTestList
os.environ['ERP5_TEST_TEST_LIST'],
)
)
with requests.Session() as session:
while to_download:
for test_name in list(to_download):
test_file_name = test_name.replace(':', '_')
download_destination = os.path.join(
coverage_data_directory,
'{test_name}.coverage.sqlite3'.format(test_name=test_file_name),
)
if os.path.exists(download_destination):
downloaded_coverage_path_set.add(download_destination)
to_download.remove(test_name)
continue
download_url = _expand_uri_template(
download_url_template, test_name=test_file_name
)
parsed_url = urlparse(download_url)
hostname = parsed_url.hostname
for auth in _get_auth_list_from_url(parsed_url):
try:
resp = session.get(download_url, auth=auth, timeout=30)
except requests.exceptions.RequestException:
self._logger.exception('Error during request, retrying')
continue
if resp.ok:
with open(download_destination + '.tmp', 'wb') as f:
f.write(resp.content)
os.rename(download_destination + '.tmp', download_destination)
self._logger.info(
'Downloaded %s coverage data from %s',
test_name,
hostname,
)
break
self._logger.critical(
'Error %s downloading coverage data for %s from %s with %s, retrying',
resp.status_code,
test_name,
hostname,
auth.__class__.__name__,
)
time.sleep(60 if resp.status_code == 404 else 5)
return downloaded_coverage_path_set
def test_coverage_report(self):
# reports must run from the root of slapos software, because we recorded
# relative paths.
os.chdir(
os.path.dirname(
os.path.dirname(
os.path.dirname(
os.path.dirname(
os.path.dirname(
os.path.dirname(__file__),
)
)
)
)
)
)
self._coverage_process.html_report(
directory=os.path.join(log_directory, 'html_report'),
show_contexts=True,
# We ignore errors because some tests execute code that does not exist on disk, causing
# errors like this:
# NoSource: No source for code: 'parts/erp5/product/ERP5/Document/UnitTest.py'.
# Aborting report output, consider using -i.
ignore_errors=True,
)
if six.PY3:
self._coverage_process.lcov_report(
outfile=os.path.join(log_directory, 'coverage.lcov'),
ignore_errors=True,
)
total_coverage = self._coverage_process.report(
file=sys.stderr,
skip_covered=True,
skip_empty=True,
ignore_errors=True,
)
self.assertGreater(
total_coverage,
self._test_runner_configuration['coverage'].get('fail-under', 50),
)
......@@ -50,14 +50,14 @@ if load:
if save_mysql:
if os.path.exists(dump_sql_path):
command = "mysql %s < %s" % (getMySQLArguments(), dump_sql_path)
_print("Restoring MySQL database with %s ... " % command)
_print("Restoring MySQL database with %s ..." % command)
_start = time.time()
subprocess.check_call(command, shell=True)
_print('done (%.3fs)\n' % (time.time() - _start))
else:
_print("Could not find MySQL dump (%r), will recreate catalog ... " % dump_sql_path)
_print("Could not find MySQL dump (%r), will recreate catalog ..." % dump_sql_path)
os.environ['erp5_tests_recreate_catalog'] = '1'
_print("Restoring static files ... ")
_print("Restoring static files ...")
else:
if save and not (neo_storage or zeo_client) and os.path.exists(data_fs_path):
_print("About to remove existing Data.fs %s (press Ctrl+C to abort)" % data_fs_path)
......@@ -193,4 +193,4 @@ if with_wendelin_core and not in_forked_process:
wcfs_server = wcfs.start(zurl)
if node_pid_list is not None:
_print("Instance at %r loaded ... " % instance_home)
_print("Instance at %r loaded ..." % instance_home)
#!/usr/bin/env python2.7
from __future__ import absolute_import
from __future__ import print_function
import argparse, sys, os, textwrap
import argparse, json, sys, os, textwrap
from erp5.util import taskdistribution
# XXX: This import is required, just to populate sys.modules['test_suite'].
......@@ -55,10 +54,11 @@ def main():
Running a full test suite on a development machine can be achieved with:
%(prog)s --node_quantity=3 --test_suite=ERP5 --xvfb_bin=/path/to/Xvfb --firefox_bin=/path/to/firefox
'''))
# Parameters included in wrappers generated by SlapOS ERP5 software release.
# To handle backward compatibity, we prefer that the generated wrapper pass
# To handle backward compatibility, we prefer that the generated wrapper pass
# these parameters as environment variables. This way, if SlapOS SR is more
# recent, the parameter will be ignored by ERP5.
slapos_wrapper_group = parser.add_argument_group(
......@@ -76,12 +76,12 @@ def main():
slapos_wrapper_group.add_argument('--bt5_path', default=None)
slapos_wrapper_group.add_argument(
'--zserver_address_list',
help='A list of comma seperated host:port for ZServer.\n'
help='A list of comma separated host:port for ZServer.\n'
'Also taken from zserver_address_list environment variable.',
default=os.getenv('zserver_address_list', ''))
slapos_wrapper_group.add_argument(
'--zserver_frontend_url_list',
help='A list of comma seperated frontend URLs, one for each of zserver_address_list,'
help='A list of comma separated frontend URLs, one for each of zserver_address_list,'
'in the same order.\nAlso taken from zserver_frontend_url_list environment variable',
default=os.getenv('zserver_frontend_url_list', ''))
......@@ -127,6 +127,9 @@ def main():
# sanity check
assert len(args.zserver_address_list) == len(args.zserver_frontend_url_list)
with open(os.environ['ERP5_TEST_RUNNER_CONFIGURATION']) as f:
test_runner_configuration = json.load(f)
suite = makeSuite(test_suite=args.test_suite,
node_quantity=args.node_quantity,
revision=revision,
......@@ -137,12 +140,17 @@ def main():
firefox_bin=args.firefox_bin,
xvfb_bin=args.xvfb_bin,
log_directory=args.log_directory)
test_result = master.createTestResult(revision, suite.getTestList(),
test_list = suite.getTestList()
test_list_json = json.dumps(test_list)
if test_runner_configuration.get('coverage', {}).get('enabled'):
test_list.append("coverage_report")
test_result = master.createTestResult(revision, test_list,
args.test_node_title, suite.allow_restart, test_suite_title,
args.project_title)
if test_result is not None:
os.environ['ERP5_TEST_RESULT_REVISION'] = test_result.revision
os.environ['ERP5_TEST_RESULT_ID'] = (test_result.test_result_path or '').split('/')[-1]
os.environ['ERP5_TEST_TEST_LIST'] = test_list_json
assert revision == test_result.revision, (revision, test_result.revision)
while suite.acquire():
......
......@@ -519,7 +519,7 @@ def runUnitTestList(test_list, verbosity=1, debug=0, run_only=None):
# testReceiptRecognition for example.
os.environ.setdefault('OMP_NUM_THREADS', '3')
_print("Loading Zope ... \n")
_print("Loading Zope ...\n")
_start = time.time()
import Testing
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment